Skip to main content

undo_core/
store.rs

1//! A tiny content-addressed blob store, git-style. We capture the *prior*
2//! contents of any file an agent touches, keyed by their SHA-256, so that even
3//! large or binary files restore byte-perfect. Identical contents are stored
4//! once.
5
6use sha2::{Digest, Sha256};
7use std::fs;
8use std::io::{self, Write};
9use std::path::{Path, PathBuf};
10
11pub struct Store {
12    root: PathBuf,
13}
14
15impl Store {
16    pub fn new(root: PathBuf) -> Self {
17        Self { root }
18    }
19
20    pub fn ensure(&self) -> io::Result<()> {
21        fs::create_dir_all(&self.root)
22    }
23
24    fn hash_hex(data: &[u8]) -> String {
25        let digest = Sha256::digest(data);
26        let mut s = String::with_capacity(64);
27        for b in digest {
28            s.push_str(&format!("{b:02x}"));
29        }
30        s
31    }
32
33    /// Store raw bytes, returning their content hash. Writing is race-free: a
34    /// concurrent writer of the same content simply loses the `create_new` and
35    /// we treat that as success (the bytes are identical by construction).
36    pub fn put_bytes(&self, data: &[u8]) -> io::Result<String> {
37        self.ensure()?;
38        let hash = Self::hash_hex(data);
39        let path = self.root.join(&hash);
40        match fs::OpenOptions::new()
41            .write(true)
42            .create_new(true)
43            .open(&path)
44        {
45            Ok(mut f) => {
46                f.write_all(data)?;
47                f.sync_all()?;
48            }
49            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
50            Err(e) => return Err(e),
51        }
52        Ok(hash)
53    }
54
55    /// Capture the current contents of a file into the store.
56    pub fn put_file(&self, path: &Path) -> io::Result<String> {
57        let data = fs::read(path)?;
58        self.put_bytes(&data)
59    }
60
61    /// Fetch previously stored bytes by hash.
62    pub fn get(&self, hash: &str) -> io::Result<Vec<u8>> {
63        fs::read(self.root.join(hash))
64    }
65}