Skip to content

Commit

Permalink
nydus-image: enhance unpack command to support extracting files to dir
Browse files Browse the repository at this point in the history
When --output is specified as a directory, the nydus-image unpack
command will return an error, which is not convenient for users.

This patch enhances this command in the following 3 aspects:
1. Allow --output to be passed as a directory. In this case, the tar
 file will be output to this subdirectory.
2. Added --untar flag to support decompressing this tar file into the
 directory specified by --output.
3. When --output is not specified, the default output is to /dev/stdout

Signed-off-by: Qinqi Qu <[email protected]>
  • Loading branch information
adamqqqplay committed Nov 20, 2023
1 parent c9fbce8 commit 3d62276
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 10 deletions.
19 changes: 14 additions & 5 deletions src/bin/nydus-image/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -720,8 +720,16 @@ fn prepare_cmd_args(bti_string: &'static str) -> App {
.arg(
Arg::new("output")
.long("output")
.help("path for output tar file")
.required(true),
.default_value("/dev/stdout")
.help("Path for output tar file, if not configured, it will default to STDOUT.")
.required(false),
)
.arg(
Arg::new("untar")
.long("untar")
.action(ArgAction::SetTrue)
.help("Untar all files to the output dir, creating dir if it does not exist.")
.required(false),
),
)
}
Expand Down Expand Up @@ -1363,11 +1371,12 @@ impl Command {
}
}
};
let untar = matches.get_flag("untar");

OCIUnpacker::new(bootstrap, backend, output)
.with_context(|| "fail to create unpacker")?
OCIUnpacker::new(bootstrap, backend, output, untar)
.with_context(|| "failed to create unpacker")?
.unpack(config)
.with_context(|| "fail to unpack")
.with_context(|| format!("failed to unpack image to: {}", output))
}

fn check(matches: &ArgMatches, build_info: &BuildTimeInfo) -> Result<()> {
Expand Down
82 changes: 77 additions & 5 deletions src/bin/nydus-image/unpack/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//
// SPDX-License-Identifier: Apache-2.0
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::fs::{create_dir_all, remove_file, File, OpenOptions};
use std::io::Read;
use std::path::{Path, PathBuf};
use std::rc::Rc;
Expand All @@ -18,7 +18,7 @@ use nydus_rafs::{
};
use nydus_storage::backend::BlobBackend;
use nydus_storage::device::BlobInfo;
use tar::{Builder, Header};
use tar::{Archive, Builder, Header};

use self::pax::{
OCIBlockBuilder, OCICharBuilder, OCIDirBuilder, OCIFifoBuilder, OCILinkBuilder, OCIRegBuilder,
Expand All @@ -32,11 +32,12 @@ pub trait Unpacker {
fn unpack(&self, config: Arc<ConfigV2>) -> Result<()>;
}

/// A unpacker with the ability to convert bootstrap file and blob file to tar
/// A unpacker with the ability to convert bootstrap file and blob file to tar or dir.
pub struct OCIUnpacker {
bootstrap: PathBuf,
blob_backend: Option<Arc<dyn BlobBackend + Send + Sync>>,
output: PathBuf,
untar: bool,

builder_factory: OCITarBuilderFactory,
}
Expand All @@ -46,6 +47,7 @@ impl OCIUnpacker {
bootstrap: &Path,
blob_backend: Option<Arc<dyn BlobBackend + Send + Sync>>,
output: &str,
untar: bool,
) -> Result<Self> {
let bootstrap = bootstrap.to_path_buf();
let output = PathBuf::from(output);
Expand All @@ -57,31 +59,72 @@ impl OCIUnpacker {
bootstrap,
blob_backend,
output,
untar,
})
}

fn load_rafs(&self, config: Arc<ConfigV2>) -> Result<RafsSuper> {
let (rs, _) = RafsSuper::load_from_file(self.bootstrap.as_path(), config, false)?;
Ok(rs)
}

fn get_unpack_path(&self) -> Result<PathBuf> {
// If output ends with path separator, then it is a dir.
let is_dir = self
.output
.to_string_lossy()
.ends_with(std::path::MAIN_SEPARATOR);

// Unpack the tar file to a subdirectory
if is_dir || self.untar {
if !self.output.exists() {
create_dir_all(&self.output)?;
}
let tar_path = self
.output
.join(self.bootstrap.file_stem().unwrap_or_default())
.with_extension("tar");

return Ok(tar_path);
}

// Unpack the tar file to the specified location
Ok(self.output.clone())
}
}

impl Unpacker for OCIUnpacker {
fn unpack(&self, config: Arc<ConfigV2>) -> Result<()> {
debug!(
"oci unpacker, bootstrap file: {:?}, output file: {:?}",
"oci unpacker, bootstrap file: {:?}, output path: {:?}",
self.bootstrap, self.output
);

let rafs = self.load_rafs(config)?;

let tar_path = self.get_unpack_path()?;
let mut builder = self
.builder_factory
.create(&rafs, &self.blob_backend, &self.output)?;
.create(&rafs, &self.blob_backend, &tar_path)?;

for (node, path) in RafsIterator::new(&rafs) {
builder.append(node, &path)?;
}
info!("successfully unpack image to: {}", tar_path.display());

// untar this tar file to self.output dir
if self.untar {
let file = File::open(&tar_path)?;
let mut tar = Archive::new(file);
tar.unpack(&self.output)?;
remove_file(&tar_path)?;

info!(
"successfully untar {} to: {}",
tar_path.display(),
self.output.display()
);
}

Ok(())
}
Expand Down Expand Up @@ -230,3 +273,32 @@ impl TarBuilder for OCITarBuilder {
bail!("node {:?} can not be unpacked", path)
}
}

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_unpack_path() {
// test data: (bootstrap, output, untar, expected_tar_path)
let test_cases = [
("./test", "target.tar", false, "target.tar"),
("test/test", "target", false, "target"),
("test/test", "target/", false, "target/test.tar"),
("/run/test.meta", "target/", false, "target/test.tar"),
("/run/test.meta", "/run/", false, "/run/test.tar"),
("./test", "target.tar", true, "target.tar/test.tar"),
("test/test", "target", true, "target/test.tar"),
("test/test", "target/", true, "target/test.tar"),
];

for (bootstrap, output, untar, expected_tar_path) in test_cases {
let unpacker = OCIUnpacker::new(Path::new(bootstrap), None, output, untar).unwrap();
let tar_path = unpacker.get_unpack_path().unwrap();
assert_eq!(
tar_path,
PathBuf::from(expected_tar_path),
"tar_path not equal to expected_tar_path"
);
}
}
}

0 comments on commit 3d62276

Please sign in to comment.