cockup 0.2.1

Yet another backup tool for various configurations
use crate::config::Symlinks;
use serde::{Deserialize, Serialize};
use std::{
    collections::BTreeMap,
    fs,
    io::Write,
    path::{Component, Path, PathBuf},
};

pub const NAME: &str = ".cockup-symlinks.json";
#[derive(Clone, Deserialize, Serialize)]
pub struct TargetLink {
    pub location: PathBuf,
    pub target: PathBuf,
}
#[derive(Clone, Deserialize, Serialize)]
pub struct Link {
    pub location: PathBuf,
    pub target: PathBuf,
    pub resolved_target: PathBuf,
    pub mode: Symlinks,
    pub backup_path: PathBuf,
    pub target_chain: Vec<TargetLink>,
}
#[derive(Deserialize, Serialize)]
pub struct Manifest {
    pub version: u32,
    pub backup_user: String,
    pub backup_home: PathBuf,
    pub canonical_home: PathBuf,
    pub links: Vec<Link>,
    #[serde(default)]
    pub ingredients: BTreeMap<String, String>,
}
impl Manifest {
    pub fn new() -> Result<Self, String> {
        let home = std::env::var_os("HOME").ok_or("HOME is not set")?;
        Ok(Self {
            version: 1,
            backup_user: std::env::var("USER").unwrap_or_default(),
            canonical_home: fs::canonicalize(&home).unwrap_or_else(|_| PathBuf::from(&home)),
            backup_home: PathBuf::from(home),
            links: Vec::new(),
            ingredients: BTreeMap::new(),
        })
    }
    pub fn load(root: &Path) -> Result<Option<Self>, String> {
        let path = root.join(NAME);
        let bytes = match fs::read(&path) {
            Ok(bytes) => bytes,
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
            Err(e) => return Err(format!("{}: {e}", path.display())),
        };
        let manifest: Self =
            serde_json::from_slice(&bytes).map_err(|e| format!("Invalid symlink manifest: {e}"))?;
        if manifest.version != 1
            || (!manifest.backup_home.is_absolute() || !manifest.canonical_home.is_absolute())
        {
            return Err("Unsupported or invalid symlink manifest".into());
        }
        let mut seen = std::collections::HashSet::new();
        for link in &manifest.links {
            if !link.location.is_absolute()
                || !link.resolved_target.is_absolute()
                || link.target_chain.iter().any(|l| !l.location.is_absolute())
                || link.mode == Symlinks::Prompt
                || link.backup_path.as_os_str().is_empty()
                || link
                    .backup_path
                    .components()
                    .any(|c| !matches!(c, Component::Normal(_)))
                || link.backup_path == Path::new(NAME)
                || !seen.insert(&link.backup_path)
            {
                return Err("Invalid symlink manifest entry".into());
            }
        }
        Ok(Some(manifest))
    }
    pub fn save(&mut self, root: &Path) -> Result<(), String> {
        self.links.sort_by(|a, b| a.backup_path.cmp(&b.backup_path));
        let bytes = serde_json::to_vec_pretty(self).map_err(|e| e.to_string())?;
        let tmp = root.join(format!("{NAME}.{}.tmp", std::process::id()));
        let mut file = fs::OpenOptions::new()
            .write(true)
            .create_new(true)
            .open(&tmp)
            .map_err(|e| format!("Cannot create manifest temporary file: {e}"))?;
        let result = (|| -> std::io::Result<()> {
            file.write_all(&bytes)?;
            file.sync_all()?;
            fs::rename(&tmp, root.join(NAME))
        })();
        if result.is_err() {
            let _ = fs::remove_file(&tmp);
        }
        result.map_err(|e| format!("Cannot save symlink manifest: {e}"))
    }
}