smugmug_cli/scanner/
mod.rs1use anyhow::{Context, Result};
2use std::path::{Path, PathBuf};
3
4pub mod walker;
5
6#[derive(Debug)]
7pub struct ScannedFile {
8 pub path: PathBuf,
9}
10
11pub fn scan_directory(path: &Path) -> Result<Vec<ScannedFile>> {
13 let entries = walker::walk_directory(path).context("Failed to walk directory")?;
14
15 let mut scanned_files = Vec::new();
16
17 for entry in entries {
18 let file_path = entry.path();
19
20 if !is_supported_file(file_path) {
22 continue;
23 }
24
25 scanned_files.push(ScannedFile {
26 path: file_path.to_path_buf(),
27 });
28 }
29
30 Ok(scanned_files)
31}
32
33pub fn is_supported_file(path: &Path) -> bool {
35 let extension = match path.extension() {
36 Some(ext) => ext.to_string_lossy().to_lowercase(),
37 None => return false,
38 };
39
40 matches!(
41 extension.as_str(),
42 "jpg" | "jpeg" | "png" | "heif" | "heic" | "gif" | "bmp" | "tiff" | "tif" |
44 "raw" | "dng" | "cr2" | "cr3" | "nef" | "arw" | "orf" | "raf" |
46 "rw2" | "pef" | "srw" | "erf" | "mrw" | "3fr" | "fff" | "iiq" |
47 "mp4" | "mov" | "avi" | "m4v" | "mkv"
49 )
50}
51
52pub fn is_raw_file(path: &Path) -> bool {
54 let extension = match path.extension() {
55 Some(ext) => ext.to_string_lossy().to_lowercase(),
56 None => return false,
57 };
58
59 matches!(
60 extension.as_str(),
61 "raw"
62 | "dng"
63 | "cr2"
64 | "cr3"
65 | "nef"
66 | "arw"
67 | "orf"
68 | "raf"
69 | "rw2"
70 | "pef"
71 | "srw"
72 | "erf"
73 | "mrw"
74 | "3fr"
75 | "fff"
76 | "iiq"
77 )
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use std::fs::{self, File};
84 use tempfile::TempDir;
85
86 fn create_test_directory() -> Result<TempDir> {
88 let temp_dir = TempDir::new()?;
89 let base_path = temp_dir.path();
90
91 File::create(base_path.join("photo1.jpg"))?;
93 File::create(base_path.join("photo2.jpeg"))?;
94 File::create(base_path.join("photo3.JPG"))?; File::create(base_path.join("image.png"))?;
96 File::create(base_path.join("IMG_1234.HEIC"))?;
97 File::create(base_path.join("raw_photo.raw"))?;
98 File::create(base_path.join("canon.cr2"))?;
99 File::create(base_path.join("nikon.nef"))?;
100 File::create(base_path.join("sony.arw"))?;
101 File::create(base_path.join("apple.heif"))?;
102 File::create(base_path.join("adobe.dng"))?;
103
104 File::create(base_path.join("video1.mp4"))?;
106 File::create(base_path.join("video2.mov"))?;
107 File::create(base_path.join("old_video.avi"))?;
108 File::create(base_path.join("VIDEO.MP4"))?; File::create(base_path.join("document.txt"))?;
112 File::create(base_path.join("spreadsheet.xlsx"))?;
113 File::create(base_path.join("archive.zip"))?;
114 File::create(base_path.join("no_extension"))?;
115 File::create(base_path.join("script.sh"))?;
116
117 let nested_dir = base_path.join("nested");
119 fs::create_dir(&nested_dir)?;
120 File::create(nested_dir.join("nested_photo.jpg"))?;
121 File::create(nested_dir.join("nested_video.mp4"))?;
122 File::create(nested_dir.join("nested_doc.txt"))?;
123
124 let deep_dir = nested_dir.join("deep").join("deeper");
126 fs::create_dir_all(&deep_dir)?;
127 File::create(deep_dir.join("deep_photo.png"))?;
128
129 Ok(temp_dir)
130 }
131
132 #[test]
133 fn test_is_supported_file_jpg() {
134 assert!(is_supported_file(Path::new("photo.jpg")));
135 assert!(is_supported_file(Path::new("photo.jpeg")));
136 assert!(is_supported_file(Path::new("PHOTO.JPG"))); assert!(is_supported_file(Path::new("PHOTO.JPEG")));
138 }
139
140 #[test]
141 fn test_is_supported_file_png() {
142 assert!(is_supported_file(Path::new("image.png")));
143 assert!(is_supported_file(Path::new("IMAGE.PNG")));
144 }
145
146 #[test]
147 fn test_is_supported_file_heif_heic() {
148 assert!(is_supported_file(Path::new("photo.heif")));
149 assert!(is_supported_file(Path::new("photo.heic")));
150 assert!(is_supported_file(Path::new("PHOTO.HEIF")));
151 assert!(is_supported_file(Path::new("PHOTO.HEIC")));
152 }
153
154 #[test]
155 fn test_is_supported_file_raw_formats() {
156 assert!(is_supported_file(Path::new("photo.raw")));
157 assert!(is_supported_file(Path::new("photo.dng")));
158 assert!(is_supported_file(Path::new("photo.cr2")));
159 assert!(is_supported_file(Path::new("photo.nef")));
160 assert!(is_supported_file(Path::new("photo.arw")));
161 assert!(is_supported_file(Path::new("PHOTO.RAW")));
162 assert!(is_supported_file(Path::new("PHOTO.DNG")));
163 }
164
165 #[test]
166 fn test_is_supported_file_video_formats() {
167 assert!(is_supported_file(Path::new("video.mp4")));
168 assert!(is_supported_file(Path::new("video.mov")));
169 assert!(is_supported_file(Path::new("video.avi")));
170 assert!(is_supported_file(Path::new("VIDEO.MP4")));
171 assert!(is_supported_file(Path::new("VIDEO.MOV")));
172 }
173
174 #[test]
175 fn test_is_supported_file_unsupported_formats() {
176 assert!(!is_supported_file(Path::new("document.txt")));
177 assert!(!is_supported_file(Path::new("document.pdf")));
178 assert!(!is_supported_file(Path::new("archive.zip")));
179 assert!(!is_supported_file(Path::new("script.sh")));
180 assert!(!is_supported_file(Path::new("data.json")));
181 assert!(!is_supported_file(Path::new("config.toml")));
182 assert!(!is_supported_file(Path::new("spreadsheet.xlsx")));
183 }
184
185 #[test]
186 fn test_is_supported_file_no_extension() {
187 assert!(!is_supported_file(Path::new("no_extension")));
188 assert!(!is_supported_file(Path::new("file_without_ext")));
189 }
190
191 #[test]
192 fn test_is_supported_file_with_path() {
193 assert!(is_supported_file(Path::new("/path/to/photo.jpg")));
194 assert!(is_supported_file(Path::new("relative/path/photo.png")));
195 assert!(!is_supported_file(Path::new("/path/to/document.txt")));
196 }
197
198 #[test]
199 fn test_scan_directory_finds_supported_files() {
200 let temp_dir = create_test_directory().expect("Failed to create test directory");
201 let scanned = scan_directory(temp_dir.path()).expect("Failed to scan directory");
202
203 assert_eq!(scanned.len(), 18);
208
209 for file in &scanned {
211 assert!(
212 is_supported_file(&file.path),
213 "File {:?} should be supported",
214 file.path
215 );
216 }
217 }
218
219 #[test]
220 fn test_scan_directory_filters_unsupported_files() {
221 let temp_dir = create_test_directory().expect("Failed to create test directory");
222 let scanned = scan_directory(temp_dir.path()).expect("Failed to scan directory");
223
224 let unsupported_extensions = ["txt", "xlsx", "zip", "sh"];
226 for file in &scanned {
227 let ext = file.path.extension().and_then(|e| e.to_str()).unwrap_or("");
228 assert!(
229 !unsupported_extensions.contains(&ext.to_lowercase().as_str()),
230 "Found unsupported file: {:?}",
231 file.path
232 );
233 }
234 }
235
236 #[test]
237 fn test_scan_directory_empty_directory() {
238 let temp_dir = TempDir::new().expect("Failed to create temp directory");
239 let scanned = scan_directory(temp_dir.path()).expect("Failed to scan empty directory");
240
241 assert_eq!(scanned.len(), 0, "Empty directory should return no files");
242 }
243
244 #[test]
245 fn test_scan_directory_only_unsupported_files() {
246 let temp_dir = TempDir::new().expect("Failed to create temp directory");
247 let base_path = temp_dir.path();
248
249 File::create(base_path.join("document.txt")).expect("Failed to create file");
251 File::create(base_path.join("data.json")).expect("Failed to create file");
252 File::create(base_path.join("no_extension")).expect("Failed to create file");
253
254 let scanned = scan_directory(temp_dir.path()).expect("Failed to scan directory");
255
256 assert_eq!(
257 scanned.len(),
258 0,
259 "Directory with only unsupported files should return no files"
260 );
261 }
262
263 #[test]
264 fn test_scan_directory_nested_structure() {
265 let temp_dir = create_test_directory().expect("Failed to create test directory");
266 let scanned = scan_directory(temp_dir.path()).expect("Failed to scan directory");
267
268 let nested_files: Vec<_> = scanned
270 .iter()
271 .filter(|f| f.path.to_string_lossy().contains("nested"))
272 .collect();
273
274 assert_eq!(
276 nested_files.len(),
277 3,
278 "Should find files in nested directories"
279 );
280 }
281
282 #[test]
283 fn test_scan_directory_invalid_path() {
284 let invalid_path = Path::new("/this/path/does/not/exist/hopefully/xyz123");
285 let result = scan_directory(invalid_path);
286
287 match result {
290 Ok(files) => assert_eq!(
291 files.len(),
292 0,
293 "Should return empty results for invalid path"
294 ),
295 Err(_) => {} }
297 }
298
299 #[test]
300 fn test_scan_directory_file_not_directory() {
301 let temp_dir = TempDir::new().expect("Failed to create temp directory");
302 let file_path = temp_dir.path().join("test.txt");
303 File::create(&file_path).expect("Failed to create file");
304
305 let result = scan_directory(&file_path);
307
308 assert!(result.is_ok() || result.is_err());
311 }
312
313 #[test]
314 fn test_scanned_file_debug_trait() {
315 let scanned = ScannedFile {
316 path: PathBuf::from("/test/path/photo.jpg"),
317 };
318
319 let debug_str = format!("{:?}", scanned);
320 assert!(debug_str.contains("ScannedFile"));
321 assert!(debug_str.contains("photo.jpg"));
322 }
323
324 #[test]
325 fn test_scan_directory_case_insensitive_extensions() {
326 let temp_dir = TempDir::new().expect("Failed to create temp directory");
327 let base_path = temp_dir.path();
328
329 File::create(base_path.join("photo1.jpg")).expect("Failed to create file");
331 File::create(base_path.join("photo2.JPG")).expect("Failed to create file");
332 File::create(base_path.join("photo3.Jpg")).expect("Failed to create file");
333 File::create(base_path.join("photo4.JpG")).expect("Failed to create file");
334
335 let scanned = scan_directory(temp_dir.path()).expect("Failed to scan directory");
336
337 assert_eq!(
338 scanned.len(),
339 4,
340 "Should find all files regardless of extension case"
341 );
342 }
343
344 #[test]
345 fn test_scan_directory_mixed_content() {
346 let temp_dir = TempDir::new().expect("Failed to create temp directory");
347 let base_path = temp_dir.path();
348
349 File::create(base_path.join("photo.jpg")).expect("Failed to create file");
351 File::create(base_path.join("document.txt")).expect("Failed to create file");
352 File::create(base_path.join("video.mp4")).expect("Failed to create file");
353 File::create(base_path.join("data.json")).expect("Failed to create file");
354 File::create(base_path.join("image.png")).expect("Failed to create file");
355
356 let scanned = scan_directory(temp_dir.path()).expect("Failed to scan directory");
357
358 assert_eq!(scanned.len(), 3, "Should find only the 3 supported files");
359 }
360
361 #[cfg(unix)]
362 #[test]
363 fn test_scan_directory_with_symlink() {
364 use std::os::unix::fs::symlink;
365
366 let temp_dir = TempDir::new().expect("Failed to create temp directory");
367 let base_path = temp_dir.path();
368
369 let real_file = base_path.join("real_photo.jpg");
371 File::create(&real_file).expect("Failed to create file");
372
373 let symlink_file = base_path.join("linked_photo.jpg");
375 symlink(&real_file, &symlink_file).expect("Failed to create symlink");
376
377 let scanned = scan_directory(temp_dir.path()).expect("Failed to scan directory");
378
379 assert_eq!(scanned.len(), 2, "Should find both real file and symlink");
381 }
382
383 #[test]
384 fn test_is_supported_file_edge_cases() {
385 assert!(is_supported_file(Path::new(".hidden.jpg")));
387 assert!(!is_supported_file(Path::new(".hidden.txt")));
388
389 assert!(is_supported_file(Path::new("my.photo.backup.jpg")));
391 assert!(!is_supported_file(Path::new("my.photo.backup.txt")));
392
393 let long_name = "a".repeat(200) + ".jpg";
395 assert!(is_supported_file(Path::new(&long_name)));
396 }
397
398 #[test]
399 fn test_scan_directory_preserves_full_paths() {
400 let temp_dir = create_test_directory().expect("Failed to create test directory");
401 let base_path = temp_dir.path();
402 let scanned = scan_directory(base_path).expect("Failed to scan directory");
403
404 for file in &scanned {
406 assert!(
407 file.path.starts_with(base_path),
408 "File path {:?} should start with base path {:?}",
409 file.path,
410 base_path
411 );
412 }
413 }
414}