This commit is contained in:
JMARyA 2025-01-05 09:57:08 +01:00
parent 69d9cd1963
commit 8d0c0a5426
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
5 changed files with 302 additions and 15 deletions

View file

@ -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

View file

@ -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::<Vec<_>>();
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) {

167
src/borg.rs Normal file
View file

@ -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);
}

View file

@ -10,6 +10,8 @@ pub struct Config {
pub rsync: Option<Vec<RsyncConfig>>,
pub borg: Option<Vec<BorgConfig>>,
pub borg_check: Option<Vec<BorgCheckConfig>>,
pub borg_prune: Option<Vec<BorgPruneConfig>>,
}
#[derive(Debug, Clone, Deserialize)]
@ -27,4 +29,35 @@ pub struct BorgConfig {
pub repo: String,
pub passphrase: Option<String>,
pub src: Vec<String>,
pub exclude: Option<Vec<String>>,
pub exclude_if_present: Option<Vec<String>>,
pub one_file_system: Option<bool>,
pub atime: Option<bool>,
pub ctime: Option<bool>,
pub no_acls: Option<bool>,
pub no_xattrs: Option<bool>,
pub comment: Option<String>,
pub compression: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BorgCheckConfig {
pub repo: String,
pub verify_data: Option<bool>,
pub repair: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BorgPruneConfig {
pub repo: String,
pub passphrase: String,
pub keep_within: String,
pub keep_last: Option<u32>,
pub keep_secondly: Option<u32>,
pub keep_minutely: Option<u32>,
pub keep_hourly: Option<u32>,
pub keep_daily: Option<u32>,
pub keep_weekly: Option<u32>,
pub keep_monthly: Option<u32>,
pub keep_yearly: Option<u32>,
}

View file

@ -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::<Vec<_>>();
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, 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();