refactor(thumbnails): one resolver for where previews live on disk (#320)

Add py/api/preview_store.py: the store of a source, a preview's path,
URL parts to disk, containment (owns), a write generation and the lock
around every preview's final rename. Every preview reader, writer and
remover now asks it instead of joining <source>/thumbnails by hand:
serve, on-demand, capture (its path resolved when the write starts),
the after-scan queue, the thumbnail plans and job payloads, clear, the
temp sweep, History covers, the auto-tag listing and the owned-preview
lookups.

Source mode only: every path is the one built before, so nothing
changes on disk or on the wire. A structural test keeps new
thumbnails joins out of the backend.
This commit is contained in:
Vito Sansevero
2026-10-31 18:51:18 -07:00
parent 8a81114dfc
commit c0328e993a
8 changed files with 809 additions and 61 deletions
+10 -5
View File
@@ -46,7 +46,7 @@ from pathlib import Path
from PIL import Image
from . import thumbnail_settings
from . import preview_store, thumbnail_settings
try:
from ...utils.logging_config import get_logger
@@ -101,10 +101,15 @@ def temp_sibling(dst, tag="part"):
def replace_unless_held(tmp, dst, guard=None):
"""Rename *tmp* over *dst*, unless ``guard()`` is false (PreviewWriteHeld)."""
if guard is not None and not guard():
raise PreviewWriteHeld(PREVIEW_HELD)
os.replace(tmp, dst)
"""Rename *tmp* over *dst*, unless ``guard()`` is false (PreviewWriteHeld).
The check and the rename run under ``preview_store.write_lock`` (#320): a
guard from ``preview_store.write_guard`` also refuses a write that started
before the thumbnail location changed."""
with preview_store.committed(guard) as allowed:
if not allowed:
raise PreviewWriteHeld(PREVIEW_HELD)
os.replace(tmp, dst)
# ``temp_sibling``'s names: ``.<stem>.<tag>-<pid>-<thread id><ext>``.