Repository: jubako/arx (language: python)

Issue:
[Feature request] Add recursive option to extract
Hi,

I try to extract a folder from my archive
if I do : 
**arx extract new_arch New2204/etc/X11/ -C ./temp/**  ... to get all files and folder in my system archive etc/X11
... the result is New2204/etc/X11 empty in ./temp/

**arx extract new_arch New2204/etc/X11/\* -C ./temp/**
... result is all files and folder in New2204/etc/X11/ BUT NOTHING in sub-folders

\# ll -R ./temp/New2204/etc/X11/
./temp/New2204/etc/X11/:
total 68K
drwxr-xr-x    8 root     root        4.0K Aug 27 17:23 ./
drwxr-xr-x    3 root     root        4.0K Aug 27 17:22 ../
-rw-r--r--    1 root     root         709 Aug 27 17:23 Xreset
drwxr-xr-x    2 root     root        4.0K Aug 27 17:23 Xreset.d/
drwxr-xr-x    2 root     root        4.0K Aug 27 17:23 Xresources/
-rw-r--r--    1 root     root        4.0K Aug 27 17:23 Xsession
drwxr-xr-x    2 root     root        4.0K Aug 27 17:23 Xsession.d/
-rw-r--r--    1 root     root         265 Aug 27 17:23 Xsession.options
drwxr-xr-x    2 root     root        4.0K Aug 27 17:23 app-defaults/
-rw-r--r--    1 root     root       17.0K Aug 27 17:23 rgb.txt
drwxr-xr-x    2 root     root        4.0K Aug 27 17:23 xkb/
drwxr-xr-x    2 root     root        4.0K Aug 27 17:23 xorg.conf.d/

./temp/New2204/etc/X11/Xreset.d:
total 8K
drwxr-xr-x    2 root     root        4.0K Aug 27 17:23 ./
drwxr-xr-x    8 root     root        4.0K Aug 27 17:23 ../

./temp/New2204/etc/X11/Xresources:
total 8K
drwxr-xr-x    2 root     root        4.0K Aug 27 17:23 ./
drwxr-xr-x    8 root     root        4.0K Aug 27 17:23 ../

./temp/New2204/etc/X11/Xsession.d:
total 8K
drwxr-xr-x    2 root     root        4.0K Aug 27 17:23 ./
drwxr-xr-x    8 root     root        4.0K Aug 27 17:23 ../
etc. etc. etc.

Even I don't understand clearly :   -L, --file-list <FILE_LIST>  Get the list of files/directories to extract from the FILE_LIST (incompatible with EXTRACT_FILES)

It's must be more useful to be able to get all files and folders using a recursive option in one command (without extra_list) .

Regards.
Philippe.


Discussion / hints:

Fix patch:
diff --git a/arx/src/extract.rs b/arx/src/extract.rs
index 0b3f069..46c7c2e 100644
--- a/arx/src/extract.rs
+++ b/arx/src/extract.rs
@@ -37,6 +37,27 @@ pub struct Options {
     #[arg(from_global)]
     verbose: u8,
 
+    /// Recursively extract directories
+    ///
+    /// Default value is true if `EXTRACT_FILES` is passed and false is `FILE_LIST` is passed.
+    #[arg(
+        short,
+        long,
+        required = false,
+        default_value_t = false,
+        default_value_ifs([
+            ("no_recurse", clap::builder::ArgPredicate::IsPresent, "false"),
+            ("extract_files", clap::builder::ArgPredicate::IsPresent, "true")
+        ]),
+        conflicts_with = "no_recurse",
+        action
+    )]
+    recurse: bool,
+
+    /// Force `--recurse` to be false.
+    #[arg(long)]
+    no_recurse: bool,
+
     #[arg(
         short = 'f',
         long = "file",
@@ -72,5 +93,11 @@ pub fn extract(options: Options) -> jbk::Result<()> {
         options.infile.as_ref().unwrap()
     };
     info!("Extract archive {:?} in {:?}", &infile, outdir);
-    arx::extract(infile, &outdir, files_to_extract, options.progress)
+    arx::extract(
+        infile,
+        &outdir,
+        files_to_extract,
+        options.recurse,
+        options.progress,
+    )
 }
diff --git a/libarx/src/tools.rs b/libarx/src/tools.rs
index ca6a03e..f3dabbb 100644
--- a/libarx/src/tools.rs
+++ b/libarx/src/tools.rs
@@ -111,6 +111,7 @@ where
     files: HashSet<crate::PathBuf>,
     base_dir: PathBuf,
     print_progress: bool,
+    recurse: bool,
 }
 
 impl Extractor<'_, '_> {
@@ -118,11 +119,25 @@ impl Extractor<'_, '_> {
         if self.files.is_empty() {
             return true;
         }
+
         if self.files.contains(current_file) {
             return true;
-        } else if is_dir {
+        }
+
+        if self.recurse {
+            // We must extract any file/dir child of a directory to extract.
+            let mut parent = current_file.parent();
+            while let Some(p) = parent {
+                if self.files.contains(p) {
+                    return true;
+                }
+                parent = p.parent();
+            }
+        }
+
+        if is_dir {
+            // We must create any dirs parent of files/dirs to extract.
             for file in &self.files {
-                // We must create the dir if it is the parent dir of the file to extract
                 let mut parent = file.parent();
                 while let Some(p) = parent {
                     if current_file == p {
@@ -251,16 +266,18 @@ pub fn extract(
     infile: &Path,
     outdir: &Path,
     files_to_extract: HashSet<crate::PathBuf>,
+    recurse: bool,
     progress: bool,
 ) -> jbk::Result<()> {
     let arx = Arx::new(infile)?;
-    extract_arx(&arx, outdir, files_to_extract, progress)
+    extract_arx(&arx, outdir, files_to_extract, recurse, progress)
 }
 
 pub fn extract_arx(
     arx: &Arx,
     outdir: &Path,
     files_to_extract: HashSet<crate::PathBuf>,
+    recurse: bool,
     progress: bool,
 ) -> jbk::Result<()> {
     let mut walker = Walker::new(arx, Default::default());
@@ -271,6 +288,7 @@ pub fn extract_arx(
             files: files_to_extract,
             base_dir: outdir.to_path_buf(),
             print_progress: progress,
+            recurse,
         };
         walker.run(&extractor)
     })
diff --git a/python/src/arx.rs b/python/src/arx.rs
index c772b05..4b0a6b8 100644
--- a/python/src/arx.rs
+++ b/python/src/arx.rs
@@ -115,7 +115,7 @@ impl Arx {
     /// Extract the whole archive in
     #[pyo3(signature=(extract_path=std::path::PathBuf::from(".")))]
     fn extract(&self, extract_path: std::path::PathBuf) -> PyResult<()> {
-        arx::extract_arx(&self.0, &extract_path, Default::default(), false)
+        arx::extract_arx(&self.0, &extract_path, Default::default(), true, false)
             .map_err(|e| PyRuntimeError::new_err(e.to_string()))
     }
 }


Test patch:
diff --git a/arx/tests/create.rs b/arx/tests/create.rs
index 41a9622..3c28598 100644
--- a/arx/tests/create.rs
+++ b/arx/tests/create.rs
@@ -5,7 +5,8 @@ mod inner {
 
     // Generate a fake directory with fake content.
     pub fn spawn_mount() -> std::io::Result<(arx_test_dir::BackgroundSession, PathBuf)> {
-        let mount_path = tempfile::TempDir::new_in(env!("CARGO_TARGET_TMPDIR")).unwrap();
+        let mount_path =
+            tempfile::TempDir::with_prefix_in("source_", env!("CARGO_TARGET_TMPDIR")).unwrap();
         let builder = arx_test_dir::ContextBuilder::new();
         let context = builder.create();
         let dir = arx_test_dir::DirEntry::new_root(context);
@@ -19,7 +20,7 @@ mod inner {
 
 #[cfg(all(unix, not(feature = "in_ci")))]
 #[test]
-fn test_create() {
+fn test_create_and_mount() {
     use inner::*;
 
     let (_source_mount_handle, source_mount_point) = spawn_mount().unwrap();
@@ -35,8 +36,9 @@ fn test_create() {
         .arg("--strip-prefix")
         .arg(&source_mount_point.file_name().unwrap())
         .arg(&source_mount_point.file_name().unwrap())
+        .arg("--progress")
         .output()
-        .expect("foo");
+        .expect("Creation should work");
     println!("Out : {}", String::from_utf8(output.stdout).unwrap());
     println!("Err : {}", String::from_utf8(output.stderr).unwrap());
     assert!(output.status.success());
@@ -58,3 +60,107 @@ fn test_create() {
     println!("Err: {}", String::from_utf8(output.stderr).unwrap());
     assert!(output.status.success());
 }
+
+#[cfg(all(unix, not(feature = "in_ci")))]
+#[test]
+fn test_create_and_extract() {
+    use inner::*;
+
+    let (_source_mount_handle, source_mount_point) = spawn_mount().unwrap();
+    let bin_path = env!("CARGO_BIN_EXE_arx");
+    let arx_file = Path::new(env!("CARGO_TARGET_TMPDIR")).join("test.arx");
+    let output = Command::new(bin_path)
+        .arg("--verbose")
+        .arg("create")
+        .arg("--outfile")
+        .arg(&arx_file)
+        .arg("-C")
+        .arg(&source_mount_point.parent().unwrap())
+        .arg("--strip-prefix")
+        .arg(&source_mount_point.file_name().unwrap())
+        .arg(&source_mount_point.file_name().unwrap())
+        .arg("--progress")
+        .output()
+        .expect("Creation should work");
+    println!("Out : {}", String::from_utf8(output.stdout).unwrap());
+    println!("Err : {}", String::from_utf8(output.stderr).unwrap());
+    assert!(output.status.success());
+    assert!(arx_file.is_file());
+
+    let extract_dir = tempfile::TempDir::new_in(env!("CARGO_TARGET_TMPDIR")).unwrap();
+    arx::extract(
+        &arx_file,
+        extract_dir.path(),
+        Default::default(),
+        true,
+        false,
+    )
+    .unwrap();
+    let output = Command::new("diff")
+        .arg("-r")
+        .arg(&source_mount_point)
+        .arg(&extract_dir.path())
+        .output()
+        .unwrap();
+    println!("Out : {}", String::from_utf8(output.stdout).unwrap());
+    println!("Err: {}", String::from_utf8(output.stderr).unwrap());
+    assert!(output.status.success());
+}
+
+#[cfg(all(unix, not(feature = "in_ci")))]
+#[test]
+fn test_create_and_extract_subdir() {
+    use inner::*;
+
+    let (_source_mount_handle, source_mount_point) = spawn_mount().unwrap();
+    let bin_path = env!("CARGO_BIN_EXE_arx");
+    let arx_file = Path::new(env!("CARGO_TARGET_TMPDIR")).join("test.arx");
+    let output = Command::new(bin_path)
+        .arg("--verbose")
+        .arg("create")
+        .arg("--outfile")
+        .arg(&arx_file)
+        .arg("-C")
+        .arg(&source_mount_point.parent().unwrap())
+        .arg("--strip-prefix")
+        .arg(&source_mount_point.file_name().unwrap())
+        .arg(&source_mount_point.file_name().unwrap())
+        .arg("--progress")
+        .output()
+        .expect("Creation should work");
+    println!("Out : {}", String::from_utf8(output.stdout).unwrap());
+    println!("Err : {}", String::from_utf8(output.stderr).unwrap());
+    assert!(output.status.success());
+    assert!(arx_file.is_file());
+
+    let extract_dir =
+        tempfile::TempDir::with_prefix_in("extract_", env!("CARGO_TARGET_TMPDIR")).unwrap();
+    arx::extract(
+        &arx_file,
+        extract_dir.path(),
+        ["OrcBlIw".into()].into(),
+        true,
+        true,
+    )
+    .unwrap();
+
+    let mut source_sub_dir = source_mount_point;
+    source_sub_dir.push("OrcBlIw");
+    let mut extract_sub_dir = extract_dir.path().to_path_buf();
+    extract_sub_dir.push("OrcBlIw");
+
+    println!(
+        "Diff {} and {}",
+        source_sub_dir.display(),
+        extract_sub_dir.display()
+    );
+    let output = Command::new("diff")
+        .arg("-r")
+        .arg(&source_sub_dir)
+        .arg(&extract_sub_dir)
+        .output()
+        .unwrap();
+    println!("Out : {}", String::from_utf8(output.stdout).unwrap());
+    println!("Err: {}", String::from_utf8(output.stderr).unwrap());
+    assert!(output.status.success());
+}