lastfm_edit/
session_persistence.rs

1use crate::types::{LastFmEditSession, LastFmError};
2use crate::Result;
3use std::fs;
4use std::path::PathBuf;
5
6/// Configurable session manager for storing session data in XDG directories.
7///
8/// This struct allows customization of the application prefix for session storage.
9/// Sessions are stored per-user in the format:
10/// `~/.local/share/{app_name}/users/{username}/session.json`
11#[derive(Clone, Debug)]
12pub struct SessionManager {
13    app_name: String,
14}
15
16impl SessionManager {
17    /// Create a new session manager with a custom application name.
18    ///
19    /// # Arguments
20    /// * `app_name` - The application name to use as the directory prefix
21    pub fn new(app_name: impl Into<String>) -> Self {
22        Self {
23            app_name: app_name.into(),
24        }
25    }
26
27    /// Get the session file path for a given username using the configured app name.
28    ///
29    /// Returns a path like: `~/.local/share/{app_name}/users/{username}/session.json`
30    ///
31    /// # Arguments
32    /// * `username` - The Last.fm username
33    ///
34    /// # Returns
35    /// Returns the path where the session should be stored, or an error if
36    /// the XDG data directory cannot be determined.
37    pub fn get_session_path(&self, username: &str) -> Result<PathBuf> {
38        let data_dir = dirs::data_dir()
39            .ok_or_else(|| LastFmError::Http("Cannot determine XDG data directory".to_string()))?;
40
41        let session_dir = data_dir.join(&self.app_name).join("users").join(username);
42
43        Ok(session_dir.join("session.json"))
44    }
45
46    /// Save a session to the XDG data directory.
47    ///
48    /// This creates the necessary directory structure and saves the session
49    /// as JSON to `~/.local/share/{app_name}/users/{username}/session.json`
50    ///
51    /// # Arguments
52    /// * `session` - The session to save
53    ///
54    /// # Returns
55    /// Returns Ok(()) on success, or an error if the save fails.
56    pub fn save_session(&self, session: &LastFmEditSession) -> Result<()> {
57        let session_path = self.get_session_path(&session.username)?;
58
59        // Create parent directories if they don't exist
60        if let Some(parent) = session_path.parent() {
61            fs::create_dir_all(parent).map_err(|e| {
62                LastFmError::Http(format!("Failed to create session directory: {e}"))
63            })?;
64        }
65
66        // Serialize session to JSON
67        let session_json = session
68            .to_json()
69            .map_err(|e| LastFmError::Http(format!("Failed to serialize session: {e}")))?;
70
71        // Write to file
72        fs::write(&session_path, session_json)
73            .map_err(|e| LastFmError::Http(format!("Failed to write session file: {e}")))?;
74
75        log::debug!("Session saved to: {}", session_path.display());
76        Ok(())
77    }
78
79    /// Load a session from the XDG data directory.
80    ///
81    /// Attempts to load a session from `~/.local/share/{app_name}/users/{username}/session.json`
82    ///
83    /// # Arguments
84    /// * `username` - The Last.fm username
85    ///
86    /// # Returns
87    /// Returns the loaded session on success, or an error if the file doesn't exist
88    /// or cannot be parsed.
89    pub fn load_session(&self, username: &str) -> Result<LastFmEditSession> {
90        let session_path = self.get_session_path(username)?;
91
92        if !session_path.exists() {
93            return Err(LastFmError::Http(format!(
94                "No saved session found for user: {username}"
95            )));
96        }
97
98        // Read and parse session file
99        let session_json = fs::read_to_string(&session_path)
100            .map_err(|e| LastFmError::Http(format!("Failed to read session file: {e}")))?;
101
102        let session = LastFmEditSession::from_json(&session_json)
103            .map_err(|e| LastFmError::Http(format!("Failed to parse session JSON: {e}")))?;
104
105        log::debug!("Session loaded from: {}", session_path.display());
106        Ok(session)
107    }
108
109    /// Check if a saved session exists for the given username.
110    ///
111    /// # Arguments
112    /// * `username` - The Last.fm username
113    ///
114    /// # Returns
115    /// Returns true if a session file exists, false otherwise.
116    pub fn session_exists(&self, username: &str) -> bool {
117        match self.get_session_path(username) {
118            Ok(path) => path.exists(),
119            Err(_) => false,
120        }
121    }
122
123    /// Remove a saved session for the given username.
124    ///
125    /// This deletes the session file from the XDG data directory.
126    ///
127    /// # Arguments
128    /// * `username` - The Last.fm username
129    ///
130    /// # Returns
131    /// Returns Ok(()) on success, or an error if the deletion fails.
132    pub fn remove_session(&self, username: &str) -> Result<()> {
133        let session_path = self.get_session_path(username)?;
134
135        if session_path.exists() {
136            fs::remove_file(&session_path)
137                .map_err(|e| LastFmError::Http(format!("Failed to remove session file: {e}")))?;
138            log::debug!("Session removed from: {}", session_path.display());
139        }
140
141        Ok(())
142    }
143
144    /// List all usernames that have saved sessions.
145    ///
146    /// Scans the XDG data directory for session files and returns the usernames.
147    ///
148    /// # Returns
149    /// Returns a vector of usernames that have saved sessions.
150    pub fn list_saved_users(&self) -> Result<Vec<String>> {
151        let data_dir = dirs::data_dir()
152            .ok_or_else(|| LastFmError::Http("Cannot determine XDG data directory".to_string()))?;
153
154        let users_dir = data_dir.join(&self.app_name).join("users");
155
156        if !users_dir.exists() {
157            return Ok(Vec::new());
158        }
159
160        let mut users = Vec::new();
161        let entries = fs::read_dir(&users_dir)
162            .map_err(|e| LastFmError::Http(format!("Failed to read users directory: {e}")))?;
163
164        for entry in entries {
165            let entry = entry
166                .map_err(|e| LastFmError::Http(format!("Failed to read directory entry: {e}")))?;
167
168            if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
169                let session_file = entry.path().join("session.json");
170                if session_file.exists() {
171                    if let Some(username) = entry.file_name().to_str() {
172                        users.push(username.to_string());
173                    }
174                }
175            }
176        }
177
178        Ok(users)
179    }
180
181    /// Get the application name used by this session manager.
182    pub fn app_name(&self) -> &str {
183        &self.app_name
184    }
185}
186
187/// Session persistence utilities for managing session data in XDG directories.
188///
189/// This module provides functionality to save and load Last.fm session data
190/// using the XDG Base Directory Specification. Sessions are stored per-user
191/// in the format: `~/.local/share/lastfm-edit/users/{username}/session.json`
192///
193/// # Deprecated
194/// Use [`SessionManager`] instead for more flexibility and customization.
195pub struct SessionPersistence;
196
197impl SessionPersistence {
198    /// Get the default session manager for lastfm-edit.
199    fn default_manager() -> SessionManager {
200        SessionManager::new("lastfm-edit")
201    }
202
203    /// Get the session file path for a given username using XDG directories.
204    ///
205    /// Returns a path like: `~/.local/share/lastfm-edit/users/{username}/session.json`
206    ///
207    /// # Arguments
208    /// * `username` - The Last.fm username
209    ///
210    /// # Returns
211    /// Returns the path where the session should be stored, or an error if
212    /// the XDG data directory cannot be determined.
213    pub fn get_session_path(username: &str) -> Result<PathBuf> {
214        Self::default_manager().get_session_path(username)
215    }
216
217    /// Save a session to the XDG data directory.
218    ///
219    /// This creates the necessary directory structure and saves the session
220    /// as JSON to `~/.local/share/lastfm-edit/users/{username}/session.json`
221    ///
222    /// # Arguments
223    /// * `session` - The session to save
224    ///
225    /// # Returns
226    /// Returns Ok(()) on success, or an error if the save fails.
227    pub fn save_session(session: &LastFmEditSession) -> Result<()> {
228        Self::default_manager().save_session(session)
229    }
230
231    /// Load a session from the XDG data directory.
232    ///
233    /// Attempts to load a session from `~/.local/share/lastfm-edit/users/{username}/session.json`
234    ///
235    /// # Arguments
236    /// * `username` - The Last.fm username
237    ///
238    /// # Returns
239    /// Returns the loaded session on success, or an error if the file doesn't exist
240    /// or cannot be parsed.
241    pub fn load_session(username: &str) -> Result<LastFmEditSession> {
242        Self::default_manager().load_session(username)
243    }
244
245    /// Check if a saved session exists for the given username.
246    ///
247    /// # Arguments
248    /// * `username` - The Last.fm username
249    ///
250    /// # Returns
251    /// Returns true if a session file exists, false otherwise.
252    pub fn session_exists(username: &str) -> bool {
253        Self::default_manager().session_exists(username)
254    }
255
256    /// Remove a saved session for the given username.
257    ///
258    /// This deletes the session file from the XDG data directory.
259    ///
260    /// # Arguments
261    /// * `username` - The Last.fm username
262    ///
263    /// # Returns
264    /// Returns Ok(()) on success, or an error if the deletion fails.
265    pub fn remove_session(username: &str) -> Result<()> {
266        Self::default_manager().remove_session(username)
267    }
268
269    /// List all usernames that have saved sessions.
270    ///
271    /// Scans the XDG data directory for session files and returns the usernames.
272    ///
273    /// # Returns
274    /// Returns a vector of usernames that have saved sessions.
275    pub fn list_saved_users() -> Result<Vec<String>> {
276        Self::default_manager().list_saved_users()
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_session_path_generation() {
286        let path = SessionPersistence::get_session_path("testuser").unwrap();
287        assert!(path
288            .to_string_lossy()
289            .contains("lastfm-edit/users/testuser/session.json"));
290    }
291
292    #[test]
293    fn test_session_exists_nonexistent() {
294        let fake_username = format!("nonexistent_user_{}", std::process::id());
295        assert!(!SessionPersistence::session_exists(&fake_username));
296    }
297}