1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use serde::{Deserialize, Serialize};
5
6use crate::error::{Result, SyncorError};
7use crate::link::{LinkId, LinkInfo};
8
9#[derive(Debug, Clone)]
19pub struct SyncorPaths {
20 config_dir: PathBuf,
21 data_dir: PathBuf,
22}
23
24impl SyncorPaths {
25 pub fn new() -> Self {
27 let home = dirs::home_dir().expect("unable to resolve home directory");
28 Self::with_home(&home)
29 }
30
31 pub fn with_home(home: &Path) -> Self {
33 let config_dir = home.join(".config").join("syncor");
34 let data_dir = home.join(".local").join("share").join("syncor");
35 Self {
36 config_dir,
37 data_dir,
38 }
39 }
40
41 pub fn config_dir(&self) -> &Path {
44 &self.config_dir
45 }
46
47 pub fn config_file(&self) -> PathBuf {
48 self.config_dir.join("config.toml")
49 }
50
51 pub fn links_file(&self) -> PathBuf {
52 self.config_dir.join("links.toml")
53 }
54
55 pub fn data_dir(&self) -> &Path {
58 &self.data_dir
59 }
60
61 pub fn socket_path(&self) -> PathBuf {
63 self.data_dir.join("syncor.sock")
64 }
65
66 pub fn pid_file(&self) -> PathBuf {
68 self.data_dir.join("syncor.pid")
69 }
70
71 pub fn log_file(&self) -> PathBuf {
73 self.data_dir.join("syncor.log")
74 }
75
76 pub fn link_dir(&self) -> PathBuf {
78 self.data_dir.join("links")
79 }
80
81 pub fn link_repo_dir(&self, link_id: &LinkId) -> PathBuf {
83 self.link_dir().join(link_id.as_str())
84 }
85
86 pub fn link_state_db(&self) -> PathBuf {
88 self.data_dir.join("state.db")
89 }
90
91 pub fn link_lock_file(&self, link_id: &LinkId) -> PathBuf {
93 self.link_dir().join(format!("{}.lock", link_id.as_str()))
94 }
95
96 pub fn ensure_dirs(&self) -> Result<()> {
98 std::fs::create_dir_all(&self.config_dir)?;
99 std::fs::create_dir_all(&self.data_dir)?;
100 std::fs::create_dir_all(self.link_dir())?;
101 Ok(())
102 }
103}
104
105impl Default for SyncorPaths {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct SyncorConfig {
118 pub debounce_secs: u64,
120 pub default_poll_interval_secs: u64,
122}
123
124impl Default for SyncorConfig {
125 fn default() -> Self {
126 Self {
127 debounce_secs: 2,
128 default_poll_interval_secs: 60,
129 }
130 }
131}
132
133impl SyncorConfig {
134 pub fn load(path: &Path) -> Result<Self> {
137 if !path.exists() {
138 return Ok(Self::default());
139 }
140 let raw = std::fs::read_to_string(path)?;
141 let cfg: Self = toml::from_str(&raw).map_err(|e| SyncorError::Config(e.to_string()))?;
142 Ok(cfg)
143 }
144
145 pub fn save(&self, path: &Path) -> Result<()> {
147 if let Some(parent) = path.parent() {
148 std::fs::create_dir_all(parent)?;
149 }
150 let raw = toml::to_string_pretty(self).map_err(|e| SyncorError::Config(e.to_string()))?;
151 std::fs::write(path, raw)?;
152 Ok(())
153 }
154}
155
156#[derive(Debug, Default, Serialize, Deserialize)]
162struct LinksFile {
163 #[serde(default)]
164 links: Vec<LinkInfo>,
165}
166
167#[derive(Debug, Default)]
169pub struct LinksRegistry {
170 by_id: HashMap<String, LinkInfo>,
172}
173
174impl LinksRegistry {
175 pub fn new() -> Self {
176 Self::default()
177 }
178
179 pub fn load(path: &Path) -> Result<Self> {
182 if !path.exists() {
183 return Ok(Self::new());
184 }
185 let raw = std::fs::read_to_string(path)?;
186 let file: LinksFile =
187 toml::from_str(&raw).map_err(|e| SyncorError::Config(e.to_string()))?;
188 let mut registry = Self::new();
189 for info in file.links {
190 registry.by_id.insert(info.id.as_str().to_owned(), info);
191 }
192 Ok(registry)
193 }
194
195 pub fn save(&self, path: &Path) -> Result<()> {
197 if let Some(parent) = path.parent() {
198 std::fs::create_dir_all(parent)?;
199 }
200 let mut links: Vec<&LinkInfo> = self.by_id.values().collect();
201 links.sort_by_key(|l| l.id.as_str());
203 let file = LinksFile {
204 links: links.into_iter().cloned().collect(),
205 };
206 let raw = toml::to_string_pretty(&file).map_err(|e| SyncorError::Config(e.to_string()))?;
207 std::fs::write(path, raw)?;
208 Ok(())
209 }
210
211 pub fn add(&mut self, info: LinkInfo) -> Result<()> {
215 if self.by_id.contains_key(info.id.as_str()) {
217 return Err(SyncorError::LinkAlreadyExists(format!(
218 "id {} already registered",
219 info.id
220 )));
221 }
222 if self.get_by_dir(&info.local_dir).is_some() {
224 return Err(SyncorError::LinkAlreadyExists(format!(
225 "directory {} is already managed by another link",
226 info.local_dir.display()
227 )));
228 }
229 self.by_id.insert(info.id.as_str().to_owned(), info);
230 Ok(())
231 }
232
233 pub fn remove(&mut self, id: &LinkId) -> Result<()> {
235 self.by_id
236 .remove(id.as_str())
237 .ok_or_else(|| SyncorError::LinkNotFound(id.to_string()))?;
238 Ok(())
239 }
240
241 pub fn get_by_name(&self, name: &str) -> Option<&LinkInfo> {
243 self.by_id.values().find(|l| l.name == name)
244 }
245
246 pub fn get_by_dir(&self, dir: &Path) -> Option<&LinkInfo> {
248 self.by_id.values().find(|l| l.local_dir == dir)
249 }
250
251 pub fn get_by_id(&self, id: &LinkId) -> Option<&LinkInfo> {
253 self.by_id.get(id.as_str())
254 }
255
256 pub fn iter(&self) -> impl Iterator<Item = &LinkInfo> {
258 self.by_id.values()
259 }
260}