use super::worktree_copy_stage::copy_file_exclusive;
use std::fs::{FileTimes, Metadata};
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Kind {
Directory,
File,
Link,
}
struct Entry {
relative: PathBuf,
kind: Kind,
}
const PARALLEL_MIN_ENTRIES: usize = 512;
fn walk(source: &Path) -> Result<Vec<Entry>, String> {
let mut found: Vec<Entry> = Vec::new();
let mut pending: Vec<PathBuf> = vec![PathBuf::new()];
while let Some(relative) = pending.pop() {
let directory: PathBuf = source.join(&relative);
let listing = std::fs::read_dir(&directory).map_err(|error| { return format!("{} cannot be read ({error})", directory.display()); })?;
for listed in listing {
let item = listed.map_err(|error| { return format!("{} cannot be listed ({error})", directory.display()); })?;
let path: PathBuf = relative.join(item.file_name());
let file_type = item.file_type().map_err(|error| { return format!("{} has no type ({error})", item.path().display()); })?;
if file_type.is_dir() {
pending.push(path.clone());
found.push(Entry { relative: path, kind: Kind::Directory });
} else if file_type.is_symlink() {
found.push(Entry { relative: path, kind: Kind::Link });
} else if file_type.is_file() {
found.push(Entry { relative: path, kind: Kind::File });
}
}
}
found.sort_by(|left: &Entry, right: &Entry| { return left.relative.cmp(&right.relative); });
return Ok(found);
}
fn place(source: &Path, destination: &Path, entry: &Entry) -> Result<(), String> {
let from: PathBuf = source.join(&entry.relative);
let to: PathBuf = destination.join(&entry.relative);
if entry.kind == Kind::Link {
let target: PathBuf = std::fs::read_link(&from).map_err(|error| { return format!("{} cannot be read as a link ({error})", from.display()); })?;
return symlink(&target, &to).map_err(|error| { return format!("{} cannot be made ({error})", to.display()); });
}
let before: Metadata = std::fs::metadata(&from).map_err(|error| { return format!("{} cannot be read ({error})", from.display()); })?;
copy_file_exclusive(&from, &to).map_err(|error| { return format!("{} cannot be cloned ({})", from.display(), error.0); })?;
std::fs::set_permissions(&to, before.permissions()).map_err(|error| { return format!("{} cannot take its mode ({error})", to.display()); })?;
let times: FileTimes = FileTimes::new().set_accessed(before.accessed().unwrap_or(std::time::UNIX_EPOCH)).set_modified(before.modified().unwrap_or(std::time::UNIX_EPOCH));
let made: std::fs::File = std::fs::File::open(&to).map_err(|error| { return format!("{} cannot be opened ({error})", to.display()); })?;
return made.set_times(times).map_err(|error| { return format!("{} cannot take its times ({error})", to.display()); });
}
pub fn clone_tree(source: &Path, destination: &Path, workers: usize) -> Result<usize, String> {
let entries: Vec<Entry> = walk(source)?;
let root: Metadata = std::fs::metadata(source).map_err(|error| { return format!("{} cannot be read ({error})", source.display()); })?;
if let Some(parent) = destination.parent() {
std::fs::create_dir_all(parent).map_err(|error| { return format!("{} cannot be made ({error})", parent.display()); })?;
}
std::fs::create_dir(destination).map_err(|error| { return format!("{} cannot be made ({error})", destination.display()); })?;
let directories: Vec<&Entry> = entries.iter().filter(|entry: &&Entry| { return entry.kind == Kind::Directory; }).collect();
for directory in &directories {
let made: PathBuf = destination.join(&directory.relative);
std::fs::create_dir(&made).map_err(|error| { return format!("{} cannot be made ({error})", made.display()); })?;
}
let leaves: Vec<&Entry> = entries.iter().filter(|entry: &&Entry| { return entry.kind != Kind::Directory; }).collect();
let used: usize = if leaves.len() < PARALLEL_MIN_ENTRIES { 1 } else { workers.max(1) };
let slice_length: usize = leaves.len().div_ceil(used).max(1);
let outcomes: Vec<Result<(), String>> = std::thread::scope(|scope| {
let handles: Vec<_> = leaves
.chunks(slice_length)
.map(|slice: &[&Entry]| {
return scope.spawn(move || {
for entry in slice {
place(source, destination, entry)?;
}
return Ok(());
});
})
.collect();
return handles.into_iter().map(|handle| { return handle.join().unwrap_or_else(|_panic| { return Err(String::from("a clone worker stopped unexpectedly")); }); }).collect();
});
for outcome in outcomes {
outcome?;
}
for directory in directories.iter().rev() {
let made: PathBuf = destination.join(&directory.relative);
let mode: std::fs::Permissions = std::fs::metadata(source.join(&directory.relative)).map_err(|error| { return format!("{} cannot be read ({error})", directory.relative.display()); })?.permissions();
std::fs::set_permissions(&made, mode).map_err(|error| { return format!("{} cannot take its mode ({error})", made.display()); })?;
}
std::fs::set_permissions(destination, root.permissions()).map_err(|error| { return format!("{} cannot take its mode ({error})", destination.display()); })?;
return Ok(entries.len());
}
#[cfg(test)]
#[path = "package_repair_tree_clone_tests.rs"]
mod tests;