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-oli.git
The following commit(s) were added to refs/heads/main by this push:
new 7670ad0 fix: cp -r command fails with unmatching path prefix (#5)
7670ad0 is described below
commit 7670ad00d8d8efa2d29691f8f9e453693fb11939
Author: Ruihang Xia <[email protected]>
AuthorDate: Mon Nov 10 23:36:03 2025 +0800
fix: cp -r command fails with unmatching path prefix (#5)
Signed-off-by: Ruihang Xia <[email protected]>
---
src/commands/cp.rs | 86 ++++++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 77 insertions(+), 9 deletions(-)
diff --git a/src/commands/cp.rs b/src/commands/cp.rs
index 23a5b24..52d876d 100644
--- a/src/commands/cp.rs
+++ b/src/commands/cp.rs
@@ -21,7 +21,9 @@ use indicatif::ProgressBar;
use indicatif::ProgressStyle;
use opendal::ErrorKind;
use opendal::Metadata;
+use std::path::Component;
use std::path::Path;
+use std::path::PathBuf;
use anyhow::Context;
use anyhow::Result;
@@ -154,20 +156,21 @@ impl CopyCmd {
let dst_root = Path::new(&final_dst_path);
let mut ds = src_op.lister_with(&src_path).recursive(true).await?;
+ let normalized_src_root =
normalize_path_for_prefix(Path::new(&src_path));
+
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 '{depath}' does not start
with source path '{src_path}'"
- )
- })?; // relative_path is a &Path
+ // Calculate relative path, ignoring differences in root
components such as leading slashes.
+ let relative_path = relative_path_from_entry(
+ &normalized_src_root,
+ Path::new(depath),
+ &src_path,
+ depath,
+ )?;
- let current_dst_path_path = dst_root.join(relative_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() {
@@ -256,3 +259,68 @@ impl CopyProgress {
Ok(written)
}
}
+
+fn normalize_path_for_prefix(path: &Path) -> PathBuf {
+ let mut normalized = PathBuf::new();
+ for component in path.components() {
+ match component {
+ Component::RootDir | Component::CurDir => continue,
+ Component::Normal(os) => normalized.push(Path::new(os)),
+ Component::ParentDir => normalized.push(".."),
+ Component::Prefix(prefix) =>
normalized.push(Path::new(prefix.as_os_str())),
+ }
+ }
+ normalized
+}
+
+fn relative_path_from_entry(
+ normalized_src_root: &Path,
+ entry: &Path,
+ original_src: &str,
+ original_entry: &str,
+) -> Result<PathBuf> {
+ let normalized_entry = normalize_path_for_prefix(entry);
+ if normalized_src_root.as_os_str().is_empty() {
+ return Ok(normalized_entry);
+ }
+
+ normalized_entry
+ .strip_prefix(normalized_src_root)
+ .map(Path::to_path_buf)
+ .with_context(|| {
+ format!(
+ "Internal error: Lister path '{original_entry}' does not start
with source path '{original_src}'"
+ )
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn normalize_path_strips_root_and_current_components() {
+ let normalized =
normalize_path_for_prefix(Path::new("/backup/./subdir/"));
+ assert_eq!(normalized, PathBuf::from("backup/subdir"));
+ }
+
+ #[test]
+ fn relative_path_ignores_leading_slash_difference() {
+ let src = "/backup/subdir/";
+ let entry = "backup/subdir/data/dir/file.parquet";
+ let normalized_src = normalize_path_for_prefix(Path::new(src));
+ let relative =
+ relative_path_from_entry(&normalized_src, Path::new(entry), src,
entry).unwrap();
+ assert_eq!(relative, PathBuf::from("data/dir/file.parquet"));
+ }
+
+ #[test]
+ fn relative_path_returns_error_for_unmatched_prefix() {
+ let src = "/backup/subdir/";
+ let entry = "backup/other/file.parquet";
+ let normalized_src = normalize_path_for_prefix(Path::new(src));
+ let err = relative_path_from_entry(&normalized_src, Path::new(entry),
src, entry)
+ .expect_err("expected prefix mismatch");
+ assert!(format!("{err}").contains("does not start with source path"));
+ }
+}