Krolyxon
Commit ab1dc3b

Branches

Tags

Refs →

remagesrcscan.rs

148 lines · 4.8 KB · Rust

1//! Turn command-line paths into the initial file list.
2
3use std::path::{Path, PathBuf};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use crate::decode;
7use crate::state::queue::{SortKey, sort_files};
8
9#[derive(Debug, Default)]
10pub struct Scan {
11 pub files: Vec<PathBuf>,
12 /// The file to open first (a single file argument opened with its siblings).
13 pub start: Option<PathBuf>,
14 /// Files with unrecognised extensions, to sniff in the background.
15 pub unknown: Vec<PathBuf>,
16 pub errors: Vec<String>,
17}
18
19pub struct ScanOptions {
20 pub recursive: bool,
21 pub siblings: bool,
22 pub sort: SortKey,
23 pub reverse: bool,
24}
25
26pub fn scan(args: &[PathBuf], opts: &ScanOptions) -> Scan {
27 let dot = [PathBuf::from(".")];
28 let args = if args.is_empty() { &dot[..] } else { args };
29 let mut out = Scan::default();
30 for arg in args {
31 let path = std::path::absolute(arg).unwrap_or_else(|_| arg.clone());
32 let Ok(meta) = std::fs::metadata(&path) else {
33 out.errors.push(format!("{}: not found", arg.display()));
34 continue;
35 };
36 if meta.is_dir() {
37 let mut files = Vec::new();
38 list_dir(&path, opts.recursive, &mut files, &mut out.unknown);
39 sort(&mut files, opts);
40 out.files.extend(files);
41 } else if opts.siblings && args.len() == 1 {
42 let parent = path.parent().unwrap_or(Path::new("/"));
43 let mut files = Vec::new();
44 list_dir(parent, false, &mut files, &mut out.unknown);
45 out.unknown.retain(|u| *u != path);
46 if !files.contains(&path) {
47 files.push(path.clone());
48 }
49 sort(&mut files, opts);
50 out.files.extend(files);
51 out.start = Some(path);
52 } else {
53 // Explicitly named files are always attempted, whatever their extension.
54 out.files.push(path);
55 }
56 }
57 out
58}
59
60fn sort(files: &mut [PathBuf], opts: &ScanOptions) {
61 sort_paths(files, opts.sort, opts.reverse);
62}
63
64/// Sort with the filesystem metadata the stat-based keys need. Can be slow: not on the UI thread.
65pub fn sort_paths(files: &mut [PathBuf], key: SortKey, reverse: bool) {
66 let meta = |p: &Path| {
67 std::fs::metadata(p).map_or((0, 0), |m| {
68 let mtime = m
69 .modified()
70 .ok()
71 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
72 .map_or(0, |d| d.as_secs() as i64);
73 (mtime, m.len())
74 })
75 };
76 let seed = SystemTime::now()
77 .duration_since(UNIX_EPOCH)
78 .map_or(1, |d| d.as_nanos() as u64);
79 sort_files(files, key, reverse, meta, seed);
80}
81
82/// Collect visible files under `dir`. Symlinked directories are not followed.
83fn list_dir(dir: &Path, recursive: bool, known: &mut Vec<PathBuf>, unknown: &mut Vec<PathBuf>) {
84 let Ok(entries) = std::fs::read_dir(dir) else {
85 log::warn!("cannot read {}", dir.display());
86 return;
87 };
88 for entry in entries.flatten() {
89 if entry.file_name().as_encoded_bytes().starts_with(b".") {
90 continue;
91 }
92 let path = entry.path();
93 let Ok(ft) = entry.file_type() else { continue };
94 if ft.is_dir() {
95 if recursive {
96 list_dir(&path, true, known, unknown);
97 }
98 continue;
99 }
100 if ft.is_symlink() && std::fs::metadata(&path).is_ok_and(|m| m.is_dir()) {
101 continue;
102 }
103 if decode::is_known_extension(&path) {
104 known.push(path);
105 } else {
106 unknown.push(path);
107 }
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 fn opts() -> ScanOptions {
116 ScanOptions {
117 recursive: false,
118 siblings: true,
119 sort: SortKey::Natural,
120 reverse: false,
121 }
122 }
123
124 #[test]
125 fn single_file_opens_with_siblings() {
126 let dir = std::env::temp_dir().join(format!("remage-scan-{}", std::process::id()));
127 std::fs::create_dir_all(dir.join("sub")).unwrap();
128 for f in ["b10.png", "b2.jpg", ".hidden.png", "notes.txt", "sub/c.png"] {
129 std::fs::write(dir.join(f), b"").unwrap();
130 }
131 let s = scan(&[dir.join("b10.png")], &opts());
132 let names: Vec<_> = s.files.iter().map(|p| p.file_name().unwrap()).collect();
133 assert_eq!(names, ["b2.jpg", "b10.png"]);
134 assert_eq!(s.start.as_deref(), Some(dir.join("b10.png").as_path()));
135 assert_eq!(s.unknown, [dir.join("notes.txt")]);
136
137 let s = scan(
138 std::slice::from_ref(&dir),
139 &ScanOptions {
140 recursive: true,
141 ..opts()
142 },
143 );
144 assert_eq!(s.files.len(), 3);
145 assert!(s.start.is_none());
146 std::fs::remove_dir_all(&dir).unwrap();
147 }
148}