feat(files): gate files. browse behind FILES_BROWSE (on|off)

Per-host toggle for the read-only, no-auth browse of the shared upload dir,
so it can stay on for the LAN-party host (oxps) and be off on the
internet-facing one (cochi). Defaults to `on` — existing hosts unchanged.

Neither layer has conditionals, so each gets the value substituted in:
- caddy: docker-compose mounts 62-files.${FILES_BROWSE:-on}.caddy at the
  fixed conf.d target (62-files.caddy -> 62-files.on.caddy, plus a
  comments-only 62-files.off.caddy stub — caddy needs a parseable file
  there). Same trick as FILES_MOUNT_MODE.
- nginx: render-nginx.sh splices nginx/files-browse.server.tmpl into
  ${FILES_BROWSE_SERVER} and adds/omits the name in the port-80 redirect.
  This is the gate that matters; caddy is bound to 127.0.0.1.

Applying a change needs render-nginx.sh --install plus a caddy
--force-recreate (single-file bind mount pins the inode). With `off`,
drop `files` from LE_SUBDOMAINS on that host. upload. is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
oierandClaude Opus 5 committed 2026-07-30 17:08:33 +02:00
1 parent 92c6f5fc8f
commit 8507ffd020
9 files changed
+122 -33

No files matched your search

+28 -2
View File
@@ -17,6 +17,11 @@
# APP_DIR host path to this repo (cert files live under APP_DIR/certs)
# — defaults to the repo root (this checkout)
# SITE_NAME installed filename (default ulicraft.conf)
# FILES_BROWSE on|off (default on) — emit the files.${BASE_DOMAIN}
# read-only browse vhost. `off` omits the server block AND
# the name from the port-80 redirect, so the host serves
# nothing for it (no cert needed either). Same var gates the
# caddy side in docker-compose.yml.
set -euo pipefail
cd "$(dirname "$0")/.." # repo root
@@ -40,7 +45,28 @@ SITE_NAME="${SITE_NAME:-ulicraft.conf}"
export BASE_DOMAIN APP_DIR
export CADDY_HTTP_PORT="${CADDY_HTTP_PORT:-80}"
rendered="$(envsubst '$BASE_DOMAIN $APP_DIR $CADDY_HTTP_PORT' < "$TEMPLATE")"
# files.${BASE_DOMAIN} — the no-auth read-only browse. Per-host toggle: on for
# the LAN-party box, off where the share must not be publicly listable. nginx
# has no conditionals, so the vhost is a separate template spliced in here (or
# replaced by a comment). Keep in sync with the caddy mount in docker-compose.yml,
# which reads the same var.
: "${FILES_BROWSE:=on}"
FILES_BROWSE_TEMPLATE="${FILES_BROWSE_TEMPLATE:-nginx/files-browse.server.tmpl}"
case "$FILES_BROWSE" in
on)
[ -f "$FILES_BROWSE_TEMPLATE" ] || die "template not found: $FILES_BROWSE_TEMPLATE"
FILES_BROWSE_NAME=" files.${BASE_DOMAIN}"
FILES_BROWSE_SERVER="$(envsubst '$BASE_DOMAIN $APP_DIR' < "$FILES_BROWSE_TEMPLATE")"
;;
off)
FILES_BROWSE_NAME=""
FILES_BROWSE_SERVER="# files.${BASE_DOMAIN} disabled on this host (FILES_BROWSE=off in .env)."
;;
*) die "FILES_BROWSE must be 'on' or 'off' (got '$FILES_BROWSE')" ;;
esac
export FILES_BROWSE_NAME FILES_BROWSE_SERVER
rendered="$(envsubst '$BASE_DOMAIN $APP_DIR $CADDY_HTTP_PORT $FILES_BROWSE_NAME $FILES_BROWSE_SERVER' < "$TEMPLATE")"
if [ "${1:-}" != "--install" ]; then
printf '%s\n' "$rendered"
@@ -52,7 +78,7 @@ command -v nginx >/dev/null 2>&1 || die "nginx not found on PATH"
avail="/etc/nginx/sites-available/$SITE_NAME"
enabled="/etc/nginx/sites-enabled/$SITE_NAME"
log "installing $TEMPLATE -> $avail (APP_DIR=$APP_DIR, CADDY_HTTP_PORT=$CADDY_HTTP_PORT)"
log "installing $TEMPLATE -> $avail (APP_DIR=$APP_DIR, CADDY_HTTP_PORT=$CADDY_HTTP_PORT, FILES_BROWSE=$FILES_BROWSE)"
printf '%s\n' "$rendered" | sudo tee "$avail" >/dev/null
sudo ln -sf "$avail" "$enabled"
sudo nginx -t