From 8d0c0a54263d63d21f6bbfbdde0a1ec92fb935ef Mon Sep 17 00:00:00 2001 From: JMARyA Date: Sun, 5 Jan 2025 09:57:08 +0100 Subject: [PATCH] borg --- config.toml | 63 +++++++++++++++++++ src/backup.rs | 41 ++++++++----- src/borg.rs | 167 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 33 ++++++++++ src/main.rs | 13 +++- 5 files changed, 302 insertions(+), 15 deletions(-) create mode 100644 src/borg.rs diff --git a/config.toml b/config.toml index 1516e65..332ab37 100644 --- a/config.toml +++ b/config.toml @@ -24,6 +24,69 @@ cephfs_snap = true # Borg Operation [[borg]] +# Repo to backup to repo = "/backup/repo.borg" + +# Passphrase passphrase = "pass" + +# Source Directories src = [ "/home/me/.config" ] + +# Excludes +exclude = [ + "some/dir" +] + +# Exclude if present (example: Do not backup directories with `.nobackup`) +exclude_if_present = [".nobackup"] + +# Stay in one filesystem +one_file_system = true + +# Backup access time +atime = false + +# Backup change time +ctime = false + +# Do not backup ACLs +no_acls = true + +# Do not backup extended attributes +no_xattrs = true + +# Comment to add to the backup +comment = "Backup of /home/me/" + +# Compression +compression = "zstd,10" + +# Borg Check Operation +[[borg_check]] +# Repository to check +repo = "/backup/repo.borg" + +# Full Data Verify +verify_data = true + +# Repair Attempt +repair = false + +# Borg Prune Operation +[[borg_prune]] +# Repository to prune +repo = "/backup/repo.borg" + +# Passphrase +passphrase = "pass" + +keep_within = "30d" +keep_last = 20 +# keep_secondly = 3 +# keep_minutely = 3 +# keep_hourly = 10 +# keep_daily = 7 +# keep_weekly = 4 +# keep_monthly = 6 +# keep_yearly = 2 diff --git a/src/backup.rs b/src/backup.rs index 5125a08..e3c4cf4 100644 --- a/src/backup.rs +++ b/src/backup.rs @@ -1,6 +1,7 @@ use yansi::{Color, Paint}; use crate::{ + borg, config::{Config, RsyncConfig}, run_command, }; @@ -12,11 +13,10 @@ pub fn ensure_exists(dir: &str) { .flatten() .collect::>(); - if !exists || entries.len() == 0 { + if !exists || entries.is_empty() { println!( - "{} {}", + "{} Directory {dir} does not exists", "Error:".paint(Color::Red), - "Directory {dir} does not exists" ); std::process::exit(1); } @@ -30,7 +30,7 @@ pub fn run_backup_rsync(conf: &RsyncConfig) { ); if let Some(dir) = &conf.ensure_exists { - ensure_exists(&dir); + ensure_exists(dir); } let mut cmd = vec!["rsync", "-avzhruP"]; @@ -49,18 +49,18 @@ pub fn run_backup_rsync(conf: &RsyncConfig) { let (snap_dir, snap_name) = cephfs_snap_create(&conf.src); cmd.push(&snap_dir); cmd.push(&conf.dest); - run_command(&cmd); + run_command(&cmd, None); cephfs_snap_remove(&conf.src, &snap_name); } else { cmd.push(&conf.src); cmd.push(&conf.dest); - run_command(&cmd); + run_command(&cmd, None); } } pub fn run_backup(conf: Config) { if let Some(script) = &conf.start_script { - run_command(&["sh", script.as_str()]); + run_command(&["sh", script.as_str()], None); } for rsync in &conf.rsync.unwrap_or_default() { @@ -68,17 +68,33 @@ pub fn run_backup(conf: Config) { } for borg in &conf.borg.unwrap_or_default() { - // TODO : Implement + borg::create_archive(borg); + } + + for prune in &conf.borg_prune.unwrap_or_default() { + borg::prune_archive(prune); + } + + for check in &conf.borg_check.unwrap_or_default() { + borg::check_archive(check); } if let Some(script) = &conf.end_script { - run_command(&["sh", script.as_str()]); + run_command(&["sh", script.as_str()], None); } } +pub fn now() -> String { + chrono::Utc::now().format("%Y_%m_%d").to_string() +} + +pub fn nowtime() -> String { + chrono::Utc::now().format("%Y_%m_%d-%H_%M").to_string() +} + pub fn cephfs_snap_create(dir: &str) -> (String, String) { let path = std::path::Path::new(dir); - let now = chrono::Utc::now().format("%Y_%m_%d").to_string(); + let now = now(); let snap_name = format!("SNAP_{now}"); let snap_dir = path.join(".snap").join(&snap_name); @@ -90,10 +106,7 @@ pub fn cephfs_snap_create(dir: &str) -> (String, String) { } } - ( - format!("{}/", snap_dir.to_str().unwrap().to_string()), - snap_name, - ) + (format!("{}/", snap_dir.to_str().unwrap()), snap_name) } pub fn cephfs_snap_remove(dir: &str, snap: &str) { diff --git a/src/borg.rs b/src/borg.rs new file mode 100644 index 0000000..47112b6 --- /dev/null +++ b/src/borg.rs @@ -0,0 +1,167 @@ +use yansi::{Color, Paint}; + +use crate::{ + backup::nowtime, + config::{BorgCheckConfig, BorgConfig, BorgPruneConfig}, + run_command, +}; + +pub fn init_repo(path: &str) { + run_command(&["borg", "init", "--encryption=repokey-blake2", path], None); +} + +pub fn create_archive(conf: &BorgConfig) { + let archive_name = format!( + "BK_{}_{}_{}", + std::fs::read_to_string("/etc/hostname").unwrap_or(String::from("UNKNOWN")), + conf.src.join("+++"), + nowtime() + ); + + println!( + "--> Running backup for {}", + conf.src.join(",").paint(Color::Yellow), + ); + println!( + "--> Creating borg archive {}", + format!("{}::{archive_name}", conf.repo).paint(Color::Yellow), + ); + + let mut cmd = vec!["borg", "create", "--stats", "--list"]; + + 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"); + } + + if conf.atime.unwrap_or_default() { + cmd.push("--atime"); + } else { + cmd.push("--noatime"); + } + + if !conf.ctime.unwrap_or(true) { + cmd.push("--noctime"); + } + + if conf.no_acls.unwrap_or_default() { + cmd.push("--noacls"); + } + + if conf.no_xattrs.unwrap_or_default() { + cmd.push("--noxattrs"); + } + + if let Some(comment) = &conf.comment { + cmd.push("--comment"); + cmd.push(comment); + } + + let zstd10 = "zstd,10".to_string(); + let comp = conf.compression.as_ref().unwrap_or(&zstd10); + cmd.push("--compression"); + cmd.push(comp); + + let repo = format!("{}::{}", conf.repo, archive_name); + + cmd.push(&repo); + + for path in &conf.src { + cmd.push(path); + } + + run_command(&cmd, conf.passphrase.clone()); +} + +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); +} diff --git a/src/config.rs b/src/config.rs index a2d5143..aa040ba 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,6 +10,8 @@ pub struct Config { pub rsync: Option>, pub borg: Option>, + pub borg_check: Option>, + pub borg_prune: Option>, } #[derive(Debug, Clone, Deserialize)] @@ -27,4 +29,35 @@ pub struct BorgConfig { pub repo: String, pub passphrase: Option, pub src: Vec, + pub exclude: Option>, + pub exclude_if_present: Option>, + pub one_file_system: Option, + pub atime: Option, + pub ctime: Option, + pub no_acls: Option, + pub no_xattrs: Option, + pub comment: Option, + pub compression: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct BorgCheckConfig { + pub repo: String, + pub verify_data: Option, + pub repair: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct BorgPruneConfig { + pub repo: String, + pub passphrase: String, + pub keep_within: String, + pub keep_last: Option, + pub keep_secondly: Option, + pub keep_minutely: Option, + pub keep_hourly: Option, + pub keep_daily: Option, + pub keep_weekly: Option, + pub keep_monthly: Option, + pub keep_yearly: Option, } diff --git a/src/main.rs b/src/main.rs index e77455a..474b348 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,12 +2,19 @@ use backup::run_backup; use yansi::{Color, Paint}; mod backup; +mod borg; mod config; 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 { @@ -15,7 +22,7 @@ fn main() { } } -pub fn run_command(cmd: &[&str]) -> (String, String) { +pub fn run_command(cmd: &[&str], borg_passphrase: Option) -> (String, String) { println!("--> {} ", cmd.join(" ").paint(Color::Blue)); let mut cmd_setup = std::process::Command::new(cmd[0]); @@ -25,6 +32,10 @@ pub fn run_command(cmd: &[&str]) -> (String, String) { .stdout(std::process::Stdio::inherit()) .stdin(std::process::Stdio::inherit()); + if let Some(pw) = borg_passphrase { + cmd_setup = cmd_setup.env("BORG_PASSPHRASE", pw); + } + let child = cmd_setup.spawn().unwrap(); let status = child.wait_with_output().unwrap();