use crate::constants::SYSTEM_IGNORE_FILES;
use glob::Pattern;
use md5::{Digest, Md5};
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use walkdir::WalkDir;
#[derive(Debug, Clone)]
pub struct FileInfo {
pub absolute_path: PathBuf,
pub relative_path: String,
pub size: u64,
pub last_modified: SystemTime,
pub md5: String,
}
pub struct FileWalker {
ignore_patterns: Vec<Pattern>,
}
impl FileWalker {
pub fn new(ignore_patterns: &[String]) -> Self {
let patterns = ignore_patterns
.iter()
.filter_map(|p| Pattern::new(p).ok())
.collect();
Self {
ignore_patterns: patterns,
}
}
pub fn walk<P: AsRef<Path>>(&self, base_path: P) -> Vec<FileInfo> {
let base_path = base_path.as_ref();
let mut files = Vec::new();
for entry in WalkDir::new(base_path)
.follow_links(false)
.into_iter()
.filter_entry(|e| !self.should_skip_entry(e, base_path))
{
let entry = match entry {
Ok(e) => e,
Err(err) => {
eprintln!(
"Warning: Cannot access {}: {}",
err.path().map(|p| p.display().to_string()).unwrap_or_default(),
err
);
continue;
}
};
if !entry.file_type().is_file() {
continue;
}
let full_path = entry.path();
let relative_path = match full_path.strip_prefix(base_path) {
Ok(p) => p.to_string_lossy().to_string(),
Err(_) => continue,
};
if self.should_ignore(&relative_path, entry.file_name().to_str().unwrap_or("")) {
continue;
}
let metadata = match entry.metadata() {
Ok(m) => m,
Err(err) => {
eprintln!("Warning: Cannot stat {}: {}", full_path.display(), err);
continue;
}
};
let md5 = match calculate_file_md5(full_path) {
Ok(hash) => hash,
Err(err) => {
eprintln!("Warning: Cannot process {}: {}", full_path.display(), err);
continue;
}
};
files.push(FileInfo {
absolute_path: full_path.to_path_buf(),
relative_path,
size: metadata.len(),
last_modified: metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH),
md5,
});
}
files
}
fn should_skip_entry(&self, entry: &walkdir::DirEntry, base_path: &Path) -> bool {
let filename = entry.file_name().to_str().unwrap_or("");
if filename == "." || filename == ".." {
return true;
}
if SYSTEM_IGNORE_FILES.contains(&filename) {
return true;
}
if entry.file_type().is_dir() {
if let Ok(rel_path) = entry.path().strip_prefix(base_path) {
let rel_str = rel_path.to_string_lossy();
for pattern in &self.ignore_patterns {
if pattern.matches(&rel_str) {
return true;
}
}
}
}
false
}
fn should_ignore(&self, relative_path: &str, filename: &str) -> bool {
if SYSTEM_IGNORE_FILES.contains(&filename) {
return true;
}
for pattern in &self.ignore_patterns {
if pattern.matches(relative_path) {
return true;
}
}
false
}
}
pub fn calculate_file_md5<P: AsRef<Path>>(file_path: P) -> std::io::Result<String> {
let mut file = File::open(file_path)?;
let mut hasher = Md5::new();
let mut buffer = [0u8; 8192];
loop {
let bytes_read = file.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
Ok(format!("{:x}", hasher.finalize()))
}
pub fn calculate_string_md5(content: &str) -> String {
let mut hasher = Md5::new();
hasher.update(content.as_bytes());
format!("{:x}", hasher.finalize())
}
pub fn format_size(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
let mut size = bytes as f64;
let mut unit_index = 0;
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
size /= 1024.0;
unit_index += 1;
}
format!("{:.2} {}", size, UNITS[unit_index])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_string_md5() {
let hash = calculate_string_md5("hello");
assert_eq!(hash, "5d41402abc4b2a76b9719d911017c592");
}
#[test]
fn test_format_size() {
assert_eq!(format_size(0), "0.00 B");
assert_eq!(format_size(1024), "1.00 KB");
assert_eq!(format_size(1024 * 1024), "1.00 MB");
assert_eq!(format_size(1536), "1.50 KB");
}
}