Move the crash-safe write-then-rename pattern into zesdex-utils::write_json_atomic and apply it across: - zesdex-cms: app_config_repo, conversation_repo, settings_repo - zesdex-iam: oauth_repo, session_repo - zesdex-entities: Conversation::save_conversation, Session::save Excluded (non-JSON format): - rewind_blob_repo (binary blob) - memory_repo (markdown + frontmatter, not JSON) - session_lock (PID string, not JSON) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
81 lines
2.7 KiB
Rust
81 lines
2.7 KiB
Rust
#![allow(
|
|
clippy::cast_possible_truncation,
|
|
clippy::cast_sign_loss,
|
|
clippy::cast_precision_loss,
|
|
clippy::cast_possible_wrap
|
|
)]
|
|
//! Filesystem-backed `SessionRepository` implementation.
|
|
//!
|
|
//! Each session is stored as `<base_dir>/sessions/<id>/session.json`.
|
|
//! Writes use a write-then-rename + fsync pattern for crash safety.
|
|
use std::path::Path;
|
|
|
|
use zesdex_utils::write_json_atomic;
|
|
|
|
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) -> anyhow::Result<Vec<Session>> {
|
|
let sessions_dir = base_dir.join("sessions");
|
|
let Ok(entries) = std::fs::read_dir(&sessions_dir) else {
|
|
return Ok(Vec::new());
|
|
};
|
|
let mut sessions = Vec::new();
|
|
for entry in entries.flatten() {
|
|
if !entry.path().is_dir() {
|
|
continue;
|
|
}
|
|
let id = entry.file_name().to_string_lossy().to_string();
|
|
if let Ok(session) = self.load_session(base_dir, &id) {
|
|
sessions.push(session);
|
|
}
|
|
}
|
|
Ok(sessions)
|
|
}
|
|
|
|
fn load_session(&self, base_dir: &Path, id: &str) -> anyhow::Result<Session> {
|
|
// Directory-traversal prevention.
|
|
if id.contains('/') || id.contains('\\') || id.contains("..") {
|
|
anyhow::bail!("invalid session id '{id}': must not contain path separators");
|
|
}
|
|
let path = base_dir.join("sessions").join(id).join("session.json");
|
|
if !path.exists() {
|
|
anyhow::bail!("session not found: {id}");
|
|
}
|
|
let data = std::fs::read_to_string(&path)?;
|
|
let session: Session = serde_json::from_str(&data)?;
|
|
Ok(session)
|
|
}
|
|
|
|
fn save_session(&self, base_dir: &Path, session: &Session) -> anyhow::Result<()> {
|
|
let dir = session.session_dir(base_dir);
|
|
std::fs::create_dir_all(&dir)?;
|
|
let path = dir.join("session.json");
|
|
write_json_atomic(&path, session, None)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn delete_session(&self, base_dir: &Path, id: &str) -> anyhow::Result<()> {
|
|
if id.contains('/') || id.contains('\\') || id.contains("..") {
|
|
anyhow::bail!("invalid session id '{id}': must not contain path separators");
|
|
}
|
|
let dir = base_dir.join("sessions").join(id);
|
|
if dir.exists() {
|
|
std::fs::remove_dir_all(&dir)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|