use crate::parallel_budget::{HostResources, sliced_workers};
use crate::private_storage::sync_directory;
use crate::side_by_side_threads::run_all_side_by_side;
use crate::worktree_copy_error::{WorktreeCopyError, raw_failure, raw_failure_of};
use crate::worktree_copy_journal::write_journal;
use crate::worktree_copy_journal_encoding_and_validation::{Journal, PendingJournal, Phase, TEMPORARY_SUFFIX};
use crate::worktree_copy_stage_leaf_copying::PARALLEL_COPY_MAX_WORKERS;
use std::path::{Path, PathBuf};
pub fn prepare_stage_removal(stage_container: &Path) -> Result<(), WorktreeCopyError> {
use std::os::unix::fs::PermissionsExt;
let mut pending: Vec<PathBuf> = vec![stage_container.to_path_buf()];
while let Some(directory) = pending.pop() {
let metadata: std::fs::Metadata = match std::fs::symlink_metadata(directory.as_path()) {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => return Err(raw_failure(&error)),
};
if !metadata.is_dir() {
continue;
}
std::fs::set_permissions(directory.as_path(), std::fs::Permissions::from_mode(0o700))
.map_err(raw_failure_of)?;
for listed in
std::fs::read_dir(directory.as_path()).map_err(raw_failure_of)?
{
let child: std::fs::DirEntry = listed.map_err(raw_failure_of)?;
let child_type: std::fs::FileType = child
.file_type()
.map_err(raw_failure_of)?;
if child_type.is_dir() {
pending.push(child.path());
}
}
}
return Ok(());
}
pub fn remove_stage(stage_container: &Path) -> Result<(), WorktreeCopyError> {
#[cfg(target_os = "linux")]
if crate::fs_kind::is_subvolume_root(stage_container).unwrap_or(false) && crate::fs_btrfs_ioctl::subvolume_destroy_at(stage_container).is_ok() {
return Ok(());
}
prepare_stage_removal(stage_container)?;
return remove_tree_if_present(stage_container);
}
pub fn remove_pending_journal(pending: &PendingJournal) -> Result<(), WorktreeCopyError> {
if pending.journal.phase != Phase::Complete {
let completed: Journal = Journal {
phase: Phase::Complete,
..pending.journal.clone()
};
write_journal(pending.path.as_path(), &completed)?;
}
remove_stage(Path::new(pending.journal.stage_container.as_str()))?;
remove_file_if_present(pending.path.as_path())?;
remove_unfinished_writes(pending.path.as_path())?;
if let Some(parent) = pending.path.parent() {
sync_directory(parent).map_err(raw_failure_of)?;
}
return Ok(());
}
const REMOVAL_SPLIT_DEPTH: usize = 3;
fn remove_subtrees_until_none_is_left((cursor, subtrees): (&std::sync::atomic::AtomicUsize, &[PathBuf])) -> std::io::Result<()> {
loop {
let index: usize = cursor.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let Some(subtree) = subtrees.get(index) else {
return Ok(());
};
std::fs::remove_dir_all(subtree.as_path())?;
}
}
pub fn remove_tree_split(path: &Path) -> std::io::Result<()> {
let mut shallow: Vec<PathBuf> = vec![path.to_path_buf()];
let mut level: Vec<PathBuf> = vec![path.to_path_buf()];
let mut subtrees: Vec<PathBuf> = Vec::new();
for depth in 0..REMOVAL_SPLIT_DEPTH {
let mut next: Vec<PathBuf> = Vec::new();
for directory in &level {
for listed in std::fs::read_dir(directory)? {
let entry: std::fs::DirEntry = listed?;
let child: PathBuf = entry.path();
if !entry.file_type()?.is_dir() {
std::fs::remove_file(child.as_path())?;
} else if depth + 1 == REMOVAL_SPLIT_DEPTH {
subtrees.push(child);
} else {
shallow.push(child.clone());
next.push(child);
}
}
}
level = next;
}
let workers: usize = sliced_workers(usize::MAX, PARALLEL_COPY_MAX_WORKERS, &HostResources::read());
let cursor: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
let mut worker_states: Vec<(&std::sync::atomic::AtomicUsize, &[PathBuf])> = Vec::new();
for _worker in 0..workers {
worker_states.push((&cursor, subtrees.as_slice()));
}
let mut outcomes: Vec<std::io::Result<()>> = Vec::new();
for joined in run_all_side_by_side(worker_states, remove_subtrees_until_none_is_left) {
outcomes.push(match joined {
Ok(outcome) => outcome,
Err(_panic) => Err(std::io::Error::other("a removal worker panicked")),
});
}
for outcome in outcomes {
outcome?;
}
for directory in shallow.iter().rev() {
std::fs::remove_dir(directory.as_path())?;
}
return Ok(());
}
pub fn remove_tree_if_present(path: &Path) -> Result<(), WorktreeCopyError> {
let removed: std::io::Result<()> = match remove_tree_split(path) {
Ok(()) => Ok(()),
Err(_split_failure) => std::fs::remove_dir_all(path),
};
match removed {
Ok(()) => return Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(raw_failure(&error)),
}
}
pub fn remove_file_if_present(path: &Path) -> Result<(), WorktreeCopyError> {
match std::fs::remove_file(path) {
Ok(()) => return Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(raw_failure(&error)),
}
}
pub fn remove_unfinished_writes(path: &Path) -> Result<(), WorktreeCopyError> {
let Some(parent) = path.parent() else {
return Ok(());
};
let prefix: String = format!(
"{}.",
path.file_name().unwrap_or_default().to_string_lossy()
);
for listed in std::fs::read_dir(parent).map_err(raw_failure_of)? {
let entry: std::fs::DirEntry = listed.map_err(raw_failure_of)?;
let name: String = entry.file_name().to_string_lossy().into_owned();
if name.starts_with(prefix.as_str()) && name.ends_with(TEMPORARY_SUFFIX) {
remove_file_if_present(entry.path().as_path())?;
}
}
return Ok(());
}