refactor(thumbnails): keep the after-scan queue's store rule and pin the write lock (#320)

A planned target keeps the plan's own containment rule (inside the
store, as the thumbnail job writes it); captures keep the serve rule.
The structural test also covers PREVIEW_FOLDER joins and the
repo-root modules; the write-lock test asserts the lock instead of
waiting on a timer; new tests cover nested roots in the after-scan
queue and a legacy upper-case preview served through the resolver.
This commit is contained in:
Vito Sansevero
2026-10-31 19:11:21 -07:00
parent c0328e993a
commit 38f5491da1
3 changed files with 73 additions and 30 deletions
+3 -1
View File
@@ -16,7 +16,9 @@ under ``write_lock`` and only when the generation is unchanged and no hold
applies (``thumbnail_encode.replace_unless_held``). Changing the location
bumps the generation under the same lock, so no write that began before can
land in the old store afterwards. Nothing reads or commits config while
``write_lock`` is held.
``write_lock`` is held. The hold check runs under it too, so hold checks take
no lock (the job registry's ``running()`` is a lock-free snapshot) and
``bump_generation`` is never called with a registry or config lock held.
All functions are blocking: call them off the event loop.
"""
+12 -5
View File
@@ -163,18 +163,25 @@ class DeferredPreview:
(#320): a queued capture follows the thumbnail location at write time,
not the one when it was queued."""
__slots__ = ("root", "rel")
__slots__ = ("root", "rel", "strict")
def __init__(self, root, rel):
def __init__(self, root, rel, *, strict=True):
self.root = Path(root)
self.rel = Path(rel)
# A capture refuses a store linked out of its container (the serve
# route's rule); a planned target keeps the plan's rule: inside the
# store, as the thumbnail job writes it.
self.strict = strict
def resolve(self):
"""The preview's path now, or None when nothing may be written."""
dst = media_preview_path(self.root, self.rel)
if dst is None or not preview_store.owns(self.root, dst):
if dst is None:
return None
return dst
if self.strict:
return dst if preview_store.owns(self.root, dst) else None
store = preview_store.store_dir(self.root)
return dst if _is_within(dst, store) else None
def _resolved(dst):
@@ -325,7 +332,7 @@ def _deferred(roots, src, dst):
continue
rel = src.relative_to(root)
if media_preview_path(root, rel) == Path(dst):
return DeferredPreview(root, rel)
return DeferredPreview(root, rel, strict=False)
return dst