use std::future::Future;
use std::io;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use crate::error::{Error, Result};
use crate::profiler::Entry;
pub(crate) trait EntryAction: Send + Sync {
fn execute<'a>(
&'a self,
entry: &'a Entry,
dest_root: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
fn undo<'a>(
&'a self,
entry: &'a Entry,
dest_root: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
fn progress_target(&self, _entry: &Entry, _dest_root: &Path) -> Option<PathBuf> {
None
}
}
#[derive(Debug, Clone)]
pub(crate) struct CopyAction {
pub overwrite: bool,
}
impl EntryAction for CopyAction {
fn execute<'a>(
&'a self,
entry: &'a Entry,
dest_root: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
let dest_path = dest_root.join(&entry.relative_path);
if let Some(parent) = dest_path.parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| classify_error(e, parent, 0))?;
}
if !self.overwrite {
match tokio::fs::metadata(&dest_path).await {
Ok(_) => return Err(Error::DestExists { path: dest_path }),
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => return Err(classify_error(e, &dest_path, 0)),
}
}
tokio::fs::copy(&entry.path, &dest_path)
.await
.map(|_| ())
.map_err(|e| classify_error(e, &entry.path, entry.size))
})
}
fn progress_target(&self, entry: &Entry, dest_root: &Path) -> Option<PathBuf> {
Some(dest_root.join(&entry.relative_path))
}
fn undo<'a>(
&'a self,
entry: &'a Entry,
dest_root: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
let dest_path = dest_root.join(&entry.relative_path);
match tokio::fs::remove_file(&dest_path).await {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(classify_error(e, &dest_path, 0)),
}
})
}
}
fn classify_error(err: io::Error, path: &Path, needed: u64) -> Error {
match err.kind() {
io::ErrorKind::NotFound => Error::SourceNotFound {
path: path.to_path_buf(),
},
io::ErrorKind::PermissionDenied => Error::PermissionDenied {
path: path.to_path_buf(),
},
io::ErrorKind::StorageFull => Error::NoSpace {
needed,
available: 0,
},
_ => Error::Io {
path: path.to_path_buf(),
source: err,
},
}
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::PathBuf;
use tempfile::tempdir;
use super::*;
fn entry(path: PathBuf, relative_path: PathBuf, size: u64) -> Entry {
Entry {
path,
relative_path,
size,
modified: None,
}
}
#[tokio::test]
async fn execute_copies_bytes_and_creates_missing_parent_dirs() {
let src_dir = tempdir().unwrap();
let dest_dir = tempdir().unwrap();
let src_path = src_dir.path().join("file.txt");
fs::write(&src_path, b"hello world").unwrap();
let relative_path = PathBuf::from("nested/deep/file.txt");
let e = entry(src_path, relative_path.clone(), 11);
let action = CopyAction { overwrite: false };
action.execute(&e, dest_dir.path()).await.unwrap();
let dest_path = dest_dir.path().join(&relative_path);
assert_eq!(fs::read(&dest_path).unwrap(), b"hello world");
}
#[tokio::test]
async fn execute_without_overwrite_fails_on_existing_destination() {
let src_dir = tempdir().unwrap();
let dest_dir = tempdir().unwrap();
let src_path = src_dir.path().join("file.txt");
fs::write(&src_path, b"new content").unwrap();
let relative_path = PathBuf::from("file.txt");
let dest_path = dest_dir.path().join(&relative_path);
fs::write(&dest_path, b"old content").unwrap();
let e = entry(src_path, relative_path, 11);
let action = CopyAction { overwrite: false };
let result = action.execute(&e, dest_dir.path()).await;
assert!(matches!(result, Err(Error::DestExists { .. })));
assert_eq!(fs::read(&dest_path).unwrap(), b"old content");
}
#[tokio::test]
async fn execute_with_overwrite_replaces_existing_destination() {
let src_dir = tempdir().unwrap();
let dest_dir = tempdir().unwrap();
let src_path = src_dir.path().join("file.txt");
fs::write(&src_path, b"new content").unwrap();
let relative_path = PathBuf::from("file.txt");
let dest_path = dest_dir.path().join(&relative_path);
fs::write(&dest_path, b"old content").unwrap();
let e = entry(src_path, relative_path, 11);
let action = CopyAction { overwrite: true };
action.execute(&e, dest_dir.path()).await.unwrap();
assert_eq!(fs::read(&dest_path).unwrap(), b"new content");
}
#[tokio::test]
async fn concurrent_parent_dir_creation_both_succeed() {
let src_dir = tempdir().unwrap();
let dest_dir = tempdir().unwrap();
let src_path_a = src_dir.path().join("a.txt");
let src_path_b = src_dir.path().join("b.txt");
fs::write(&src_path_a, b"a").unwrap();
fs::write(&src_path_b, b"b").unwrap();
let entry_a = entry(src_path_a, PathBuf::from("shared/a.txt"), 1);
let entry_b = entry(src_path_b, PathBuf::from("shared/b.txt"), 1);
let action = CopyAction { overwrite: false };
let dest_root = dest_dir.path().to_path_buf();
let (result_a, result_b) = tokio::join!(
action.execute(&entry_a, &dest_root),
action.execute(&entry_b, &dest_root),
);
result_a.unwrap();
result_b.unwrap();
assert!(dest_dir.path().join("shared/a.txt").exists());
assert!(dest_dir.path().join("shared/b.txt").exists());
}
#[tokio::test]
async fn undo_removes_exactly_the_destination_it_created() {
let src_dir = tempdir().unwrap();
let dest_dir = tempdir().unwrap();
let src_path = src_dir.path().join("file.txt");
fs::write(&src_path, b"data").unwrap();
let relative_path = PathBuf::from("file.txt");
let e = entry(src_path, relative_path.clone(), 4);
let sibling = dest_dir.path().join("sibling.txt");
fs::write(&sibling, b"leave me alone").unwrap();
let action = CopyAction { overwrite: false };
action.execute(&e, dest_dir.path()).await.unwrap();
let dest_path = dest_dir.path().join(&relative_path);
assert!(dest_path.exists());
action.undo(&e, dest_dir.path()).await.unwrap();
assert!(!dest_path.exists());
assert!(sibling.exists());
}
}