Krolyxon
Commit f2bd31a

Branches

Tags

Refs →

remagesrcscan.rs

143 lines · 4.6 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 let meta = |p: &Path| {
62 std::fs::metadata(p).map_or((0, 0), |m| {
63 let mtime = m
64 .modified()
65 .ok()
66 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
67 .map_or(0, |d| d.as_secs() as i64);
68 (mtime, m.len())
69 })
70 };
71 let seed = SystemTime::now()
72 .duration_since(UNIX_EPOCH)
73 .map_or(1, |d| d.as_nanos() as u64);
74 sort_files(files, opts.sort, opts.reverse, meta, seed);
75}
76
77/// Collect visible files under `dir`. Symlinked directories are not followed.
78fn list_dir(dir: &Path, recursive: bool, known: &mut Vec<PathBuf>, unknown: &mut Vec<PathBuf>) {
79 let Ok(entries) = std::fs::read_dir(dir) else {
80 log::warn!("cannot read {}", dir.display());
81 return;
82 };
83 for entry in entries.flatten() {
84 if entry.file_name().as_encoded_bytes().starts_with(b".") {
85 continue;
86 }
87 let path = entry.path();
88 let Ok(ft) = entry.file_type() else { continue };
89 if ft.is_dir() {
90 if recursive {
91 list_dir(&path, true, known, unknown);
92 }
93 continue;
94 }
95 if ft.is_symlink() && std::fs::metadata(&path).is_ok_and(|m| m.is_dir()) {
96 continue;
97 }
98 if decode::is_known_extension(&path) {
99 known.push(path);
100 } else {
101 unknown.push(path);
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 fn opts() -> ScanOptions {
111 ScanOptions {
112 recursive: false,
113 siblings: true,
114 sort: SortKey::Natural,
115 reverse: false,
116 }
117 }
118
119 #[test]
120 fn single_file_opens_with_siblings() {
121 let dir = std::env::temp_dir().join(format!("remage-scan-{}", std::process::id()));
122 std::fs::create_dir_all(dir.join("sub")).unwrap();
123 for f in ["b10.png", "b2.jpg", ".hidden.png", "notes.txt", "sub/c.png"] {
124 std::fs::write(dir.join(f), b"").unwrap();
125 }
126 let s = scan(&[dir.join("b10.png")], &opts());
127 let names: Vec<_> = s.files.iter().map(|p| p.file_name().unwrap()).collect();
128 assert_eq!(names, ["b2.jpg", "b10.png"]);
129 assert_eq!(s.start.as_deref(), Some(dir.join("b10.png").as_path()));
130 assert_eq!(s.unknown, [dir.join("notes.txt")]);
131
132 let s = scan(
133 std::slice::from_ref(&dir),
134 &ScanOptions {
135 recursive: true,
136 ..opts()
137 },
138 );
139 assert_eq!(s.files.len(), 3);
140 assert!(s.start.is_none());
141 std::fs::remove_dir_all(&dir).unwrap();
142 }
143}