rewrote pipeline as draggable card strip with per-rule config popovers, added right-click menus to pipeline cards, sidebar tree, and file list, preset import/export with BRU format support, new rules (hash, swap, truncate, sanitize, padding, randomize, text editor, folder name, transliterate), settings dialog with all sections, overlay collision containment, tooltips on icon buttons, empty pipeline default
123 lines
3.2 KiB
Rust
123 lines
3.2 KiB
Rust
use std::path::{Path, PathBuf};
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use walkdir::WalkDir;
|
|
|
|
use crate::filter::FilterConfig;
|
|
use crate::FileEntry;
|
|
|
|
pub struct FileScanner {
|
|
pub root: PathBuf,
|
|
pub filters: FilterConfig,
|
|
}
|
|
|
|
impl FileScanner {
|
|
pub fn new(root: PathBuf, filters: FilterConfig) -> Self {
|
|
Self { root, filters }
|
|
}
|
|
|
|
pub fn scan(&self) -> Vec<FileEntry> {
|
|
let max_depth = self.filters.subfolder_depth.map(|d| d + 1).unwrap_or(usize::MAX);
|
|
|
|
let walker = WalkDir::new(&self.root)
|
|
.max_depth(max_depth)
|
|
.follow_links(false);
|
|
|
|
let mut entries: Vec<FileEntry> = Vec::new();
|
|
|
|
for result in walker {
|
|
let dir_entry = match result {
|
|
Ok(e) => e,
|
|
Err(_) => continue,
|
|
};
|
|
|
|
// skip the root directory itself
|
|
if dir_entry.path() == self.root {
|
|
continue;
|
|
}
|
|
|
|
let metadata = match dir_entry.metadata() {
|
|
Ok(m) => m,
|
|
Err(_) => continue,
|
|
};
|
|
|
|
let path = dir_entry.path().to_path_buf();
|
|
let name = dir_entry.file_name().to_string_lossy().to_string();
|
|
let is_dir = metadata.is_dir();
|
|
let is_hidden = is_hidden_file(&path);
|
|
|
|
let (stem, extension) = if is_dir {
|
|
(name.clone(), String::new())
|
|
} else {
|
|
split_filename(&name)
|
|
};
|
|
|
|
let created = file_created(&metadata);
|
|
let modified = file_modified(&metadata);
|
|
let accessed = file_accessed(&metadata);
|
|
|
|
let entry = FileEntry {
|
|
path,
|
|
name,
|
|
stem,
|
|
extension,
|
|
size: metadata.len(),
|
|
is_dir,
|
|
is_hidden,
|
|
created,
|
|
modified,
|
|
accessed,
|
|
};
|
|
|
|
if self.filters.matches(&entry) {
|
|
entries.push(entry);
|
|
}
|
|
}
|
|
|
|
// natural sort
|
|
entries.sort_by(|a, b| natord::compare(&a.name, &b.name));
|
|
entries
|
|
}
|
|
}
|
|
|
|
fn split_filename(name: &str) -> (String, String) {
|
|
match name.rsplit_once('.') {
|
|
Some((stem, ext)) if !stem.is_empty() => (stem.to_string(), ext.to_string()),
|
|
_ => (name.to_string(), String::new()),
|
|
}
|
|
}
|
|
|
|
fn is_hidden_file(path: &Path) -> bool {
|
|
if path.file_name()
|
|
.map(|n| n.to_string_lossy().starts_with('.'))
|
|
.unwrap_or(false)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
use std::os::windows::fs::MetadataExt;
|
|
if let Ok(meta) = std::fs::metadata(path) {
|
|
const FILE_ATTRIBUTE_HIDDEN: u32 = 0x2;
|
|
if meta.file_attributes() & FILE_ATTRIBUTE_HIDDEN != 0 {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
fn file_created(meta: &std::fs::Metadata) -> Option<DateTime<Utc>> {
|
|
meta.created().ok().map(|t| DateTime::<Utc>::from(t))
|
|
}
|
|
|
|
fn file_modified(meta: &std::fs::Metadata) -> Option<DateTime<Utc>> {
|
|
meta.modified().ok().map(|t| DateTime::<Utc>::from(t))
|
|
}
|
|
|
|
fn file_accessed(meta: &std::fs::Metadata) -> Option<DateTime<Utc>> {
|
|
meta.accessed().ok().map(|t| DateTime::<Utc>::from(t))
|
|
}
|