CAN Service: content-addressable storage with HTTP API, SQLite metadata, file-based blob storage, thumbnail generation, and integrity verification. can-sync v1: P2P sync sidecar using iroh-docs for encrypted peer-to-peer replication with library/filter-based selective sync. Fully builds but being superseded by v2 (simplified full-mirror approach). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
113 lines
3.2 KiB
Rust
113 lines
3.2 KiB
Rust
use serde::Deserialize;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct Config {
|
|
pub storage_root: PathBuf,
|
|
#[serde(default = "default_admin_token")]
|
|
pub admin_token: String,
|
|
#[serde(default = "default_true")]
|
|
pub enable_thumbnail_cache: bool,
|
|
#[serde(default = "default_rebuild_threshold")]
|
|
pub rebuild_error_threshold: u32,
|
|
#[serde(default = "default_verify_interval")]
|
|
pub verify_interval_hours: u64,
|
|
}
|
|
|
|
fn default_admin_token() -> String {
|
|
"changeme".to_string()
|
|
}
|
|
fn default_true() -> bool {
|
|
true
|
|
}
|
|
fn default_rebuild_threshold() -> u32 {
|
|
50
|
|
}
|
|
fn default_verify_interval() -> u64 {
|
|
12
|
|
}
|
|
|
|
impl Config {
|
|
pub fn load(path: &Path) -> anyhow::Result<Self> {
|
|
let contents = std::fs::read_to_string(path)?;
|
|
let config: Config = serde_yaml::from_str(&contents)?;
|
|
Ok(config)
|
|
}
|
|
|
|
pub fn db_path(&self) -> PathBuf {
|
|
self.storage_root.join(".can.db")
|
|
}
|
|
|
|
pub fn trash_dir(&self) -> PathBuf {
|
|
self.storage_root.join(".trash")
|
|
}
|
|
|
|
pub fn thumbs_dir(&self) -> PathBuf {
|
|
self.storage_root.join(".thumbs")
|
|
}
|
|
|
|
pub fn ensure_dirs(&self) -> anyhow::Result<()> {
|
|
std::fs::create_dir_all(&self.storage_root)?;
|
|
std::fs::create_dir_all(self.trash_dir())?;
|
|
if self.enable_thumbnail_cache {
|
|
std::fs::create_dir_all(self.thumbs_dir())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use tempfile::TempDir;
|
|
|
|
#[test]
|
|
fn test_load_config() {
|
|
let dir = TempDir::new().unwrap();
|
|
let config_path = dir.path().join("config.yaml");
|
|
std::fs::write(
|
|
&config_path,
|
|
r#"
|
|
storage_root: "/tmp/can_test"
|
|
admin_token: "test_token"
|
|
enable_thumbnail_cache: false
|
|
rebuild_error_threshold: 10
|
|
verify_interval_hours: 6
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let config = Config::load(&config_path).unwrap();
|
|
assert_eq!(config.storage_root, PathBuf::from("/tmp/can_test"));
|
|
assert_eq!(config.admin_token, "test_token");
|
|
assert!(!config.enable_thumbnail_cache);
|
|
assert_eq!(config.rebuild_error_threshold, 10);
|
|
assert_eq!(config.verify_interval_hours, 6);
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_defaults() {
|
|
let dir = TempDir::new().unwrap();
|
|
let config_path = dir.path().join("config.yaml");
|
|
std::fs::write(&config_path, "storage_root: /tmp/test\n").unwrap();
|
|
|
|
let config = Config::load(&config_path).unwrap();
|
|
assert_eq!(config.admin_token, "changeme");
|
|
assert!(config.enable_thumbnail_cache);
|
|
assert_eq!(config.rebuild_error_threshold, 50);
|
|
assert_eq!(config.verify_interval_hours, 12);
|
|
}
|
|
|
|
#[test]
|
|
fn test_derived_paths() {
|
|
let dir = TempDir::new().unwrap();
|
|
let config_path = dir.path().join("config.yaml");
|
|
std::fs::write(&config_path, "storage_root: /data/can\n").unwrap();
|
|
let config = Config::load(&config_path).unwrap();
|
|
|
|
assert_eq!(config.db_path(), PathBuf::from("/data/can/.can.db"));
|
|
assert_eq!(config.trash_dir(), PathBuf::from("/data/can/.trash"));
|
|
assert_eq!(config.thumbs_dir(), PathBuf::from("/data/can/.thumbs"));
|
|
}
|
|
}
|