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}