Compare commits

..

1 Commits

Author SHA1 Message Date
Cameron Cordes
f50655fb21 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:21:17 +00:00
2 changed files with 24 additions and 27 deletions

View File

@@ -123,11 +123,7 @@ 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( let found = enumerate_indexable_files(dir.path(), &["@eaDir".to_string()], None);
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()]);
} }
@@ -139,11 +135,7 @@ 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( let found = enumerate_indexable_files(dir.path(), &[".thumbnails".to_string()], None);
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()]);
} }
@@ -151,15 +143,8 @@ 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(&[ let dir = make_tree(&["private/secret.jpg", "public/keep.jpg"]);
"private/secret.jpg", let found = enumerate_indexable_files(dir.path(), &["/private".to_string()], None);
"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()]);
} }
@@ -173,7 +158,10 @@ 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!(rel_paths(&found), vec!["a.jpg".to_string(), "b.mp4".to_string()]); assert_eq!(
rel_paths(&found),
vec!["a.jpg".to_string(), "b.mp4".to_string()]
);
} }
#[test] #[test]

View File

@@ -2309,7 +2309,8 @@ 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).unwrap_or_default() dao.get_rows_missing_hash(context, cap + 1)
.unwrap_or_default()
}; };
if rows.is_empty() { if rows.is_empty() {
return 0; return 0;
@@ -2335,9 +2336,13 @@ 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) = if let Err(e) = dao.backfill_content_hash(
dao.backfill_content_hash(context, library.id, rel_path, &id.content_hash, id.size_bytes) context,
{ 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
@@ -2348,7 +2353,11 @@ fn backfill_unhashed_backlog(
} }
} }
Err(e) => { Err(e) => {
debug!("face_watch: hash compute failed for {} ({:?})", abs.display(), e); debug!(
"face_watch: hash compute failed for {} ({:?})",
abs.display(),
e
);
errors += 1; errors += 1;
} }
} }