lastfm_edit/
session_persistence.rs

1use crate::session::LastFmEditSession;
2use crate::{LastFmError, Result};
3use std::fs;
4use std::path::PathBuf;
5
6/// Session persistence utilities for managing session data in XDG directories.
7///
8/// This module provides functionality to save and load Last.fm session data
9/// using the XDG Base Directory Specification. Sessions are stored per-user
10/// in the format: `~/.local/share/lastfm-edit/users/{username}/session.json`
11pub struct SessionPersistence;
12
13impl SessionPersistence {
14    /// Get the session file path for a given username using XDG directories.
15    ///
16    /// Returns a path like: `~/.local/share/lastfm-edit/users/{username}/session.json`
17    ///
18    /// # Arguments
19    /// * `username` - The Last.fm username
20    ///
21    /// # Returns
22    /// Returns the path where the session should be stored, or an error if
23    /// the XDG data directory cannot be determined.
24    pub fn get_session_path(username: &str) -> Result<PathBuf> {
25        let data_dir = dirs::data_dir()
26            .ok_or_else(|| LastFmError::Http("Cannot determine XDG data directory".to_string()))?;
27
28        let session_dir = data_dir.join("lastfm-edit").join("users").join(username);
29
30        Ok(session_dir.join("session.json"))
31    }
32
33    /// Save a session to the XDG data directory.
34    ///
35    /// This creates the necessary directory structure and saves the session
36    /// as JSON to `~/.local/share/lastfm-edit/users/{username}/session.json`
37    ///
38    /// # Arguments
39    /// * `session` - The session to save
40    ///
41    /// # Returns
42    /// Returns Ok(()) on success, or an error if the save fails.
43    pub fn save_session(session: &LastFmEditSession) -> Result<()> {
44        let session_path = Self::get_session_path(&session.username)?;
45
46        // Create parent directories if they don't exist
47        if let Some(parent) = session_path.parent() {
48            fs::create_dir_all(parent).map_err(|e| {
49                LastFmError::Http(format!("Failed to create session directory: {e}"))
50            })?;
51        }
52
53        // Serialize session to JSON
54        let session_json = session
55            .to_json()
56            .map_err(|e| LastFmError::Http(format!("Failed to serialize session: {e}")))?;
57
58        // Write to file
59        fs::write(&session_path, session_json)
60            .map_err(|e| LastFmError::Http(format!("Failed to write session file: {e}")))?;
61
62        log::debug!("Session saved to: {}", session_path.display());
63        Ok(())
64    }
65
66    /// Load a session from the XDG data directory.
67    ///
68    /// Attempts to load a session from `~/.local/share/lastfm-edit/users/{username}/session.json`
69    ///
70    /// # Arguments
71    /// * `username` - The Last.fm username
72    ///
73    /// # Returns
74    /// Returns the loaded session on success, or an error if the file doesn't exist
75    /// or cannot be parsed.
76    pub fn load_session(username: &str) -> Result<LastFmEditSession> {
77        let session_path = Self::get_session_path(username)?;
78
79        if !session_path.exists() {
80            return Err(LastFmError::Http(format!(
81                "No saved session found for user: {username}"
82            )));
83        }
84
85        // Read and parse session file
86        let session_json = fs::read_to_string(&session_path)
87            .map_err(|e| LastFmError::Http(format!("Failed to read session file: {e}")))?;
88
89        let session = LastFmEditSession::from_json(&session_json)
90            .map_err(|e| LastFmError::Http(format!("Failed to parse session JSON: {e}")))?;
91
92        log::debug!("Session loaded from: {}", session_path.display());
93        Ok(session)
94    }
95
96    /// Check if a saved session exists for the given username.
97    ///
98    /// # Arguments
99    /// * `username` - The Last.fm username
100    ///
101    /// # Returns
102    /// Returns true if a session file exists, false otherwise.
103    pub fn session_exists(username: &str) -> bool {
104        match Self::get_session_path(username) {
105            Ok(path) => path.exists(),
106            Err(_) => false,
107        }
108    }
109
110    /// Remove a saved session for the given username.
111    ///
112    /// This deletes the session file from the XDG data directory.
113    ///
114    /// # Arguments
115    /// * `username` - The Last.fm username
116    ///
117    /// # Returns
118    /// Returns Ok(()) on success, or an error if the deletion fails.
119    pub fn remove_session(username: &str) -> Result<()> {
120        let session_path = Self::get_session_path(username)?;
121
122        if session_path.exists() {
123            fs::remove_file(&session_path)
124                .map_err(|e| LastFmError::Http(format!("Failed to remove session file: {e}")))?;
125            log::debug!("Session removed from: {}", session_path.display());
126        }
127
128        Ok(())
129    }
130
131    /// List all usernames that have saved sessions.
132    ///
133    /// Scans the XDG data directory for session files and returns the usernames.
134    ///
135    /// # Returns
136    /// Returns a vector of usernames that have saved sessions.
137    pub fn list_saved_users() -> Result<Vec<String>> {
138        let data_dir = dirs::data_dir()
139            .ok_or_else(|| LastFmError::Http("Cannot determine XDG data directory".to_string()))?;
140
141        let users_dir = data_dir.join("lastfm-edit").join("users");
142
143        if !users_dir.exists() {
144            return Ok(Vec::new());
145        }
146
147        let mut users = Vec::new();
148        let entries = fs::read_dir(&users_dir)
149            .map_err(|e| LastFmError::Http(format!("Failed to read users directory: {e}")))?;
150
151        for entry in entries {
152            let entry = entry
153                .map_err(|e| LastFmError::Http(format!("Failed to read directory entry: {e}")))?;
154
155            if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
156                let session_file = entry.path().join("session.json");
157                if session_file.exists() {
158                    if let Some(username) = entry.file_name().to_str() {
159                        users.push(username.to_string());
160                    }
161                }
162            }
163        }
164
165        Ok(users)
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_session_path_generation() {
175        let path = SessionPersistence::get_session_path("testuser").unwrap();
176        assert!(path
177            .to_string_lossy()
178            .contains("lastfm-edit/users/testuser/session.json"));
179    }
180
181    #[test]
182    fn test_session_exists_nonexistent() {
183        let fake_username = format!("nonexistent_user_{}", std::process::id());
184        assert!(!SessionPersistence::session_exists(&fake_username));
185    }
186}