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;
#[derive(Debug, Eq, PartialEq)]
pub struct Cloned {
pub made: usize,
pub left_out: Vec<PathBuf>,
}
fn walk(source: &Path) -> Result<(Vec<Entry>, Vec<PathBuf>), String> {
let mut found: Vec<Entry> = Vec::new();
let mut left_out: Vec<PathBuf> = Vec::new();
let mut pending: Vec<PathBuf> = vec![PathBuf::new()];
while let Some(relative) = pending.pop() {
let directory: PathBuf = source.join(&relative);
let listing = match std::fs::read_dir(&directory) {
Ok(listed_directory) => listed_directory,
Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied && !relative.as_os_str().is_empty() => {
left_out.push(relative);
continue;
}
Err(error) => return Err(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); });
left_out.sort();
return Ok((found, left_out));
}
fn place(source: &Path, destination: &Path, entry: &Entry) -> Result<Option<PathBuf>, 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(|()| { return None; }).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()); })?;
if let Err(error) = copy_file_exclusive(&from, &to) {
if matches!(std::fs::File::open(&from), Err(ref denied) if denied.kind() == std::io::ErrorKind::PermissionDenied) {
let _removed = std::fs::remove_file(&to);
return Ok(Some(entry.relative.clone()));
}
return Err(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(|()| { return None; }).map_err(|error| { return format!("{} cannot take its times ({error})", to.display()); });
}
pub fn clone_tree(source: &Path, destination: &Path, workers: usize) -> Result<Cloned, String> {
let (entries, mut left_out): (Vec<Entry>, Vec<PathBuf>) = 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<Vec<PathBuf>, String>> = std::thread::scope(|scope| {
let handles: Vec<_> = leaves
.chunks(slice_length)
.map(|slice: &[&Entry]| {
return scope.spawn(move || {
let mut skipped: Vec<PathBuf> = Vec::new();
for entry in slice {
skipped.extend(place(source, destination, entry)?);
}
return Ok(skipped);
});
})
.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 {
left_out.extend(outcome?);
}
left_out.sort();
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()); })?;
let skipped_files: usize = entries.iter().filter(|entry: &&Entry| { return entry.kind != Kind::Directory && left_out.contains(&entry.relative); }).count();
return Ok(Cloned { made: entries.len() - skipped_files, left_out });
}
#[cfg(test)]
#[path = "package_repair_tree_clone_tests.rs"]
mod tests;