You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2025/05/14 07:28:22 UTC

(opendal) branch main updated: feat(bin/oli): support cp to dir (#6140)

This is an automated email from the ASF dual-hosted git repository.

xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new 1b953a577 feat(bin/oli): support cp to dir (#6140)
1b953a577 is described below

commit 1b953a577761527ec71090b30432d751b51f8b22
Author: Asuka Minato <i...@asukaminato.eu.org>
AuthorDate: Wed May 14 16:28:16 2025 +0900

    feat(bin/oli): support cp to dir (#6140)
    
    * cp dir
    
    * keep origin
    
    * cargo test
    
    * cache
    
    * path
    
    * rm some comment
    
    * collapsible-else-if
    
    * try sccache
    
    * try
    
    * disable cache
    
    * retry sccache
    
    https://github.com/apache/infrastructure-actions/commit/08ba8a15e8867c67f62bcc7f8773711a0dd2bd47
    
    * with_context
    
    * fix /
    
    * cp -r test ok now
    
    * fmt
    
    * update test
    
    * update test
    
    * fix test
    
    * try size -> len
    
    * ignore size
    
    * fix ls test
    
    * fix folder regex
    
    * fix stat
    
    * fix regex && check ls output
---
 .github/workflows/ci_bin_oli.yml        |  14 +++-
 bin/oli/src/commands/cp.rs              | 129 ++++++++++++++++++++++++++++----
 bin/oli/tests/integration/cp.rs         | 105 ++++++++++++++++++++++++--
 bin/oli/tests/integration/ls.rs         |  19 ++---
 bin/oli/tests/integration/mv.rs         | 108 +++++++++++++-------------
 bin/oli/tests/integration/rm.rs         |  52 ++++++-------
 bin/oli/tests/integration/stat.rs       |   2 +-
 bin/oli/tests/integration/test_utils.rs |   8 +-
 8 files changed, 315 insertions(+), 122 deletions(-)

diff --git a/.github/workflows/ci_bin_oli.yml b/.github/workflows/ci_bin_oli.yml
index fc146eea8..7ab8ef671 100644
--- a/.github/workflows/ci_bin_oli.yml
+++ b/.github/workflows/ci_bin_oli.yml
@@ -34,7 +34,7 @@ concurrency:
   cancel-in-progress: true
 
 jobs:
-  check_clippy:
+  check_clippy_and_test:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
@@ -45,7 +45,13 @@ jobs:
           need-rocksdb: true
           need-protoc: true
           github-token: ${{ secrets.GITHUB_TOKEN }}
-
-      - name: Cargo clippy
+      - name: Run sccache-cache
+        uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad
+      - name: Cargo clippy && test
         working-directory: bin/oli
-        run: cargo clippy --all-targets --all-features -- -D warnings
+        env:
+          SCCACHE_GHA_ENABLED: "true"
+          RUSTC_WRAPPER: "sccache"
+        run: |
+          cargo clippy --all-targets --all-features -- -D warnings
+          cargo test --all-targets --all-features
diff --git a/bin/oli/src/commands/cp.rs b/bin/oli/src/commands/cp.rs
index 5f4cafb64..c24035b33 100644
--- a/bin/oli/src/commands/cp.rs
+++ b/bin/oli/src/commands/cp.rs
@@ -15,12 +15,15 @@
 // specific language governing permissions and limitations
 // under the License.
 
+use anyhow::bail;
 use futures::AsyncBufReadExt;
 use indicatif::ProgressBar;
 use indicatif::ProgressStyle;
+use opendal::ErrorKind;
 use opendal::Metadata;
 use std::path::Path;
 
+use anyhow::Context;
 use anyhow::Result;
 use futures::AsyncWriteExt;
 use futures::TryStreamExt;
@@ -63,15 +66,53 @@ impl CopyCmd {
     pub fn run(self) -> Result<()> {
         make_tokio_runtime(1).block_on(self.do_run())
     }
+
     async fn do_run(self) -> Result<()> {
         let cfg = Config::load(&self.config_params.config)?;
 
         let (src_op, src_path) = cfg.parse_location(&self.source)?;
-
         let (dst_op, dst_path) = cfg.parse_location(&self.destination)?;
 
+        let final_dst_path = match dst_op.stat(&dst_path).await {
+            Ok(dst_meta) if dst_meta.mode().is_dir() => {
+                if self.recursive {
+                    dst_path.clone()
+                } else if let Some(filename) = Path::new(&src_path).file_name() {
+                    Path::new(&dst_path)
+                        .join(filename)
+                        .to_string_lossy()
+                        .to_string()
+                } else {
+                    bail!(
+                        "Cannot copy source '{}' into directory '{}': Source has no filename.",
+                        src_path,
+                        dst_path
+                    );
+                }
+            }
+            Ok(_) => {
+                // Destination exists but is a file. Overwrite it (non-recursive)
+                // or error (recursive, handled below).
+                if self.recursive {
+                    bail!(
+                        "Recursive copy destination '{}' exists but is not a directory.",
+                        dst_path
+                    );
+                }
+                dst_path.clone()
+            }
+            Err(e) if e.kind() == ErrorKind::NotFound => dst_path.clone(),
+            Err(e) => {
+                return Err(e.into());
+            }
+        };
+
         if !self.recursive {
-            let mut dst_w = dst_op.writer(&dst_path).await?.into_futures_async_write();
+            // Non-recursive copy: Use the final_dst_path directly.
+            let mut dst_w = dst_op
+                .writer(&final_dst_path)
+                .await?
+                .into_futures_async_write();
             let src_meta = src_op.stat(&src_path).await?;
             let reader = src_op.reader_with(&src_path).chunk(8 * 1024 * 1024).await?;
             let buf_reader = reader
@@ -84,28 +125,88 @@ impl CopyCmd {
             return Ok(());
         }
 
-        let dst_root = Path::new(&dst_path);
+        // Recursive copy: Ensure the base destination directory exists or create it.
+        // Note: final_dst_path here refers to the original dst_path if it was a dir or didn't exist.
+        match dst_op.stat(&final_dst_path).await {
+            Ok(meta) if meta.mode().is_dir() => {
+                // Base destination directory exists.
+            }
+            Ok(_) => {
+                bail!(
+                    "Recursive copy destination '{}' exists but is not a directory.",
+                    final_dst_path
+                );
+            }
+            Err(e) if e.kind() == ErrorKind::NotFound => {
+                let mut path_to_create = final_dst_path.clone();
+                if !path_to_create.ends_with('/') {
+                    path_to_create.push('/');
+                }
+                dst_op.create_dir(&path_to_create).await?;
+            }
+            Err(e) => {
+                // Another error occurred trying to stat the base destination.
+                return Err(e.into());
+            }
+        }
+
+        // Proceed with recursive copy logic. dst_root is the target directory.
+        let dst_root = Path::new(&final_dst_path);
         let mut ds = src_op.lister_with(&src_path).recursive(true).await?;
-        let prefix = src_path.strip_prefix('/').unwrap_or(src_path.as_str());
+
         while let Some(de) = ds.try_next().await? {
             let meta = de.metadata();
+            let depath = de.path();
+
+            // Calculate relative path using Path::strip_prefix
+            let src_root_path = Path::new(&src_path);
+            let entry_path = Path::new(depath);
+            let relative_path = entry_path.strip_prefix(src_root_path).with_context(|| {
+                format!(
+                    "Internal error: Lister path '{}' does not start with source path '{}'",
+                    depath, src_path
+                )
+            })?; // relative_path is a &Path
+
+            let current_dst_path_path = dst_root.join(relative_path);
+            let current_dst_path = current_dst_path_path.to_string_lossy().to_string();
+
             if meta.mode().is_dir() {
+                let mut dir_path_to_create = current_dst_path.clone();
+                if !dir_path_to_create.ends_with('/') {
+                    dir_path_to_create.push('/');
+                }
+                dst_op.create_dir(&dir_path_to_create).await?;
                 continue;
             }
-            let depath = de.path();
-            let fp = depath
-                .strip_prefix('/')
-                .unwrap_or(depath)
-                .strip_prefix(prefix)
-                .expect("invalid path");
-            let reader = src_op.reader_with(de.path()).chunk(8 * 1024 * 1024).await?;
+
+            // Explicitly stat the source file to get fresh metadata
+            let fresh_meta = src_op.stat(depath).await.with_context(|| {
+                format!(
+                    "Failed to stat source file '{}' before recursive copy",
+                    depath
+                )
+            })?;
+
+            // Use the Path object `current_dst_path_path` to check parent
+            if let Some(parent_path) = current_dst_path_path.parent() {
+                if parent_path != dst_root {
+                    let mut parent_dir_string = parent_path.to_string_lossy().into_owned();
+                    if !parent_dir_string.ends_with('/') {
+                        parent_dir_string.push('/');
+                    }
+                    dst_op.create_dir(&parent_dir_string).await?;
+                }
+            }
+
+            let reader = src_op.reader_with(depath).chunk(8 * 1024 * 1024).await?;
             let buf_reader = reader
-                .into_futures_async_read(0..meta.content_length())
+                .into_futures_async_read(0..fresh_meta.content_length())
                 .await?;
 
-            let copy_progress = CopyProgress::new(meta, de.path().to_string());
+            let copy_progress = CopyProgress::new(&fresh_meta, depath.to_string());
             let mut writer = dst_op
-                .writer(&dst_root.join(fp).to_string_lossy())
+                .writer(&current_dst_path)
                 .await?
                 .into_futures_async_write();
 
diff --git a/bin/oli/tests/integration/cp.rs b/bin/oli/tests/integration/cp.rs
index 294d865fe..3783a9ea2 100644
--- a/bin/oli/tests/integration/cp.rs
+++ b/bin/oli/tests/integration/cp.rs
@@ -54,15 +54,106 @@ async fn test_cp_for_path_in_current_dir() -> Result<()> {
         .current_dir(dir.path())
         .assert()
         .success();
+    assert_snapshot!(directory_snapshot(dir.path()).with_content(true), @r"
++-------------------------------------+
+| Path                 Type   Content |
++=====================================+
+| [TEMP_DIR]           DIR            |
+| [TEMP_DIR]/dst.txt   FILE   hello   |
+| [TEMP_DIR]/src.txt   FILE   hello   |
++-------------------------------------+
+    ");
+    Ok(())
+}
+
+#[tokio::test]
+async fn test_cp_file_to_existing_dir() -> Result<()> {
+    let dir = tempdir()?;
+    let source_dir = dir.path().join("source");
+    let dest_dir = dir.path().join("dest");
+    fs::create_dir_all(&source_dir)?;
+    fs::create_dir_all(&dest_dir)?;
+
+    let source_file_name = "test_file.txt";
+    let source_file_path = source_dir.join(source_file_name);
+    let source_content = "hello";
+    fs::write(&source_file_path, source_content)?;
+
+    // Use paths directly as arguments for local fs operations
+    let source_arg = source_file_path.to_str().unwrap();
+    let dest_arg = dest_dir.to_str().unwrap();
+
+    oli()
+        .arg("cp")
+        .arg(source_arg)
+        .arg(dest_arg)
+        .assert()
+        .success();
+
+    assert_snapshot!(directory_snapshot(dir.path()).with_content(true), @r"
++--------------------------------------------------+
+| Path                              Type   Content |
++==================================================+
+| [TEMP_DIR]                        DIR            |
+| [TEMP_DIR]/dest                   DIR            |
+| [TEMP_DIR]/dest/test_file.txt     FILE   hello   |
+| [TEMP_DIR]/source                 DIR            |
+| [TEMP_DIR]/source/test_file.txt   FILE   hello   |
++--------------------------------------------------+
+    ");
+    Ok(())
+}
+
+#[tokio::test]
+async fn test_recursive_cp_dir_to_new_dir() -> Result<()> {
+    let dir = tempdir()?;
+    let source_base_dir = dir.path().join("source_root");
+    let dest_base_dir = dir.path().join("dest_root");
+
+    // Create source directory structure
+    let source_dir = source_base_dir.join("source_dir");
+    fs::create_dir_all(&source_dir)?;
+
+    let file1_path = source_dir.join("file1.txt");
+    fs::write(&file1_path, "file1_content")?;
+
+    let sub_dir_path = source_dir.join("sub_dir");
+    fs::create_dir(&sub_dir_path)?;
 
+    let file2_path = sub_dir_path.join("file2.txt");
+    fs::write(&file2_path, "file2_content")?;
+
+    let file3_path = source_dir.join("file3.txt");
+    fs::write(&file3_path, "file3_content")?;
+
+    // Define destination path (should not exist yet)
+    let dest_dir_path = dest_base_dir.join("dest_dir");
+
+    oli()
+        .arg("cp")
+        .arg("-r")
+        .arg(source_dir.to_str().unwrap())
+        .arg(dest_dir_path.to_str().unwrap())
+        .assert()
+        .success();
     assert_snapshot!(directory_snapshot(dir.path()).with_content(true), @r"
-    +----------------------------------------------------+
-    | Path                 Type   Size (bytes)   Content |
-    +====================================================+
-    | [TEMP_DIR]           DIR    128                    |
-    | [TEMP_DIR]/dst.txt   FILE   5              hello   |
-    | [TEMP_DIR]/src.txt   FILE   5              hello   |
-    +----------------------------------------------------+
++----------------------------------------------------------------------------+
+| Path                                                  Type   Content       |
++============================================================================+
+| [TEMP_DIR]                                            DIR                  |
+| [TEMP_DIR]/dest_root                                  DIR                  |
+| [TEMP_DIR]/dest_root/dest_dir                         DIR                  |
+| [TEMP_DIR]/dest_root/dest_dir/file1.txt               FILE   file1_content |
+| [TEMP_DIR]/dest_root/dest_dir/file3.txt               FILE   file3_content |
+| [TEMP_DIR]/dest_root/dest_dir/sub_dir                 DIR                  |
+| [TEMP_DIR]/dest_root/dest_dir/sub_dir/file2.txt       FILE   file2_content |
+| [TEMP_DIR]/source_root                                DIR                  |
+| [TEMP_DIR]/source_root/source_dir                     DIR                  |
+| [TEMP_DIR]/source_root/source_dir/file1.txt           FILE   file1_content |
+| [TEMP_DIR]/source_root/source_dir/file3.txt           FILE   file3_content |
+| [TEMP_DIR]/source_root/source_dir/sub_dir             DIR                  |
+| [TEMP_DIR]/source_root/source_dir/sub_dir/file2.txt   FILE   file2_content |
++----------------------------------------------------------------------------+
     ");
     Ok(())
 }
diff --git a/bin/oli/tests/integration/ls.rs b/bin/oli/tests/integration/ls.rs
index 454ffad47..1ba546f39 100644
--- a/bin/oli/tests/integration/ls.rs
+++ b/bin/oli/tests/integration/ls.rs
@@ -33,17 +33,14 @@ async fn test_basic_ls() -> Result<()> {
     fs::write(&dst_path_3, expect)?;
 
     let current_dir = dir.path().to_string_lossy().to_string() + "/";
-    assert_cmd_snapshot!(oli().arg("ls").arg(current_dir), @r"
-    success: true
-    exit_code: 0
-    ----- stdout -----
-    [TEMP_DIR]/
-    dst_1.txt
-    dst_2.txt
-    dst_3.txt
-
-    ----- stderr -----
-    ");
+    let t = oli().arg("ls").arg(current_dir).assert().success();
+    let output = String::from_utf8(t.get_output().stdout.clone())?;
+    let mut output_list = output
+        .split("\n")
+        .filter(|x| !x.starts_with(".tmp") && !x.is_empty())
+        .collect::<Vec<_>>();
+    output_list.sort();
+    assert_eq!(output_list, ["dst_1.txt", "dst_2.txt", "dst_3.txt"]);
 
     Ok(())
 }
diff --git a/bin/oli/tests/integration/mv.rs b/bin/oli/tests/integration/mv.rs
index c16a741c7..986086a28 100644
--- a/bin/oli/tests/integration/mv.rs
+++ b/bin/oli/tests/integration/mv.rs
@@ -26,14 +26,13 @@ async fn test_basic_mv() -> Result<()> {
     let dst_path = dir.path().join("dst.txt");
     let expect = "hello";
     fs::write(&src_path, expect)?;
-
     assert_snapshot!(directory_snapshot(dir.path()).with_content(true), @r"
-    +----------------------------------------------------+
-    | Path                 Type   Size (bytes)   Content |
-    +====================================================+
-    | [TEMP_DIR]           DIR    96                     |
-    | [TEMP_DIR]/src.txt   FILE   5              hello   |
-    +----------------------------------------------------+
++-------------------------------------+
+| Path                 Type   Content |
++=====================================+
+| [TEMP_DIR]           DIR            |
+| [TEMP_DIR]/src.txt   FILE   hello   |
++-------------------------------------+
     ");
 
     oli()
@@ -44,12 +43,12 @@ async fn test_basic_mv() -> Result<()> {
         .success();
 
     assert_snapshot!(directory_snapshot(dir.path()).with_content(true), @r"
-    +----------------------------------------------------+
-    | Path                 Type   Size (bytes)   Content |
-    +====================================================+
-    | [TEMP_DIR]           DIR    96                     |
-    | [TEMP_DIR]/dst.txt   FILE   5              hello   |
-    +----------------------------------------------------+
++-------------------------------------+
+| Path                 Type   Content |
++=====================================+
+| [TEMP_DIR]           DIR            |
+| [TEMP_DIR]/dst.txt   FILE   hello   |
++-------------------------------------+
     ");
     Ok(())
 }
@@ -70,22 +69,22 @@ async fn test_move_a_file_to_a_dir() -> Result<()> {
         .arg(dst_path)
         .assert()
         .success();
-
     assert_snapshot!(directory_snapshot(src_dir.path()).with_content(true), @r"
-    +--------------------------------------------+
-    | Path         Type   Size (bytes)   Content |
-    +============================================+
-    | [TEMP_DIR]   DIR    64                     |
-    +--------------------------------------------+
++-----------------------------+
+| Path         Type   Content |
++=============================+
+| [TEMP_DIR]   DIR            |
++-----------------------------+
     ");
+
     assert_snapshot!(directory_snapshot(dst_dir.path()).with_content(true), @r"
-    +----------------------------------------------------+
-    | Path                 Type   Size (bytes)   Content |
-    +====================================================+
-    | [TEMP_DIR]           DIR    96                     |
-    | [TEMP_DIR]/dir       DIR    96                     |
-    | [TEMP_DIR]/src.txt   FILE   5              hello   |
-    +----------------------------------------------------+
++-----------------------------------------+
+| Path                     Type   Content |
++=========================================+
+| [TEMP_DIR]               DIR            |
+| [TEMP_DIR]/dir           DIR            |
+| [TEMP_DIR]/dir/src.txt   FILE   hello   |
++-----------------------------------------+
     ");
 
     Ok(())
@@ -108,19 +107,20 @@ async fn test_mv_with_recursive() -> Result<()> {
     fs::write(&src_file2, file2_content).expect("write file2 error");
 
     let src_empty_dir = src_path.join("empty_dir/");
+
     fs::create_dir(&src_empty_dir)?;
 
-    insta::assert_snapshot!(directory_snapshot(&src_root), @r"
-    +--------------------------------------------+
-    | Path                   Type   Size (bytes) |
-    +============================================+
-    | [TEMP_DIR]             DIR    96           |
-    | [TEMP_DIR]/src         DIR    160          |
-    | [TEMP_DIR]/dir         DIR    96           |
-    | [TEMP_DIR]/file2.txt   FILE   5            |
-    | [TEMP_DIR]/empty_dir   DIR    64           |
-    | [TEMP_DIR]/file1.txt   FILE   5            |
-    +--------------------------------------------+
+    insta::assert_snapshot!(directory_snapshot(&src_root).with_content(true), @r"
++-----------------------------------------------+
+| Path                           Type   Content |
++===============================================+
+| [TEMP_DIR]                     DIR            |
+| [TEMP_DIR]/src                 DIR            |
+| [TEMP_DIR]/src/dir             DIR            |
+| [TEMP_DIR]/src/dir/file2.txt   FILE   file2   |
+| [TEMP_DIR]/src/empty_dir       DIR            |
+| [TEMP_DIR]/src/file1.txt       FILE   file1   |
++-----------------------------------------------+
     ");
 
     let dst_path = tempfile::tempdir()?;
@@ -132,25 +132,25 @@ async fn test_mv_with_recursive() -> Result<()> {
         .arg("-r")
         .assert()
         .success();
-
-    assert_snapshot!(directory_snapshot(&src_root), @r"
-    +--------------------------------------+
-    | Path             Type   Size (bytes) |
-    +======================================+
-    | [TEMP_DIR]       DIR    96           |
-    | [TEMP_DIR]/src   DIR    64           |
-    +--------------------------------------+
+    assert_snapshot!(directory_snapshot(&src_root).with_content(true), @r"
++---------------------------------+
+| Path             Type   Content |
++=================================+
+| [TEMP_DIR]       DIR            |
+| [TEMP_DIR]/src   DIR            |
++---------------------------------+
     ");
+
     assert_snapshot!(directory_snapshot(&dst_path).with_content(true), @r"
-    +------------------------------------------------------+
-    | Path                   Type   Size (bytes)   Content |
-    +======================================================+
-    | [TEMP_DIR]             DIR    160                    |
-    | [TEMP_DIR]/dir         DIR    96                     |
-    | [TEMP_DIR]/file2.txt   FILE   5              file2   |
-    | [TEMP_DIR]/empty_dir   DIR    64                     |
-    | [TEMP_DIR]/file1.txt   FILE   5              file1   |
-    +------------------------------------------------------+
++-------------------------------------------+
+| Path                       Type   Content |
++===========================================+
+| [TEMP_DIR]                 DIR            |
+| [TEMP_DIR]/dir             DIR            |
+| [TEMP_DIR]/dir/file2.txt   FILE   file2   |
+| [TEMP_DIR]/empty_dir       DIR            |
+| [TEMP_DIR]/file1.txt       FILE   file1   |
++-------------------------------------------+
     ");
 
     Ok(())
diff --git a/bin/oli/tests/integration/rm.rs b/bin/oli/tests/integration/rm.rs
index 8d8dd0ffa..92e90840d 100644
--- a/bin/oli/tests/integration/rm.rs
+++ b/bin/oli/tests/integration/rm.rs
@@ -26,23 +26,23 @@ async fn test_basic_rm() -> Result<()> {
     let dst_path = dir.path().join("dst.txt");
     let expect = "hello";
     fs::write(&dst_path, expect)?;
-    assert_snapshot!(directory_snapshot(dir.path()), @r"
-    +------------------------------------------+
-    | Path                 Type   Size (bytes) |
-    +==========================================+
-    | [TEMP_DIR]           DIR    96           |
-    | [TEMP_DIR]/dst.txt   FILE   5            |
-    +------------------------------------------+
+    assert_snapshot!(directory_snapshot(dir.path()).with_content(true), @r"
+    +-------------------------------------+
+    | Path                 Type   Content |
+    +=====================================+
+    | [TEMP_DIR]           DIR            |
+    | [TEMP_DIR]/dst.txt   FILE   hello   |
+    +-------------------------------------+
     ");
 
     oli().arg("rm").arg(dst_path).assert().success();
 
-    assert_snapshot!(directory_snapshot(dir.path()), @r"
-    +----------------------------------+
-    | Path         Type   Size (bytes) |
-    +==================================+
-    | [TEMP_DIR]   DIR    64           |
-    +----------------------------------+
+    assert_snapshot!(directory_snapshot(dir.path()).with_content(true), @r"
++-----------------------------+
+| Path         Type   Content |
++=============================+
+| [TEMP_DIR]   DIR            |
++-----------------------------+
     ");
     Ok(())
 }
@@ -54,13 +54,13 @@ async fn test_rm_for_path_in_current_dir() -> Result<()> {
     let expect = "hello";
     fs::write(&dst_path, expect)?;
 
-    assert_snapshot!(directory_snapshot(dir.path()), @r"
-    +------------------------------------------+
-    | Path                 Type   Size (bytes) |
-    +==========================================+
-    | [TEMP_DIR]           DIR    96           |
-    | [TEMP_DIR]/dst.txt   FILE   5            |
-    +------------------------------------------+
+    assert_snapshot!(directory_snapshot(dir.path()).with_content(true), @r"
+    +-------------------------------------+
+    | Path                 Type   Content |
+    +=====================================+
+    | [TEMP_DIR]           DIR            |
+    | [TEMP_DIR]/dst.txt   FILE   hello   |
+    +-------------------------------------+
     ");
 
     oli()
@@ -70,12 +70,12 @@ async fn test_rm_for_path_in_current_dir() -> Result<()> {
         .assert()
         .success();
 
-    assert_snapshot!(directory_snapshot(dir.path()), @r"
-    +----------------------------------+
-    | Path         Type   Size (bytes) |
-    +==================================+
-    | [TEMP_DIR]   DIR    64           |
-    +----------------------------------+
+    assert_snapshot!(directory_snapshot(dir.path()).with_content(true), @r"
+    +-----------------------------+
+    | Path         Type   Content |
+    +=============================+
+    | [TEMP_DIR]   DIR            |
+    +-----------------------------+
     ");
 
     Ok(())
diff --git a/bin/oli/tests/integration/stat.rs b/bin/oli/tests/integration/stat.rs
index 6a04de386..3cea0ace2 100644
--- a/bin/oli/tests/integration/stat.rs
+++ b/bin/oli/tests/integration/stat.rs
@@ -31,7 +31,7 @@ async fn test_basic_stat() -> Result<()> {
     success: true
     exit_code: 0
     ----- stdout -----
-    [TEMP_DIR]/dst.txt
+    path: [TEMP_DIR]/dst.txt
     size: 5
     type: file
     last-modified: [TIMESTAMP]
diff --git a/bin/oli/tests/integration/test_utils.rs b/bin/oli/tests/integration/test_utils.rs
index 83f2e0fcd..945e39214 100644
--- a/bin/oli/tests/integration/test_utils.rs
+++ b/bin/oli/tests/integration/test_utils.rs
@@ -20,7 +20,6 @@ pub use assert_cmd::prelude::*;
 pub use tempfile::tempdir;
 
 use std::{
-    os::unix::fs::MetadataExt,
     path::{self, PathBuf},
     process::Command,
 };
@@ -45,7 +44,7 @@ pub fn directory_snapshot(dir: impl AsRef<path::Path>) -> DirectorySnapshot {
         dir: dir.as_ref().to_path_buf(),
         // default formatting options
         with_type: true,
-        with_size: true,
+        with_size: false,
         with_content: false,
     }
 }
@@ -110,7 +109,7 @@ impl std::fmt::Display for DirectorySnapshot {
                 );
             }
             if self.with_size {
-                row.push(format!("{}", entry.metadata().unwrap().size()));
+                row.push(format!("{}", entry.metadata().unwrap().len()));
             }
             if self.with_content && entry.file_type().is_file() {
                 let content = std::fs::read_to_string(entry.path()).unwrap();
@@ -139,8 +138,7 @@ macro_rules! assert_snapshot {
 pub(crate) use assert_snapshot;
 
 pub const REPLACEMENTS: &[(&str, &str)] = &[
-    (r".*\.tmp\S+/", "[TEMP_DIR]/"),
-    (r".*\.tmp\S+", "[TEMP_DIR]"),
+    (r"/.*\.tmp[^/]+", "[TEMP_DIR]"), // New regex for specific /.tmpXXXX patterns
     (
         r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{9} UTC)",
         "[TIMESTAMP]",