a moved file relocates its ladder instead of rebuilding it, and orphans go

Moving a file into a folder made the sync look at a new path, find nothing, and
convert it again — minutes of work to produce bytes that already existed a
directory away — while the old folder stayed behind forever, because nothing ever
deleted one.

A move preserves size and mtime, and both are already in the manifest and in the
stat. So the sync indexes what is already derived on that pair, and a file that
has moved has its ladder RENAMED across.

An ambiguous match is refused rather than guessed. Two originals can share a size
and an mtime — a camera dump can write a whole card in one second — and
rebuilding is merely slow where attaching the wrong photographs to a name is
wrong.

Orphans are removed afterwards, and only on a WHOLE-TREE sync. A scoped run knows
about one folder, so everything outside it looks abandoned; deleting on that
basis would take out the library the first time somebody re-synced a subfolder.
Safe otherwise because derived/ is reproducible by definition, which is what its
name has meant since these two folders stopped being called `user` and `treated`.

Both are counted, recorded in the ledger and shown only when they happened, so an
ordinary run stays one short line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 20:42:20 +01:00
co-authored by Claude Opus 5
parent b634de5604
commit 01652f3480
4 changed files with 193 additions and 4 deletions
+9
View File
@@ -46,6 +46,10 @@ type SyncState = {
done: number;
built: number;
skipped: number;
/** Ladders renamed into place because their original moved, rather than rebuilt. */
moved: number;
/** Derived folders removed because their original is gone. Whole-tree syncs only. */
pruned: number;
errors: SyncError[];
current: string[];
startedAt: string | null;
@@ -61,6 +65,8 @@ type Run = {
total: number;
built: number;
skipped: number;
moved: number;
pruned: number;
errors: SyncError[];
fatal: string | null;
};
@@ -251,6 +257,9 @@ export const MediaSync = ({ onSettled }: { onSettled?: (settled: boolean) => voi
{run.scope ? `${run.scope} · ` : ''}
{run.built} built
{run.skipped ? `, ${run.skipped} current` : ''}
{/* Only when they happened, so an ordinary run stays one short line. */}
{run.moved ? `, ${run.moved} moved` : ''}
{run.pruned ? `, ${run.pruned} orphans removed` : ''}
{run.errors.length ? `, ${run.errors.length} failed` : ''}
{run.fatal ? ` — ${run.fatal}` : ''}
{run.trigger === 'watcher' ? ' · on upload' : ''}