From 1d3a1b7a60b082a5b5ea5c783fa1fc5ee71f3c01 Mon Sep 17 00:00:00 2001 From: JMARyA Date: Fri, 25 Apr 2025 13:06:16 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20restic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.toml | 29 +++++++ src/backup.rs | 6 +- src/borg.rs | 27 +++++-- src/config.rs | 52 ++++++++++++ src/main.rs | 17 ++-- src/restic.rs | 213 ++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 327 insertions(+), 17 deletions(-) create mode 100644 src/restic.rs diff --git a/config.toml b/config.toml index 705d362..4f4a6a2 100644 --- a/config.toml +++ b/config.toml @@ -96,3 +96,32 @@ # keep_weekly = 4 # keep_monthly = 6 # keep_yearly = 2 + +# Restic Operation +# [[restic]] +# repo = "/backup/repo.restic" +# passphrase = "password" +# src = [ +# "/dir" +# ] + +# exclude = [ +# "/exclude" +# ] + +# exclude_caches = true +# exclude_if_present = [ +# ".nobk" +# ] +# reread = true +# one_file_system = true +# concurrency = 4 +# tags = [ +# "tag1" +# ] + +# compression = "auto" + +# ensure_exists = "/dir" +# cephfs_snap = true +# same_path = true diff --git a/src/backup.rs b/src/backup.rs index 1bd43bb..a214468 100644 --- a/src/backup.rs +++ b/src/backup.rs @@ -3,7 +3,7 @@ use yansi::{Color, Paint}; use crate::{ borg, config::{Config, RsyncConfig}, - run_command, + restic, run_command, }; pub fn ensure_exists(dir: &str) { @@ -71,6 +71,10 @@ pub fn run_backup(conf: Config) { borg::create_archive(borg); } + for restic in &conf.restic.unwrap_or_default() { + restic::create_archive(restic); + } + for prune in &conf.borg_prune.unwrap_or_default() { borg::prune_archive(prune); } diff --git a/src/borg.rs b/src/borg.rs index 00605f5..8a96eb1 100644 --- a/src/borg.rs +++ b/src/borg.rs @@ -6,10 +6,6 @@ use crate::{ run_command, }; -pub fn init_repo(path: &str) { - run_command(&["borg", "init", "--encryption=repokey-blake2", path], None); -} - pub fn bind_mount(src: &str, dst: &str) { run_command(&["mount", "--bind", src, dst], None); } @@ -125,7 +121,12 @@ pub fn create_archive(conf: &BorgConfig) { cmd.extend(dirs.iter().map(|x| x.0.as_str())); - run_command(&cmd, conf.passphrase.clone()); + run_command( + &cmd, + conf.passphrase + .clone() + .map(|pass| vec![("BORG_PASSPHRASE".to_string(), pass)]), + ); for cleanup in &snaps { cephfs_snap_remove_dir(&cleanup.0); @@ -194,10 +195,22 @@ pub fn prune_archive(conf: &BorgPruneConfig) { .unwrap_or_default(); cmd.push(&binding); - run_command(&cmd, Some(conf.passphrase.clone())); + run_command( + &cmd, + Some(vec![( + "BORG_PASSPHRASE".to_string(), + conf.passphrase.clone(), + )]), + ); let cmd = vec!["borg", "compact", &conf.repo]; - run_command(&cmd, Some(conf.passphrase.clone())); + run_command( + &cmd, + Some(vec![( + "BORG_PASSPHRASE".to_string(), + conf.passphrase.clone(), + )]), + ); } pub fn check_archive(conf: &BorgCheckConfig) { diff --git a/src/config.rs b/src/config.rs index 5b211fc..f4f0800 100644 --- a/src/config.rs +++ b/src/config.rs @@ -20,6 +20,9 @@ pub struct Config { /// Configuration for Borg prune jobs to manage repository snapshots. pub borg_prune: Option>, + + /// Configuration for Borg backup jobs. + pub restic: Option>, } /// Configuration for an individual rsync job. @@ -139,3 +142,52 @@ pub struct BorgPruneConfig { /// Retain the last `n` yearly archives. pub keep_yearly: Option, } + +// TODO : restic support + +/// Configuration for an individual restic backup job. +#[derive(Debug, Clone, Deserialize)] +pub struct ResticConfig { + /// Borg repository path. + pub repo: String, + + /// Optional passphrase for the repository. + pub passphrase: String, + + /// List of source paths to include in the backup. + pub src: Vec, + + /// List of patterns to exclude from the backup. + pub exclude: Option>, + + /// Cache directories marked with CACHEDIR.TAG will be excluded + pub exclude_caches: Option, + + /// Reread all files even if unchanged + pub reread: Option, + + /// List of marker files; directories containing these will be excluded. + pub exclude_if_present: Option>, + + /// Whether to limit the backup to a single filesystem. + pub one_file_system: Option, + + /// Read concurrency + pub concurrency: Option, + + /// Optional comment to associate with the backup. + pub tags: Option>, + + /// Compression mode to use for the backup. + // TODO : + pub compression: Option, + + /// Ensure a specific directory exists before running the backup. + pub ensure_exists: Option, + + /// Create CephFS snapshots before the backup. + pub cephfs_snap: Option, + + /// Bind mount to consistent path + pub same_path: Option, +} diff --git a/src/main.rs b/src/main.rs index 474b348..7a41ae7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,17 +4,14 @@ use yansi::{Color, Paint}; mod backup; mod borg; mod config; +mod restic; + +// TODO : add basic ctrl+c support for ending bk tasks instead of everything and ensure cleanups fn main() { let args = std::env::args().collect::>(); if let Some(conf) = args.get(1) { - if conf.as_str() == "init" { - let repo = args.get(2).unwrap(); - borg::init_repo(repo); - std::process::exit(0); - } - let conf = toml::from_str(&std::fs::read_to_string(conf).unwrap()).unwrap(); run_backup(conf); } else { @@ -22,7 +19,7 @@ fn main() { } } -pub fn run_command(cmd: &[&str], borg_passphrase: Option) -> (String, String) { +pub fn run_command(cmd: &[&str], env: Option>) -> (String, String) { println!("--> {} ", cmd.join(" ").paint(Color::Blue)); let mut cmd_setup = std::process::Command::new(cmd[0]); @@ -32,8 +29,10 @@ pub fn run_command(cmd: &[&str], borg_passphrase: Option) -> (String, St .stdout(std::process::Stdio::inherit()) .stdin(std::process::Stdio::inherit()); - if let Some(pw) = borg_passphrase { - cmd_setup = cmd_setup.env("BORG_PASSPHRASE", pw); + if let Some(pw) = env { + for e in pw { + cmd_setup = cmd_setup.env(e.0, e.1); + } } let child = cmd_setup.spawn().unwrap(); diff --git a/src/restic.rs b/src/restic.rs new file mode 100644 index 0000000..10d761c --- /dev/null +++ b/src/restic.rs @@ -0,0 +1,213 @@ +use yansi::{Color, Paint}; + +use crate::{ + backup::{cephfs_snap_create, cephfs_snap_remove_dir, ensure_exists, nowtime}, + config::{BorgCheckConfig, BorgConfig, BorgPruneConfig, ResticConfig}, + run_command, +}; + +pub fn bind_mount(src: &str, dst: &str) { + run_command(&["mount", "--bind", src, dst], None); +} + +pub fn umount(mount: &str) { + run_command(&["umount", mount], None); +} + +pub fn create_archive(conf: &ResticConfig) { + if let Some(dir) = &conf.ensure_exists { + ensure_exists(dir); + } + + println!( + "--> Running backup for {}", + conf.src.join(",").paint(Color::Yellow), + ); + println!("--> Creating restic archive"); + + let mut cmd = vec!["restic", "backup"]; + + let empty = Vec::new(); + for ex in conf.exclude.as_ref().unwrap_or(&empty) { + cmd.push("--exclude"); + cmd.push(ex); + } + + for ex in conf.exclude_if_present.as_ref().unwrap_or(&empty) { + cmd.push("--exclude-if-present"); + cmd.push(ex); + } + + if conf.one_file_system.unwrap_or_default() { + cmd.push("--one-file-system"); + } + + let c = conf.concurrency.unwrap_or(2).to_string(); + cmd.push("--read-concurrency"); + cmd.push(&c); + + let tags = conf.tags.clone().unwrap_or_default(); + + for t in &tags { + cmd.push("--tag"); + cmd.push(&t); + } + + if conf.reread.unwrap_or_default() { + cmd.push("--force"); + } + + if conf.exclude_caches.unwrap_or_default() { + cmd.push("--exclude-caches"); + } + + // TODO : fix compression options + let zstd10 = "zstd,10".to_string(); + let comp = conf.compression.as_ref().unwrap_or(&zstd10); + cmd.push("--compression"); + cmd.push(comp); + + cmd.push(&conf.repo); + + let mut snaps = Vec::new(); + + if conf.cephfs_snap.unwrap_or_default() { + for path in &conf.src { + let snap = cephfs_snap_create(&path); + snaps.push((snap.0, snap.1, path)); + } + } + + let mut dirs = if snaps.is_empty() { + conf.src + .clone() + .into_iter() + .map(|x| (x.clone(), x)) + .collect() + } else { + snaps + .iter() + .map(|x| (x.0.clone(), x.2.clone())) + .collect::>() + }; + + let mut mounts = Vec::new(); + if conf.same_path.unwrap_or_default() { + for (path, orig) in &dirs { + let name = orig.replace("/", "_"); + println!("--> Creating consistent path /bk/{}", name); + std::fs::create_dir_all(&format!("/bk/{name}")).unwrap(); + bind_mount(path, &format!("/bk/{name}")); + mounts.push((format!("/bk/{name}"), path.clone())); + } + + dirs = mounts.clone(); + } + + cmd.extend(dirs.iter().map(|x| x.0.as_str())); + + run_command( + &cmd, + Some(vec![( + "RESTIC_PASSWORD".to_string(), + conf.passphrase.clone(), + )]), + ); + + for cleanup in &snaps { + cephfs_snap_remove_dir(&cleanup.0); + println!("--> Cleaning up snap {}", cleanup.0); + } + + for (cleanup, _) in &mounts { + println!("--> Cleaning up mount {}", cleanup); + umount(&cleanup); + } +} + +// TODO : todo + +/* +pub fn prune_archive(conf: &BorgPruneConfig) { + println!("--> Pruning borg repo {}", conf.repo.paint(Color::Yellow),); + + let mut cmd = vec!["borg", "prune", "--stats", "--list"]; + + cmd.push(&conf.keep_within); + + let binding = conf + .keep_last + .as_ref() + .map(|x| format!("--keep-last={x}")) + .unwrap_or_default(); +cmd.push(&binding); +let binding = conf +.keep_secondly +.as_ref() +.map(|x| format!("--keep-secondly={x}")) +.unwrap_or_default(); +cmd.push(&binding); +let binding = conf +.keep_minutely +.as_ref() +.map(|x| format!("--keep-minutely={x}")) +.unwrap_or_default(); +cmd.push(&binding); +let binding = conf +.keep_hourly +.as_ref() + .map(|x| format!("--keep-hourly={x}")) + .unwrap_or_default(); + cmd.push(&binding); + let binding = conf + .keep_daily + .as_ref() + .map(|x| format!("--keep-daily={x}")) + .unwrap_or_default(); +cmd.push(&binding); +let binding = conf +.keep_weekly +.as_ref() +.map(|x| format!("--keep-weekly={x}")) +.unwrap_or_default(); +cmd.push(&binding); +let binding = conf +.keep_monthly +.as_ref() +.map(|x| format!("--keep-monthly={x}")) +.unwrap_or_default(); +cmd.push(&binding); +let binding = conf +.keep_yearly +.as_ref() +.map(|x| format!("--keep-yearly={x}")) +.unwrap_or_default(); +cmd.push(&binding); + +run_command(&cmd, Some(conf.passphrase.clone())); + +let cmd = vec!["borg", "compact", &conf.repo]; +run_command(&cmd, Some(conf.passphrase.clone())); +} + +pub fn check_archive(conf: &BorgCheckConfig) { + println!("--> Checking borg repo {}", conf.repo.paint(Color::Yellow),); + + let mut cmd = vec!["borg", "check"]; + + if conf.verify_data.unwrap_or_default() { + cmd.push("--verify-data"); + } else { + cmd.push("--repository-only"); + cmd.push("--archives-only"); + } + + if conf.repair.unwrap_or_default() { + cmd.push("--repair"); + } + + cmd.push(&conf.repo); + + run_command(&cmd, None); +} +*/