Compare commits

..

1 Commits

Author SHA1 Message Date
Cameron Cordes
a48744c7ad indexer: apply EXCLUDED_DIRS to remaining WalkDir callers
Audit follow-up to 5bf4956. The same `@eaDir` pruning that protects
the indexer also needs to protect the other walks under library roots:

- `create_thumbnails` walks every file in every library to generate
  thumbnails. Without EXCLUDED_DIRS, it would generate thumbnails of
  Synology's `SYNOFILE_THUMB_*.jpg` thumbnails (thumbnails of thumbnails).
- `update_media_counts` walks for the prometheus IMAGE / VIDEO gauges.
  Without EXCLUDED_DIRS, the gauges over-count by however many phantom
  `@eaDir` images live alongside the real photos.
- `cleanup_orphaned_playlists` walks BASE_PATH searching for source
  videos by filename. EXCLUDED_DIRS isn't a behavior change for typical
  Synology mounts (no .mp4 in @eaDir), but it's a correctness win for
  any operator-defined exclude that happens to contain video.

Refactor: add `walk_library_files(base, excluded_dirs) -> Vec<DirEntry>`
to file_scan.rs as the shared primitive. `enumerate_indexable_files`
now layers media-type + mtime filters on top of it. One new test
covers the lower-level helper (returns all extensions, prunes excluded
subtrees).

`generate_video_gifs` (currently `#[allow(dead_code)]`, not reachable
from main) gets the `update_media_counts` signature update and reads
EXCLUDED_DIRS from env so a future revival isn't broken — but its
WalkDir walk stays raw because the dual lib/bin compile makes the
file_scan module path non-trivial there. Tagged with a comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 20:17:51 +00:00
2 changed files with 27 additions and 24 deletions

View File

@@ -123,7 +123,11 @@ mod tests {
"vacation/@eaDir/IMG_0001.jpg/SYNOFILE_THUMB_XL.jpg", "vacation/@eaDir/IMG_0001.jpg/SYNOFILE_THUMB_XL.jpg",
"@eaDir/top_level_thumb.jpg", "@eaDir/top_level_thumb.jpg",
]); ]);
let found = enumerate_indexable_files(dir.path(), &["@eaDir".to_string()], None); let found = enumerate_indexable_files(
dir.path(),
&["@eaDir".to_string()],
None,
);
assert_eq!(rel_paths(&found), vec!["vacation/IMG_0001.jpg".to_string()]); assert_eq!(rel_paths(&found), vec!["vacation/IMG_0001.jpg".to_string()]);
} }
@@ -135,7 +139,11 @@ mod tests {
"a/.thumbnails/cached.jpg", "a/.thumbnails/cached.jpg",
"a/b/.thumbnails/nested.jpg", "a/b/.thumbnails/nested.jpg",
]); ]);
let found = enumerate_indexable_files(dir.path(), &[".thumbnails".to_string()], None); let found = enumerate_indexable_files(
dir.path(),
&[".thumbnails".to_string()],
None,
);
assert_eq!(rel_paths(&found), vec!["a/b/photo.jpg".to_string()]); assert_eq!(rel_paths(&found), vec!["a/b/photo.jpg".to_string()]);
} }
@@ -143,8 +151,15 @@ mod tests {
fn excludes_absolute_under_base() { fn excludes_absolute_under_base() {
// Leading-'/' entries are interpreted as paths under the library // Leading-'/' entries are interpreted as paths under the library
// root (see PathExcluder::new). // root (see PathExcluder::new).
let dir = make_tree(&["private/secret.jpg", "public/keep.jpg"]); let dir = make_tree(&[
let found = enumerate_indexable_files(dir.path(), &["/private".to_string()], None); "private/secret.jpg",
"public/keep.jpg",
]);
let found = enumerate_indexable_files(
dir.path(),
&["/private".to_string()],
None,
);
assert_eq!(rel_paths(&found), vec!["public/keep.jpg".to_string()]); assert_eq!(rel_paths(&found), vec!["public/keep.jpg".to_string()]);
} }
@@ -158,10 +173,7 @@ mod tests {
"e.jpg.bak", // wrong ext "e.jpg.bak", // wrong ext
]); ]);
let found = enumerate_indexable_files(dir.path(), &[], None); let found = enumerate_indexable_files(dir.path(), &[], None);
assert_eq!( assert_eq!(rel_paths(&found), vec!["a.jpg".to_string(), "b.mp4".to_string()]);
rel_paths(&found),
vec!["a.jpg".to_string(), "b.mp4".to_string()]
);
} }
#[test] #[test]

View File

@@ -2309,8 +2309,7 @@ fn backfill_unhashed_backlog(
// library's tick. Negligible cost given the cap. // library's tick. Negligible cost given the cap.
let rows: Vec<(i32, String)> = { let rows: Vec<(i32, String)> = {
let mut dao = exif_dao.lock().expect("Unable to lock ExifDao"); let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");
dao.get_rows_missing_hash(context, cap + 1) dao.get_rows_missing_hash(context, cap + 1).unwrap_or_default()
.unwrap_or_default()
}; };
if rows.is_empty() { if rows.is_empty() {
return 0; return 0;
@@ -2336,13 +2335,9 @@ fn backfill_unhashed_backlog(
match content_hash::compute(&abs) { match content_hash::compute(&abs) {
Ok(id) => { Ok(id) => {
let mut dao = exif_dao.lock().expect("Unable to lock ExifDao"); let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");
if let Err(e) = dao.backfill_content_hash( if let Err(e) =
context, dao.backfill_content_hash(context, library.id, rel_path, &id.content_hash, id.size_bytes)
library.id, {
rel_path,
&id.content_hash,
id.size_bytes,
) {
warn!( warn!(
"face_watch: backfill_content_hash failed for {}: {:?}", "face_watch: backfill_content_hash failed for {}: {:?}",
rel_path, e rel_path, e
@@ -2353,11 +2348,7 @@ fn backfill_unhashed_backlog(
} }
} }
Err(e) => { Err(e) => {
debug!( debug!("face_watch: hash compute failed for {} ({:?})", abs.display(), e);
"face_watch: hash compute failed for {} ({:?})",
abs.display(),
e
);
errors += 1; errors += 1;
} }
} }