Skip to main content

rho_core/
storage.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use crate::{RhoResult, ensure_parent, file_digest, validate_relative_safe_path};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct StorageEntry {
8    pub path: String,
9    pub digest: String,
10    pub bytes: u64,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct StateDigest(pub String);
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ProposalId(pub String);
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct SignedChangeSet {
21    pub id: String,
22    pub bytes: Vec<u8>,
23}
24
25pub trait StorageBackend {
26    fn get(&self, path_or_hash: &str) -> RhoResult<Vec<u8>>;
27    fn put(
28        &self,
29        path: &str,
30        bytes: &[u8],
31        expected_previous_digest: Option<&str>,
32    ) -> RhoResult<String>;
33    fn list(&self, prefix: &str) -> RhoResult<Vec<StorageEntry>>;
34    fn head(&self, space_id: &str) -> RhoResult<StateDigest>;
35    fn propose(&self, change_set: &SignedChangeSet) -> RhoResult<ProposalId>;
36    fn fetch_proposal(&self, id: &ProposalId) -> RhoResult<SignedChangeSet>;
37}
38
39#[derive(Debug, Clone)]
40pub struct LocalFsStorage {
41    root: PathBuf,
42}
43
44impl LocalFsStorage {
45    pub fn new(root: impl Into<PathBuf>) -> Self {
46        Self { root: root.into() }
47    }
48
49    fn path(&self, relative: &str) -> RhoResult<PathBuf> {
50        validate_relative_safe_path(relative)?;
51        Ok(self.root.join(relative))
52    }
53}
54
55impl StorageBackend for LocalFsStorage {
56    fn get(&self, path_or_hash: &str) -> RhoResult<Vec<u8>> {
57        Ok(fs::read(self.path(path_or_hash)?)?)
58    }
59
60    fn put(
61        &self,
62        path: &str,
63        bytes: &[u8],
64        expected_previous_digest: Option<&str>,
65    ) -> RhoResult<String> {
66        let target = self.path(path)?;
67        if let Some(expected) = expected_previous_digest {
68            let actual = if target.is_file() {
69                file_digest(&target)?
70            } else {
71                String::new()
72            };
73            if actual != expected {
74                return Err(format!(
75                    "previous digest mismatch for {path}: expected {expected}, got {actual}"
76                )
77                .into());
78            }
79        }
80        ensure_parent(&target)?;
81        fs::write(&target, bytes)?;
82        file_digest(&target)
83    }
84
85    fn list(&self, prefix: &str) -> RhoResult<Vec<StorageEntry>> {
86        let base = self.path(prefix)?;
87        let mut entries = Vec::new();
88        if !base.exists() {
89            return Ok(entries);
90        }
91        collect_entries(&self.root, &base, &mut entries)?;
92        entries.sort_by(|a, b| a.path.cmp(&b.path));
93        Ok(entries)
94    }
95
96    fn head(&self, space_id: &str) -> RhoResult<StateDigest> {
97        let head_path = format!("rho/heads/{space_id}.txt");
98        let text = fs::read_to_string(self.path(&head_path)?)?;
99        Ok(StateDigest(text.trim().to_string()))
100    }
101
102    fn propose(&self, change_set: &SignedChangeSet) -> RhoResult<ProposalId> {
103        validate_relative_safe_path(&change_set.id)?;
104        let path = format!("rho/proposals/{}.yaml", change_set.id);
105        self.put(&path, &change_set.bytes, None)?;
106        Ok(ProposalId(change_set.id.clone()))
107    }
108
109    fn fetch_proposal(&self, id: &ProposalId) -> RhoResult<SignedChangeSet> {
110        validate_relative_safe_path(&id.0)?;
111        let path = format!("rho/proposals/{}.yaml", id.0);
112        Ok(SignedChangeSet {
113            id: id.0.clone(),
114            bytes: self.get(&path)?,
115        })
116    }
117}
118
119fn collect_entries(root: &Path, dir: &Path, entries: &mut Vec<StorageEntry>) -> RhoResult<()> {
120    if dir.is_file() {
121        let relative = dir
122            .strip_prefix(root)?
123            .to_string_lossy()
124            .replace(std::path::MAIN_SEPARATOR, "/");
125        entries.push(StorageEntry {
126            path: relative,
127            digest: file_digest(dir)?,
128            bytes: fs::metadata(dir)?.len(),
129        });
130        return Ok(());
131    }
132    for entry in fs::read_dir(dir)? {
133        collect_entries(root, &entry?.path(), entries)?;
134    }
135    Ok(())
136}