Download
92 lines 2.5 KiBRustModified 2026-10-16 raw
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};

pub const CONFIG_FILENAME: &str = "backer-upper.toml";

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SyncMode {
    Backup,
    Media,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncJob {
    pub name: String,
    pub source: PathBuf,
    pub destination: PathBuf,
    pub excludes: Vec<String>,
    pub mode: SyncMode,
    pub enabled: bool,
    /// Skip directories tagged with a CACHEDIR.TAG file (Cache Directory Tagging Specification).
    #[serde(default = "default_true")]
    pub exclude_caches: bool,
}

fn default_true() -> bool {
    true
}

impl SyncJob {
    pub fn new(name: impl Into<String>, source: impl Into<PathBuf>) -> Self {
        let source: PathBuf = source.into();
        let dest_name = source
            .file_name()
            .map(PathBuf::from)
            .unwrap_or_else(|| PathBuf::from("backup"));
        SyncJob {
            name: name.into(),
            source,
            destination: dest_name,
            excludes: Vec::new(),
            mode: SyncMode::Backup,
            enabled: true,
            exclude_caches: true,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    pub jobs: Vec<SyncJob>,
    pub last_backup: Option<chrono::DateTime<chrono::Local>>,
}

impl Default for Config {
    fn default() -> Self {
        Config {
            jobs: vec![SyncJob::new("Home", dirs_home())],
            last_backup: None,
        }
    }
}

fn dirs_home() -> PathBuf {
    std::env::var("HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from("/home"))
}

impl Config {
    pub fn load(drive_root: &Path) -> Result<Self> {
        let path = drive_root.join(CONFIG_FILENAME);
        if path.exists() {
            let text = std::fs::read_to_string(&path)
                .with_context(|| format!("reading {}", path.display()))?;
            let cfg: Config =
                toml::from_str(&text).with_context(|| format!("parsing {}", path.display()))?;
            Ok(cfg)
        } else {
            let cfg = Config::default();
            cfg.save(drive_root)?;
            Ok(cfg)
        }
    }

    pub fn save(&self, drive_root: &Path) -> Result<()> {
        let path = drive_root.join(CONFIG_FILENAME);
        let text = toml::to_string_pretty(self).context("serializing config")?;
        std::fs::write(&path, text).with_context(|| format!("writing {}", path.display()))?;
        Ok(())
    }
}