use anyhow::{Context, Result};
use futures::future::try_join_all;
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
pub fn dir_size(path: &Path) -> Result<u64> {
let mut size = 0;
for entry in fs::read_dir(path)? {
let entry = entry?;
let metadata = entry.metadata()?;
if metadata.is_dir() {
size += dir_size(&entry.path())?;
} else {
size += metadata.len();
}
}
Ok(size)
}
pub async fn get_directory_size(path: &Path) -> Result<u64> {
let path = path.to_path_buf();
tokio::task::spawn_blocking(move || dir_size(&path))
.await
.context("Failed to join directory size calculation task")?
}
pub fn calculate_checksum(path: &Path) -> Result<String> {
let content = fs::read(path)
.with_context(|| format!("Failed to read file for checksum: {}", path.display()))?;
let mut hasher = Sha256::new();
hasher.update(&content);
let result = hasher.finalize();
Ok(hex::encode(result))
}
pub async fn calculate_checksums_parallel(paths: &[PathBuf]) -> Result<Vec<(PathBuf, String)>> {
if paths.is_empty() {
return Ok(Vec::new());
}
let mut tasks = Vec::new();
for (index, path) in paths.iter().enumerate() {
let path = path.clone();
let task = tokio::task::spawn_blocking(move || {
calculate_checksum(&path).map(|checksum| (index, path, checksum))
});
tasks.push(task);
}
let results = try_join_all(tasks).await.context("Failed to join checksum calculation tasks")?;
let mut successes = Vec::new();
let mut errors = Vec::new();
for result in results {
match result {
Ok((index, path, checksum)) => successes.push((index, path, checksum)),
Err(e) => errors.push(e),
}
}
if !errors.is_empty() {
let error_msgs: Vec<String> =
errors.into_iter().map(|error| format!(" {error}")).collect();
return Err(anyhow::anyhow!(
"Failed to calculate checksums for {} files:\n{}",
error_msgs.len(),
error_msgs.join("\n")
));
}
successes.sort_by_key(|(index, _, _)| *index);
let ordered_results: Vec<(PathBuf, String)> =
successes.into_iter().map(|(_, path, checksum)| (path, checksum)).collect();
Ok(ordered_results)
}
pub fn file_exists_and_readable(path: &Path) -> bool {
path.exists() && path.is_file() && fs::metadata(path).is_ok()
}
pub fn get_modified_time(path: &Path) -> Result<std::time::SystemTime> {
let metadata = fs::metadata(path)
.with_context(|| format!("Failed to get metadata for: {}", path.display()))?;
metadata
.modified()
.with_context(|| format!("Failed to get modification time for: {}", path.display()))
}
pub fn compare_file_times(path1: &Path, path2: &Path) -> Result<std::cmp::Ordering> {
let time1 = get_modified_time(path1)?;
let time2 = get_modified_time(path2)?;
Ok(time1.cmp(&time2))
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_dir_size() {
let temp = tempdir().unwrap();
let dir = temp.path();
std::fs::write(dir.join("file1.txt"), "12345").unwrap();
std::fs::write(dir.join("file2.txt"), "123456789").unwrap();
super::super::dirs::ensure_dir(&dir.join("subdir")).unwrap();
std::fs::write(dir.join("subdir/file3.txt"), "abc").unwrap();
let size = dir_size(dir).unwrap();
assert_eq!(size, 17); }
#[test]
fn test_calculate_checksum() {
let temp = tempdir().unwrap();
let file = temp.path().join("checksum_test.txt");
std::fs::write(&file, "test content").unwrap();
let checksum = calculate_checksum(&file).unwrap();
assert!(!checksum.is_empty());
assert_eq!(checksum.len(), 64); }
#[tokio::test]
async fn test_calculate_checksums_parallel() {
let temp = tempdir().unwrap();
let file1 = temp.path().join("file1.txt");
let file2 = temp.path().join("file2.txt");
std::fs::write(&file1, "content1").unwrap();
std::fs::write(&file2, "content2").unwrap();
let paths = vec![file1.clone(), file2.clone()];
let results = calculate_checksums_parallel(&paths).await.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].0, file1);
assert_eq!(results[1].0, file2);
assert!(!results[0].1.is_empty());
assert!(!results[1].1.is_empty());
}
#[tokio::test]
async fn test_calculate_checksums_parallel_empty() {
let results = calculate_checksums_parallel(&[]).await.unwrap();
assert!(results.is_empty());
}
#[test]
fn test_calculate_checksum_edge_cases() {
let temp = tempdir().unwrap();
let empty = temp.path().join("empty.txt");
std::fs::write(&empty, "").unwrap();
let checksum = calculate_checksum(&empty).unwrap();
assert_eq!(checksum.len(), 64);
assert_eq!(checksum, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
let nonexistent = temp.path().join("nonexistent.txt");
let result = calculate_checksum(&nonexistent);
assert!(result.is_err());
let large = temp.path().join("large.txt");
let large_content = vec![b'a'; 1024 * 1024];
std::fs::write(&large, &large_content).unwrap();
let checksum = calculate_checksum(&large).unwrap();
assert_eq!(checksum.len(), 64);
}
#[tokio::test]
async fn test_calculate_checksums_parallel_errors() {
let temp = tempdir().unwrap();
let valid = temp.path().join("valid.txt");
let invalid = temp.path().join("invalid.txt");
std::fs::write(&valid, "content").unwrap();
let paths = vec![valid.clone(), invalid.clone()];
let result = calculate_checksums_parallel(&paths).await;
assert!(result.is_err());
}
#[test]
fn test_dir_size_edge_cases() {
let temp = tempdir().unwrap();
let empty_dir = temp.path().join("empty");
super::super::dirs::ensure_dir(&empty_dir).unwrap();
assert_eq!(dir_size(&empty_dir).unwrap(), 0);
let nonexistent = temp.path().join("nonexistent");
let result = dir_size(&nonexistent);
assert!(result.is_err());
#[cfg(unix)]
{
let dir = temp.path().join("with_symlink");
super::super::dirs::ensure_dir(&dir).unwrap();
std::fs::write(dir.join("file.txt"), "12345").unwrap();
let target = temp.path().join("target");
std::fs::write(&target, "123456789").unwrap();
std::os::unix::fs::symlink(&target, dir.join("link")).unwrap();
let size = dir_size(&dir).unwrap();
assert!(size >= 5);
assert!(size < 1_000_000);
}
}
}