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]]
|
||||
# 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
|
||||
|
|
|
@ -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
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 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>,
|
||||
}
|
||||
|
|
13
src/main.rs
13
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::<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();
|
||||
|
|
Loading…
Reference in a new issue