use std::future::Future;
use std::io;
use std::path::Path;
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>>;
}
#[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 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());
}
}