fix(photos): bridge hash-named gallery 404s to local named set (black-down)
Public /photos/ vhost serves the descriptive-named admin photo set from local disk since black:8081 photos-origin was decommissioned (2026-06-27), but the deployed gallery bundle addresses photos by 12-hex content hash — every image 404s. Add relink-photo-hashes.sh: extracts the name->hash map from the LIVE quinn.www bundle and (re)creates <hash> -> <named> symlinks in the admin photo dir, so both naming schemes resolve. Idempotent; self-corrects to whatever frontend is deployed; becomes inert when a photos origin returns and the vhost reverts to proxy_pass. Hooked into quinn.admin/deploy.sh step 4c after the photo rsync. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b3cb0efd1d
commit
34048f1e1a
2 changed files with 120 additions and 0 deletions
|
|
@ -222,6 +222,15 @@ if [[ -d "$PHOTOS_SRC" ]]; then
|
|||
ssh "$REMOTE" "install -d -o www-data -g www-data ${REMOTE_PHOTOS}"
|
||||
rsync -avz "${PHOTOS_SRC}/" "${REMOTE}:${REMOTE_PHOTOS}/"
|
||||
ssh "$REMOTE" "chown -R www-data:www-data ${REMOTE_PHOTOS}"
|
||||
|
||||
# Black-down bridge: the public /photos/ vhost currently serves this admin
|
||||
# named set from local disk (origin black:8081 is gone). The deployed gallery
|
||||
# bundle requests 12-hex content-hash names, so regenerate <hash> -> <named>
|
||||
# symlinks here, derived from the LIVE www bundle. Idempotent; becomes inert
|
||||
# once a photos origin returns and the vhost reverts to proxy_pass.
|
||||
# See scripts/relink-photo-hashes.sh for the full rationale + retirement note.
|
||||
"${SCRIPT_DIR}/scripts/relink-photo-hashes.sh" "$REMOTE" "$REMOTE_PHOTOS" \
|
||||
|| echo " WARNING: photo hash relink failed (gallery may 404 until re-run)." >&2
|
||||
else
|
||||
echo "==> [4c/10] WARNING: ${PHOTOS_SRC} not found on this host — skipping admin gallery photo sync." >&2
|
||||
echo " Admin gallery thumbnails + variant lenses will 404 until the photo set is present." >&2
|
||||
|
|
|
|||
111
deployments/@domains/quinn.admin/scripts/relink-photo-hashes.sh
Executable file
111
deployments/@domains/quinn.admin/scripts/relink-photo-hashes.sh
Executable file
|
|
@ -0,0 +1,111 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# relink-photo-hashes.sh — black-down emergency-fallback bridge for /photos/.
|
||||
#
|
||||
# WHY THIS EXISTS
|
||||
# The public gallery (quinn.www) addresses every photo by a 12-hex content
|
||||
# hash (e.g. /photos/33359fb60aab.jpeg). Those hashes are produced by the
|
||||
# protect-photos pipeline at build time and baked into the deployed bundle as
|
||||
# the `virtual:photo-hashes` name->hash map. In normal operation the hashed
|
||||
# files are served by the photos origin (black:8081, track B) via the public
|
||||
# vhost's `proxy_pass http://black_photos`.
|
||||
#
|
||||
# black was decommissioned 2026-06-27, so the public /photos/ location was
|
||||
# re-pointed to local disk (`root /var/www/quinn.admin; try_files $uri =404;`)
|
||||
# serving the DESCRIPTIVE-named admin photo set (cage-harness-purple.jpeg, …).
|
||||
# The deployed bundle still requests HASHED names, so without a bridge every
|
||||
# gallery image 404s.
|
||||
#
|
||||
# This script regenerates `<hash> -> <named-file>` symlinks inside the admin
|
||||
# photo dir so both naming schemes resolve. The name->hash map is read from
|
||||
# the LIVE quinn.www bundle, so the symlinks always track whatever frontend is
|
||||
# actually deployed — no dependence on reproducing protect-photos hashes.
|
||||
#
|
||||
# WHEN IT RUNS
|
||||
# Invoked by quinn.admin/deploy.sh step [4c] after the named photo set is
|
||||
# rsynced (an additive rsync can add new named files whose hash aliases are
|
||||
# missing). Safe to run standalone and repeatedly — it is idempotent.
|
||||
#
|
||||
# RETIREMENT
|
||||
# When a photos origin returns and the public vhost's /photos/ location is
|
||||
# reverted to proxy_pass/proxy_cache (see the .pre-blackdead-photoslocal-*
|
||||
# nginx backup on vps-0), this bridge is no longer needed: the symlinks become
|
||||
# inert and this step can be removed from deploy.sh.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
REMOTE="${1:-quinn-vps}"
|
||||
REMOTE_PHOTOS="${2:-/var/www/quinn.admin/photos}"
|
||||
SITE_URL="${PHOTOS_SITE_URL:-https://transquinnftw.com}"
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
echo "==> [relink-photo-hashes] Resolving live bundle hash map from ${SITE_URL} ..."
|
||||
|
||||
# 1. Find the deployed main JS bundle (Vite emits /assets/index-<hash>.js).
|
||||
INDEX_HTML="$(curl -fsSL -A 'Mozilla/5.0' "${SITE_URL}/")"
|
||||
JS_PATH="$(printf '%s' "$INDEX_HTML" \
|
||||
| grep -oE '/assets/index-[A-Za-z0-9_-]+\.js' | head -1)"
|
||||
if [[ -z "$JS_PATH" ]]; then
|
||||
echo " WARNING: could not locate /assets/index-*.js in ${SITE_URL} — skipping relink." >&2
|
||||
exit 0
|
||||
fi
|
||||
curl -fsSL -A 'Mozilla/5.0' "${SITE_URL}${JS_PATH}" -o "${TMP}/bundle.js"
|
||||
|
||||
# 2. Extract the embedded name->hash photo map (the protect-photos manifest).
|
||||
# Located by scanning for the first object literal whose values are 12-hex
|
||||
# image filenames — robust to the minifier's variable name for the map.
|
||||
node - "${TMP}/bundle.js" "${TMP}/photo-map.tsv" <<'NODE'
|
||||
const fs = require('fs');
|
||||
// argv: [node, '-', <bundle>, <out>] — '-' (stdin script marker) occupies argv[1].
|
||||
const [, , src, out] = process.argv;
|
||||
const js = fs.readFileSync(src, 'utf8');
|
||||
const HASH_VAL = /^[0-9a-f]{12}\.(jpe?g|webp|png)$/;
|
||||
// Find every `={` that begins a candidate object, brace-match it, JSON-parse,
|
||||
// and keep the one whose values are all hashed image filenames.
|
||||
let best = null;
|
||||
for (let i = 0; i < js.length - 1; i++) {
|
||||
if (js[i] !== '=' || js[i + 1] !== '{') continue;
|
||||
let depth = 0, j = i + 1;
|
||||
for (; j < js.length; j++) {
|
||||
const c = js[j];
|
||||
if (c === '{') depth++;
|
||||
else if (c === '}') { depth--; if (depth === 0) { j++; break; } }
|
||||
}
|
||||
let obj;
|
||||
try { obj = JSON.parse(js.slice(i + 1, j)); } catch { continue; }
|
||||
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) continue;
|
||||
const keys = Object.keys(obj);
|
||||
if (keys.length < 10) continue;
|
||||
if (!keys.every(k => typeof obj[k] === 'string' && HASH_VAL.test(obj[k]))) continue;
|
||||
if (!best || keys.length > Object.keys(best).length) best = obj;
|
||||
}
|
||||
if (!best) { console.error('no photo hash map found in bundle'); process.exit(2); }
|
||||
const lines = Object.entries(best).map(([name, hash]) => `${name}\t${hash}`);
|
||||
fs.writeFileSync(out, lines.join('\n') + '\n');
|
||||
console.error(` extracted ${lines.length} name->hash entries`);
|
||||
NODE
|
||||
|
||||
# 3. Ship the map and (re)create the symlinks on the remote, only where the
|
||||
# named target file is actually present. Idempotent: ln -sf overwrites.
|
||||
scp -q "${TMP}/photo-map.tsv" "${REMOTE}:/tmp/photo-map.tsv"
|
||||
ssh "$REMOTE" "REMOTE_PHOTOS='${REMOTE_PHOTOS}' bash -s" <<'REMOTE_SH'
|
||||
set -euo pipefail
|
||||
cd "$REMOTE_PHOTOS"
|
||||
created=0; missing=0
|
||||
while IFS=$'\t' read -r name hash; do
|
||||
[ -z "${name:-}" ] && continue
|
||||
if [ -f "$name" ]; then
|
||||
ln -sf "$name" "$hash"
|
||||
created=$((created + 1))
|
||||
else
|
||||
missing=$((missing + 1))
|
||||
fi
|
||||
done < /tmp/photo-map.tsv
|
||||
rm -f /tmp/photo-map.tsv
|
||||
chown -h www-data:www-data "$REMOTE_PHOTOS"/*.jpeg "$REMOTE_PHOTOS"/*.jpg "$REMOTE_PHOTOS"/*.webp "$REMOTE_PHOTOS"/*.png 2>/dev/null || true
|
||||
echo " relinked ${created} hash aliases (${missing} with no named target on disk)"
|
||||
REMOTE_SH
|
||||
|
||||
echo "==> [relink-photo-hashes] Done."
|
||||
Loading…
Add table
Reference in a new issue