Skip to main content

hard_sync_core/
config.rs

1use std::path::PathBuf;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6// ── Types ─────────────────────────────────────────────────────────────────────
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9#[serde(rename_all = "snake_case")]
10pub enum SourceSide {
11    Base,
12    Target,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16#[serde(rename_all = "snake_case")]
17pub enum DeleteBehavior {
18    Trash,
19    Delete,
20    Ignore,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct DriveId {
25    pub label: Option<String>,
26    pub uuid: Option<String>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct SoundConfig {
31    pub sync_start: Option<PathBuf>,
32    pub sync_done: Option<PathBuf>,
33    pub sync_error: Option<PathBuf>,
34}
35
36impl Default for SoundConfig {
37    fn default() -> Self {
38        Self {
39            sync_start: None,
40            sync_done: None,
41            sync_error: None,
42        }
43    }
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct PairConfig {
48    pub name: String,
49    pub base: PathBuf,
50    pub target: PathBuf,
51    pub source: SourceSide,
52    pub drive_id: Option<DriveId>,
53    pub ignore: Vec<String>,
54    pub delete_behavior: DeleteBehavior,
55    pub sounds: SoundConfig,
56    pub created_at: DateTime<Utc>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct AppConfig {
61    pub version: u32,
62    pub pairs: Vec<PairConfig>,
63}
64
65impl Default for AppConfig {
66    fn default() -> Self {
67        Self {
68            version: 1,
69            pairs: Vec::new(),
70        }
71    }
72}
73
74// ── Config file path ───────────────────────────────────────────────────────────
75
76pub fn get_config_path() -> Result<PathBuf, String> {
77    config_path()
78}
79
80pub fn reset_config() -> Result<(), String> {
81    let path = config_path()?;
82    if path.exists() {
83        std::fs::remove_file(&path)
84            .map_err(|e| format!("Failed to delete config: {}", e))?;
85    }
86    Ok(())
87}
88
89fn config_path() -> Result<PathBuf, String> {
90    let dir = dirs::config_dir()
91        .ok_or_else(|| "Could not determine config directory".to_string())?
92        .join("hard-sync");
93    Ok(dir.join("config.json"))
94}
95
96// ── Read / Write ───────────────────────────────────────────────────────────────
97
98pub fn load_config() -> Result<AppConfig, String> {
99    let path = config_path()?;
100    if !path.exists() {
101        return Ok(AppConfig::default());
102    }
103    let raw = std::fs::read_to_string(&path)
104        .map_err(|e| format!("Failed to read config: {}", e))?;
105    serde_json::from_str(&raw)
106        .map_err(|e| format!("Failed to parse config: {}", e))
107}
108
109pub fn save_config(config: &AppConfig) -> Result<(), String> {
110    let path = config_path()?;
111    if let Some(parent) = path.parent() {
112        std::fs::create_dir_all(parent)
113            .map_err(|e| format!("Failed to create config directory: {}", e))?;
114    }
115    let raw = serde_json::to_string_pretty(config)
116        .map_err(|e| format!("Failed to serialize config: {}", e))?;
117    std::fs::write(&path, raw)
118        .map_err(|e| format!("Failed to write config: {}", e))
119}
120
121// ── CRUD ───────────────────────────────────────────────────────────────────────
122
123pub fn add_pair(pair: PairConfig) -> Result<(), String> {
124    let mut config = load_config()?;
125    if config.pairs.iter().any(|p| p.name == pair.name) {
126        return Err(format!("A pair named \"{}\" already exists", pair.name));
127    }
128    config.pairs.push(pair);
129    save_config(&config)
130}
131
132pub fn get_pair(name: &str) -> Result<PairConfig, String> {
133    let config = load_config()?;
134    config
135        .pairs
136        .into_iter()
137        .find(|p| p.name == name)
138        .ok_or_else(|| format!("No pair named \"{}\"", name))
139}
140
141pub fn list_pairs() -> Result<Vec<PairConfig>, String> {
142    let config = load_config()?;
143    Ok(config.pairs)
144}
145
146pub fn remove_pair(name: &str) -> Result<(), String> {
147    let mut config = load_config()?;
148    let before = config.pairs.len();
149    config.pairs.retain(|p| p.name != name);
150    if config.pairs.len() == before {
151        return Err(format!("No pair named \"{}\"", name));
152    }
153    save_config(&config)
154}
155
156pub fn set_source(name: &str, source: SourceSide) -> Result<(), String> {
157    let mut config = load_config()?;
158    let pair = config
159        .pairs
160        .iter_mut()
161        .find(|p| p.name == name)
162        .ok_or_else(|| format!("No pair named \"{}\"", name))?;
163    pair.source = source;
164    save_config(&config)
165}
166
167pub fn update_pair(updated: PairConfig) -> Result<(), String> {
168    let mut config = load_config()?;
169    let pair = config
170        .pairs
171        .iter_mut()
172        .find(|p| p.name == updated.name)
173        .ok_or_else(|| format!("No pair named \"{}\"", updated.name))?;
174    *pair = updated;
175    save_config(&config)
176}