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 pub max_file_size_bytes: u64,
22 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}