use std::collections::HashMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::error::{Result, SyncorError};
use crate::link::{LinkId, LinkInfo};
#[derive(Debug, Clone)]
pub struct SyncorPaths {
config_dir: PathBuf,
data_dir: PathBuf,
}
impl SyncorPaths {
pub fn new() -> Self {
let home = dirs::home_dir().expect("unable to resolve home directory");
Self::with_home(&home)
}
pub fn with_home(home: &Path) -> Self {
let config_dir = home.join(".config").join("syncor");
let data_dir = home.join(".local").join("share").join("syncor");
Self {
config_dir,
data_dir,
}
}
pub fn config_dir(&self) -> &Path {
&self.config_dir
}
pub fn config_file(&self) -> PathBuf {
self.config_dir.join("config.toml")
}
pub fn links_file(&self) -> PathBuf {
self.config_dir.join("links.toml")
}
pub fn data_dir(&self) -> &Path {
&self.data_dir
}
pub fn socket_path(&self) -> PathBuf {
self.data_dir.join("syncor.sock")
}
pub fn pid_file(&self) -> PathBuf {
self.data_dir.join("syncor.pid")
}
pub fn log_file(&self) -> PathBuf {
self.data_dir.join("syncor.log")
}
pub fn link_dir(&self) -> PathBuf {
self.data_dir.join("links")
}
pub fn link_repo_dir(&self, link_id: &LinkId) -> PathBuf {
self.link_dir().join(link_id.as_str())
}
pub fn link_state_db(&self) -> PathBuf {
self.data_dir.join("state.db")
}
pub fn link_lock_file(&self, link_id: &LinkId) -> PathBuf {
self.link_dir().join(format!("{}.lock", link_id.as_str()))
}
pub fn ensure_dirs(&self) -> Result<()> {
std::fs::create_dir_all(&self.config_dir)?;
std::fs::create_dir_all(&self.data_dir)?;
std::fs::create_dir_all(self.link_dir())?;
Ok(())
}
}
impl Default for SyncorPaths {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncorConfig {
pub debounce_secs: u64,
pub default_poll_interval_secs: u64,
}
impl Default for SyncorConfig {
fn default() -> Self {
Self {
debounce_secs: 2,
default_poll_interval_secs: 60,
}
}
}
impl SyncorConfig {
pub fn load(path: &Path) -> Result<Self> {
if !path.exists() {
return Ok(Self::default());
}
let raw = std::fs::read_to_string(path)?;
let cfg: Self = toml::from_str(&raw).map_err(|e| SyncorError::Config(e.to_string()))?;
Ok(cfg)
}
pub fn save(&self, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let raw = toml::to_string_pretty(self).map_err(|e| SyncorError::Config(e.to_string()))?;
std::fs::write(path, raw)?;
Ok(())
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct LinksFile {
#[serde(default)]
links: Vec<LinkInfo>,
}
#[derive(Debug, Default)]
pub struct LinksRegistry {
by_id: HashMap<String, LinkInfo>,
}
impl LinksRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn load(path: &Path) -> Result<Self> {
if !path.exists() {
return Ok(Self::new());
}
let raw = std::fs::read_to_string(path)?;
let file: LinksFile =
toml::from_str(&raw).map_err(|e| SyncorError::Config(e.to_string()))?;
let mut registry = Self::new();
for info in file.links {
registry.by_id.insert(info.id.as_str().to_owned(), info);
}
Ok(registry)
}
pub fn save(&self, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut links: Vec<&LinkInfo> = self.by_id.values().collect();
links.sort_by_key(|l| l.id.as_str());
let file = LinksFile {
links: links.into_iter().cloned().collect(),
};
let raw = toml::to_string_pretty(&file).map_err(|e| SyncorError::Config(e.to_string()))?;
std::fs::write(path, raw)?;
Ok(())
}
pub fn add(&mut self, info: LinkInfo) -> Result<()> {
if self.by_id.contains_key(info.id.as_str()) {
return Err(SyncorError::LinkAlreadyExists(format!(
"id {} already registered",
info.id
)));
}
if self.get_by_dir(&info.local_dir).is_some() {
return Err(SyncorError::LinkAlreadyExists(format!(
"directory {} is already managed by another link",
info.local_dir.display()
)));
}
self.by_id.insert(info.id.as_str().to_owned(), info);
Ok(())
}
pub fn remove(&mut self, id: &LinkId) -> Result<()> {
self.by_id
.remove(id.as_str())
.ok_or_else(|| SyncorError::LinkNotFound(id.to_string()))?;
Ok(())
}
pub fn get_by_name(&self, name: &str) -> Option<&LinkInfo> {
self.by_id.values().find(|l| l.name == name)
}
pub fn get_by_dir(&self, dir: &Path) -> Option<&LinkInfo> {
self.by_id.values().find(|l| l.local_dir == dir)
}
pub fn get_by_id(&self, id: &LinkId) -> Option<&LinkInfo> {
self.by_id.get(id.as_str())
}
pub fn iter(&self) -> impl Iterator<Item = &LinkInfo> {
self.by_id.values()
}
}