diff options
author | Leonard Kugis <leonard@kug.is> | 2025-04-12 13:39:46 +0200 |
---|---|---|
committer | Leonard Kugis <leonard@kug.is> | 2025-04-12 13:39:46 +0200 |
commit | 632e2a5a020764fb3c544f592b5fe66cc055c44a (patch) | |
tree | ba2780b2a1a1b73219e37ce52ff711322105161d /backup | |
parent | ccb7982ce7ef088bf0fc626a263af1d83d3756a9 (diff) | |
download | scripts-632e2a5a020764fb3c544f592b5fe66cc055c44a.tar.gz |
Implemented backup in rust
Diffstat (limited to 'backup')
-rw-r--r-- | backup/Cargo.toml | 14 | ||||
-rw-r--r-- | backup/src/main.rs | 148 |
2 files changed, 162 insertions, 0 deletions
diff --git a/backup/Cargo.toml b/backup/Cargo.toml new file mode 100644 index 0000000..699fe53 --- /dev/null +++ b/backup/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "backup" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +walkdir = "2.4.0" +tar = "0.4" +zstd = "0.13" +chrono = "0.4" +regex = "1" +duct = "0.13" # für shell-like commands
\ No newline at end of file diff --git a/backup/src/main.rs b/backup/src/main.rs new file mode 100644 index 0000000..d2210af --- /dev/null +++ b/backup/src/main.rs @@ -0,0 +1,148 @@ +use chrono::Local; +use duct::cmd; +use regex::Regex; +use serde::Deserialize; +use std::{ + env, + error::Error, + fs::{self, File}, + io::{BufRead, BufReader}, + path::{Path, PathBuf}, + process::Command, +}; + +#[derive(Debug, Deserialize)] +struct BackupConfig { + backup_dir: String, + rotate_dir: String, + timestamp_file: String, + source_file: String, + exclude_file: String, +} + +#[derive(Debug, Deserialize)] +struct Config { + backup: BackupConfig, +} + +fn read_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>, Box<dyn Error>> { + let file = File::open(path)?; + let reader = BufReader::new(file); + Ok(reader.lines().filter_map(Result::ok).filter(|l| !l.trim().is_empty()).collect()) +} + +fn get_backup_number(backup_dir: &Path) -> Result<u32, Box<dyn Error>> { + let mut highest = 0; + let pattern = Regex::new(r"backup-(\d{2})\.tar\.zst")?; + + for entry in fs::read_dir(backup_dir)? { + let path = entry?.path(); + if let Some(fname) = path.file_name().and_then(|n| n.to_str()) { + if let Some(caps) = pattern.captures(fname) { + if let Ok(num) = caps[1].parse::<u32>() { + if num > highest { + highest = num; + } + } + } + } + } + + Ok(highest) +} + +fn main() -> Result<(), Box<dyn Error>> { + let base_dir = PathBuf::from("/root/scripts"); + let config_path = base_dir.join("config.json"); + let config_str = fs::read_to_string(&config_path)?; + let config: Config = serde_json::from_str(&config_str)?; + + let backup_dir = Path::new(&config.backup.backup_dir); + let rotate_dir = Path::new(&config.backup.rotate_dir); + let timestamp_file = Path::new(&config.backup.timestamp_file); + let source_list = read_lines(&config.backup.source_file)?; + let exclude_list = read_lines(&config.backup.exclude_file)?; + + fs::create_dir_all(backup_dir)?; + + let mut source_paths: Vec<String> = vec![]; + for s in &source_list { + if Path::new(s).exists() { + source_paths.push(s.clone()); + } else { + eprintln!("Warnung: '{}' existiert nicht", s); + } + } + + let mut exclude_args: Vec<String> = vec![]; + for e in &exclude_list { + if Path::new(e).exists() { + exclude_args.push(format!("--exclude={}", e)); + } else { + eprintln!("Warnung: '{}' existiert nicht", e); + } + } + + let mut backup_nr = get_backup_number(backup_dir)? + 1; + if backup_nr > 30 { + backup_nr = 1; + + let now = Local::now(); + let rotate_subdir = rotate_dir.join(format!("{}-{}", now.format("%Y-%m-%d"), now.format("%H-%M-%S-%3f"))); + fs::create_dir_all(&rotate_subdir)?; + + for entry in fs::read_dir(backup_dir)? { + let entry = entry?; + let dest = rotate_subdir.join(entry.file_name()); + fs::rename(entry.path(), dest)?; + } + + // pg_dumpall via duct (ähnlich wie Bash pipe) + let dump1 = rotate_subdir.join("backup_mastodon-db-1.sql.zst"); + let dump2 = rotate_subdir.join("backup_db.sql.zst"); + + cmd!("docker", "exec", "mastodon-db-1", "pg_dumpall", "-U", "postgres") + .pipe(cmd!("zstd", "-9")) + .stdout_file(File::create(dump1)?) + .run()?; + + cmd!("pg_dumpall", "-U", "postgres") + .pipe(cmd!("zstd", "-9")) + .stdout_file(File::create(dump2)?) + .run()?; + } + + let backup_filename = format!("backup-{:02}.tar.zst", backup_nr); + let backup_path = backup_dir.join(&backup_filename); + + let mut tar_cmd = Command::new("tar"); + tar_cmd + .arg("-cvp") + .arg("-g") + .arg(timestamp_file) + .arg("-f") + .arg(&backup_path) + .arg("-I") + .arg("zstd -9 -T1"); + + for excl in &exclude_args { + tar_cmd.arg(excl); + } + for src in &source_paths { + tar_cmd.arg(src); + } + + let tar_status = tar_cmd.status()?; + if !tar_status.success() { + return Err("Fehler beim Erstellen des Tar-Backups".into()); + } + + // Log-Dateien behandeln + cmd!("find", "/var/log", "-type", "f", "-name", "*.log", "-exec", "truncate", "-s", "0", "{}", ";") + .run()?; + cmd!("find", "/var/log", "-type", "f", "-name", "*.gz", "-exec", "rm", "-f", "{}", ";") + .run()?; + + println!("Backup erfolgreich als {}", backup_path.display()); + Ok(()) +} |