Files
picpeak/backend/__tests__/integration
Paul Nothaft 576924fa57 fix(faces): scan external/reference photos instead of skipping them (#1090) (#1091)
* fix(faces): scan external/reference photos instead of skipping them (#1090)

faceProcessor short-circuited every photo with source_origin 'external'
or 'reference' straight to 'skipped', before the sidecar was ever
contacted. On an external-media install that is the entire library — the
reporter's gallery sat at 0/3230 with every row skipped and no error, and
a rescan changed nothing.

The guard was correct when written: resolvePhotoStorageKey returns null
for anything outside managed storage, so ensurePreviewImage could not
build a preview and there was nothing to send. #1078 removed that
limitation one release earlier — ensurePreviewImage now reads externals
straight off the mount via resolvePhotoFilePath and writes the preview
into managed storage, so the key faceProcessor already fetches through
getStorage() is readable like any other. The guard outlived its reason.

Photos whose source is genuinely gone still return a null preview key
and land in the existing 'failed' branch, which is the honest outcome:
that is a broken photo, not an unsupported one. The blanket skip was
absorbing those too.

No migration or manual reset needed — enqueueEvent already re-queues
rows with face_status in (NULL, 'failed', 'skipped'), so previously
skipped photos get picked up on the next scan.

* fix(faces): queue external imports for scanning (#1090)

The other half of the same bug, found by external review — and my first
counter-argument against it was wrong.

Managed uploads are enqueued by photoProcessor, which writes face_status
'pending' once a photo is processed (photoProcessor.js:573, commented as
"the only correct place to enqueue"). External media never goes through
photoProcessor at all: adminExternalMedia inserts rows directly, leaving
face_status NULL.

faceQueue.claimNextPhoto only claims 'pending' (faceQueue.js:64), so an
import into an already-enabled event produced nothing until someone
pressed Re-scan. Lifting the skip guard alone made external photos
scannable but still not scanned — which looks like a complete fix right
up until you import a photo.

Resolved once per import rather than per file, since it is a per-event
setting and the loop can run to a thousand files, and guarded on both
the global flag and the per-event toggle exactly as photoProcessor
guards it, so installs without the feature still never write a
face_status. A failure to read the setting logs and imports anyway — the
photos are the point.

No video guard: walkDir only collects jpg/jpeg/png/webp, so nothing
faceProcessor would skip as video can arrive through this route.

* fix(faces): enqueue imports only after the event path is written

External review caught a race I introduced in the previous commit.

Marking rows 'pending' as they were inserted published claimable work
while events.external_path still held the old value — or none at all, on
a first import, since the route only writes it after the entire
thumbnail loop. The face worker polls continuously, so on any import
long enough to matter (the loop is ~100-300ms per photo, and the
reporter's library is 6500+) it would claim those rows, resolve them
against the wrong directory and mark them permanently 'failed' — a state
only an explicit Re-scan clears. That is strictly worse than the
unscanned photos this set out to fix.

Ids are now collected during the loop and marked pending in one pass
after the event path is written, chunked at 500 because SQLite caps a
statement at 999 bound parameters.

The test now drives the real route instead of re-implementing its logic,
and observes the mid-loop state from inside the per-photo thumbnail
call — the only hook that can see the window the race lived in. Verified
it discriminates: deleting the enqueue fails two tests, and moving it
back onto the insert fails the ordering test specifically.

* fix(faces): read the face setting after the import, not before

Third external-review round. The setting was captured before a loop that
runs for many minutes on a large library, so an admin who enabled
detection during an import left every photo imported after that moment
at NULL forever — the toggle endpoint only queues rows that already
existed when it fired.

Ids are now collected unconditionally and the setting is evaluated
immediately before the queue update, off a freshly read event row. The
guard is unchanged in substance: both the global flag and the per-event
toggle, so installs without the feature still never write a face_status.

Test flips the toggle from inside the mocked per-photo thumbnail call,
which is the same mid-loop hook the ordering test uses.

---------

Co-authored-by: Paul Nothaft <paul@MacStudio-von-Paul.local>
2026-08-20 07:18:45 +02:00
..