use crate::side_by_side_threads::run_all_side_by_side;
use crate::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 = crate::parallel_budget::PARALLEL_BREAK_EVEN_ENTRIES;
#[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 = say_where(listed, directory.as_path(), "cannot be listed")?;
let path: PathBuf = relative.join(item.file_name());
let file_type = say_where(item.file_type(), item.path().as_path(), "has no type")?;
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(compare_by_relative_path);
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 = say_where(std::fs::read_link(&from), from.as_path(), "cannot be read as a link")?;
say_where(symlink(&target, &to), to.as_path(), "cannot be made")?;
return Ok(None);
}
let before: Metadata = say_where(std::fs::metadata(&from), from.as_path(), "cannot be read")?;
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));
}
say_where(std::fs::set_permissions(&to, before.permissions()), to.as_path(), "cannot take its mode")?;
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 = say_where(std::fs::File::open(&to), to.as_path(), "cannot be opened")?;
say_where(made.set_times(times), to.as_path(), "cannot take its times")?;
return Ok(None);
}
fn say_where<T>(result: Result<T, std::io::Error>, path: &Path, what: &str) -> Result<T, String> {
return match result {
Ok(value) => Ok(value),
Err(error) => Err(format!("{} {what} ({error})", path.display())),
};
}
fn compare_by_relative_path(left: &Entry, right: &Entry) -> std::cmp::Ordering {
return left.relative.cmp(&right.relative);
}
fn place_slices_side_by_side(
leaves: &[&Entry],
slice_length: usize,
source: &Path,
destination: &Path,
) -> Vec<Result<Vec<PathBuf>, String>> {
let mut slices: Vec<(&Path, &Path, &[&Entry])> = Vec::new();
for slice in leaves.chunks(slice_length) {
slices.push((source, destination, slice));
}
let mut outcomes: Vec<Result<Vec<PathBuf>, String>> = Vec::new();
for joined in run_all_side_by_side(slices, place_slice) {
outcomes.push(match joined {
Ok(outcome) => outcome,
Err(_panic) => Err(String::from("a clone worker stopped unexpectedly")),
});
}
return outcomes;
}
fn place_slice((source, destination, slice): (&Path, &Path, &[&Entry])) -> Result<Vec<PathBuf>, String> {
let mut skipped: Vec<PathBuf> = Vec::new();
for entry in slice {
skipped.extend(place(source, destination, entry)?);
}
return Ok(skipped);
}
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 = say_where(std::fs::metadata(source), source, "cannot be read")?;
if let Some(parent) = destination.parent() {
say_where(std::fs::create_dir_all(parent), parent, "cannot be made")?;
}
say_where(std::fs::create_dir(destination), destination, "cannot be made")?;
let mut directories: Vec<&Entry> = Vec::new();
let mut leaves: Vec<&Entry> = Vec::new();
for entry in &entries {
if entry.kind == Kind::Directory {
directories.push(entry);
} else {
leaves.push(entry);
}
}
for directory in &directories {
let made: PathBuf = destination.join(&directory.relative);
say_where(std::fs::create_dir(&made), made.as_path(), "cannot be made")?;
}
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>> = place_slices_side_by_side(&leaves, slice_length, source, destination);
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 source_directory: Metadata = say_where(std::fs::metadata(source.join(&directory.relative)), directory.relative.as_path(), "cannot be read")?;
say_where(std::fs::set_permissions(&made, source_directory.permissions()), made.as_path(), "cannot take its mode")?;
}
say_where(std::fs::set_permissions(destination, root.permissions()), destination, "cannot take its mode")?;
let mut skipped_files: usize = 0;
for entry in &entries {
if entry.kind != Kind::Directory && left_out.contains(&entry.relative) {
skipped_files += 1;
}
}
return Ok(Cloned { made: entries.len() - skipped_files, left_out });
}
#[cfg(test)]
#[path = "package_repair_tree_clone_tests.rs"]
mod tests;