//! Filesystem-backed `SessionRepository` implementation. //! //! Each session is stored as `/sessions//session.json`. //! Writes use a write-then-rename + fsync pattern for crash safety. //! //! # Flow //! //! - **`list_sessions`** — enumerate `/sessions/` subdirectories, //! attempt `load_session` on each (silently skipping failures). //! - **`load_session`** — reads and deserialises `session.json`. //! - **`save_session`** — creates session directory, writes JSON atomically. //! - **`delete_session`** — removes the session directory. //! //! # Security //! //! Session IDs are validated at construction via [`SessionId::new`], so //! directory-traversal attacks are prevented by the type system — no //! per-method checks needed. //! //! # Components //! //! - `FileSystemSessionRepository` — stateless singleton implementing `SessionRepository` use std::path::Path; use tracing; use zesdex_entities::domain::auth::SessionId; use zesdex_utils::write_json_atomic; use crate::domain::error::RepositoryError; use crate::domain::repository::SessionRepository; use crate::domain::session::Session; /// Concrete filesystem session repository. #[derive(Debug, Clone, Default)] pub struct FileSystemSessionRepository; impl FileSystemSessionRepository { /// Create a new filesystem session repository. pub fn new() -> Self { FileSystemSessionRepository } } impl SessionRepository for FileSystemSessionRepository { fn list_sessions(&self, base_dir: &Path) -> Result, RepositoryError> { let sessions_dir = base_dir.join("sessions"); let entries = match std::fs::read_dir(&sessions_dir) { Ok(e) => e, Err(e) if e.kind() == std::io::ErrorKind::NotFound => { tracing::warn!(path = %sessions_dir.display(), "sessions directory not found"); return Ok(Vec::new()); } Err(e) => return Err(RepositoryError::Io(e)), }; let mut sessions = Vec::new(); for entry in entries.flatten() { if !entry.path().is_dir() { continue; } let name = entry.file_name().to_string_lossy().to_string(); // Directory names from UUIDs are always valid session IDs. if let Ok(sid) = SessionId::new(&name) { if let Ok(session) = self.load_session(base_dir, &sid) { sessions.push(session); } } } tracing::debug!(count = sessions.len(), "listed sessions"); Ok(sessions) } fn load_session(&self, base_dir: &Path, id: &SessionId) -> Result { let path = base_dir.join("sessions").join(id.as_str()).join("session.json"); if !path.exists() { return Err(RepositoryError::NotFound(format!( "session not found: {}", id.as_str() ))); } tracing::debug!(session_id = %id, path = %path.display(), "loading session"); let data = std::fs::read_to_string(&path)?; // → RepositoryError let session: Session = serde_json::from_str(&data)?; // → RepositoryError Ok(session) } fn save_session(&self, base_dir: &Path, session: &Session) -> Result<(), RepositoryError> { let dir = session.session_dir(base_dir); std::fs::create_dir_all(&dir)?; // → RepositoryError let path = dir.join("session.json"); tracing::debug!(session_id = %session.id, path = %path.display(), "saving session"); write_json_atomic(&path, session, None)?; Ok(()) } fn delete_session(&self, base_dir: &Path, id: &SessionId) -> Result<(), RepositoryError> { let dir = base_dir.join("sessions").join(id.as_str()); tracing::debug!(session_id = %id, path = %dir.display(), "deleting session"); if dir.exists() { std::fs::remove_dir_all(&dir)?; // → RepositoryError } Ok(()) } }