aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLeonard Kugis <leonard@kug.is>2026-01-19 21:30:17 +0100
committerLeonard Kugis <leonard@kug.is>2026-01-19 21:30:17 +0100
commitcf472dd80aedab34e7907f86b5d64ea268a4127d (patch)
treeae8d3386d162dff96dff4e537c7a348ab8f3c5fe
parent7df2fb341747003cee73cf8e819e6cec824edaba (diff)
downloadsquashr-cf472dd80aedab34e7907f86b5d64ea268a4127d.tar.gz
Implemented unify operation
-rwxr-xr-xsrc/main.rs165
1 files changed, 165 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs
index 3b6f2cf..a2c8b5d 100755
--- a/src/main.rs
+++ b/src/main.rs
@@ -107,6 +107,7 @@ enum Cmd {
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 },
}
#[derive(Debug, Clone, Deserialize)]
@@ -377,6 +378,7 @@ fn main() -> Result<()> {
Cmd::Mount { s, target } => cmd_mount(&mut ctx, s, &target),
Cmd::Delete { s } => cmd_delete(&mut ctx, s),
Cmd::Umount { target } => cmd_umount(&mut ctx, target.as_deref()),
+ Cmd::Unify { s, target } => cmd_unify(&mut ctx, s, &target),
}
}
@@ -1405,6 +1407,169 @@ fn cmd_mount(ctx:&mut Ctx, s:Option<usize>, target:&Path)->Result<()>{
Ok(())
}
+fn target_is_luks(p: &Path) -> bool {
+ p.file_name()
+ .and_then(|s| s.to_str())
+ .map(|s| s.ends_with(".luks"))
+ .unwrap_or(false)
+}
+
+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))
+}
+
+fn close_mount_mapper_best_effort(img: &Path) {
+ if img.extension().and_then(|e| e.to_str()) == Some("luks") {
+ if let Some(mapper) = mapper_name_for_mount_image(img) {
+ let _ = Command::new("cryptsetup").arg("close").arg(&mapper).status();
+ }
+ }
+}
+
+fn cmd_unify(ctx: &mut Ctx, s: usize, target: &Path) -> Result<()> {
+ require_root("unify (mounts / overlay)")?;
+
+ let snaps = ctx.list_snapshots()?;
+ if snaps.is_empty() {
+ bail!("No snapshots found.");
+ }
+ let max = snaps.len();
+ if s == 0 || s > max {
+ 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
+
+ // 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); }
+
+ let mut cmd = Command::new("mksquashfs");
+ cmd.arg(&view).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 (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) {
+ encrypt_into_luks(ctx, &tmp_plain, &target_abs)?;
+ let _ = fs::remove_file(&tmp_plain);
+ } else {
+ 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")));
+ fs::create_dir_all(&mnt)?;
+ mount_image_ro(ctx, &img, &mnt)?;
+ lowers.push(abspath(&mnt));
+ mounted_imgs.push(img);
+ }
+
+ let upper = abspath(&ctx.temp_path("unify.overlay.upper"));
+ let work = abspath(&ctx.temp_path("unify.overlay.work"));
+ fs::create_dir_all(&upper)?;
+ fs::create_dir_all(&work)?;
+
+ 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); }
+
+ let mut cmd = Command::new("mksquashfs");
+ cmd.arg(&view).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 (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()))?;
+ }
+
+ if target_is_luks(&target_abs) {
+ encrypt_into_luks(ctx, &tmp_plain, &target_abs)?;
+ let _ = fs::remove_file(&tmp_plain);
+ } else {
+ 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);
+ for mnt in lowers {
+ let _ = fs::remove_dir_all(mnt);
+ }
+
+ println!("Unified snapshot up to {:04} → {}", s, target_abs.display());
+ Ok(())
+}
+
/* ---------- Delete / New ---------- */
fn cmd_delete(ctx:&mut Ctx, s:usize)->Result<()>{