use std::fs;
use std::path::{Path, PathBuf};
use super::scanner::{scan_directory};
use super::filter::should_exclude;
use futures::stream::{FuturesUnordered, StreamExt};
use tokio::sync::Semaphore;
use std::sync::Arc;
use std::collections::HashSet;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::fs::File;
pub async fn copy_file(source: &Path, target: &Path, dry_run: bool,progress_counter: Option<&Arc<AtomicU64>>,) -> std::io::Result<()> {
if dry_run {
return Ok(());
}
if let Some(parent) = target.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let mut src = File::open(source).await?;
let mut dst = File::create(target).await?;
let mut buffer = vec![0u8; 64 * 1024]; loop {
match src.read(&mut buffer).await {
Ok(0) => break, Ok(n) => {
dst.write_all(&buffer[..n]).await?;
if let Some(counter) = progress_counter {
counter.fetch_add(n as u64, Ordering::Relaxed);
}
}
Err(e) => return Err(e),
}
}
dst.flush().await?;
Ok(())
}
pub fn compute_blake3_hash(path: &Path) -> std::io::Result<[u8; 32]> {
let mut file = fs::File::open(path)?;
let mut hasher = blake3::Hasher::new();
std::io::copy(&mut file, &mut hasher)?;
Ok(hasher.finalize().into())
}
pub async fn delete_extra_files(
source: &PathBuf,
target: &PathBuf,
dry_run: bool,
exclude: &[String],
delete_exclude: &[String],
) -> anyhow::Result<(Vec<PathBuf>, Vec<PathBuf>, Vec<(PathBuf, String)>)> {
let source_files: HashSet<String> = scan_directory(source, exclude, false)?
.into_iter()
.filter_map(|info| {
info.path
.strip_prefix(source)
.ok()
.map(|rel| rel.to_string_lossy().to_string())
})
.collect();
let mut to_delete = Vec::new();
scan_target_for_deletion(
target,
target,
&source,
&source_files,
exclude,
delete_exclude,
&mut to_delete,
)
.await?;
let mut deleted = Vec::new();
let mut would_delete = Vec::new();
let mut delete_errors = Vec::new();
if dry_run {
would_delete = to_delete.clone();
} else {
let semaphore = Arc::new(Semaphore::new(16)); let mut tasks = FuturesUnordered::new();
for path in &to_delete {
let path_clone = path.clone();
let semaphore_clone = semaphore.clone();
let task = tokio::spawn(async move {
let _permit = semaphore_clone.acquire().await.unwrap();
let result = tokio::fs::remove_file(&path_clone).await;
(result, path_clone)
});
tasks.push(task);
}
while let Some(result) = tasks.next().await {
match result {
Ok((Ok(()), path)) => {
deleted.push(path.clone());
would_delete.push(path);
}
Ok((Err(e), path)) => {
delete_errors.push((path, e.to_string()));
}
Err(join_error) => {
delete_errors.push((PathBuf::new(), join_error.to_string()));
}
}
}
}
Ok((deleted, would_delete, delete_errors))
}
pub async fn scan_target_for_deletion(
current: &PathBuf,
target_root: &PathBuf,
source_root: &PathBuf,
source_files: &HashSet<String>,
exclude: &[String],
delete_exclude: &[String],
to_delete: &mut Vec<PathBuf>,
) -> std::io::Result<()> {
let mut dir = tokio::fs::read_dir(current).await?;
while let Some(entry) = dir.next_entry().await? {
let path = entry.path();
if path.is_dir() {
let future = scan_target_for_deletion(
&path,
target_root,
source_root,
source_files,
exclude,
delete_exclude,
to_delete,
);
Box::pin(future).await?;
} else {
if let Ok(rel_path) = path.strip_prefix(target_root) {
let rel_str = rel_path.to_string_lossy().to_string();
if !source_files.contains(&rel_str)
&& !should_exclude(&path, target_root, exclude)
&& !should_exclude(&path, target_root, delete_exclude)
{
to_delete.push(path);
}
}
}
}
Ok(())
}