feat(thumbnails): move thumbnails between the sources and one folder (#320)

POST /images/thumbnails/plan reviews a move (what is copied, removed and
left, free space, too-long Windows paths, sources no longer listed) and
hands out a token; POST /jobs/thumbnails {mode: move} runs it as a
thumbnails job: hold automatic previews, copy and check every preview on
the job pool, switch with one config commit that records the old copies,
then remove them. Per-file errors still switch; a folder-wide error, a
cancel before the switch or a crash leaves nothing changed (a journal is
undone at the next start); old copies that could not go are removed by a
finishing job at the next start. Leaving a folder removes the previews of
this install's sources that are no longer listed (Q320-8).

Recreate folder and Use this folder answer an unavailable folder; the
Thumbnail maintenance scope prune removes the thumbnails of removed
sources.
This commit is contained in:
Vito Sansevero
2026-10-31 20:29:06 -07:00
parent c14e1af87f
commit a33b0f4648
16 changed files with 2239 additions and 6 deletions
+11 -2
View File
@@ -114,6 +114,7 @@ def replace_unless_held(tmp, dst, guard=None):
# ``temp_sibling``'s names: ``.<stem>.<tag>-<pid>-<thread id><ext>``.
_TEMP_NAME = re.compile(r"^\..+\.(?:part|src)-(\d+)-\d+\.[A-Za-z0-9]+$")
_MOVE_TEMP_NAME = re.compile(r"\.pm-move-[0-9a-f]{8}\.tmp$")
# A temp at least this old (seconds) outlived its write: no encode runs longer.
STRAY_TEMP_AGE = 4 * ENCODE_TIMEOUT
# When this process started (this module's import): a temp carrying its PID
@@ -138,7 +139,9 @@ def sweep_stray_temps(folder, min_age=None):
for root, _dirs, files in os.walk(folder):
for name in files:
match = _TEMP_NAME.match(name)
if match is None:
# A move's copy temp (#320): ``<name>.pm-move-<8 hex>.tmp``.
move_temp = match is None and _MOVE_TEMP_NAME.search(name)
if match is None and not move_temp:
continue
path = Path(root) / name
try:
@@ -147,7 +150,13 @@ def sweep_stray_temps(folder, min_age=None):
continue
if min_age and (
now - stat.st_mtime < min_age
or (match.group(1) == own and stat.st_mtime >= PROCESS_STARTED)
or move_temp
and stat.st_mtime >= PROCESS_STARTED
or (
match is not None
and match.group(1) == own
and stat.st_mtime >= PROCESS_STARTED
)
):
continue
path.unlink()