gestura_core/checkpoints/
store.rs

1//! File-backed checkpoint store.
2
3use std::{fs, path::PathBuf};
4
5use super::types::{Checkpoint, CheckpointError, CheckpointId, CheckpointMetadata};
6
7/// Default directory for persisted checkpoints.
8///
9/// This is intentionally outside sandbox workspaces.
10pub fn default_checkpoints_dir() -> PathBuf {
11    crate::config::AppConfig::data_dir().join("checkpoints")
12}
13
14/// File-backed store (one JSON file per checkpoint).
15#[derive(Debug, Clone)]
16pub struct FileCheckpointStore {
17    dir: PathBuf,
18}
19
20impl FileCheckpointStore {
21    /// Create a store rooted at a custom directory.
22    pub fn new(dir: PathBuf) -> Self {
23        Self { dir }
24    }
25
26    /// Create a store using the default directory.
27    pub fn new_default() -> Self {
28        Self::new(default_checkpoints_dir())
29    }
30
31    /// Ensure the backing directory exists.
32    fn ensure_dir(&self) -> Result<(), CheckpointError> {
33        fs::create_dir_all(&self.dir)?;
34        Ok(())
35    }
36
37    /// Compute the on-disk path for a checkpoint JSON file.
38    fn path_for(&self, id: &CheckpointId) -> PathBuf {
39        self.dir.join(format!("{id}.json"))
40    }
41
42    /// Persist a checkpoint to disk.
43    pub fn save(&self, checkpoint: &Checkpoint) -> Result<(), CheckpointError> {
44        self.ensure_dir()?;
45        let path = self.path_for(&checkpoint.metadata.id);
46        let bytes = serde_json::to_vec_pretty(checkpoint)?;
47        fs::write(path, bytes)?;
48        Ok(())
49    }
50
51    /// Load a checkpoint from disk.
52    pub fn load(&self, id: &CheckpointId) -> Result<Checkpoint, CheckpointError> {
53        self.ensure_dir()?;
54        let path = self.path_for(id);
55        let bytes = fs::read(&path).map_err(|e| {
56            if e.kind() == std::io::ErrorKind::NotFound {
57                CheckpointError::NotFound(*id)
58            } else {
59                CheckpointError::Io(e)
60            }
61        })?;
62        Ok(serde_json::from_slice(&bytes)?)
63    }
64
65    /// Delete a checkpoint.
66    pub fn delete(&self, id: &CheckpointId) -> Result<(), CheckpointError> {
67        self.ensure_dir()?;
68        let path = self.path_for(id);
69        fs::remove_file(&path).map_err(|e| {
70            if e.kind() == std::io::ErrorKind::NotFound {
71                CheckpointError::NotFound(*id)
72            } else {
73                CheckpointError::Io(e)
74            }
75        })?;
76        Ok(())
77    }
78
79    /// List metadata for all checkpoints in the store.
80    pub fn list_metadata(&self) -> Result<Vec<CheckpointMetadata>, CheckpointError> {
81        self.ensure_dir()?;
82        let mut out = Vec::new();
83        for entry in fs::read_dir(&self.dir)? {
84            let entry = entry?;
85            let path = entry.path();
86            if path.extension().and_then(|e| e.to_str()) != Some("json") {
87                continue;
88            }
89            let bytes = fs::read(&path)?;
90            let ckpt: Checkpoint = serde_json::from_slice(&bytes)?;
91            out.push(ckpt.metadata);
92        }
93        Ok(out)
94    }
95}