9cbdb70e13
Two coupled problems, operator-flagged 2026-06-01: "missing thumbnails
but triggering backfill found nothing."
1. **Backfill UI was a black box.** `POST /api/thumbnails/backfill`
returned just `{celery_task_id}` and the admin card said
"Enqueued." with no counts. There was no way to tell whether the
scan found 0 candidates, 5000 candidates, or whether the worker
even picked up the task. "Found nothing" was indistinguishable
from a broken queue.
Fix: refactor the scan into a sync helper (`_run_backfill_scan`)
shared by the Celery task and the API endpoint. The API now runs
the scan in an executor and returns `{scanned, enqueued, ok,
regenerated}`. The actual thumbnail generation work still goes
to the thumbnail Celery queue per row via
`generate_thumbnail.delay()` — the scan itself is fast
(SELECT id+thumbnail_path + a file.stat() per row).
2. **`_thumb_is_valid` accepted header-only corrupt files.** The
magic-byte check passed for any 8-byte file starting with a JPEG
or PNG header, including empty/truncated/zero-pad files that
browsers render as broken. Backfill counted these as `ok` and
never regenerated.
Fix: also require file size ≥ MIN_THUMB_BYTES (256). Real
thumbnails are minimum ~2KB even on solid-color sources; header-
only corrupt files top out around 12 bytes. 256 is well above
the corrupt floor and well below any legitimate thumbnail.
Plus the admin card now shows the per-run counts instead of
"Enqueued.":
Scanned 5,432 · enqueued 3 (2 regenerated) · 5,429 ok
29 lines
1.1 KiB
Python
29 lines
1.1 KiB
Python
"""Thumbnail admin API: backfill trigger."""
|
|
|
|
import asyncio
|
|
|
|
from quart import Blueprint, jsonify
|
|
|
|
thumbnails_bp = Blueprint("thumbnails", __name__, url_prefix="/api/thumbnails")
|
|
|
|
|
|
@thumbnails_bp.route("/backfill", methods=["POST"])
|
|
async def trigger_backfill():
|
|
"""Run the backfill scan synchronously, return the counts. The actual
|
|
thumbnail generation work is still off-loaded to the thumbnail Celery
|
|
queue via `generate_thumbnail.delay()` per missing row — so this
|
|
handler is fast even on a 100k-image library (a scan is just SELECT
|
|
id, thumbnail_path + a file.stat() per row, no heavy work).
|
|
|
|
Operator-flagged 2026-06-01: the previous fire-and-forget shape
|
|
returned `{celery_task_id}` only, so the admin UI had no idea whether
|
|
backfill found 0 or 5000 candidates — \"found nothing\" was
|
|
indistinguishable from \"the worker isn't picking up the task.\""""
|
|
from ..tasks.thumbnail import _run_backfill_scan
|
|
|
|
# Sync scan inside an executor so we don't block the event loop.
|
|
counts = await asyncio.get_running_loop().run_in_executor(
|
|
None, _run_backfill_scan,
|
|
)
|
|
return jsonify(counts), 200
|