use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::{
fs,
path::{Path, PathBuf},
};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
pub node_name: String,
pub listen: String,
pub discovery_port: u16,
pub sync_dir: PathBuf,
pub shared_key: String,
pub exclude: Vec<String>,
pub auto_pull_seconds: u64,
pub max_file_size_bytes: u64,
pub max_files: usize,
}
impl Default for Config {
fn default() -> Self {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
Self {
node_name: hostname::get()
.unwrap_or_default()
.to_string_lossy()
.into_owned(),
listen: "0.0.0.0:8787".into(),
discovery_port: 8788,
sync_dir: home.join(".codex"),
shared_key: "请修改为所有电脑相同的随机密钥".into(),
exclude: vec![
"auth.json".into(),
"credentials.json".into(),
"logs".into(),
"cache".into(),
"tmp".into(),
"codex-sync-backups".into(),
".DS_Store".into(),
],
auto_pull_seconds: 0,
max_file_size_bytes: 256 * 1024 * 1024,
max_files: 100_000,
}
}
}
impl Config {
pub fn default_path() -> PathBuf {
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("codex-sync/config.toml")
}
pub fn init(path: &Path, force: bool) -> Result<()> {
if path.exists() && !force {
bail!("配置已存在;如需覆盖请添加 --force");
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, toml::to_string_pretty(&Self::default())?)?;
#[cfg(unix)]
fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
Ok(())
}
pub fn load(path: &Path) -> Result<Self> {
let text = fs::read_to_string(path)
.with_context(|| format!("无法读取 {},请先运行 codex-sync init", path.display()))?;
let cfg: Self = toml::from_str(&text).context("配置格式错误")?;
if cfg.shared_key.starts_with("请修改") {
bail!("请先修改配置中的 shared_key");
}
if cfg.shared_key.chars().count() < 32 {
bail!("shared_key 至少需要 32 个字符,请使用随机长字符串");
}
if cfg.max_file_size_bytes == 0 || cfg.max_files == 0 {
bail!("max_file_size_bytes 和 max_files 必须大于 0");
}
Ok(cfg)
}
pub fn node_id(&self) -> String {
blake3::hash(format!("{}:{}", self.node_name, self.shared_key).as_bytes()).to_hex()[..12]
.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_short_shared_key() {
let root =
std::env::temp_dir().join(format!("codex-sync-config-test-{}", std::process::id()));
let _ = fs::remove_file(&root);
fs::write(&root, "shared_key = 'short'\n").unwrap();
assert!(Config::load(&root).is_err());
let _ = fs::remove_file(root);
}
}