✨ restic
This commit is contained in:
parent
12f101bf46
commit
1d3a1b7a60
6 changed files with 327 additions and 17 deletions
29
config.toml
29
config.toml
|
@ -96,3 +96,32 @@
|
||||||
# keep_weekly = 4
|
# keep_weekly = 4
|
||||||
# keep_monthly = 6
|
# keep_monthly = 6
|
||||||
# keep_yearly = 2
|
# 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
|
||||||
|
|
|
@ -3,7 +3,7 @@ use yansi::{Color, Paint};
|
||||||
use crate::{
|
use crate::{
|
||||||
borg,
|
borg,
|
||||||
config::{Config, RsyncConfig},
|
config::{Config, RsyncConfig},
|
||||||
run_command,
|
restic, run_command,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn ensure_exists(dir: &str) {
|
pub fn ensure_exists(dir: &str) {
|
||||||
|
@ -71,6 +71,10 @@ pub fn run_backup(conf: Config) {
|
||||||
borg::create_archive(borg);
|
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() {
|
for prune in &conf.borg_prune.unwrap_or_default() {
|
||||||
borg::prune_archive(prune);
|
borg::prune_archive(prune);
|
||||||
}
|
}
|
||||||
|
|
27
src/borg.rs
27
src/borg.rs
|
@ -6,10 +6,6 @@ use crate::{
|
||||||
run_command,
|
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) {
|
pub fn bind_mount(src: &str, dst: &str) {
|
||||||
run_command(&["mount", "--bind", src, dst], None);
|
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()));
|
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 {
|
for cleanup in &snaps {
|
||||||
cephfs_snap_remove_dir(&cleanup.0);
|
cephfs_snap_remove_dir(&cleanup.0);
|
||||||
|
@ -194,10 +195,22 @@ pub fn prune_archive(conf: &BorgPruneConfig) {
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
cmd.push(&binding);
|
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];
|
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) {
|
pub fn check_archive(conf: &BorgCheckConfig) {
|
||||||
|
|
|
@ -20,6 +20,9 @@ pub struct Config {
|
||||||
|
|
||||||
/// Configuration for Borg prune jobs to manage repository snapshots.
|
/// Configuration for Borg prune jobs to manage repository snapshots.
|
||||||
pub borg_prune: Option<Vec<BorgPruneConfig>>,
|
pub borg_prune: Option<Vec<BorgPruneConfig>>,
|
||||||
|
|
||||||
|
/// Configuration for Borg backup jobs.
|
||||||
|
pub restic: Option<Vec<ResticConfig>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration for an individual rsync job.
|
/// Configuration for an individual rsync job.
|
||||||
|
@ -139,3 +142,52 @@ pub struct BorgPruneConfig {
|
||||||
/// Retain the last `n` yearly archives.
|
/// Retain the last `n` yearly archives.
|
||||||
pub keep_yearly: Option<u32>,
|
pub keep_yearly: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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<String>,
|
||||||
|
|
||||||
|
/// List of patterns to exclude from the backup.
|
||||||
|
pub exclude: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Cache directories marked with CACHEDIR.TAG will be excluded
|
||||||
|
pub exclude_caches: Option<bool>,
|
||||||
|
|
||||||
|
/// Reread all files even if unchanged
|
||||||
|
pub reread: Option<bool>,
|
||||||
|
|
||||||
|
/// List of marker files; directories containing these will be excluded.
|
||||||
|
pub exclude_if_present: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Whether to limit the backup to a single filesystem.
|
||||||
|
pub one_file_system: Option<bool>,
|
||||||
|
|
||||||
|
/// Read concurrency
|
||||||
|
pub concurrency: Option<u64>,
|
||||||
|
|
||||||
|
/// Optional comment to associate with the backup.
|
||||||
|
pub tags: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Compression mode to use for the backup.
|
||||||
|
// TODO :
|
||||||
|
pub compression: Option<String>,
|
||||||
|
|
||||||
|
/// Ensure a specific directory exists before running the backup.
|
||||||
|
pub ensure_exists: Option<String>,
|
||||||
|
|
||||||
|
/// Create CephFS snapshots before the backup.
|
||||||
|
pub cephfs_snap: Option<bool>,
|
||||||
|
|
||||||
|
/// Bind mount to consistent path
|
||||||
|
pub same_path: Option<bool>,
|
||||||
|
}
|
||||||
|
|
17
src/main.rs
17
src/main.rs
|
@ -4,17 +4,14 @@ use yansi::{Color, Paint};
|
||||||
mod backup;
|
mod backup;
|
||||||
mod borg;
|
mod borg;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod restic;
|
||||||
|
|
||||||
|
// TODO : add basic ctrl+c support for ending bk tasks instead of everything and ensure cleanups
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let args = std::env::args().collect::<Vec<_>>();
|
let args = std::env::args().collect::<Vec<_>>();
|
||||||
|
|
||||||
if let Some(conf) = args.get(1) {
|
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();
|
let conf = toml::from_str(&std::fs::read_to_string(conf).unwrap()).unwrap();
|
||||||
run_backup(conf);
|
run_backup(conf);
|
||||||
} else {
|
} else {
|
||||||
|
@ -22,7 +19,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_command(cmd: &[&str], borg_passphrase: Option<String>) -> (String, String) {
|
pub fn run_command(cmd: &[&str], env: Option<Vec<(String, String)>>) -> (String, String) {
|
||||||
println!("--> {} ", cmd.join(" ").paint(Color::Blue));
|
println!("--> {} ", cmd.join(" ").paint(Color::Blue));
|
||||||
|
|
||||||
let mut cmd_setup = std::process::Command::new(cmd[0]);
|
let mut cmd_setup = std::process::Command::new(cmd[0]);
|
||||||
|
@ -32,8 +29,10 @@ pub fn run_command(cmd: &[&str], borg_passphrase: Option<String>) -> (String, St
|
||||||
.stdout(std::process::Stdio::inherit())
|
.stdout(std::process::Stdio::inherit())
|
||||||
.stdin(std::process::Stdio::inherit());
|
.stdin(std::process::Stdio::inherit());
|
||||||
|
|
||||||
if let Some(pw) = borg_passphrase {
|
if let Some(pw) = env {
|
||||||
cmd_setup = cmd_setup.env("BORG_PASSPHRASE", pw);
|
for e in pw {
|
||||||
|
cmd_setup = cmd_setup.env(e.0, e.1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let child = cmd_setup.spawn().unwrap();
|
let child = cmd_setup.spawn().unwrap();
|
||||||
|
|
213
src/restic.rs
Normal file
213
src/restic.rs
Normal file
|
@ -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::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
*/
|
Loading…
Add table
Add a link
Reference in a new issue