diff options
| author | Leonard Kugis <leonard@kug.is> | 2026-01-19 21:39:25 +0100 |
|---|---|---|
| committer | Leonard Kugis <leonard@kug.is> | 2026-01-19 21:39:25 +0100 |
| commit | 3b34a2be2aab19ff4ed80cb58b956c6db5725e20 (patch) | |
| tree | de96c82c7da6bbaee54022760ad1830321f8ca11 | |
| parent | cf472dd80aedab34e7907f86b5d64ea268a4127d (diff) | |
| download | squashr-3b34a2be2aab19ff4ed80cb58b956c6db5725e20.tar.gz | |
| -rwxr-xr-x | src/main.rs | 171 |
1 files changed, 23 insertions, 148 deletions
diff --git a/src/main.rs b/src/main.rs index a2c8b5d..6f9239a 100755 --- a/src/main.rs +++ b/src/main.rs @@ -29,7 +29,6 @@ use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use walkdir::WalkDir; -// kleine Helfer für Verbose-Logs macro_rules! vlog { ($ctx:expr, $($arg:tt)*) => { if $ctx.verbose { @@ -82,15 +81,12 @@ struct Cli { squashr_tar_args: Option<String>, - /// Passphrase für cryptsetup; ACHTUNG: Sichtbar im Prozesslisting. #[arg(long)] squashr_cryptsetup_pass: Option<String>, - /// Datei mit Passphrase (roh, binär möglich); hat Vorrang, wenn beides gesetzt ist. #[arg(long, value_hint=ValueHint::FilePath)] squashr_cryptsetup_pass_file: Option<PathBuf>, - /// Mehr Ausgaben, inkl. Auflistung gesicherter Dateien #[arg(short, long)] verbose: bool, @@ -105,7 +101,6 @@ enum Cmd { New, Mount { #[arg(short = 's')] s: Option<usize>, target: PathBuf }, Delete { #[arg(short = 's')] s: usize }, - /// Hängt SquashR-relevante Mounts aus (ohne Argument: alle; mit Pfad: nur diesen). Umount { target: Option<PathBuf> }, Unify { #[arg(short = 's')] s: usize, target: PathBuf }, } @@ -167,7 +162,7 @@ fn load_config(path: Option<&Path>) -> Result<Config> { let mut cfg = Config::default(); if let Some(p) = path { if p.exists() { - let text = fs::read_to_string(p).with_context(|| format!("Konfig lesen: {}", p.display()))?; + let text = fs::read_to_string(p).with_context(|| format!("Reading config: {}", p.display()))?; let mut map: HashMap<String, String> = HashMap::new(); for line in text.lines() { let l = line.trim(); @@ -247,7 +242,7 @@ struct Ctx { state_dir: PathBuf, mounts_dir: PathBuf, work_dir: PathBuf, - tar_snapshot: PathBuf, // GNU-tar Snapshot-Datei (materialisiert für tar) + tar_snapshot: PathBuf, min_keep: usize, max_keep: Option<usize>, include: Vec<PathBuf>, @@ -280,11 +275,10 @@ impl Ctx { let max_raw = cfg.SQUASHR_N_SNAPSHOTS_MAX.unwrap_or(0); let max_keep = if max_raw == 0 { None } else { Some(max_raw) }; - // Passphrase ermitteln (CLI-String hat Vorrang vor Datei) let luks_pass = if let Some(p) = cfg.SQUASHR_CRYPTSETUP_PASS { Some(p.into_bytes()) } else if let Some(f) = cfg.SQUASHR_CRYPTSETUP_PASS_FILE { - Some(fs::read(&f).with_context(|| format!("Pass-Datei lesen: {}", f.display()))?) + Some(fs::read(&f).with_context(|| format!("Reading key file: {}", f.display()))?) } else { None }; @@ -337,7 +331,6 @@ impl Ctx { .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" }) @@ -394,7 +387,6 @@ fn abs_key(p: &Path) -> Result<String> { fn parse_snap_index(p:&Path)->Result<usize>{ let fname = p.file_name().and_then(|s| s.to_str()).ok_or_else(||anyhow!("Invalid filename: {}", p.display()))?; - // robust: nur führende 4 Ziffern extrahieren let re = Regex::new(r"^(\d{4})").unwrap(); let caps = re.captures(fname).ok_or_else(|| anyhow!("No index found: {}", p.display()))?; Ok(caps.get(1).unwrap().as_str().parse::<usize>().unwrap()) @@ -465,7 +457,6 @@ fn build_prune_set_for_root(abs_root: &Path, ctx: &Ctx) -> Vec<PathBuf> { v.push(ex_abs); } } - // stets interne Verzeichnisse ausschließen for auto in [&ctx.state_dir, &ctx.mounts_dir, &ctx.work_dir] { let ex_abs = canonicalize_or_same(auto); if ex_abs.starts_with(abs_root) { v.push(ex_abs); } @@ -517,8 +508,6 @@ fn collect_manifest(ctx: &Ctx) -> Result<(HashMap<String, PathBuf>, BTreeSet<Str Ok((roots, manifest)) } -/* ---------- TAR Streaming Filter: GNU 'D' dumpdir strip ---------- */ - fn is_zero_block(b: &[u8]) -> bool { b.iter().all(|&x| x == 0) } @@ -535,7 +524,6 @@ fn parse_tar_size_octal(field: &[u8]) -> u64 { fn parse_tar_size(field: &[u8]) -> u64 { if field.is_empty() { return 0; } - // GNU base-256? if (field[0] & 0x80) != 0 { let mut v: u128 = 0; let mut first = true; @@ -546,7 +534,6 @@ fn parse_tar_size(field: &[u8]) -> u64 { } return v as u64; } - // Standard: oktal bis Space/NUL let mut s = 0u64; for &c in field { match c { @@ -593,14 +580,14 @@ fn is_valid_tar_header(h: &[u8; 512]) -> bool { fn copy_blocks_resync<R: Read, W: Write>( r: &mut R, - mut out: Option<&mut W>, // <- mut + mut out: Option<&mut W>, mut bytes: u64, ) -> Result<[u8; 512]> { let mut buf = vec![0u8; 128 * 1024]; while bytes > 0 { let take = (bytes as usize).min(buf.len()); r.read_exact(&mut buf[..take])?; - if let Some(w) = out.as_mut() { // <- as_mut statt as_ref + if let Some(w) = out.as_mut() { w.write_all(&buf[..take])?; } bytes -= take as u64; @@ -612,7 +599,7 @@ fn copy_blocks_resync<R: Read, W: Write>( if is_valid_tar_header(&next) { return Ok(next); } - if let Some(w) = out.as_mut() { // <- as_mut statt as_ref + if let Some(w) = out.as_mut() { w.write_all(&next)?; } } @@ -620,18 +607,14 @@ fn copy_blocks_resync<R: Read, W: Write>( fn forward_tar_strip_gnu_dumpdirs<R: Read, W: Write>(mut r: R, mut w: W) -> Result<()> { let mut header = [0u8; 512]; - let mut pending: Vec<Vec<u8>> = Vec::new(); // komplette Records L/K/x/g puffern + let mut pending: Vec<Vec<u8>> = Vec::new(); - // erstes Header-Block lesen r.read_exact(&mut header).with_context(|| "read tar header")?; loop { - // Trailer? if is_zero_block(&header) { - // zweiter Zero-Block gehört dazu let mut zero2 = [0u8; 512]; r.read_exact(&mut zero2).with_context(|| "read tar trailing zero")?; - // evtl. pending verwerfen (sollte leer sein) pending.clear(); w.write_all(&header)?; w.write_all(&zero2)?; @@ -648,7 +631,6 @@ fn forward_tar_strip_gnu_dumpdirs<R: Read, W: Write>(mut r: R, mut w: W) -> Resu let padded = (size + 511) & !511; match typeflag { - // L/K/x/g: vollständig puffern (Header + Payload (+ evtl. Spill)) 'L' | 'K' | 'x' | 'g' => { let mut rec: Vec<u8> = Vec::with_capacity(512); rec.extend_from_slice(&header); @@ -658,14 +640,12 @@ fn forward_tar_strip_gnu_dumpdirs<R: Read, W: Write>(mut r: R, mut w: W) -> Resu continue; // weiter mit neuem 'header' } - // GNU dumpdir → komplett wegwerfen, inkl. evtl. vorausgehender L/K/x/g 'D' => { header = copy_blocks_resync(&mut r, None::<&mut W>, padded)?; pending.clear(); continue; } - // alle „normalen“ Einträge: erst pending ausgeben, dann diesen Record _ => { for rec in pending.drain(..) { w.write_all(&rec)?; } // aktuellen Header schreiben @@ -680,8 +660,6 @@ fn forward_tar_strip_gnu_dumpdirs<R: Read, W: Write>(mut r: R, mut w: W) -> Resu Ok(()) } -/* ---------- Meta-Container (Manifeste + tar.snapshot) ---------- */ - fn meta_existing(ctx: &Ctx) -> Option<PathBuf> { let luks = ctx.meta_luks_path(); let plain = ctx.meta_plain_path(); @@ -731,7 +709,6 @@ fn extract_file_from_meta(ctx:&Ctx, filename:&str, dest_path:&Path) -> Result<bo 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")?; @@ -743,7 +720,6 @@ fn extract_file_from_meta(ctx:&Ctx, filename:&str, dest_path:&Path) -> Result<bo fs::rename(&srcf, dest_path)?; present = true; } - // aufräumen let _ = fs::remove_dir_all(&tmp); close_meta_mapper(mapper); Ok(present) @@ -753,7 +729,6 @@ fn extract_file_from_meta(ctx:&Ctx, filename:&str, dest_path:&Path) -> Result<bo } 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.work_dir) { for e in rd.flatten() { @@ -767,7 +742,6 @@ fn cleanup_plain_meta_files(ctx:&Ctx) { } 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"); @@ -778,11 +752,9 @@ fn rebuild_meta_from_staging(ctx:&Ctx, staging:&Path) -> Result<()> { } 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); @@ -793,7 +765,6 @@ fn rebuild_meta_from_staging(ctx:&Ctx, staging:&Path) -> Result<()> { fs::rename(&tmp_plain, &plain)?; } - // Klartextreste entfernen cleanup_plain_meta_files(ctx); Ok(()) } @@ -801,7 +772,7 @@ fn rebuild_meta_from_staging(ctx:&Ctx, staging:&Path) -> Result<()> { fn save_manifest_to_dir(dest_dir:&Path, idx: usize, manifest: &BTreeSet<String>) -> 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()))?; + let mut f = fs::File::create(&path).with_context(|| format!("Writing manifest: {}", path.display()))?; for line in manifest { writeln!(f, "{line}")?; } @@ -809,22 +780,20 @@ fn save_manifest_to_dir(dest_dir:&Path, idx: usize, manifest: &BTreeSet<String>) } fn load_manifest(ctx:&Ctx, idx: usize) -> Result<BTreeSet<String>> { - // 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()))?; + .with_context(|| format!("Reading manifest: {}", 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()))?; + .with_context(|| format!("Reading manifest: {}", legacy.display()))?; let _ = fs::remove_dir_all(&tmp); return Ok(text.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect()); } @@ -832,8 +801,6 @@ fn load_manifest(ctx:&Ctx, idx: usize) -> Result<BTreeSet<String>> { bail!("manifest {} not found", fname); } -/* ---------- TAR → SQFSTAR ---------- */ - fn build_squash_image_tar_sqfstar( ctx: &Ctx, out: &Path, @@ -846,7 +813,6 @@ fn build_squash_image_tar_sqfstar( .with_context(|| format!("remove old output: {}", out.display()))?; } - // --- sqfstar: liest Tar von stdin, schreibt SquashFS nach `out` let mut sq = std::process::Command::new("sqfstar"); if ctx.comp_enable { sq.arg("-comp").arg(&ctx.comp_algo); @@ -860,10 +826,8 @@ fn build_squash_image_tar_sqfstar( .take() .ok_or_else(|| anyhow::anyhow!("cannot open sqfstar stdin"))?; - // --- tar: POSIX/PAX + listed-incremental, KEINE Filterei let mut tar = std::process::Command::new("tar"); - // Excludes relativ zu / let mut all_excludes: Vec<std::path::PathBuf> = ctx.exclude.clone(); for auto in [&ctx.state_dir, &ctx.mounts_dir, &ctx.work_dir] { all_excludes.push(auto.clone()); @@ -876,8 +840,6 @@ fn build_squash_image_tar_sqfstar( } tar.arg("--format=posix") - // Entfernt das GNU.dumpdir-Attribut aus PAX-XHeaders → keine Warnungen mehr - //.arg("--pax-option").arg("delete=GNU.dumpdir,exthdr.name=%d/PaxHeaders/%f") .arg("-C").arg("/") .arg("--null") .arg("--files-from=-") @@ -886,7 +848,6 @@ fn build_squash_image_tar_sqfstar( .arg("--numeric-owner") .arg("--ignore-failed-read"); - // Benutzerdefinierte TAR-Args (am Ende → überschreiben Defaults, "last wins") if let Some(extra) = ctx.tar_args.as_ref() { for tok in shell_split(extra) { tar.arg(tok); @@ -898,7 +859,6 @@ fn build_squash_image_tar_sqfstar( tar.stderr(std::process::Stdio::inherit()); let mut tar_child = tar.spawn().with_context(|| "Unable to start tar")?; - // Pfadliste (roots) in tar stdin schreiben, 0-terminiert { let mut w = std::io::BufWriter::new( tar_child.stdin.take().ok_or_else(|| anyhow::anyhow!("cannot open tar stdin"))? @@ -912,7 +872,6 @@ fn build_squash_image_tar_sqfstar( w.flush()?; } - // tar stdout *direkt* zu sqfstar stdin pumpen (ohne Filter) { let mut tar_stdout = tar_child.stdout .take() @@ -923,7 +882,6 @@ fn build_squash_image_tar_sqfstar( } drop(sq_stdin); - // Exit-Codes prüfen let tar_status = tar_child.wait().with_context(|| "waiting for tar failed")?; let tar_code = tar_status.code().unwrap_or(-1); let tar_nonzero = tar_code != 0; @@ -979,8 +937,6 @@ fn encrypt_into_luks(ctx:&Ctx, plain:&Path, out_luks:&Path)->Result<()>{ Ok(()) } -/* ---------- Mount Helfer / FUSE Fallback ---------- */ - fn try_mount_kernel_squashfs(source:&Path, mnt:&Path, use_loop:bool) -> Result<bool> { let mut cmd = Command::new("mount"); cmd.arg("-t").arg("squashfs"); @@ -1003,8 +959,6 @@ fn try_mount_fuse_squashfs(source:&Path, mnt:&Path) -> Result<bool> { } } -/* ---------- Mount-Info Utilities ---------- */ - #[derive(Debug)] struct MountEntry { src:String, tgt:PathBuf, fstype:String, opts:String } @@ -1012,7 +966,6 @@ fn read_proc_mounts() -> Result<Vec<MountEntry>> { let text = fs::read_to_string("/proc/mounts").context("read /proc/mounts")?; let mut out = Vec::new(); for line in text.lines() { - // /proc/mounts: src tgt fstype opts 0 0 let mut it = line.split_whitespace(); let (Some(src), Some(tgt), Some(fstype), Some(opts)) = (it.next(), it.next(), it.next(), it.next()) else { continue }; out.push(MountEntry{ @@ -1041,7 +994,6 @@ fn sort_paths_deep_first(mut v: Vec<PathBuf>) -> Vec<PathBuf> { } fn loop_backing_file(dev: &str) -> Option<PathBuf> { - // Erwartet "/dev/loopX" let name = Path::new(dev).file_name()?.to_string_lossy().to_string(); let candidates = [ format!("/sys/block/{}/loop/backing_file", name), @@ -1099,10 +1051,7 @@ fn purge_dir(dir: &Path) { } } -/* ---------- 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(()) } @@ -1115,12 +1064,10 @@ fn truncate_logs(ctx:&Ctx)->Result<()>{ let p = entry.path(); if p.is_file() { let name = p.file_name().and_then(|s| s.to_str()).unwrap_or(""); - // komprimierte Logs löschen if name.ends_with(".gz") || name.ends_with(".xz") || name.ends_with(".zst") || name.ends_with(".bz2") { let _ = fs::remove_file(p); continue; } - // unkomprimierte Logs auf Länge 0 setzen let _ = fs::OpenOptions::new() .write(true) .open(p) @@ -1135,25 +1082,22 @@ 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; - // 1) Manifest aufnehmen (inkl. Excludes & interne Verzeichnisse) let (roots, manifest_now) = collect_manifest(ctx)?; let manifest_prev = if next_idx > 1 { load_manifest(ctx, next_idx - 1).ok() } else { None }; - // 2) Falls keine Änderung: minimalistisches Image erstellen (wie bisher) let no_changes = manifest_prev.as_ref().map_or(false, |m| m == &manifest_now); let plain_img = ctx.temp_path("snapshot.plain.squashfs"); let mut tar_warn = false; if no_changes { - vlog!(ctx, "[backup] no changes → creating minimal image {}", plain_img.display()); + vlog!(ctx, "[backup] no changes -> creating minimal image {}", plain_img.display()); let empty_src = ctx.temp_path("empty.src"); fs::create_dir_all(&empty_src)?; let mut cmd = Command::new("mksquashfs"); @@ -1164,11 +1108,9 @@ fn cmd_backup(ctx:&mut Ctx)->Result<()>{ } run(&mut cmd, "mksquashfs (empty)")?; } else { - // 3) tar (listed-incremental) → Filter (strip 'D') → sqfstar tar_warn = build_squash_image_tar_sqfstar(ctx, &plain_img, &roots)?; } - // 4) Optional: LUKS-Container schreiben let final_path = if ctx.luks_enable { let out = ctx.snapshot_path(next_idx, true); encrypt_into_luks(ctx, &plain_img, &out)?; @@ -1180,7 +1122,6 @@ fn cmd_backup(ctx:&mut Ctx)->Result<()>{ out }; - // 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 @@ -1215,8 +1156,6 @@ fn rotate_if_needed(ctx:&mut Ctx)->Result<()>{ Ok(()) } -/* ---------- Whiteouts / Mount / Overlay ---------- */ - fn create_whiteouts_unlink_list(ctx:&Ctx, upto:usize)->Result<Vec<String>>{ if upto == 0 { return Ok(vec![]); } let present = load_manifest(ctx, upto).unwrap_or_default(); @@ -1249,14 +1188,12 @@ fn mount_image_ro(ctx:&Ctx, img:&Path, mnt:&Path)->Result<()>{ let mnt_abs = abspath(mnt); if img.extension().and_then(|e| e.to_str()) == Some("luks") { - // LUKS → /dev/mapper/<mapper> let mapper = format!( "squashr_mount_{}", img.file_stem().and_then(|s| s.to_str()).unwrap_or("img") ); let dev = format!("/dev/mapper/{}", mapper); - // evtl. Alt-Mapping schließen let _ = Command::new("cryptsetup").arg("close").arg(&mapper).status(); let mut o = Command::new("cryptsetup"); @@ -1264,45 +1201,41 @@ fn mount_image_ro(ctx:&Ctx, img:&Path, mnt:&Path)->Result<()>{ o.arg("open").arg(img).arg(&mapper); cryptsetup_run(&mut o, ctx.luks_pass.as_deref(), "cryptsetup open (mount)")?; - // 1) Kernel-Mount versuchen match try_mount_kernel_squashfs(Path::new(&dev), &mnt_abs, false) { Ok(true) => return Ok(()), Ok(false) => { - eprintln!("[warn] Kernel-SquashFS-Mount fehlgeschlagen – versuche FUSE (squashfuse)."); + eprintln!("[warn] Kernel mount failed, attempting FUSE (squashfuse)."); } Err(e) => { - eprintln!("[warn] Kernel-SquashFS-Mount Fehler: {e} – versuche FUSE (squashfuse)."); + eprintln!("[warn] Kernel mount failed: {e} – attempting FUSE (squashfuse)."); } } - // 2) FUSE-Fallback versuchen match try_mount_fuse_squashfs(Path::new(&dev), &mnt_abs) { Ok(true) => return Ok(()), Ok(false) => { - // Mapping sauber schließen let _ = Command::new("cryptsetup").arg("close").arg(&mapper).status(); - bail!("FUSE-Mount (squashfuse) fehlgeschlagen. Prüfe Kernelmodul 'squashfs' oder installiere 'squashfuse'."); + bail!("FUSE mount (squashfuse) failed."); } Err(e) => { let _ = Command::new("cryptsetup").arg("close").arg(&mapper).status(); - bail!("{e}. Prüfe Kernelmodul 'squashfs' oder installiere 'squashfuse'."); + bail!("{e}. FUSE mount failed."); } } } else { - // Plain SquashFS-Datei match try_mount_kernel_squashfs(img, &mnt_abs, true) { Ok(true) => return Ok(()), Ok(false) => { - eprintln!("[warn] Kernel-SquashFS-Mount fehlgeschlagen – versuche FUSE (squashfuse)."); + eprintln!("[warn] Kernel mount failed, attempting FUSE (squashfuse)."); } Err(e) => { - eprintln!("[warn] Kernel-SquashFS-Mount Fehler: {e} – versuche FUSE (squashfuse)."); + eprintln!("[warn] Kernel mount failed: {e} – attempting FUSE (squashfuse)."); } } match try_mount_fuse_squashfs(img, &mnt_abs) { Ok(true) => Ok(()), - Ok(false) => bail!("FUSE-Mount (squashfuse) fehlgeschlagen. Prüfe Kernelmodul 'squashfs' oder installiere 'squashfuse'."), - Err(e) => bail!("{e}. Prüfe Kernelmodul 'squashfs' oder installiere 'squashfuse'."), + Ok(false) => bail!("FUSE mount failed (squashfuse)."), + Err(e) => bail!("{e}. FUSE mount failed."), } } } @@ -1323,32 +1256,26 @@ fn mount_overlay(lowerdirs:&str, upper:&Path, work:&Path, target:&Path)->Result< Ok(()) } -/* ---------- Umount (robust, idempotent) ---------- */ - fn umount(path:&Path)->Result<()>{ require_root("umount")?; - // Wenn nicht gemountet → OK (idempotent) if !is_target_mounted(path) { return Ok(()); } - // Erst normal versuchen let mut cmd = Command::new("umount"); cmd.arg(path); - let status = cmd.status().with_context(|| "umount aufrufen")?; + let status = cmd.status().with_context(|| "calling umount")?; if status.success() { return Ok(()); } - // FUSE-Fälle: fusermount3/fusermount let _ = Command::new("fusermount3").arg("-u").arg(path).status(); let status2 = Command::new("fusermount").arg("-u").arg(path).status(); if let Ok(s) = status2 { if s.success() { return Ok(()); } } - // Letzter Versuch: lazy let _ = Command::new("umount").arg("-l").arg(path).status(); if !is_target_mounted(path) { return Ok(()); } - bail!("umount/fusermount fehlgeschlagen für {}", path.display()); + bail!("umount/fusermount failed for {}", path.display()); } fn find_snapshot_file(ctx:&Ctx, idx:usize)->Result<PathBuf>{ @@ -1378,7 +1305,6 @@ fn cmd_mount(ctx:&mut Ctx, s:Option<usize>, target:&Path)->Result<()>{ return Ok(()); } - // MULTI-LAYER let mut lowers: Vec<PathBuf> = vec![]; for i in 1..=upto { let img = find_snapshot_file(ctx, i)?; @@ -1415,9 +1341,6 @@ fn target_is_luks(p: &Path) -> bool { } fn mapper_name_for_mount_image(img: &Path) -> Option<String> { - // Muss exakt zur Logik in mount_image_ro passen: - // mapper = "squashr_mount_<file_stem>" - // Bei "*.squashfs.luks" ist file_stem typischerweise "0001.squashfs" let stem = img.file_stem()?.to_string_lossy(); Some(format!("squashr_mount_{}", stem)) } @@ -1442,24 +1365,19 @@ fn cmd_unify(ctx: &mut Ctx, s: usize, target: &Path) -> Result<()> { bail!("Snapshot -s {} is invalid (1..={}).", s, max); } - // Zielpfad absolut/kanonisch (best effort) let target_abs = abspath(target); - // temporäre Arbeitsverzeichnisse let view = abspath(&ctx.temp_path("unify.view")); fs::create_dir_all(&view)?; - // Mounts für lowerdirs let mut lowers: Vec<PathBuf> = Vec::new(); - let mut mounted_imgs: Vec<PathBuf> = Vec::new(); // für mapper-close cleanup + let mut mounted_imgs: Vec<PathBuf> = Vec::new(); - // Sonderfall: nur Snapshot 1 → direkt mounten (kein Overlay nötig) if s == 1 { let img = find_snapshot_file(ctx, 1)?; mounted_imgs.push(img.clone()); mount_image_ro(ctx, &img, &view)?; - // SquashFS aus dem View bauen let tmp_plain = ctx.temp_path("unify.plain.squashfs"); if tmp_plain.exists() { let _ = fs::remove_file(&tmp_plain); } @@ -1473,11 +1391,9 @@ fn cmd_unify(ctx: &mut Ctx, s: usize, target: &Path) -> Result<()> { } run(&mut cmd, "mksquashfs (unify)")?; - // aushängen + mapper schließen let _ = umount(&view); close_mount_mapper_best_effort(&img); - // Output schreiben (plain oder luks anhand target) if target_abs.exists() { fs::remove_file(&target_abs)?; } if target_is_luks(&target_abs) { @@ -1487,14 +1403,12 @@ fn cmd_unify(ctx: &mut Ctx, s: usize, target: &Path) -> Result<()> { fs::rename(&tmp_plain, &target_abs)?; } - // best-effort cleanup let _ = fs::remove_dir_all(&view); println!("Unified snapshot up to {:04} → {}", s, target_abs.display()); return Ok(()); } - // MULTI-LAYER unify: 1..=s mounten → overlay → deletes → mksquashfs for i in 1..=s { let img = find_snapshot_file(ctx, i)?; let mnt = ctx.mounts_dir.join(format!("unify_{:04}_{}", i, chrono::Local::now().format("%Y%m%d%H%M%S"))); @@ -1512,13 +1426,11 @@ fn cmd_unify(ctx: &mut Ctx, s: usize, target: &Path) -> Result<()> { let loweropt = lowers.iter().rev().map(|p| p.display().to_string()).join(":"); // neuester zuerst mount_overlay(&loweropt, &upper, &work, &view)?; - // Deletes/Whiteouts wie bei cmd_mount let deletes = create_whiteouts_unlink_list(ctx, s)?; if !deletes.is_empty() { apply_whiteouts_via_unlink(&view, &deletes)?; } - // neues SquashFS aus dem View let tmp_plain = ctx.temp_path("unify.plain.squashfs"); if tmp_plain.exists() { let _ = fs::remove_file(&tmp_plain); } @@ -1532,20 +1444,16 @@ fn cmd_unify(ctx: &mut Ctx, s: usize, target: &Path) -> Result<()> { } run(&mut cmd, "mksquashfs (unify)")?; - // unmount overlay + lowers let _ = umount(&view); - // Lower mounts aushängen (reverse depth egal, sind eigenständige mountpoints) for mnt in &lowers { let _ = umount(mnt); } - // Mapper schließen (nur falls LUKS) for img in mounted_imgs { close_mount_mapper_best_effort(&img); } - // Output schreiben if target_abs.exists() { fs::remove_file(&target_abs) .with_context(|| format!("remove existing target: {}", target_abs.display()))?; @@ -1558,7 +1466,6 @@ fn cmd_unify(ctx: &mut Ctx, s: usize, target: &Path) -> Result<()> { fs::rename(&tmp_plain, &target_abs)?; } - // best-effort cleanup let _ = fs::remove_dir_all(&view); let _ = fs::remove_dir_all(&upper); let _ = fs::remove_dir_all(&work); @@ -1570,8 +1477,6 @@ fn cmd_unify(ctx: &mut Ctx, s: usize, target: &Path) -> Result<()> { Ok(()) } -/* ---------- Delete / New ---------- */ - fn cmd_delete(ctx:&mut Ctx, s:usize)->Result<()>{ let path_plain = ctx.snapshot_path(s, false); let path_luks = ctx.snapshot_path(s, true); @@ -1579,7 +1484,6 @@ 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)?; } - // Alle nachfolgenden Snapshots umnummerieren let snaps = ctx.list_snapshots()?; for p in snaps { let n = parse_snap_index(&p)?; @@ -1590,12 +1494,10 @@ fn cmd_delete(ctx:&mut Ctx, s:usize)->Result<()>{ } } - // 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 = staging.join(Ctx::manifest_name(k)); @@ -1614,7 +1516,6 @@ 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)?; } - // 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); @@ -1623,8 +1524,6 @@ fn cmd_new(ctx:&mut Ctx)->Result<()>{ cmd_backup(ctx) } -/* ---------- Umount Command ---------- */ - fn cmd_umount(ctx:&mut Ctx, target: Option<&Path>) -> Result<()> { require_root("umount")?; @@ -1645,13 +1544,11 @@ fn cmd_umount(ctx:&mut Ctx, target: Option<&Path>) -> Result<()> { } } } else { - // 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-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=") { @@ -1663,7 +1560,6 @@ fn cmd_umount(ctx:&mut Ctx, target: Option<&Path>) -> Result<()> { } } } - // 3) Mapper-SquashFS for m in &mounts_before { if m.fstype == "squashfs" && m.src.starts_with("/dev/mapper/squashr_") { todo.push(m.tgt.clone()); @@ -1671,7 +1567,6 @@ fn cmd_umount(ctx:&mut Ctx, target: Option<&Path>) -> Result<()> { mappers_in_use.insert(mapper); } } - // 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) { @@ -1682,7 +1577,6 @@ fn cmd_umount(ctx:&mut Ctx, target: Option<&Path>) -> Result<()> { } } } - // 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); @@ -1699,13 +1593,10 @@ fn cmd_umount(ctx:&mut Ctx, target: Option<&Path>) -> Result<()> { } if target.is_none() { - // 6) Alle /dev/mapper/squashr_* aufnehmen (auch wenn nirgendwo gemountet) for m in list_squashr_mappers() { mappers_in_use.insert(m); } - // 7) Alle Loop-Devices, deren backing_file im state_dir liegt (auch ohne Mount) for l in list_loops_on_state(ctx) { loops_in_use.insert(l); } } - // Nur aktuell gemountete Ziele behalten let mounted_now: HashSet<PathBuf> = read_proc_mounts()? .into_iter() .map(|m| abspath(&m.tgt)) @@ -1719,7 +1610,6 @@ fn cmd_umount(ctx:&mut Ctx, target: Option<&Path>) -> Result<()> { let mut did_something = !todo.is_empty(); let todo = sort_paths_deep_first(todo); - // Aushängen let mut errors = Vec::new(); for mpt in &todo { match umount(mpt) { @@ -1731,10 +1621,8 @@ fn cmd_umount(ctx:&mut Ctx, target: Option<&Path>) -> Result<()> { } } - // Nach dem Aushängen erneut Mounts einlesen let mounts_after = read_proc_mounts().unwrap_or_default(); - // LUKS-Mapper schließen (nur, wenn nicht mehr gemountet) for mapper in mappers_in_use { let devpath = format!("/dev/mapper/{mapper}"); let still_mounted = mounts_after.iter().any(|m| m.src == devpath); @@ -1748,7 +1636,6 @@ fn cmd_umount(ctx:&mut Ctx, target: Option<&Path>) -> Result<()> { } } - // Loop-Devices lösen for loopdev in loops_in_use { let still_mounted = mounts_after.iter().any(|m| m.src == loopdev); if !still_mounted { @@ -1763,10 +1650,8 @@ fn cmd_umount(ctx:&mut Ctx, target: Option<&Path>) -> Result<()> { } } - // Workdir radikal leeren (temporäre Klartext-/Overlay-Reste) if target.is_none() { purge_dir(&ctx.work_dir); - // leere Mount-Unterverzeichnisse entfernen (best effort) let _ = purge_dir(&ctx.mounts_dir); } @@ -1785,8 +1670,6 @@ fn cmd_umount(ctx:&mut Ctx, target: Option<&Path>) -> Result<()> { } } -/* ---------- Minimize ---------- */ - fn cmd_minimize(ctx:&mut Ctx, n_opt:Option<usize>)->Result<()>{ let target = n_opt.unwrap_or(ctx.min_keep); if target < ctx.min_keep { @@ -1801,8 +1684,6 @@ fn cmd_minimize(ctx:&mut Ctx, n_opt:Option<usize>)->Result<()>{ Ok(()) } -/* ---------- Merge ---------- */ - fn merge_first_two(ctx:&mut Ctx)->Result<()>{ require_root("merge (mounts / overlay)")?; let snaps = ctx.list_snapshots()?; @@ -1826,10 +1707,9 @@ fn merge_first_two(ctx:&mut Ctx)->Result<()>{ fs::create_dir_all(&upper)?; fs::create_dir_all(&work)?; fs::create_dir_all(&view)?; - let loweropt = format!("{}:{}", abspath(&m1).display(), abspath(&m2).display()); // s2 über s1 + let loweropt = format!("{}:{}", abspath(&m1).display(), abspath(&m2).display()); mount_overlay(&loweropt, &upper, &work, &view)?; - // Unlinks gemäß manifest_2 (Zielzustand) gegenüber manifest_1 let present = load_manifest(ctx, idx2).unwrap_or_default(); let past = load_manifest(ctx, idx1).unwrap_or_default(); let deletes: Vec<String> = past.difference(&present).cloned().collect(); @@ -1837,7 +1717,6 @@ fn merge_first_two(ctx:&mut Ctx)->Result<()>{ apply_whiteouts_via_unlink(&abspath(&view), &deletes)?; } - // neues SquashFS aus dem View let tmp_plain = ctx.temp_path("merge.plain.sqsh"); let mut cmd = Command::new("mksquashfs"); cmd.arg(&view).arg(&tmp_plain).arg("-no-progress").arg("-no-recovery"); @@ -1862,7 +1741,6 @@ fn merge_first_two(ctx:&mut Ctx)->Result<()>{ } if s2.exists(){ fs::remove_file(s2)?; } - // Indizes > idx2 runterzählen (Dateien) let rest = ctx.list_snapshots()?; for p in rest { let n = parse_snap_index(&p)?; @@ -1873,15 +1751,12 @@ fn merge_first_two(ctx:&mut Ctx)->Result<()>{ } } - // 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 = staging.join(Ctx::manifest_name(k)); |
