Skip to main content

harness_lock_core/
store.rs

1//! Content-addressed blob store.
2
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use crate::error::{Error, Result};
7use crate::hash::sha256_bytes;
8
9/// Write-once, read-by-hash blob store rooted at a directory.
10///
11/// `put` returns a `sha256:<hex>` reference; `get` resolves it. Blobs are
12/// addressed by their hex digest on disk.
13pub struct Store {
14    root: PathBuf,
15}
16
17impl Store {
18    pub fn new(root: impl Into<PathBuf>) -> Self {
19        Store { root: root.into() }
20    }
21
22    pub fn root(&self) -> &Path {
23        &self.root
24    }
25
26    /// Store bytes; returns `sha256:<hex>`.
27    pub fn put_bytes(&self, data: &[u8]) -> Result<String> {
28        fs::create_dir_all(&self.root)?;
29        let digest = sha256_bytes(data);
30        let target = self.path_for(&digest);
31        if !target.exists() {
32            fs::write(&target, data)?;
33        }
34        Ok(digest)
35    }
36
37    pub fn put_text(&self, text: &str) -> Result<String> {
38        self.put_bytes(text.as_bytes())
39    }
40
41    pub fn put_value(&self, v: &serde_json::Value) -> Result<String> {
42        self.put_text(&crate::hash::canon_json(v))
43    }
44
45    pub fn get_bytes(&self, reference: &str) -> Result<Vec<u8>> {
46        let p = self.path_for(reference);
47        if !p.exists() {
48            return Err(Error::Store(format!("blob not in store: {reference}")));
49        }
50        Ok(fs::read(p)?)
51    }
52
53    pub fn get_text(&self, reference: &str) -> Result<String> {
54        Ok(String::from_utf8_lossy(&self.get_bytes(reference)?).into_owned())
55    }
56
57    pub fn exists(&self, reference: &str) -> bool {
58        self.path_for(reference).exists()
59    }
60
61    pub fn count(&self) -> usize {
62        match fs::read_dir(&self.root) {
63            Ok(rd) => rd.filter_map(|e| e.ok()).filter(|e| e.path().is_file()).count(),
64            Err(_) => 0,
65        }
66    }
67
68    fn path_for(&self, reference: &str) -> PathBuf {
69        let hex = if let Some(idx) = reference.find(':') {
70            &reference[idx + 1..]
71        } else {
72            reference
73        };
74        self.root.join(hex)
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn roundtrip() {
84        let dir = std::env::temp_dir().join(format!("hlock-store-{}", std::process::id()));
85        let s = Store::new(&dir);
86        let ref_ = s.put_text("hello").unwrap();
87        assert!(ref_.starts_with("sha256:"));
88        assert_eq!(s.get_text(&ref_).unwrap(), "hello");
89        assert!(s.exists(&ref_));
90        assert_eq!(s.count(), 1);
91        s.put_text("hello").unwrap();
92        assert_eq!(s.count(), 1);
93        let _ = fs::remove_dir_all(&dir);
94    }
95}