new-site/extract-erpnext-assets.sh
justin dcea3c29bb portal: serve /files/ (logo) from stable host path, fix recurring 403
nginx served /files/ via alias straight into /var/lib/docker/volumes/... but
/var/lib/docker is root 0700 (no traverse for www-data) and docker resets that
perm on restart -> recurring 403 on /files/pw-logo.png (broken portal logo).

Sync the site's public /files/ into /opt/erpnext-assets/assets/files (already
www-data-owned, nginx-traversable, never touched by docker) during asset
extraction, and verify the logo is present. nginx /files/ alias must point here
(separate nginx change applied on server).
2026-06-02 22:18:30 -05:00

87 lines
3.9 KiB
Bash
Executable file

#!/usr/bin/env bash
# extract-erpnext-assets.sh — Sync ERPNext/Frappe static assets to the host
# directory that nginx serves for portal.performancewest.net.
#
# WHY THIS EXISTS
# The portal serves Frappe's static assets from a host copy at
# /opt/erpnext-assets/assets/ (fast, long-cache, off-loads ERPNext). Frappe
# emits content-hashed filenames (e.g. login.bundle.SSJHYWGF.css) and a
# manifest (sites/assets/assets.json). Every `bench build`/`bench migrate`
# (which runs on an ERPNext image rebuild) changes those hashes. If the host
# copy is not re-synced, the HTML references new hashes that 404 on the host
# → the portal renders with NO CSS.
#
# This script re-extracts the current assets so the host copy and the
# manifest always match. It is idempotent and safe to run any time.
#
# RUN IT after: an ERPNext image rebuild, `bench build`, or `bench migrate`.
# deploy.sh calls it automatically for the `erpnext` target.
set -euo pipefail
CONTAINER="${ERPNEXT_CONTAINER:-performancewest-erpnext-1}"
DEST="${ERPNEXT_ASSETS_DIR:-/opt/erpnext-assets/assets}"
# docker may need sudo depending on the host; honor a DOCKER override.
DOCKER="${DOCKER:-sudo -n docker}"
echo "=== Extracting ERPNext assets from ${CONTAINER} -> ${DEST} ==="
if ! $DOCKER inspect "$CONTAINER" >/dev/null 2>&1; then
echo "ERROR: container ${CONTAINER} not found" >&2
exit 1
fi
# Make sure the in-container manifest matches the built bundles. A bare image
# rebuild can leave assets.json stale relative to dist/, which also breaks CSS.
# `bench build` regenerates both consistently; cheap no-op if already current.
echo "--- Ensuring assets are built (bench build) ---"
$DOCKER exec "$CONTAINER" bench build >/tmp/erpnext-build.log 2>&1 || {
echo "WARN: bench build returned non-zero; see /tmp/erpnext-build.log" >&2
}
sudo rm -rf "${DEST}/frappe" "${DEST}/erpnext"
sudo mkdir -p "$DEST"
# frappe + erpnext public/ dirs (sites/assets/<app> symlinks here).
$DOCKER exec "$CONTAINER" tar cf - \
-C /home/frappe/frappe-bench/apps/frappe/frappe public \
| sudo tar xf - -C "$DEST" --transform='s|^public|frappe|'
$DOCKER exec "$CONTAINER" tar cf - \
-C /home/frappe/frappe-bench/apps/erpnext/erpnext public \
| sudo tar xf - -C "$DEST" --transform='s|^public|erpnext|'
# The manifest nginx/Frappe read for hash->file lookups.
$DOCKER exec "$CONTAINER" cat \
/home/frappe/frappe-bench/sites/assets/assets.json \
| sudo tee "${DEST}/assets.json" >/dev/null
# Site-uploaded public /files/ (e.g. the portal logo pw-logo.png). nginx cannot
# traverse the raw docker volume (/var/lib/docker is root 0700 and docker resets
# it on restart -> recurring 403s), so we serve /files/ from this stable
# www-data-owned host path instead. Re-synced here on every deploy.
SITE="${ERPNEXT_SITE:-performancewest.net}"
FILES_SRC="/home/frappe/frappe-bench/sites/${SITE}/public/files"
echo "--- Syncing site /files/ (logo, uploads) ---"
sudo rm -rf "${DEST}/files"
sudo mkdir -p "${DEST}/files"
$DOCKER exec "$CONTAINER" sh -c "[ -d '${FILES_SRC}' ] && tar cf - -C '${FILES_SRC}' . || true" \
| sudo tar xf - -C "${DEST}/files" 2>/dev/null || true
sudo chown -R www-data:www-data /opt/erpnext-assets
sudo nginx -s reload 2>/dev/null || true
# Verify the manifest's login bundle actually exists on the host now.
LOGIN_HASH="$($DOCKER exec "$CONTAINER" sh -c \
"grep -o 'login.bundle.[A-Z0-9]*.css' /home/frappe/frappe-bench/sites/assets/assets.json | head -1" || true)"
if [ -n "$LOGIN_HASH" ] && [ ! -f "${DEST}/frappe/dist/css/${LOGIN_HASH}" ]; then
echo "ERROR: manifest references ${LOGIN_HASH} but it is missing on host." >&2
echo " Portal CSS would be broken. Aborting." >&2
exit 2
fi
echo "=== Done. Assets at ${DEST} (login bundle: ${LOGIN_HASH:-unknown}) ==="
# Verify the portal logo made it across (served at /files/pw-logo.png).
if [ ! -f "${DEST}/files/pw-logo.png" ]; then
echo "WARN: ${DEST}/files/pw-logo.png missing — portal logo may be broken." >&2
fi