From e67e3a50889aaa8eefbb334ef408057a2411963f Mon Sep 17 00:00:00 2001 From: Leonard Kugis Date: Thu, 16 Oct 2025 00:38:51 +0200 Subject: Also encrypting / packing meta files --- src/main.rs | 298 +++++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 234 insertions(+), 64 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index 4eb544a..59008e3 100755 --- a/src/main.rs +++ b/src/main.rs @@ -240,7 +240,7 @@ struct Ctx { state_dir: PathBuf, mounts_dir: PathBuf, work_dir: PathBuf, - tar_snapshot: PathBuf, // GNU-tar Snapshot-Datei + tar_snapshot: PathBuf, // GNU-tar Snapshot-Datei (materialisiert für tar) min_keep: usize, max_keep: Option, include: Vec, @@ -312,6 +312,11 @@ impl Ctx { .map(|s| s.ends_with(".squashfs") || s.ends_with(".squashfs.luks")) .unwrap_or(false) }) + .filter(|p| { + // meta-Container NICHT als "Snapshot" zählen + let fname = p.file_name().and_then(|s| s.to_str()).unwrap_or(""); + fname != "meta.squashfs" && fname != "meta.squashfs.luks" + }) .collect(); entries.sort(); Ok(entries) @@ -322,10 +327,13 @@ impl Ctx { self.state_dir.join(name) } - fn manifest_path(&self, idx: usize) -> PathBuf { - self.state_dir.join(format!("manifest_{:04}.txt", idx)) + fn manifest_name(idx: usize) -> String { + format!("manifest_{:04}.txt", idx) } + fn meta_plain_path(&self) -> PathBuf { self.state_dir.join("meta.squashfs") } + fn meta_luks_path(&self) -> PathBuf { self.state_dir.join("meta.squashfs.luks") } + fn temp_path(&self, stem: &str) -> PathBuf { let ts = chrono::Local::now().format("%Y%m%d-%H%M%S"); self.work_dir.join(format!("{}_{}", stem, ts)) @@ -377,6 +385,10 @@ fn run(cmd:&mut Command, desc:&str)->Result<()>{ Ok(()) } +fn run_ok_status(cmd:&mut Command)->std::io::Result{ + cmd.status() +} + fn run_interactive(cmd:&mut Command, desc:&str)->Result<()>{ cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit()).stderr(Stdio::inherit()); run(cmd, desc) @@ -480,29 +492,12 @@ fn collect_manifest(ctx: &Ctx) -> Result<(HashMap, BTreeSet) -> Result<()> { - let path = ctx.manifest_path(idx); - let mut f = fs::File::create(&path).with_context(|| format!("manifest schreiben: {}", path.display()))?; - for line in manifest { - writeln!(f, "{line}")?; - } - Ok(()) -} - -fn load_manifest(ctx:&Ctx, idx: usize) -> Result> { - let path = ctx.manifest_path(idx); - let text = fs::read_to_string(&path) - .with_context(|| format!("manifest lesen: {}", path.display()))?; - Ok(text.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect()) -} - -/* ---------- TAR Streaming Filter: nur GNU dumpdir ('D') ausfiltern ---------- */ +/* ---------- TAR Streaming Filter: GNU 'D' dumpdir strip ---------- */ fn is_zero_block(b: &[u8]) -> bool { b.iter().all(|&x| x == 0) } -// Achtung: nur klassische oktale Größen (reicht in der Praxis). fn parse_tar_size_octal(field: &[u8]) -> u64 { let mut s = 0u64; for &c in field { @@ -513,18 +508,14 @@ fn parse_tar_size_octal(field: &[u8]) -> u64 { s } -/// Filtert einen Tar-Stream: -/// - **überspringt GNU incremental dumpdirs** (Typeflag 'D') -/// - alle anderen Header/Datenteile werden unverändert durchgereicht. fn forward_tar_strip_gnu_dumpdirs(mut r: R, mut w: W) -> Result<()> { let mut header = [0u8; 512]; - let mut buf = vec![0u8; 1024 * 1024]; // 1 MiB Puffer + let mut buf = vec![0u8; 1024 * 1024]; loop { r.read_exact(&mut header).with_context(|| "read tar header")?; if is_zero_block(&header) { - // Ende: zwei Nullblöcke weiterreichen w.write_all(&header)?; r.read_exact(&mut header).with_context(|| "read tar trailing zero")?; w.write_all(&header)?; @@ -537,7 +528,6 @@ fn forward_tar_strip_gnu_dumpdirs(mut r: R, mut w: W) -> Resu let padded = ((size + 511) / 512) * 512; if typeflag == 'D' { - // GNU dumpdir: Daten überspringen, NICHT schreiben let mut to_skip = padded; while to_skip > 0 { let take = to_skip.min(buf.len() as u64) as usize; @@ -547,7 +537,6 @@ fn forward_tar_strip_gnu_dumpdirs(mut r: R, mut w: W) -> Resu continue; } - // normaler Eintrag → Header + Daten durchreichen w.write_all(&header)?; let mut remaining = padded as i64; while remaining > 0 { @@ -561,6 +550,160 @@ fn forward_tar_strip_gnu_dumpdirs(mut r: R, mut w: W) -> Resu Ok(()) } +/* ---------- Meta-Container (Manifeste + tar.snapshot) ---------- */ + +fn meta_existing(ctx: &Ctx) -> Option { + let luks = ctx.meta_luks_path(); + let plain = ctx.meta_plain_path(); + if luks.exists() { Some(luks) } + else if plain.exists() { Some(plain) } + else { None } +} + +fn open_meta_read(ctx:&Ctx) -> Result /*mapper*/)>> { + if let Some(img) = meta_existing(ctx) { + if img.extension().and_then(|e| e.to_str()) == Some("luks") { + require_root("open meta LUKS")?; + let mapper = "squashr_meta".to_string(); + let mut o = Command::new("cryptsetup"); + if let Some(args)=ctx.luks_open_args.as_ref(){ for t in shell_split(args){ o.arg(t);} } + o.arg("open").arg(&img).arg(&mapper); + cryptsetup_run(&mut o, ctx.luks_pass.as_deref(), "cryptsetup open (meta)")?; + Ok(Some((PathBuf::from(format!("/dev/mapper/{mapper}")), Some(mapper)))) + } else { + Ok(Some((img, None))) + } + } else { + Ok(None) + } +} + +fn close_meta_mapper(mapper: Option) { + if let Some(m) = mapper { + let _ = Command::new("cryptsetup").arg("close").arg(&m).status(); + } +} + +fn extract_all_meta_to(ctx:&Ctx, dest:&Path) -> Result<()> { + if let Some((src, mapper)) = open_meta_read(ctx)? { + fs::create_dir_all(dest)?; + let mut cmd = Command::new("unsquashfs"); + cmd.arg("-d").arg(dest).arg(&src); + run(&mut cmd, "unsquashfs meta")?; + close_meta_mapper(mapper); + } else { + fs::create_dir_all(dest)?; + } + Ok(()) +} + +fn extract_file_from_meta(ctx:&Ctx, filename:&str, dest_path:&Path) -> Result { + if let Some((src, mapper)) = open_meta_read(ctx)? { + let tmp = ctx.temp_path("meta.extract"); + fs::create_dir_all(&tmp)?; + // nur die gewünschte Datei extrahieren + let mut cmd = Command::new("unsquashfs"); + cmd.arg("-d").arg(&tmp).arg(&src).arg(filename); + let st = run_ok_status(&mut cmd).with_context(|| "run unsquashfs")?; + let ok = st.success(); + let srcf = tmp.join(filename); + let mut present = false; + if ok && srcf.exists() { + fs::create_dir_all(dest_path.parent().unwrap_or_else(|| Path::new(".")))?; + fs::rename(&srcf, dest_path)?; + present = true; + } + // aufräumen + let _ = fs::remove_dir_all(&tmp); + close_meta_mapper(mapper); + Ok(present) + } else { + Ok(false) + } +} + +fn cleanup_plain_meta_files(ctx:&Ctx) { + // tar.snapshot + alle manifest_* im state_dir entfernen, falls vorhanden + let _ = fs::remove_file(&ctx.tar_snapshot); + if let Ok(rd) = fs::read_dir(&ctx.state_dir) { + for e in rd.flatten() { + if let Some(name) = e.file_name().to_str() { + if name.starts_with("manifest_") && name.ends_with(".txt") { + let _ = fs::remove_file(e.path()); + } + } + } + } +} + +fn rebuild_meta_from_staging(ctx:&Ctx, staging:&Path) -> Result<()> { + // meta tmp plain + let tmp_plain = ctx.temp_path("meta.plain.squashfs"); + if tmp_plain.exists() { let _ = fs::remove_file(&tmp_plain); } + let mut cmd = Command::new("mksquashfs"); + cmd.arg(staging).arg(&tmp_plain).arg("-no-progress").arg("-no-recovery"); + if ctx.comp_enable { + cmd.arg("-comp").arg(&ctx.comp_algo); + if let Some(extra)=ctx.comp_args.as_ref(){ for tok in shell_split(extra){ cmd.arg(tok);} } + } + run(&mut cmd, "mksquashfs (meta)")?; + + // Ziel festlegen (verschlüsselt/unkryptiert) + let plain = ctx.meta_plain_path(); + let luks = ctx.meta_luks_path(); + + // vorhandene Varianten löschen, um Inkonsistenzen zu vermeiden + let _ = fs::remove_file(&plain); + let _ = fs::remove_file(&luks); + + if ctx.luks_enable { + encrypt_into_luks(ctx, &tmp_plain, &luks)?; + let _ = fs::remove_file(&tmp_plain); + } else { + fs::rename(&tmp_plain, &plain)?; + } + + // Klartextreste entfernen + cleanup_plain_meta_files(ctx); + Ok(()) +} + +fn save_manifest_to_dir(dest_dir:&Path, idx: usize, manifest: &BTreeSet) -> Result<()> { + fs::create_dir_all(dest_dir)?; + let path = dest_dir.join(Ctx::manifest_name(idx)); + let mut f = fs::File::create(&path).with_context(|| format!("manifest schreiben: {}", path.display()))?; + for line in manifest { + writeln!(f, "{line}")?; + } + Ok(()) +} + +fn load_manifest(ctx:&Ctx, idx: usize) -> Result> { + // Primär aus Meta-Container lesen + let fname = Ctx::manifest_name(idx); + let tmp = ctx.temp_path("manifest.read"); + fs::create_dir_all(&tmp)?; + let out = tmp.join(&fname); + if extract_file_from_meta(ctx, &fname, &out)? { + let text = fs::read_to_string(&out) + .with_context(|| format!("manifest lesen: {}", out.display()))?; + let _ = fs::remove_dir_all(&tmp); + return Ok(text.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect()); + } + // Fallback (falls sehr früher Zustand): Klartextdatei + let legacy = ctx.state_dir.join(&fname); + if legacy.exists() { + let text = fs::read_to_string(&legacy) + .with_context(|| format!("manifest lesen: {}", legacy.display()))?; + let _ = fs::remove_dir_all(&tmp); + return Ok(text.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect()); + } + let _ = fs::remove_dir_all(&tmp); + bail!("manifest {} not found", fname); +} + +/* ---------- TAR → SQFSTAR ---------- */ + /// Erzeugt inkrementellen *tar* und streamt ihn (mit Filter) in `sqfstar`. /// Rückgabewert: true, falls tar mit Non-Zero beendet wurde (Warnung). fn build_squash_image_tar_sqfstar( @@ -708,7 +851,7 @@ fn encrypt_into_luks(ctx:&Ctx, plain:&Path, out_luks:&Path)->Result<()>{ Ok(()) } -/* ---------- Mount Helfer: Kernel → FUSE Fallback ---------- */ +/* ---------- Mount Helfer / FUSE Fallback ---------- */ fn try_mount_kernel_squashfs(source:&Path, mnt:&Path, use_loop:bool) -> Result { let mut cmd = Command::new("mount"); @@ -787,10 +930,19 @@ fn loop_backing_file(dev: &str) -> Option { /* ---------- Backup ---------- */ +fn ensure_tar_snapshot_materialized(ctx:&Ctx) -> Result<()> { + // Falls in Meta enthalten → herausziehen, sonst tar lässt neu schreiben + let _ = extract_file_from_meta(ctx, "tar.snapshot", &ctx.tar_snapshot)?; + Ok(()) +} + fn cmd_backup(ctx:&mut Ctx)->Result<()>{ ensure_includes_nonempty(ctx)?; truncate_logs(ctx)?; + // Vorbereitungen: tar.snapshot bereitstellen + ensure_tar_snapshot_materialized(ctx)?; + let snaps = ctx.list_snapshots()?; let next_idx = snaps.len() + 1; @@ -833,8 +985,19 @@ fn cmd_backup(ctx:&mut Ctx)->Result<()>{ out }; - // 5) Manifest speichern - save_manifest(ctx, next_idx, &manifest_now)?; + // 5) Meta-Container neu bauen (alle Manifeste + tar.snapshot) + let staging = ctx.temp_path("meta.staging"); + extract_all_meta_to(ctx, &staging)?; + // neues Manifest hinein + save_manifest_to_dir(&staging, next_idx, &manifest_now)?; + // aktualisierte tar.snapshot hinein (falls vorhanden) + if ctx.tar_snapshot.exists() { + let dst = staging.join("tar.snapshot"); + if dst.exists() { let _ = fs::remove_file(&dst); } + fs::copy(&ctx.tar_snapshot, &dst)?; + } + rebuild_meta_from_staging(ctx, &staging)?; + let _ = fs::remove_dir_all(&staging); if tar_warn { eprintln!("[warn] Snapshot {:04} created, but tar reported non-zero exit. Some files may be missing or changed during read.", next_idx); @@ -1058,10 +1221,7 @@ fn cmd_delete(ctx:&mut Ctx, s:usize)->Result<()>{ if path_plain.exists(){ fs::remove_file(&path_plain)?; } if path_luks.exists(){ fs::remove_file(&path_luks)?; } - // Manifest des gelöschten Snapshots entfernen - let _ = fs::remove_file(ctx.manifest_path(s)); - - // Alle nachfolgenden Snapshots und Manifeste umnummerieren + // Alle nachfolgenden Snapshots umnummerieren let snaps = ctx.list_snapshots()?; for p in snaps { let n = parse_snap_index(&p)?; @@ -1071,15 +1231,24 @@ fn cmd_delete(ctx:&mut Ctx, s:usize)->Result<()>{ fs::rename(&p, &newp)?; } } - // Manifeste verschieben + + // Meta-Container anpassen: manifest_s löschen, >s dekrementieren + let staging = ctx.temp_path("meta.staging"); + extract_all_meta_to(ctx, &staging)?; + let to_del = staging.join(Ctx::manifest_name(s)); + let _ = fs::remove_file(&to_del); + // renumber let mut k = s + 1; loop { - let from = ctx.manifest_path(k); + let from = staging.join(Ctx::manifest_name(k)); if !from.exists() { break; } - let to = ctx.manifest_path(k - 1); + let to = staging.join(Ctx::manifest_name(k - 1)); + if to.exists() { let _ = fs::remove_file(&to); } fs::rename(&from, &to)?; k += 1; } + rebuild_meta_from_staging(ctx, &staging)?; + let _ = fs::remove_dir_all(&staging); println!("Deleted snapshot {}, decrementing children.", s); Ok(()) @@ -1087,15 +1256,11 @@ fn cmd_delete(ctx:&mut Ctx, s:usize)->Result<()>{ fn cmd_new(ctx:&mut Ctx)->Result<()>{ for s in ctx.list_snapshots()? { fs::remove_file(s)?; } - // Manifeste und tar.snapshot löschen - for e in fs::read_dir(&ctx.state_dir)? { - let p = e?.path(); - if let Some(name) = p.file_name().and_then(|s| s.to_str()) { - if name.starts_with("manifest_") || name == "tar.snapshot" { - let _ = fs::remove_file(&p); - } - } - } + // Meta-Container & Klartextreste löschen + let _ = fs::remove_file(ctx.meta_plain_path()); + let _ = fs::remove_file(ctx.meta_luks_path()); + cleanup_plain_meta_files(ctx); + println!("State cleared. Creating new initial snapshot."); cmd_backup(ctx) } @@ -1111,11 +1276,9 @@ fn cmd_umount(ctx:&mut Ctx, target: Option<&Path>) -> Result<()> { let mut loops_in_use: HashSet = HashSet::new(); if let Some(t) = target { - // nur diesen Ziel-Pfad (falls gemountet) if is_target_mounted(t) { todo.push(abspath(t)); } - // Quelle merken (für späteres Close/Detach) if let Some(m) = mounts_before.iter().find(|m| abspath(&m.tgt) == abspath(t)) { if m.src.starts_with("/dev/mapper/squashr_") { mappers_in_use.insert(Path::new(&m.src).file_name().unwrap_or_default().to_string_lossy().to_string()); @@ -1124,13 +1287,13 @@ fn cmd_umount(ctx:&mut Ctx, target: Option<&Path>) -> Result<()> { } } } else { - // 1) Alle Mounts unter mounts_dir (snap_XXXX, merge_XXXX, etc.) + // 1) Alle Mounts unter mounts_dir for m in &mounts_before { if m.tgt.starts_with(&ctx.mounts_dir) { todo.push(m.tgt.clone()); } } - // 2) Overlay-Ziel(e), die von SquashR erstellt wurden: erkennbar an upperdir in ctx.work_dir + // 2) Overlay-Ziele mit upperdir in work_dir for m in &mounts_before { if m.fstype == "overlay" && m.opts.contains("upperdir=") { if let Some(start) = m.opts.find("upperdir=") { @@ -1142,7 +1305,7 @@ fn cmd_umount(ctx:&mut Ctx, target: Option<&Path>) -> Result<()> { } } } - // 3) Einzelsnapshot-Mounts (LUKS): Quelle /dev/mapper/squashr_* + // 3) Mapper-SquashFS for m in &mounts_before { if m.fstype == "squashfs" && m.src.starts_with("/dev/mapper/squashr_") { todo.push(m.tgt.clone()); @@ -1150,7 +1313,7 @@ fn cmd_umount(ctx:&mut Ctx, target: Option<&Path>) -> Result<()> { mappers_in_use.insert(mapper); } } - // 4) Einzelsnapshot-Mounts (Plain/Loop): Quelle /dev/loopX mit Backing-File im state_dir + // 4) Loop-SquashFS aus state_dir for m in &mounts_before { if m.fstype == "squashfs" && m.src.starts_with("/dev/loop") { if let Some(back) = loop_backing_file(&m.src) { @@ -1161,7 +1324,7 @@ fn cmd_umount(ctx:&mut Ctx, target: Option<&Path>) -> Result<()> { } } } - // 5) FUSE-Mounts von SquashFS-Dateien aus state_dir (oder Mapper) + // 5) FUSE mounts auf state_dir-Quellen oder Mapper for m in &mounts_before { if m.fstype.starts_with("fuse") { let src_path = PathBuf::from(&m.src); @@ -1177,7 +1340,7 @@ fn cmd_umount(ctx:&mut Ctx, target: Option<&Path>) -> Result<()> { } } - // Nur aktuell gemountete Ziele behalten (robust gegen stale Pfade) + // Nur aktuell gemountete Ziele behalten let mounted_now: HashSet = read_proc_mounts()? .into_iter() .map(|m| abspath(&m.tgt)) @@ -1220,11 +1383,10 @@ fn cmd_umount(ctx:&mut Ctx, target: Option<&Path>) -> Result<()> { } } - // Loop-Devices lösen, falls Backing-File im state_dir und nicht mehr gemountet + // Loop-Devices lösen for loopdev in loops_in_use { let still_mounted = mounts_after.iter().any(|m| m.src == loopdev); if !still_mounted { - // nur wenn Device existiert if Path::new(&loopdev).exists() { let _ = Command::new("losetup").arg("-d").arg(&loopdev).status(); vlog!(ctx, "[losetup] detach {}", loopdev); @@ -1328,7 +1490,7 @@ fn merge_first_two(ctx:&mut Ctx)->Result<()>{ } if s2.exists(){ fs::remove_file(s2)?; } - // Indizes > idx2 runterzählen (Dateien + Manifeste) + // Indizes > idx2 runterzählen (Dateien) let rest = ctx.list_snapshots()?; for p in rest { let n = parse_snap_index(&p)?; @@ -1338,19 +1500,27 @@ fn merge_first_two(ctx:&mut Ctx)->Result<()>{ fs::rename(&p, &newp)?; } } - // Manifeste: manifest_1 := manifest_2, danach alle >2 dekrementieren - let m1p = ctx.manifest_path(1); - let m2p = ctx.manifest_path(2); - if m1p.exists(){ fs::remove_file(&m1p).ok(); } + + // Meta-Container anpassen: manifest_1 := manifest_2; danach alle >2 dekrementieren + let staging = ctx.temp_path("meta.staging"); + extract_all_meta_to(ctx, &staging)?; + // manifest_2 -> manifest_1 (overwrite) + let m1p = staging.join(Ctx::manifest_name(1)); + let m2p = staging.join(Ctx::manifest_name(2)); + if m1p.exists(){ let _ = fs::remove_file(&m1p); } if m2p.exists(){ fs::rename(&m2p, &m1p).ok(); } + // ab 3 dekrementieren let mut k = 3usize; loop { - let from = ctx.manifest_path(k); + let from = staging.join(Ctx::manifest_name(k)); if !from.exists() { break; } - let to = ctx.manifest_path(k-1); + let to = staging.join(Ctx::manifest_name(k-1)); + if to.exists() { let _ = fs::remove_file(&to); } fs::rename(&from, &to)?; k += 1; } + rebuild_meta_from_staging(ctx, &staging)?; + let _ = fs::remove_dir_all(&staging); println!("Merged snapshots {:04} + {:04} → {:04}.", idx1, idx2, idx1); Ok(()) -- cgit v1.2.3