aboutsummaryrefslogtreecommitdiffstats
path: root/src/main.rs
diff options
context:
space:
mode:
authorLeonard Kugis <leonard@kug.is>2025-10-16 00:38:51 +0200
committerLeonard Kugis <leonard@kug.is>2025-10-16 00:38:51 +0200
commite67e3a50889aaa8eefbb334ef408057a2411963f (patch)
tree78d5cfbe71b077d493cfbdac0430dbcc74432506 /src/main.rs
parentf7c27b648ced54d4075d77ba3f11d8078d1e73be (diff)
downloadsquashr-e67e3a50889aaa8eefbb334ef408057a2411963f.tar.gz
Also encrypting / packing meta files
Diffstat (limited to 'src/main.rs')
-rwxr-xr-xsrc/main.rs298
1 files changed, 234 insertions, 64 deletions
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<usize>,
include: Vec<PathBuf>,
@@ -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<std::process::ExitStatus>{
+ 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<String, PathBuf>, BTreeSet<Str
Ok((roots, manifest))
}
-fn save_manifest(ctx: &Ctx, idx: usize, manifest: &BTreeSet<String>) -> 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<BTreeSet<String>> {
- 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<R: Read, W: Write>(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<R: Read, W: Write>(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<R: Read, W: Write>(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<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();
+ if luks.exists() { Some(luks) }
+ else if plain.exists() { Some(plain) }
+ else { None }
+}
+
+fn open_meta_read(ctx:&Ctx) -> Result<Option<(PathBuf /*src*/, Option<String> /*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<String>) {
+ 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<bool> {
+ 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<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()))?;
+ for line in manifest {
+ writeln!(f, "{line}")?;
+ }
+ Ok(())
+}
+
+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()))?;
+ 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<bool> {
let mut cmd = Command::new("mount");
@@ -787,10 +930,19 @@ fn loop_backing_file(dev: &str) -> Option<PathBuf> {
/* ---------- 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<String> = 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<PathBuf> = 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(())