Skip to main content

codex_sync/
config.rs

1use anyhow::{Context, Result, bail};
2use serde::{Deserialize, Serialize};
3#[cfg(unix)]
4use std::os::unix::fs::PermissionsExt;
5use std::{
6    fs,
7    path::{Path, PathBuf},
8};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(default)]
12pub struct Config {
13    pub node_name: String,
14    pub listen: String,
15    pub discovery_port: u16,
16    pub sync_dir: PathBuf,
17    pub shared_key: String,
18    pub exclude: Vec<String>,
19    pub auto_pull_seconds: u64,
20    /// Maximum individual file size accepted from LAN or offline snapshots.
21    pub max_file_size_bytes: u64,
22    /// Maximum number of files accepted in one manifest.
23    pub max_files: usize,
24}
25
26impl Default for Config {
27    fn default() -> Self {
28        let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
29        Self {
30            node_name: hostname::get()
31                .unwrap_or_default()
32                .to_string_lossy()
33                .into_owned(),
34            listen: "0.0.0.0:8787".into(),
35            discovery_port: 8788,
36            sync_dir: home.join(".codex"),
37            shared_key: "请修改为所有电脑相同的随机密钥".into(),
38            exclude: vec![
39                "auth.json".into(),
40                "credentials.json".into(),
41                "logs".into(),
42                "cache".into(),
43                "tmp".into(),
44                "codex-sync-backups".into(),
45                ".DS_Store".into(),
46            ],
47            auto_pull_seconds: 0,
48            max_file_size_bytes: 256 * 1024 * 1024,
49            max_files: 100_000,
50        }
51    }
52}
53
54impl Config {
55    pub fn default_path() -> PathBuf {
56        dirs::config_dir()
57            .unwrap_or_else(|| PathBuf::from("."))
58            .join("codex-sync/config.toml")
59    }
60
61    pub fn init(path: &Path, force: bool) -> Result<()> {
62        if path.exists() && !force {
63            bail!("配置已存在;如需覆盖请添加 --force");
64        }
65        if let Some(parent) = path.parent() {
66            fs::create_dir_all(parent)?;
67        }
68        fs::write(path, toml::to_string_pretty(&Self::default())?)?;
69        #[cfg(unix)]
70        fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
71        Ok(())
72    }
73
74    pub fn load(path: &Path) -> Result<Self> {
75        let text = fs::read_to_string(path)
76            .with_context(|| format!("无法读取 {},请先运行 codex-sync init", path.display()))?;
77        let cfg: Self = toml::from_str(&text).context("配置格式错误")?;
78        if cfg.shared_key.starts_with("请修改") {
79            bail!("请先修改配置中的 shared_key");
80        }
81        if cfg.shared_key.chars().count() < 32 {
82            bail!("shared_key 至少需要 32 个字符,请使用随机长字符串");
83        }
84        if cfg.max_file_size_bytes == 0 || cfg.max_files == 0 {
85            bail!("max_file_size_bytes 和 max_files 必须大于 0");
86        }
87        Ok(cfg)
88    }
89
90    pub fn node_id(&self) -> String {
91        blake3::hash(format!("{}:{}", self.node_name, self.shared_key).as_bytes()).to_hex()[..12]
92            .to_string()
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn rejects_short_shared_key() {
102        let root =
103            std::env::temp_dir().join(format!("codex-sync-config-test-{}", std::process::id()));
104        let _ = fs::remove_file(&root);
105        fs::write(&root, "shared_key = 'short'\n").unwrap();
106        assert!(Config::load(&root).is_err());
107        let _ = fs::remove_file(root);
108    }
109}