use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::core::safe_delete::is_safe_to_delete;
use crate::core::types::DeleteResult;
pub async fn delete(
path: &Path,
scan_root: &Path,
targets: &[String],
dry_run: bool,
) -> DeleteResult {
if !is_safe_to_delete(path, targets) {
return DeleteResult::fail(path, "Path basename is not in the target list");
}
let canon_path = match std::fs::canonicalize(path) {
Ok(p) => p,
Err(e) => return DeleteResult::fail(path, format!("canonicalize failed: {e}")),
};
let canon_root = match std::fs::canonicalize(scan_root) {
Ok(p) => p,
Err(e) => return DeleteResult::fail(path, format!("scan root canonicalize failed: {e}")),
};
if !canon_path.starts_with(&canon_root) {
return DeleteResult::fail(path, "Path is outside the scan root");
}
if dry_run {
let ms = 200 + (sub_nano_jitter() % 4000);
tokio::time::sleep(Duration::from_millis(ms)).await;
return DeleteResult::ok(path);
}
let pb: PathBuf = canon_path;
match tokio::task::spawn_blocking(move || std::fs::remove_dir_all(&pb)).await {
Ok(Ok(())) => DeleteResult::ok(path),
Ok(Err(e)) => DeleteResult::fail(path, format!("remove_dir_all failed: {e}")),
Err(e) => DeleteResult::fail(path, format!("join error: {e}")),
}
}
fn sub_nano_jitter() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.subsec_nanos() as u64).unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn jitter_returns_a_value() {
let _ = sub_nano_jitter();
}
#[tokio::test]
async fn dry_run_against_nonexistent_path_still_rejected_at_canonicalize() {
let res = delete(
Path::new("/no/such/path/node_modules"),
Path::new("/tmp"),
&["node_modules".into()],
true,
)
.await;
assert!(!res.success);
assert!(res.error.unwrap().to_lowercase().contains("canonicalize"));
}
#[tokio::test]
async fn empty_targets_always_rejects() {
let res = delete(Path::new("/tmp/foo"), Path::new("/tmp"), &[], false).await;
assert!(!res.success);
}
}