* fix(external-media): store external paths from the media root (#1163) Stable twin of #1168. Stacked on the #1162 twin, which supplies deleteDuplicatePhotos. Importing a second folder into an event silently invalidated every photo already in it. external_relpath was stored relative to events.external_path, and every import overwrites that column, so the older rows were rebased onto the new folder. Nothing errored and the grid still rendered — thumbnails are written to local storage during the import while the base path is still correct — so only the things that need the original broke. The reporter had 7547 of 8004 rows pointing into the void. - external_relpath is now relative to EXTERNAL_MEDIA_ROOT, so a row is self-describing and nothing an admin does to the event can move it. - migration 177 folds each event's base into its rows. Where the current resolution is missing it walks up for an ancestor holding a file of the same name AND the size the import recorded — existence alone would let a deleted file adopt an unrelated namesake and serve the wrong original. Rows it cannot place keep resolving where they resolve today, and the probe is skipped entirely when the mount is unreachable. - probing is read-only and runs first; the rewrites and the marker commit together, so an interrupted fold cannot be folded twice. - rewrites are staged through a per-row parking value, because a final path can equal another row's current one; and migration 177 re-throws without the driver's error code, which run-migrations-safe would otherwise read as "schema already exists". - the fold also runs after a .picpeak restore, since knex_migrations is excluded from the archive, and a failure there is reported rather than presented as a clean restore. - drops the duplicate-leaf-segment guess in photoResolver, which papered over this same double-prefixing. Divergence from the main twin: no face-scan requeue reordering. Face recognition is main-only, so the hazard of queueing rows against unconverted paths does not exist on this branch — in picpeakImportService or in restoreService. Verified on this branch: 23 new tests pass, and the four suites carrying base-relative fixtures were updated. Full suite leaves the same 5 pre-existing failures as origin/stable, unchanged. * fix(external-media): the fold's staging value must be storable on Postgres (#1163) External review found this on this branch first; it was on both. The two-pass rewrite parks each row on a temporary value, and that value was written with a leading NUL. SQLite stores NUL in TEXT without complaint; Postgres rejects it outright — "invalid byte sequence for encoding UTF8: 0x00" — so migration 177 rolled back on exactly the installs that need the two-pass repair, and only on the engine most of them run. Restores hit the same wall and reported the conversion as failed. The prefix is ordinary text now. Adds a gated Postgres test alongside the existing picpeakRestorePg one, because a SQLite-only suite structurally cannot catch this class: restoring the NUL makes exactly the two-pass repair case fail with that error, and nothing else. --------- Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
co-authored by
Paul Nothaft
parent
e9fcf4960e
commit
2b1c3588ae
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Migration 177: external_relpath becomes relative to EXTERNAL_MEDIA_ROOT (#1163).
|
||||
*
|
||||
* It used to be relative to events.external_path, which every import
|
||||
* overwrites — so importing a second folder into an event rebased every photo
|
||||
* already in it onto the new folder. Nothing errored. Thumbnails are written to
|
||||
* local storage during the import while the base path is still correct, so the
|
||||
* grid kept rendering and only the things that need the ORIGINAL broke: preview
|
||||
* generation, the lightbox, downloads. The reporter had 7547 of 8004 rows
|
||||
* resolving to files that do not exist, and spent a while chasing it as a CPU
|
||||
* problem.
|
||||
*
|
||||
* The work — including the on-disk repair of events that have already been
|
||||
* rebased, and why it refuses to guess below current behaviour — lives in
|
||||
* services/externalRelpathFold.js, because a .picpeak restore has to run it
|
||||
* too: knex_migrations is excluded from the archive, so a pre-#1163 backup
|
||||
* lands base-relative rows on an instance that has already migrated.
|
||||
*/
|
||||
|
||||
const { foldExternalRelpaths } = require('../../src/services/externalRelpathFold');
|
||||
|
||||
exports.up = async function(knex) {
|
||||
try {
|
||||
await foldExternalRelpaths(knex, (msg) => console.log(`177_external_relpath_from_root: ${msg}`));
|
||||
} catch (err) {
|
||||
// Re-thrown WITHOUT the driver's error code. run-migrations-safe.js treats
|
||||
// 23505 / 42P07 / 42701 / 42710 as "schema already exists" and marks the
|
||||
// migration applied (run-migrations-safe.js:138) — so a unique-violation
|
||||
// rolling this fold back would be recorded as a success, leaving every
|
||||
// external path in the old format under a resolver that reads them
|
||||
// differently, with nothing to trigger a retry.
|
||||
throw new Error(
|
||||
`177_external_relpath_from_root failed and was rolled back: ${err.message}. `
|
||||
+ 'External photo paths are unchanged; resolve the cause and re-run the migration.'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Irreversible by design, and a deliberate no-op rather than a partial undo.
|
||||
*
|
||||
* The base each row was folded with is not recorded anywhere:
|
||||
* events.external_path holds whatever the LAST import set, which for a
|
||||
* repaired row is the wrong answer and is exactly what broke these installs.
|
||||
* Stripping it back off would re-break them.
|
||||
*
|
||||
* The idempotency marker stays for the same reason — clearing it would let
|
||||
* up() run a second time and fold every path twice.
|
||||
*/
|
||||
exports.down = async function() {
|
||||
console.log('177_external_relpath_from_root: rollback is a no-op (see header)');
|
||||
};
|
||||
Reference in New Issue
Block a user