use std::path::{Path, PathBuf};
use anyhow::Result;
use tokio::fs;
use std::os::unix::fs::PermissionsExt;
pub struct FileUtils;
impl FileUtils {
pub fn safe_join<P: AsRef<Path>>(base: P, path: P) -> Result<PathBuf> {
let base = base.as_ref();
let path = path.as_ref();
let sanitized = path.strip_prefix("/").unwrap_or(path);
let sanitized = sanitized.strip_prefix("./").unwrap_or(sanitized);
let mut result = base.to_path_buf();
for component in sanitized.components() {
match component {
std::path::Component::Normal(name) => {
result.push(name);
}
std::path::Component::ParentDir => {
if !result.pop() || !result.starts_with(base) {
return Err(anyhow::anyhow!("Path traversal attempt detected"));
}
}
std::path::Component::CurDir => {
}
_ => {
return Err(anyhow::anyhow!("Invalid path component"));
}
}
}
if !result.starts_with(base) {
return Err(anyhow::anyhow!("Path outside of allowed directory"));
}
Ok(result)
}
pub async fn get_enhanced_metadata(path: &Path) -> Result<EnhancedFileMetadata> {
let metadata = fs::metadata(path).await?;
let is_text = crate::utils::is_text_file(path);
let mime_type = mime_guess::from_path(path).first_or_octet_stream().to_string();
Ok(EnhancedFileMetadata {
size: metadata.len(),
modified: metadata.modified()?.into(),
created: metadata.created().ok().map(Into::into),
is_file: metadata.is_file(),
is_dir: metadata.is_dir(),
is_text,
mime_type,
permissions: format!("{:o}", metadata.permissions().mode()),
})
}
pub async fn copy_with_progress<F>(
from: &Path,
to: &Path,
progress_callback: F,
) -> Result<()>
where
F: Fn(u64, u64) + Send + 'static,
{
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut source = fs::File::open(from).await?;
let mut dest = fs::File::create(to).await?;
let total_size = source.metadata().await?.len();
let mut copied = 0u64;
let mut buffer = vec![0u8; 64 * 1024];
loop {
let bytes_read = source.read(&mut buffer).await?;
if bytes_read == 0 {
break;
}
dest.write_all(&buffer[..bytes_read]).await?;
copied += bytes_read as u64;
progress_callback(copied, total_size);
}
dest.flush().await?;
Ok(())
}
pub fn calculate_dir_size(path: &Path) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<u64>> + Send + '_>> {
Box::pin(async move {
let mut total_size = 0u64;
let mut dir = fs::read_dir(path).await?;
while let Some(entry) = dir.next_entry().await? {
let metadata = entry.metadata().await?;
if metadata.is_file() {
total_size += metadata.len();
} else if metadata.is_dir() {
total_size += Self::calculate_dir_size(&entry.path()).await?;
}
}
Ok(total_size)
})
}
pub async fn find_duplicates(paths: &[PathBuf]) -> Result<Vec<Vec<PathBuf>>> {
use std::collections::HashMap;
use sha2::{Sha256, Digest};
let mut hash_map: HashMap<String, Vec<PathBuf>> = HashMap::new();
for path in paths {
if path.is_file() {
let content = fs::read(path).await?;
let mut hasher = Sha256::new();
hasher.update(&content);
let hash = format!("{:x}", hasher.finalize());
hash_map.entry(hash).or_default().push(path.clone());
}
}
Ok(hash_map.into_values().filter(|v| v.len() > 1).collect())
}
}
#[derive(Debug, Clone)]
pub struct EnhancedFileMetadata {
pub size: u64,
pub modified: chrono::DateTime<chrono::Utc>,
pub created: Option<chrono::DateTime<chrono::Utc>>,
pub is_file: bool,
pub is_dir: bool,
pub is_text: bool,
pub mime_type: String,
pub permissions: String,
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_safe_join() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
let result = FileUtils::safe_join(base, Path::new("test.txt")).unwrap();
assert!(result.starts_with(base));
let result = FileUtils::safe_join(base, Path::new("../../../etc/passwd"));
assert!(result.is_err());
let valid_result = FileUtils::safe_join(base, Path::new("test.txt")).unwrap();
assert!(valid_result.starts_with(base));
}
#[tokio::test]
async fn test_enhanced_metadata() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
fs::write(&file_path, "test content").await.unwrap();
let metadata = FileUtils::get_enhanced_metadata(&file_path).await.unwrap();
assert!(metadata.is_file);
assert!(!metadata.is_dir);
assert!(metadata.is_text);
assert_eq!(metadata.size, 12);
}
}