Skip to main content

braid_core/fs/
versions.rs

1use crate::core::Version;
2use crate::core::{BraidError, Result};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::PathBuf;
6use tokio::fs;
7
8#[derive(Debug, Serialize, Deserialize, Default)]
9pub struct VersionStore {
10    // Map URL/Path -> Version Info
11    pub file_versions: HashMap<String, FileVersion>,
12    #[serde(skip)]
13    pub path: PathBuf,
14}
15
16#[derive(Debug, Serialize, Deserialize, Clone)]
17pub struct FileVersion {
18    pub current_version: Vec<String>, // Braid versions are DAGs (set of IDs)
19    pub parents: Vec<String>,
20    /// Content hash for this version (SHA-256).
21    #[serde(default)]
22    pub content_hash: Option<String>,
23}
24
25impl VersionStore {
26    pub async fn load() -> Result<Self> {
27        let store_path = get_store_path()?;
28        Self::load_from(store_path).await
29    }
30
31    pub async fn load_from(path: PathBuf) -> Result<Self> {
32        if !path.exists() {
33            return Ok(Self {
34                file_versions: HashMap::new(),
35                path,
36            });
37        }
38
39        let content = fs::read_to_string(&path).await?;
40        let mut store: VersionStore = serde_json::from_str(&content).unwrap_or_default();
41        store.path = path;
42        Ok(store)
43    }
44
45    pub async fn save(&self) -> Result<()> {
46        if let Some(parent) = self.path.parent() {
47            fs::create_dir_all(parent)
48                .await
49                .map_err(|e| BraidError::Io(e))?;
50        }
51        let content = serde_json::to_string_pretty(self).map_err(|e| BraidError::Json(e))?;
52        fs::write(&self.path, content)
53            .await
54            .map_err(|e| BraidError::Io(e))?;
55        Ok(())
56    }
57
58    pub fn update(&mut self, url: &str, version: Vec<Version>, parents: Vec<Version>) {
59        self.file_versions.insert(
60            url.to_string(),
61            FileVersion {
62                current_version: version.iter().map(|v| v.to_string()).collect(),
63                parents: parents.iter().map(|v| v.to_string()).collect(),
64                content_hash: None,
65            },
66        );
67    }
68
69    /// Update version with content hash.
70    pub fn update_with_hash(
71        &mut self,
72        url: &str,
73        version: Vec<Version>,
74        parents: Vec<Version>,
75        hash: Option<String>,
76    ) {
77        self.file_versions.insert(
78            url.to_string(),
79            FileVersion {
80                current_version: version.iter().map(|v| v.to_string()).collect(),
81                parents: parents.iter().map(|v| v.to_string()).collect(),
82                content_hash: hash,
83            },
84        );
85    }
86
87    pub fn get(&self, url: &str) -> Option<&FileVersion> {
88        self.file_versions.get(url)
89    }
90
91    /// Get version by content hash.
92    /// Matches JS `hash_to_version_cache` lookup from braidfs/index.js.
93    pub fn get_version_by_hash(&self, _fullpath: &str, hash: &str) -> Option<Vec<String>> {
94        // Search all versions for matching hash
95        for (_, fv) in &self.file_versions {
96            if fv.content_hash.as_deref() == Some(hash) {
97                return Some(fv.current_version.clone());
98            }
99        }
100        None
101    }
102
103    /// Set content hash for a path.
104    pub fn set_content_hash(&mut self, url: &str, hash: String) {
105        if let Some(fv) = self.file_versions.get_mut(url) {
106            fv.content_hash = Some(hash);
107        }
108    }
109}
110
111fn get_store_path() -> Result<PathBuf> {
112    if let Ok(root) = std::env::var("BRAID_ROOT") {
113        return Ok(PathBuf::from(root).join(".braidfs").join("versions.json"));
114    }
115    let home =
116        dirs::home_dir().ok_or_else(|| BraidError::Fs("Could not find home directory".into()))?;
117    Ok(home.join("http").join(".braidfs").join("versions.json"))
118}