ricecoder_sessions/
store.rs

1//! Session persistence to disk
2
3use crate::error::{SessionError, SessionResult};
4use crate::models::Session;
5use std::fs;
6use std::path::{Path, PathBuf};
7use tracing::{debug, error, info};
8
9/// Manages session persistence to disk
10#[derive(Debug, Clone)]
11pub struct SessionStore {
12    /// Base directory for storing sessions
13    sessions_dir: PathBuf,
14    /// Archive directory for deleted sessions
15    archive_dir: PathBuf,
16}
17
18impl SessionStore {
19    /// Create a new session store with default directories
20    pub fn new() -> SessionResult<Self> {
21        let sessions_dir = Self::get_sessions_dir()?;
22        let archive_dir = Self::get_archive_dir()?;
23
24        // Ensure directories exist
25        fs::create_dir_all(&sessions_dir)?;
26        fs::create_dir_all(&archive_dir)?;
27
28        debug!(
29            "SessionStore initialized with sessions_dir: {:?}",
30            sessions_dir
31        );
32
33        Ok(Self {
34            sessions_dir,
35            archive_dir,
36        })
37    }
38
39    /// Create a session store with custom directories (for testing)
40    pub fn with_dirs(sessions_dir: PathBuf, archive_dir: PathBuf) -> SessionResult<Self> {
41        fs::create_dir_all(&sessions_dir)?;
42        fs::create_dir_all(&archive_dir)?;
43
44        Ok(Self {
45            sessions_dir,
46            archive_dir,
47        })
48    }
49
50    /// Get the default sessions directory (~/.ricecoder/sessions/)
51    fn get_sessions_dir() -> SessionResult<PathBuf> {
52        let home = dirs::home_dir().ok_or_else(|| {
53            SessionError::ConfigError("Could not determine home directory".to_string())
54        })?;
55        Ok(home.join(".ricecoder").join("sessions"))
56    }
57
58    /// Get the default archive directory (~/.ricecoder/sessions/archive/)
59    fn get_archive_dir() -> SessionResult<PathBuf> {
60        let home = dirs::home_dir().ok_or_else(|| {
61            SessionError::ConfigError("Could not determine home directory".to_string())
62        })?;
63        Ok(home.join(".ricecoder").join("sessions").join("archive"))
64    }
65
66    /// Get the path for a session file
67    fn session_path(&self, session_id: &str) -> PathBuf {
68        self.sessions_dir.join(format!("{}.json", session_id))
69    }
70
71    /// Get the path for an archived session file
72    fn archive_path(&self, session_id: &str) -> PathBuf {
73        self.archive_dir.join(format!("{}.json", session_id))
74    }
75
76    /// Save a session to disk
77    pub async fn save(&self, session: &Session) -> SessionResult<()> {
78        let path = self.session_path(&session.id);
79
80        // Serialize session to JSON
81        let json_data = serde_json::to_string_pretty(session)?;
82
83        // Write to file
84        fs::write(&path, json_data)?;
85
86        info!("Session saved: {} at {:?}", session.id, path);
87
88        Ok(())
89    }
90
91    /// Load a session from disk
92    pub async fn load(&self, session_id: &str) -> SessionResult<Session> {
93        let path = self.session_path(session_id);
94
95        if !path.exists() {
96            return Err(SessionError::NotFound(format!(
97                "Session file not found: {}",
98                session_id
99            )));
100        }
101
102        // Read file
103        let json_data = fs::read_to_string(&path)?;
104
105        // Deserialize from JSON
106        let session: Session = serde_json::from_str(&json_data)?;
107
108        debug!("Session loaded: {} from {:?}", session_id, path);
109
110        Ok(session)
111    }
112
113    /// List all persisted sessions
114    pub async fn list(&self) -> SessionResult<Vec<Session>> {
115        let mut sessions = Vec::new();
116
117        // Read all JSON files in sessions directory
118        let entries = fs::read_dir(&self.sessions_dir)?;
119
120        for entry in entries {
121            let entry = entry?;
122            let path = entry.path();
123
124            // Skip directories and non-JSON files
125            if !path.is_file() {
126                continue;
127            }
128
129            if path.extension().and_then(|s| s.to_str()) != Some("json") {
130                continue;
131            }
132
133            // Try to load the session
134            match fs::read_to_string(&path) {
135                Ok(json_data) => match serde_json::from_str::<Session>(&json_data) {
136                    Ok(session) => sessions.push(session),
137                    Err(e) => {
138                        error!("Failed to deserialize session from {:?}: {}", path, e);
139                    }
140                },
141                Err(e) => {
142                    error!("Failed to read session file {:?}: {}", path, e);
143                }
144            }
145        }
146
147        debug!("Listed {} sessions", sessions.len());
148
149        Ok(sessions)
150    }
151
152    /// Export a session to a user-specified file
153    pub async fn export(&self, session_id: &str, export_path: &Path) -> SessionResult<()> {
154        // Load the session
155        let session = self.load(session_id).await?;
156
157        // Serialize to JSON
158        let json_data = serde_json::to_string_pretty(&session)?;
159
160        // Write to export path
161        fs::write(export_path, json_data)?;
162
163        info!("Session exported: {} to {:?}", session_id, export_path);
164
165        Ok(())
166    }
167
168    /// Delete a session and archive it
169    pub async fn delete(&self, session_id: &str) -> SessionResult<()> {
170        let session_path = self.session_path(session_id);
171        let archive_path = self.archive_path(session_id);
172
173        if !session_path.exists() {
174            return Err(SessionError::NotFound(format!(
175                "Session not found: {}",
176                session_id
177            )));
178        }
179
180        // Read the session file
181        let json_data = fs::read_to_string(&session_path)?;
182
183        // Write to archive
184        fs::write(&archive_path, json_data)?;
185
186        // Delete the original
187        fs::remove_file(&session_path)?;
188
189        info!("Session deleted and archived: {}", session_id);
190
191        Ok(())
192    }
193
194    /// Check if a session exists
195    pub fn exists(&self, session_id: &str) -> bool {
196        self.session_path(session_id).exists()
197    }
198
199    /// Get the sessions directory path
200    pub fn sessions_dir(&self) -> &Path {
201        &self.sessions_dir
202    }
203
204    /// Get the archive directory path
205    pub fn archive_dir(&self) -> &Path {
206        &self.archive_dir
207    }
208}
209
210impl Default for SessionStore {
211    fn default() -> Self {
212        Self::new().expect("Failed to create default SessionStore")
213    }
214}