use chrono::Local; use clap::Parser; use duct::cmd; use regex::Regex; use serde::Deserialize; use std::{ error::Error, fs::{self, File}, io::{BufRead, BufReader}, path::{Path, PathBuf}, process::Command, }; /// Backup-Tool mit inkrementeller Sicherung und Rotation #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { /// Pfad zur Konfigurationsdatei (JSON) #[arg(short, long)] config: PathBuf, /// Ausführliche Ausgabe #[arg(short, long, default_value_t = false)] verbose: bool, } #[derive(Debug, Deserialize)] struct Config { backup_dir: String, rotate_dir: String, timestamp_file: String, source_file: String, exclude_file: String, cycles: Option, zstd_level: Option, zstd_threads: Option, skip_db: Option } fn read_lines>(path: P) -> Result, Box> { 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> { 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::() { if num > highest { highest = num; } } } } } Ok(highest) } fn main() -> Result<(), Box> { let args = Args::parse(); let config_str = fs::read_to_string(&args.config)?; let config: Config = serde_json::from_str(&config_str)?; let backup_dir = Path::new(&config.backup_dir); let rotate_dir = Path::new(&config.rotate_dir); let timestamp_file = Path::new(&config.timestamp_file); let source_list = read_lines(&config.source_file)?; let exclude_list = read_lines(&config.exclude_file)?; let zstd_level = config.zstd_level.unwrap_or(9).clamp(1, 19); let zstd_threads = config.zstd_threads.unwrap_or(1); let zstd_cmd = format!("zstd -{} -T{}", zstd_level, zstd_threads); fs::create_dir_all(backup_dir)?; let mut source_paths: Vec = vec![]; for s in &source_list { if Path::new(s).exists() { source_paths.push(s.clone()); } else if args.verbose { eprintln!("WARNING: '{}' existiert nicht", s); } } let mut exclude_args: Vec = vec![]; for e in &exclude_list { if Path::new(e).exists() { exclude_args.push(format!("--exclude={}", e)); } else if args.verbose { eprintln!("WARNING: '{}' existiert nicht", e); } } let mut backup_nr = get_backup_number(backup_dir)? + 1; if backup_nr > config.cycles.unwrap_or(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)?; } if args.verbose { println!("INFO: Backup-Rotation durchgeführt"); } // Nur wenn skip_db != true if config.skip_db.unwrap_or(false) == false { // Dump 1 let dump1 = rotate_subdir.join("backup_mastodon-db-1.sql.zst"); cmd!("docker", "exec", "mastodon-db-1", "pg_dumpall", "-U", "postgres") .pipe(cmd!("sh", "-c", &zstd_cmd)) .stdout_file(File::create(dump1)?) .run()?; // Dump 2 let dump2 = rotate_subdir.join("backup_db.sql.zst"); cmd!("pg_dumpall", "-U", "postgres") .pipe(cmd!("sh", "-c", &zstd_cmd)) .stdout_file(File::create(dump2)?) .run()?; if args.verbose { println!("INFO: Datenbank-Dumps gespeichert"); } } else if args.verbose { println!("INFO: Datenbank-Dumps wurden übersprungen (skip_db_dumps=true)"); } } 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"); for excl in &exclude_args { tar_cmd.arg(excl); } tar_cmd .arg("-cvp") .arg("-I") .arg(zstd_cmd) .arg("-f") .arg(&backup_path) .arg("-g") .arg(timestamp_file); for src in &source_paths { tar_cmd.arg(src); } let tar_status = tar_cmd.status()?; // 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()?; if !tar_status.success() { return Err("ERROR: Fehler beim Erstellen des Tar-Backups".into()); } println!("INFO: Backup erfolgreich als: {}", backup_path.display()); Ok(()) }