borg
This commit is contained in:
parent
69d9cd1963
commit
8d0c0a5426
5 changed files with 302 additions and 15 deletions
63
config.toml
63
config.toml
|
@ -24,6 +24,69 @@ cephfs_snap = true
|
||||||
|
|
||||||
# Borg Operation
|
# Borg Operation
|
||||||
[[borg]]
|
[[borg]]
|
||||||
|
# Repo to backup to
|
||||||
repo = "/backup/repo.borg"
|
repo = "/backup/repo.borg"
|
||||||
|
|
||||||
|
# Passphrase
|
||||||
passphrase = "pass"
|
passphrase = "pass"
|
||||||
|
|
||||||
|
# Source Directories
|
||||||
src = [ "/home/me/.config" ]
|
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
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use yansi::{Color, Paint};
|
use yansi::{Color, Paint};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
borg,
|
||||||
config::{Config, RsyncConfig},
|
config::{Config, RsyncConfig},
|
||||||
run_command,
|
run_command,
|
||||||
};
|
};
|
||||||
|
@ -12,11 +13,10 @@ pub fn ensure_exists(dir: &str) {
|
||||||
.flatten()
|
.flatten()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
if !exists || entries.len() == 0 {
|
if !exists || entries.is_empty() {
|
||||||
println!(
|
println!(
|
||||||
"{} {}",
|
"{} Directory {dir} does not exists",
|
||||||
"Error:".paint(Color::Red),
|
"Error:".paint(Color::Red),
|
||||||
"Directory {dir} does not exists"
|
|
||||||
);
|
);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ pub fn run_backup_rsync(conf: &RsyncConfig) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(dir) = &conf.ensure_exists {
|
if let Some(dir) = &conf.ensure_exists {
|
||||||
ensure_exists(&dir);
|
ensure_exists(dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut cmd = vec!["rsync", "-avzhruP"];
|
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);
|
let (snap_dir, snap_name) = cephfs_snap_create(&conf.src);
|
||||||
cmd.push(&snap_dir);
|
cmd.push(&snap_dir);
|
||||||
cmd.push(&conf.dest);
|
cmd.push(&conf.dest);
|
||||||
run_command(&cmd);
|
run_command(&cmd, None);
|
||||||
cephfs_snap_remove(&conf.src, &snap_name);
|
cephfs_snap_remove(&conf.src, &snap_name);
|
||||||
} else {
|
} else {
|
||||||
cmd.push(&conf.src);
|
cmd.push(&conf.src);
|
||||||
cmd.push(&conf.dest);
|
cmd.push(&conf.dest);
|
||||||
run_command(&cmd);
|
run_command(&cmd, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_backup(conf: Config) {
|
pub fn run_backup(conf: Config) {
|
||||||
if let Some(script) = &conf.start_script {
|
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() {
|
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() {
|
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 {
|
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) {
|
pub fn cephfs_snap_create(dir: &str) -> (String, String) {
|
||||||
let path = std::path::Path::new(dir);
|
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_name = format!("SNAP_{now}");
|
||||||
let snap_dir = path.join(".snap").join(&snap_name);
|
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()), snap_name)
|
||||||
format!("{}/", snap_dir.to_str().unwrap().to_string()),
|
|
||||||
snap_name,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cephfs_snap_remove(dir: &str, snap: &str) {
|
pub fn cephfs_snap_remove(dir: &str, snap: &str) {
|
||||||
|
|
167
src/borg.rs
Normal file
167
src/borg.rs
Normal 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);
|
||||||
|
}
|
|
@ -10,6 +10,8 @@ pub struct Config {
|
||||||
|
|
||||||
pub rsync: Option<Vec<RsyncConfig>>,
|
pub rsync: Option<Vec<RsyncConfig>>,
|
||||||
pub borg: Option<Vec<BorgConfig>>,
|
pub borg: Option<Vec<BorgConfig>>,
|
||||||
|
pub borg_check: Option<Vec<BorgCheckConfig>>,
|
||||||
|
pub borg_prune: Option<Vec<BorgPruneConfig>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
@ -27,4 +29,35 @@ pub struct BorgConfig {
|
||||||
pub repo: String,
|
pub repo: String,
|
||||||
pub passphrase: Option<String>,
|
pub passphrase: Option<String>,
|
||||||
pub src: Vec<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>,
|
||||||
}
|
}
|
||||||
|
|
13
src/main.rs
13
src/main.rs
|
@ -2,12 +2,19 @@ use backup::run_backup;
|
||||||
use yansi::{Color, Paint};
|
use yansi::{Color, Paint};
|
||||||
|
|
||||||
mod backup;
|
mod backup;
|
||||||
|
mod borg;
|
||||||
mod config;
|
mod config;
|
||||||
|
|
||||||
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 {
|
||||||
|
@ -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));
|
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]);
|
||||||
|
@ -25,6 +32,10 @@ pub fn run_command(cmd: &[&str]) -> (String, String) {
|
||||||
.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 {
|
||||||
|
cmd_setup = cmd_setup.env("BORG_PASSPHRASE", pw);
|
||||||
|
}
|
||||||
|
|
||||||
let child = cmd_setup.spawn().unwrap();
|
let child = cmd_setup.spawn().unwrap();
|
||||||
|
|
||||||
let status = child.wait_with_output().unwrap();
|
let status = child.wait_with_output().unwrap();
|
||||||
|
|
Loading…
Reference in a new issue