use ignore::WalkBuilder;
use rayon::prelude::*;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct FileInfo {
pub path: String,
pub size: u64,
#[allow(dead_code)]
pub is_dir: bool,
}
pub fn scan_directory(root: &Path) -> Vec<FileInfo> {
let walker = WalkBuilder::new(root)
.hidden(false)
.git_ignore(true)
.git_global(true)
.git_exclude(true)
.ignore(true)
.parents(true)
.build();
walker
.into_iter()
.par_bridge()
.filter_map(|entry_result| {
let entry = entry_result.ok()?;
let path = entry.path();
if path == root {
return None;
}
if path.components().any(|c| c.as_os_str() == ".git") {
return None;
}
let metadata = entry.metadata().ok()?;
if metadata.is_dir() {
return None;
}
let relative_path = match path.strip_prefix(root) {
Ok(stripped) => stripped
.to_str()
.map(|s| s.to_string())
.unwrap_or_else(|| stripped.to_string_lossy().to_string()),
Err(_) => {
return None;
}
};
if relative_path.is_empty() {
return None;
}
Some(FileInfo {
path: relative_path,
size: metadata.len(),
is_dir: false, })
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_scan_directory() {
let dir = tempdir().unwrap();
let root = dir.path();
fs::write(root.join("test.txt"), "hello").unwrap();
fs::create_dir(root.join("subdir")).unwrap();
fs::write(root.join("subdir/nested.txt"), "world").unwrap();
let files = scan_directory(root);
assert!(files.iter().any(|f| f.path == "test.txt"));
assert!(
files
.iter()
.any(|f| f.path == "subdir/nested.txt" || f.path == "subdir\\nested.txt")
);
}
#[test]
fn test_scan_directory_file_size() {
let dir = tempdir().unwrap();
let root = dir.path();
let content = "hello world";
fs::write(root.join("sized.txt"), content).unwrap();
let files = scan_directory(root);
let sized_file = files.iter().find(|f| f.path == "sized.txt").unwrap();
assert_eq!(sized_file.size, content.len() as u64);
}
#[test]
fn test_scan_directory_excludes_git() {
let dir = tempdir().unwrap();
let root = dir.path();
fs::create_dir(root.join(".git")).unwrap();
fs::write(root.join(".git/config"), "git config").unwrap();
fs::write(root.join("regular.txt"), "regular file").unwrap();
let files = scan_directory(root);
assert!(!files.iter().any(|f| f.path.contains(".git")));
assert!(files.iter().any(|f| f.path == "regular.txt"));
}
#[test]
fn test_scan_directory_empty() {
let dir = tempdir().unwrap();
let root = dir.path();
let files = scan_directory(root);
assert!(files.is_empty());
}
#[test]
fn test_scan_directory_nested_structure() {
let dir = tempdir().unwrap();
let root = dir.path();
fs::create_dir_all(root.join("a/b/c/d")).unwrap();
fs::write(root.join("a/b/c/d/deep.txt"), "deep file").unwrap();
let files = scan_directory(root);
assert!(files.iter().any(|f| f.path.contains("deep.txt")));
}
#[test]
fn test_scan_excludes_directories() {
let dir = tempdir().unwrap();
let root = dir.path();
fs::create_dir(root.join("testdir")).unwrap();
fs::write(root.join("testfile.txt"), "content").unwrap();
fs::write(root.join("testdir/nested.txt"), "nested").unwrap();
let files = scan_directory(root);
assert!(!files.iter().any(|f| f.path == "testdir"));
assert!(files.iter().any(|f| f.path == "testfile.txt"));
assert!(
files
.iter()
.any(|f| f.path == "testdir/nested.txt" || f.path == "testdir\\nested.txt")
);
assert!(files.iter().all(|f| !f.is_dir));
}
}