Skip to main content

braid_core/fs/
config.rs

1use crate::core::{BraidError, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::PathBuf;
5use tokio::fs;
6
7#[derive(Debug, Serialize, Deserialize, Clone)]
8pub struct Config {
9    #[serde(default)]
10    pub peer_id: String,
11    #[serde(default)]
12    pub sync: HashMap<String, bool>,
13    #[serde(default)]
14    pub cookies: HashMap<String, String>,
15    #[serde(default)]
16    pub identities: HashMap<String, String>,
17    #[serde(default = "default_port")]
18    pub port: u16,
19    /// Patterns to ignore (from .braidignore)
20    #[serde(default)]
21    pub ignore_patterns: Vec<String>,
22    /// Debounce delay in milliseconds for file changes
23    #[serde(default = "default_debounce_ms")]
24    pub debounce_ms: u64,
25}
26
27fn default_debounce_ms() -> u64 {
28    100
29}
30
31fn default_port() -> u16 {
32    45678
33}
34
35impl Config {
36    pub async fn load() -> Result<Self> {
37        let config_path = get_config_path()?;
38
39        if !config_path.exists() {
40            return Ok(Config::default());
41        }
42
43        let content = fs::read_to_string(&config_path)
44            .await
45            .map_err(|e| BraidError::Io(e))?;
46
47        let mut config: Config = serde_json::from_str(&content).map_err(|e| BraidError::Json(e))?;
48
49        if config.peer_id.is_empty() {
50            config.peer_id = format!("braidfs_{}", &uuid::Uuid::new_v4().to_string()[..8]);
51            config.save().await?;
52        }
53
54        Ok(config)
55    }
56
57    pub async fn save(&self) -> Result<()> {
58        let config_path = get_config_path()?;
59
60        if let Some(parent) = config_path.parent() {
61            fs::create_dir_all(parent)
62                .await
63                .map_err(|e| BraidError::Io(e))?;
64        }
65
66        let content = serde_json::to_string_pretty(self).map_err(|e| BraidError::Json(e))?;
67        let _ = fs::write(&config_path, content)
68            .await
69            .map_err(|e| BraidError::Io(e))?;
70
71        Ok(())
72    }
73}
74
75impl Default for Config {
76    fn default() -> Self {
77        Self {
78            peer_id: format!("braidfs_{}", &uuid::Uuid::new_v4().to_string()[..8]),
79            sync: HashMap::new(),
80            cookies: HashMap::new(),
81            identities: HashMap::new(),
82            port: default_port(),
83            ignore_patterns: default_ignore_patterns(),
84            debounce_ms: default_debounce_ms(),
85        }
86    }
87}
88
89/// Default patterns to ignore (.git, node_modules, etc.)
90fn default_ignore_patterns() -> Vec<String> {
91    vec![
92        ".git".to_string(),
93        ".git/**".to_string(),
94        "node_modules/**".to_string(),
95        ".DS_Store".to_string(),
96        "*.swp".to_string(),
97        "*.swo".to_string(),
98        "*~".to_string(),
99        ".braidfs/**".to_string(),
100    ]
101}
102
103pub fn get_config_path() -> Result<PathBuf> {
104    let root = get_root_dir()?;
105    Ok(root.join(".braidfs").join("config"))
106}
107
108pub fn get_root_dir() -> Result<PathBuf> {
109    let root_str = std::env::var("BRAID_ROOT").unwrap_or_else(|_| {
110        let home = dirs::home_dir().expect("Could not find home directory");
111        home.join("http").to_string_lossy().to_string()
112    });
113
114    let root = PathBuf::from(root_str);
115    if let Ok(abs) = std::fs::canonicalize(&root) {
116        Ok(abs)
117    } else {
118        Ok(std::env::current_dir()
119            .map_err(|e| BraidError::Io(e))?
120            .join(root))
121    }
122}
123
124/// Get the trash directory for deleted files.
125pub fn get_trash_dir() -> Result<PathBuf> {
126    let root = get_root_dir()?;
127    Ok(root.join(".braidfs").join("trash"))
128}
129
130/// Check if a file is binary based on its extension.
131pub fn is_binary(filename: &str) -> bool {
132    let binary_extensions = [
133        ".jpg", ".jpeg", ".png", ".gif", ".mp4", ".mp3", ".zip", ".tar", ".rar", ".pdf", ".doc",
134        ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".exe", ".dll", ".so", ".dylib", ".bin", ".iso",
135        ".img", ".bmp", ".tiff", ".svg", ".webp", ".avi", ".mov", ".wmv", ".flv", ".mkv", ".wav",
136        ".flac", ".aac", ".ogg", ".wma", ".7z", ".gz", ".bz2", ".xz",
137    ];
138
139    let filename_lower = filename.to_lowercase();
140    binary_extensions
141        .iter()
142        .any(|ext| filename_lower.ends_with(ext))
143}
144
145/// Check if a path should be skipped during sync.
146pub fn skip_file(path: &str) -> bool {
147    if path.contains('#') {
148        return true;
149    }
150    if path.ends_with(".DS_Store") {
151        return true;
152    }
153    if path.starts_with(".braidfs")
154        && !path.starts_with(".braidfs/config")
155        && !path.starts_with(".braidfs/errors")
156    {
157        return true;
158    }
159    false
160}
161
162/// Move a file to the trash directory instead of deleting it.
163pub async fn trash_file(fullpath: &std::path::Path, path: &str) -> Result<PathBuf> {
164    let trash_dir = get_trash_dir()?;
165    tokio::fs::create_dir_all(&trash_dir).await?;
166
167    let random = uuid::Uuid::new_v4().to_string()[..8].to_string();
168    let filename = path.replace(['/', '\\'], "_");
169    let dest = trash_dir.join(format!("{}_{}", filename, random));
170
171    tokio::fs::rename(fullpath, &dest).await?;
172    tracing::warn!("Moved unsynced file to trash: {:?} -> {:?}", fullpath, dest);
173
174    Ok(dest)
175}