Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(copy): add more copying APIs to LocalAsset #62

Merged
merged 3 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions src/compression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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)
Expand Down
89 changes: 89 additions & 0 deletions src/dirs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//! Utilities for working with directories
//!
//! Right now just a wrapper around WalkDirs that does some utf8 conversions and strip_prefixing,
//! since we always end up doing that.

use crate::error::*;
use camino::{Utf8Path, Utf8PathBuf};

/// Walk through this dir's descendants with `walkdirs`
pub fn walk_dir(dir: impl AsRef<Utf8Path>) -> AxoassetWalkDir {
let dir = dir.as_ref();
AxoassetWalkDir {
root_dir: dir.to_owned(),
inner: walkdir::WalkDir::new(dir),
}
}

/// Wrapper around [`walkdir::WalkDir`][].
pub struct AxoassetWalkDir {
root_dir: Utf8PathBuf,
inner: walkdir::WalkDir,
}

/// Wrapper around [`walkdir::IntoIter`][].
pub struct AxoassetIntoIter {
root_dir: Utf8PathBuf,
inner: walkdir::IntoIter,
}

/// Wrapper around [`walkdir::DirEntry`][].
pub struct AxoassetDirEntry {
/// full path to the entry
pub full_path: Utf8PathBuf,
/// path to the entry relative to the dir passed to [`walk_dir`][].
pub rel_path: Utf8PathBuf,
/// Inner contents
pub entry: walkdir::DirEntry,
}

impl IntoIterator for AxoassetWalkDir {
type IntoIter = AxoassetIntoIter;
type Item = Result<AxoassetDirEntry>;
fn into_iter(self) -> Self::IntoIter {
AxoassetIntoIter {
root_dir: self.root_dir,
inner: self.inner.into_iter(),
}
}
}

impl Iterator for AxoassetIntoIter {
type Item = Result<AxoassetDirEntry>;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|next| {
let entry = next.map_err(|e| AxoassetError::WalkDirFailed {
origin_path: self.root_dir.clone(),
details: e,
})?;

let full_path = Utf8PathBuf::from_path_buf(entry.path().to_owned())
.map_err(|details| AxoassetError::Utf8Path { path: details })?;
let rel_path = full_path
.strip_prefix(&self.root_dir)
.map_err(|_| AxoassetError::PathNesting {
root_dir: self.root_dir.clone(),
child_dir: full_path.clone(),
})?
.to_owned();

Ok(AxoassetDirEntry {
full_path,
rel_path,
entry,
})
})
}
}

impl std::ops::Deref for AxoassetDirEntry {
type Target = walkdir::DirEntry;
fn deref(&self) -> &Self::Target {
&self.entry
}
}
impl std::ops::DerefMut for AxoassetDirEntry {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.entry
}
}
20 changes: 20 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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")]
Expand Down
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ 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")]
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;
Expand Down Expand Up @@ -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<PathBuf> {
pub async fn copy(origin_path: &str, dest_dir: &str) -> Result<Utf8PathBuf> {
#[cfg(feature = "remote")]
if is_remote(origin_path)? {
return RemoteAsset::copy(origin_path, dest_dir).await;
Expand Down
93 changes: 89 additions & 4 deletions src/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -212,12 +212,97 @@ impl LocalAsset {
Ok(())
}

/// Copies an asset from one location on the local filesystem to another
/// Copies an asset from one location on the local filesystem to the given directory
///
/// 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<Utf8Path>,
dest_dir: impl AsRef<Utf8Path>,
) -> Result<PathBuf> {
LocalAsset::load(origin_path)?.write(dest_dir)
) -> Result<Utf8PathBuf> {
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
///
/// Both paths are assumed to be file names.
pub fn copy_named(
origin_path: impl AsRef<Utf8Path>,
dest_path: impl AsRef<Utf8Path>,
) -> Result<()> {
let origin_path = origin_path.as_ref();
let dest_path = dest_path.as_ref();

fs::copy(origin_path, dest_path).map_err(|e| AxoassetError::LocalAssetCopyFailed {
origin_path: origin_path.to_string(),
dest_path: dest_path.to_string(),
details: e,
})?;

Ok(())
}

/// Recursively copies a directory from one location to the given directory
///
/// 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<Utf8Path>,
dest_dir: impl AsRef<Utf8Path>,
) -> Result<Utf8PathBuf> {
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_dir_named(origin_path, &dest_path)?;

Ok(dest_path)
}

/// Recursively copies a directory from one location to another
///
/// Both paths are assumed to be the names of the directory being copied
/// (i.e. dest_path is not the parent dir).
pub fn copy_dir_named(
origin_path: impl AsRef<Utf8Path>,
dest_path: impl AsRef<Utf8Path>,
) -> Result<()> {
let origin_path = origin_path.as_ref();
let dest_path = dest_path.as_ref();

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!)
LocalAsset::create_dir(to)?;
} else if entry.file_type().is_file() {
// copy files
LocalAsset::copy_named(from, to)?;
} else {
// other kinds of file presumed to be symlinks which we don't handle
debug_assert!(
entry.file_type().is_symlink(),
"unknown type of file at {from}, axoasset needs to be updated to support this!"
);
}
}
Ok(())
}

/// Get the current working directory
Expand Down
8 changes: 5 additions & 3 deletions src/remote.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -60,15 +62,15 @@ impl RemoteAsset {
}

/// Copies an asset to the local filesystem.
pub async fn copy(origin_path: &str, dest_dir: &str) -> Result<PathBuf> {
pub async fn copy(origin_path: &str, dest_dir: &str) -> Result<Utf8PathBuf> {
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,
}),
}
Expand Down
Loading