Krolyxon
Tag v0.1.1

Branches

Tags

Refs →

remagesrcscan.rs

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