From cf2a9f9112e462cbd4888f2845ccf97b25eb8db4 Mon Sep 17 00:00:00 2001 From: Aria Beingessner Date: Tue, 8 Aug 2023 14:14:00 -0400 Subject: [PATCH] feat(copy): factor out a WalkDirs wrapper and clean up copy code --- src/compression.rs | 15 ++++--------- src/error.rs | 20 ++++++++++++++++++ src/lib.rs | 4 +++- src/local.rs | 52 ++++++++++++++++++---------------------------- src/remote.rs | 8 ++++--- 5 files changed, 52 insertions(+), 47 deletions(-) diff --git a/src/compression.rs b/src/compression.rs index d6cc782..adcbe32 100644 --- a/src/compression.rs +++ b/src/compression.rs @@ -166,14 +166,14 @@ pub(crate) fn zip_dir( fs::File, io::{Read, Write}, }; - use zip::{result::ZipError, write::FileOptions, CompressionMethod}; + use zip::{write::FileOptions, CompressionMethod}; let file = File::create(dest_path)?; // The `zip` crate lacks the conveniences of the `tar` crate so we need to manually // walk through all the subdirs of `src_path` and copy each entry. walkdir streamlines // that process for us. - let walkdir = walkdir::WalkDir::new(src_path); + let walkdir = crate::dirs::walk_dir(src_path); let it = walkdir.into_iter(); let mut zip = zip::ZipWriter::new(file); @@ -190,15 +190,8 @@ pub(crate) fn zip_dir( let mut buffer = Vec::new(); for entry in it.filter_map(|e| e.ok()) { - let path = entry.path(); - // Get the relative path of this file/dir that will be used in the zip - let Some(name) = path - .strip_prefix(src_path) - .ok() - .and_then(Utf8Path::from_path) - else { - return Err(ZipError::UnsupportedArchive("unsupported path format")); - }; + let name = &entry.rel_path; + let path = &entry.full_path; // Optionally apply the root prefix let name = if let Some(root) = with_root { root.join(name) diff --git a/src/error.rs b/src/error.rs index 3543d46..714495c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -292,6 +292,16 @@ pub enum AxoassetError { /// The problematic path path: std::path::PathBuf, }, + /// This error indicates we tried to strip_prefix a path that should have been + /// a descendant of another, but it didn't work. + #[error("Child wasn't nested under its parent: {root_dir} => {child_dir}")] + #[diagnostic(help("Are symlinks involved?"))] + PathNesting { + /// The root/ancestor dir + root_dir: camino::Utf8PathBuf, + /// THe child/descendent path + child_dir: camino::Utf8PathBuf, + }, #[error("Failed to find {desired_filename} in an ancestor of {start_dir}")] /// This error indicates we failed to find the desired file in an ancestor of the search dir. @@ -302,6 +312,16 @@ pub enum AxoassetError { desired_filename: String, }, + #[error("Failed to walk to ancestor of {origin_path}")] + /// Walkdir failed to yield an entry + WalkDirFailed { + /// The root path we were trying to walkdirs + origin_path: camino::Utf8PathBuf, + /// Inner walkdir error + #[source] + details: walkdir::Error, + }, + /// This error indicates we tried to deserialize some JSON with serde_json /// but failed. #[cfg(feature = "json-serde")] diff --git a/src/lib.rs b/src/lib.rs index 9ea1ade..77c53a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ use std::path::PathBuf; #[cfg(any(feature = "compression-zip", feature = "compression-tar"))] pub(crate) mod compression; +pub(crate) mod dirs; pub(crate) mod error; pub(crate) mod local; #[cfg(feature = "remote")] @@ -20,6 +21,7 @@ pub(crate) mod remote; pub(crate) mod source; pub(crate) mod spanned; +use camino::Utf8PathBuf; pub use error::AxoassetError; use error::Result; pub use local::LocalAsset; @@ -83,7 +85,7 @@ impl Asset { /// Copies an asset, returning the path to the copy destination on the /// local filesystem. - pub async fn copy(origin_path: &str, dest_dir: &str) -> Result { + pub async fn copy(origin_path: &str, dest_dir: &str) -> Result { #[cfg(feature = "remote")] if is_remote(origin_path)? { return RemoteAsset::copy(origin_path, dest_dir).await; diff --git a/src/local.rs b/src/local.rs index aed2d8a..6a8c6cd 100644 --- a/src/local.rs +++ b/src/local.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use camino::{Utf8Path, Utf8PathBuf}; -use crate::error::*; +use crate::{dirs, error::*}; /// A local asset contains a path on the local filesystem and its contents #[derive(Debug)] @@ -216,11 +216,20 @@ impl LocalAsset { /// /// The destination will use the same file name as the origin has. /// If you want to specify the destination file's name, use [`LocalAsset::copy_named`][]. + /// + /// The returned path is the resulting file. pub fn copy( origin_path: impl AsRef, dest_dir: impl AsRef, - ) -> Result { - LocalAsset::load(origin_path)?.write(dest_dir) + ) -> Result { + let origin_path = origin_path.as_ref(); + let dest_dir = dest_dir.as_ref(); + + let filename = Self::filename(origin_path)?; + let dest_path = dest_dir.join(filename); + Self::copy_named(origin_path, &dest_path)?; + + Ok(dest_path) } /// Copies an asset from one location on the local filesystem to another @@ -247,6 +256,8 @@ impl LocalAsset { /// The destination will use the same dir name as the origin has, so /// dest_dir is the *parent* of the copied directory. If you want to specify the destination's /// dir name, use [`LocalAsset::copy_dir_named`][]. + /// + /// The returned path is the resulting dir. pub fn copy_dir( origin_path: impl AsRef, dest_dir: impl AsRef, @@ -257,6 +268,7 @@ impl LocalAsset { let filename = Self::filename(origin_path)?; let dest_path = dest_dir.join(filename); Self::copy_dir_named(origin_path, &dest_path)?; + Ok(dest_path) } @@ -271,38 +283,14 @@ impl LocalAsset { let origin_path = origin_path.as_ref(); let dest_path = dest_path.as_ref(); - for entry in walkdir::WalkDir::new(origin_path) { - let entry = entry.map_err(|e| AxoassetError::LocalAssetCopyFailed { - origin_path: origin_path.to_string(), - dest_path: dest_path.to_string(), - details: e.into_io_error(), - })?; - - let from = Utf8PathBuf::from_path_buf(entry.path().to_owned()) - .map_err(|details| AxoassetError::Utf8Path { path: details })?; - let suffix = from.strip_prefix(origin_path).map_err(|_| { - AxoassetError::LocalAssetCopyFailed { - origin_path: origin_path.to_string(), - dest_path: dest_path.to_string(), - details: None, - } - })?; - let to = dest_path.join(suffix); + for entry in dirs::walk_dir(origin_path) { + let entry = entry?; + let from = &entry.full_path; + let to = dest_path.join(&entry.rel_path); if entry.file_type().is_dir() { // create directories (even empty ones!) - if let Err(e) = fs::create_dir(to) { - match e.kind() { - std::io::ErrorKind::AlreadyExists => {} - _ => { - return Err(AxoassetError::LocalAssetCopyFailed { - origin_path: origin_path.to_string(), - dest_path: dest_path.to_string(), - details: Some(e), - }) - } - } - } + LocalAsset::create_dir(to)?; } else if entry.file_type().is_file() { // copy files LocalAsset::copy_named(from, to)?; diff --git a/src/remote.rs b/src/remote.rs index 6141b15..84099fb 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -1,6 +1,8 @@ use std::fs; use std::path::{Path, PathBuf}; +use camino::{Utf8Path, Utf8PathBuf}; + use crate::error::*; /// A remote asset is an asset that is fetched over the network. @@ -60,15 +62,15 @@ impl RemoteAsset { } /// Copies an asset to the local filesystem. - pub async fn copy(origin_path: &str, dest_dir: &str) -> Result { + pub async fn copy(origin_path: &str, dest_dir: &str) -> Result { match RemoteAsset::load(origin_path).await { Ok(a) => { - let dest_path = Path::new(dest_dir).join(a.filename); + let dest_path = Utf8Path::new(dest_dir).join(a.filename); match fs::write(&dest_path, a.contents) { Ok(_) => Ok(dest_path), Err(details) => Err(AxoassetError::RemoteAssetWriteFailed { origin_path: origin_path.to_string(), - dest_path: dest_path.display().to_string(), + dest_path: dest_path.to_string(), details, }), }