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>(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 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 = 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 = 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(()) }