pub mod hardlink;
pub mod hasher;
pub mod path_utils;
pub mod walker;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::SystemTime;
pub use hardlink::HardlinkTracker;
pub use hasher::{hash_to_hex, hex_to_hash, Hash, Hasher, PREHASH_SIZE};
pub use path_utils::{
is_nfc, normalize_path_str, normalize_path_str_cow, normalize_pathbuf, path_key, paths_equal,
paths_equal_normalized,
};
use regex::Regex;
pub use walker::Walker;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FileCategory {
Images,
Videos,
Audio,
Documents,
Archives,
}
impl FileCategory {
pub fn extensions(&self) -> &'static [&'static str] {
match self {
FileCategory::Images => &["jpg", "jpeg", "png", "gif", "bmp", "webp", "tiff", "svg"],
FileCategory::Videos => &["mp4", "mkv", "avi", "mov", "wmv", "flv", "webm"],
FileCategory::Audio => &["mp3", "wav", "flac", "m4a", "ogg", "wma"],
FileCategory::Documents => &[
"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "rtf", "odt", "ods",
"odp",
],
FileCategory::Archives => &["zip", "tar", "gz", "7z", "rar", "bz2", "xz"],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct FileEntry {
pub path: PathBuf,
pub size: u64,
pub modified: SystemTime,
pub is_symlink: bool,
pub is_hardlink: bool,
}
impl FileEntry {
#[must_use]
pub fn new(path: PathBuf, size: u64, modified: SystemTime) -> Self {
Self {
path,
size,
modified,
is_symlink: false,
is_hardlink: false,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct WalkerConfig {
pub follow_symlinks: bool,
pub skip_hidden: bool,
pub min_size: Option<u64>,
pub max_size: Option<u64>,
pub newer_than: Option<SystemTime>,
pub older_than: Option<SystemTime>,
pub ignore_patterns: Vec<String>,
pub regex_include: Vec<Regex>,
pub regex_exclude: Vec<Regex>,
pub file_categories: Vec<FileCategory>,
}
impl WalkerConfig {
#[must_use]
pub fn new(
follow_symlinks: bool,
skip_hidden: bool,
min_size: Option<u64>,
max_size: Option<u64>,
newer_than: Option<SystemTime>,
older_than: Option<SystemTime>,
ignore_patterns: Vec<String>,
) -> Self {
Self {
follow_symlinks,
skip_hidden,
min_size,
max_size,
newer_than,
older_than,
ignore_patterns,
regex_include: Vec::new(),
regex_exclude: Vec::new(),
file_categories: Vec::new(),
}
}
#[must_use]
pub fn with_follow_symlinks(mut self, follow: bool) -> Self {
self.follow_symlinks = follow;
self
}
#[must_use]
pub fn with_skip_hidden(mut self, skip: bool) -> Self {
self.skip_hidden = skip;
self
}
#[must_use]
pub fn with_min_size(mut self, size: Option<u64>) -> Self {
self.min_size = size;
self
}
#[must_use]
pub fn with_max_size(mut self, size: Option<u64>) -> Self {
self.max_size = size;
self
}
#[must_use]
pub fn with_newer_than(mut self, time: Option<SystemTime>) -> Self {
self.newer_than = time;
self
}
#[must_use]
pub fn with_older_than(mut self, time: Option<SystemTime>) -> Self {
self.older_than = time;
self
}
#[must_use]
pub fn with_patterns(mut self, patterns: Vec<String>) -> Self {
self.ignore_patterns = patterns;
self
}
#[must_use]
pub fn with_regex_include(mut self, regexes: Vec<Regex>) -> Self {
self.regex_include = regexes;
self
}
#[must_use]
pub fn with_regex_exclude(mut self, regexes: Vec<Regex>) -> Self {
self.regex_exclude = regexes;
self
}
#[must_use]
pub fn with_file_categories(mut self, categories: Vec<FileCategory>) -> Self {
self.file_categories = categories;
self
}
}
#[derive(thiserror::Error, Debug)]
pub enum ScanError {
#[error("Permission denied: {0}")]
PermissionDenied(PathBuf),
#[error("Path not found: {0}")]
NotFound(PathBuf),
#[error("Not a directory: {0}")]
NotADirectory(PathBuf),
#[error("I/O error for {path}: {source}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
}
#[derive(thiserror::Error, Debug)]
pub enum HashError {
#[error("File not found: {0}")]
NotFound(PathBuf),
#[error("Permission denied: {0}")]
PermissionDenied(PathBuf),
#[error("I/O error for {path}: {source}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_entry_new() {
let entry = FileEntry::new(PathBuf::from("/test/file.txt"), 1024, SystemTime::now());
assert_eq!(entry.path, PathBuf::from("/test/file.txt"));
assert_eq!(entry.size, 1024);
assert!(!entry.is_symlink);
assert!(!entry.is_hardlink);
}
#[test]
fn test_walker_config_default() {
let config = WalkerConfig::default();
assert!(!config.follow_symlinks);
assert!(!config.skip_hidden);
assert!(config.min_size.is_none());
assert!(config.max_size.is_none());
assert!(config.ignore_patterns.is_empty());
}
#[test]
fn test_walker_config_new() {
let config = WalkerConfig::new(
true,
true,
Some(1024),
Some(1_000_000),
None,
None,
vec!["*.tmp".to_string()],
);
assert!(config.follow_symlinks);
assert!(config.skip_hidden);
assert_eq!(config.min_size, Some(1024));
assert_eq!(config.max_size, Some(1_000_000));
assert!(config.newer_than.is_none());
assert!(config.older_than.is_none());
assert_eq!(config.ignore_patterns, vec!["*.tmp".to_string()]);
}
#[test]
fn test_scan_error_display() {
let err = ScanError::PermissionDenied(PathBuf::from("/test"));
assert_eq!(err.to_string(), "Permission denied: /test");
let err = ScanError::NotFound(PathBuf::from("/missing"));
assert_eq!(err.to_string(), "Path not found: /missing");
let err = ScanError::NotADirectory(PathBuf::from("/file.txt"));
assert_eq!(err.to_string(), "Not a directory: /file.txt");
}
#[test]
fn test_hash_error_display() {
let err = HashError::NotFound(PathBuf::from("/test"));
assert_eq!(err.to_string(), "File not found: /test");
let err = HashError::PermissionDenied(PathBuf::from("/secret"));
assert_eq!(err.to_string(), "Permission denied: /secret");
}
}