From 4475dff141fa73cc7620be94ab9acff3ecd74539 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Wed, 17 Jun 2026 00:11:12 +0200 Subject: [PATCH] feat: add progressbar for extraction and initramfs creation --- src/extract.rs | 169 ++++++++++++++++++++++--------------------------- src/qemu_vm.rs | 33 +++++++--- 2 files changed, 100 insertions(+), 102 deletions(-) diff --git a/src/extract.rs b/src/extract.rs index 670fcde..174042f 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -1,5 +1,6 @@ use crate::veprintln; use anyhow::{Context, Result}; +use indicatif::{ProgressBar, ProgressStyle}; use std::fs::File; use std::io::{BufReader, Read}; use std::path::Path; @@ -130,16 +131,18 @@ fn extract_oci_layer(layer_path: &Path, dest: &Path) -> Result<()> { let reader = BufReader::new(file); if layer_name.ends_with(".tar.gz") || layer_name.ends_with(".tgz") { - extract_archive_with_whiteouts( + extract_with_progress( tar::Archive::new(flate2::read::GzDecoder::new(reader)), dest, + "Extracting OCI layer", ) } else if layer_name.ends_with(".tar.xz") || layer_name.ends_with(".txz") { - extract_archive_with_whiteouts(tar::Archive::new(xz2::read::XzDecoder::new(reader)), dest) + extract_with_progress(tar::Archive::new(xz2::read::XzDecoder::new(reader)), dest, "Extracting OCI layer") } else if layer_name.ends_with(".tar.zst") || layer_name.ends_with(".tar.zstd") { - extract_archive_with_whiteouts( + extract_with_progress( tar::Archive::new(zstd::stream::read::Decoder::new(reader)?), dest, + "Extracting OCI layer", ) } else { // Fall back to magic-byte detection @@ -148,63 +151,100 @@ fn extract_oci_layer(layer_path: &Path, dest: &Path) -> Result<()> { let _ = peek.read_exact(&mut magic); // short reads are fine for detection drop(peek); match magic { - [0x1f, 0x8b, ..] => extract_archive_with_whiteouts( + [0x1f, 0x8b, ..] => extract_with_progress( tar::Archive::new(flate2::read::GzDecoder::new(BufReader::new(File::open( layer_path, )?))), dest, + "Extracting OCI layer", ), - [0xfd, b'7', b'z', b'X', b'Z', 0x00] => extract_archive_with_whiteouts( + [0xfd, b'7', b'z', b'X', b'Z', 0x00] => extract_with_progress( tar::Archive::new(xz2::read::XzDecoder::new(BufReader::new(File::open( layer_path, )?))), dest, + "Extracting OCI layer", ), - [0x28, 0xb5, 0x2f, 0xfd, ..] => extract_archive_with_whiteouts( + [0x28, 0xb5, 0x2f, 0xfd, ..] => extract_with_progress( tar::Archive::new(zstd::stream::read::Decoder::new(BufReader::new( File::open(layer_path)?, ))?), dest, + "Extracting OCI layer", ), - _ => extract_archive_with_whiteouts( + _ => extract_with_progress( tar::Archive::new(BufReader::new(File::open(layer_path)?)), dest, + "Extracting OCI layer", ), } } } -/// Apply one OCI layer archive to `dest`, interpreting Docker whiteout markers: -/// -/// - `.wh.` — Delete `` from a lower layer that was already -/// extracted into `dest`. -/// - `.wh..wh..opq` — Opaque whiteout: the directory that contains this entry -/// is new in this layer; delete everything already in that -/// directory from lower layers before applying new content. -/// -/// All other entries are extracted normally via `Entry::unpack_in`. -fn extract_archive_with_whiteouts( - mut archive: tar::Archive, - dest: &Path, -) -> Result<()> { +fn extract_gz(reader: R, dest: &Path) -> Result<()> { + let gz_decoder = flate2::read::GzDecoder::new(reader); + let archive = tar::Archive::new(gz_decoder); + + extract_with_progress(archive, dest, "Extracting gzip archive")?; + + Ok(()) +} + +fn extract_xz(reader: R, dest: &Path) -> Result<()> { + let xz_decoder = xz2::read::XzDecoder::new(reader); + let archive = tar::Archive::new(xz_decoder); + + extract_with_progress(archive, dest, "Extracting xz archive")?; + + Ok(()) +} + +fn extract_zst(reader: R, dest: &Path) -> Result<()> { + let zst_decoder = zstd::Decoder::new(reader)?; + let archive = tar::Archive::new(zst_decoder); + + extract_with_progress(archive, dest, "Extracting zstd archive")?; + + Ok(()) +} + +fn extract_tar(reader: R, dest: &Path) -> Result<()> { + let archive = tar::Archive::new(reader); + + extract_with_progress(archive, dest, "Extracting tar archive")?; + + Ok(()) +} + +/// Extract tar archive with progress bar, handling whiteout files for OCI layers +fn extract_with_progress(mut archive: tar::Archive, dest: &Path, msg: &str) -> Result<()> { + let pb = ProgressBar::new_spinner(); + pb.set_style(ProgressStyle::default_spinner() + .template("{spinner:.green} {msg} ({pos} files)") + .unwrap()); + pb.set_message(msg.to_string()); + archive.set_preserve_permissions(true); archive.set_preserve_ownerships(false); archive.set_unpack_xattrs(false); - for entry in archive.entries().context("Failed to iterate tar entries")? { - let mut entry = entry.context("Failed to read tar entry")?; - - // Clone the path before any mutable borrow of entry (needed for unpack_in) + let entries = archive.entries() + .context("Failed to read archive entries")?; + + for entry in entries { + let mut entry = entry.context("Failed to read archive entry")?; + + // Clone the path before any mutable borrow of entry let path = entry.path().context("Invalid tar entry path")?.into_owned(); - + let filename = path .file_name() .map(|n| n.to_string_lossy().into_owned()) .unwrap_or_default(); + // Handle whiteout files (OCI layer markers for deletions) if filename == ".wh..wh..opq" { - // Opaque whiteout: clear all previously-extracted content in the - // parent directory so only this layer's content is visible. + // Opaque whiteout: clear all previously-extracted content in the parent directory let parent = path.parent().unwrap_or(Path::new("")); let dest_dir = dest.join(parent); if dest_dir.symlink_metadata().is_ok() { @@ -218,24 +258,26 @@ fn extract_archive_with_whiteouts( })?; } } - // Do not extract the .wh..wh..opq marker itself. + // Do not extract the .wh..wh..opq marker itself } else if let Some(real_name) = filename.strip_prefix(".wh.") { - // Regular whiteout: delete the named path from lower layers. + // Regular whiteout: delete the named path from lower layers let parent = path.parent().unwrap_or(Path::new("")); let target = dest.join(parent).join(real_name); - // symlink_metadata (lstat) does not follow symlinks, so a dangling - // symlink is correctly detected and removed rather than silently skipped. if target.symlink_metadata().is_ok() { remove_path(&target) .with_context(|| format!("Whiteout: failed to remove {}", target.display()))?; } - // Do not extract the .wh.* marker itself. + // Do not extract the .wh.* marker itself } else { - entry - .unpack_in(dest) + entry.unpack_in(dest) .with_context(|| format!("Failed to extract {}", path.display()))?; } + + pb.inc(1); } + + pb.finish_and_clear(); + veprintln!("Extracted {} files", pb.position()); Ok(()) } @@ -251,62 +293,3 @@ fn remove_path(path: &Path) -> std::io::Result<()> { std::fs::remove_file(path) } } - -fn extract_gz(reader: R, dest: &Path) -> Result<()> { - let gz_decoder = flate2::read::GzDecoder::new(reader); - let mut archive = tar::Archive::new(gz_decoder); - - archive.set_preserve_permissions(true); - archive.set_preserve_ownerships(false); - archive.set_unpack_xattrs(false); - - archive - .unpack(dest) - .context("Failed to extract gzip archive")?; - - Ok(()) -} - -fn extract_xz(reader: R, dest: &Path) -> Result<()> { - let xz_decoder = xz2::read::XzDecoder::new(reader); - let mut archive = tar::Archive::new(xz_decoder); - - archive.set_preserve_permissions(true); - archive.set_preserve_ownerships(false); - archive.set_unpack_xattrs(false); - - archive - .unpack(dest) - .context("Failed to extract xz archive")?; - - Ok(()) -} - -fn extract_zst(reader: R, dest: &Path) -> Result<()> { - let zst_decoder = zstd::Decoder::new(reader)?; - let mut archive = tar::Archive::new(zst_decoder); - - archive.set_preserve_permissions(true); - archive.set_preserve_ownerships(false); - archive.set_unpack_xattrs(false); - - archive - .unpack(dest) - .context("Failed to extract zstd archive")?; - - Ok(()) -} - -fn extract_tar(reader: R, dest: &Path) -> Result<()> { - let mut archive = tar::Archive::new(reader); - - archive.set_preserve_permissions(true); - archive.set_preserve_ownerships(false); - archive.set_unpack_xattrs(false); - - archive - .unpack(dest) - .context("Failed to extract tar archive")?; - - Ok(()) -} diff --git a/src/qemu_vm.rs b/src/qemu_vm.rs index 8c9ac7b..6f89b2d 100644 --- a/src/qemu_vm.rs +++ b/src/qemu_vm.rs @@ -1,6 +1,7 @@ use crate::veprintln; use anyhow::{anyhow, Context, Result}; use cpio::{newc, NewcBuilder}; +use indicatif::{ProgressBar, ProgressStyle}; use std::io::Write; use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; @@ -147,13 +148,20 @@ fn get_arch_package_suffix(arch: &str) -> &str { /// Create a gzipped cpio initramfs from a directory fn create_initramfs(rootfs: &PathBuf) -> Result { - veprintln!("Creating initramfs from rootfs..."); - // Create a temporary file for the initramfs let initramfs_path = rootfs.parent().unwrap().join("initramfs.cpio.gz"); - // Create the cpio archive - let cpio_data = create_cpio_archive(rootfs)?; + // Create progress bar + let pb = ProgressBar::new_spinner(); + pb.set_style(ProgressStyle::default_spinner() + .template("{spinner:.green} {msg} ({pos} files)") + .unwrap()); + pb.set_message("Scanning rootfs..."); + + // Create the cpio archive with progress + let cpio_data = create_cpio_archive(rootfs, &pb)?; + + pb.set_message("Compressing initramfs..."); // Compress with gzip let mut output_file = std::fs::File::create(&initramfs_path) @@ -165,21 +173,25 @@ fn create_initramfs(rootfs: &PathBuf) -> Result { encoder.finish() .context("Failed to finalize gzip compression")?; - veprintln!("Initramfs created: {} bytes (uncompressed)", cpio_data.len()); + // Clear progress bar and print message only in verbose mode + pb.finish_and_clear(); + veprintln!("Initramfs created: {} bytes, {} files", cpio_data.len(), pb.position()); Ok(initramfs_path) } /// Create a newc-format cpio archive from a directory using the cpio crate -fn create_cpio_archive(rootfs: &Path) -> Result> { +fn create_cpio_archive(rootfs: &Path, pb: &ProgressBar) -> Result> { let mut archive = Vec::new(); // Collect all entries with their data - let entries = collect_entries(rootfs, rootfs)?; + let entries = collect_entries(rootfs, rootfs, pb)?; // Collect entry names for checking existence later let entry_names: Vec<&str> = entries.iter().map(|(n, _, _, _, _)| n.as_str()).collect(); + pb.set_message("Writing initramfs..."); + // Write each entry using the cpio crate for (name, mode, mtime, nlink, data) in &entries { let file_size = data.len() as u32; @@ -243,7 +255,7 @@ fn create_cpio_archive(rootfs: &Path) -> Result> { } /// Collect all filesystem entries recursively -fn collect_entries(base: &Path, current: &Path) -> Result)>> { +fn collect_entries(base: &Path, current: &Path, pb: &ProgressBar) -> Result)>> { let mut entries = Vec::new(); // Read directory entries @@ -267,6 +279,9 @@ fn collect_entries(base: &Path, current: &Path) -> Result Result