Downloads official instruction PDFs from LEGO, located via the Brickset API, one set per interval (10 minutes by default) so neither API gets hammered. Official sets only; MOCs are excluded throughout. - New admin page at /admin/instruction-fetch: queue builder, pacing controls, live countdown and per-set job history - Database-backed queue (instruction_fetch_jobs) and last-run timestamp, so pacing survives restarts - Background worker thread, plus 'flask instructions queue/run/status' for Task Scheduler or cron - SHA-256 dedupe (instructions.sha256), PDF header check, size cap and streaming downloads via temp files - brickset_api: getInstructions2 by set number and getInstructions by setID (the old code sent a set number to the setID method); getSets params now proper JSON; getKeyUsageStats added - Schema top-ups applied automatically at boot: new table plus instructions.sha256/source - Docker: fetcher env vars passed through compose, no worker thread in the entrypoint bootstrap
82 lines
2.8 KiB
Bash
82 lines
2.8 KiB
Bash
#!/usr/bin/env bash
|
|
# LEGO Instructions Manager - container entrypoint.
|
|
#
|
|
# Runs as root just long enough to fix volume ownership, then waits for the
|
|
# database, creates any missing tables, and drops to the unprivileged user.
|
|
#
|
|
# Created by: Jess Rogerson (yelling commands at Claude.AI)
|
|
set -euo pipefail
|
|
|
|
APP_USER=lim
|
|
|
|
log() { echo "[entrypoint] $*"; }
|
|
|
|
# --- volume ownership -------------------------------------------------------
|
|
# A freshly created named volume mounts as root:root, so the app user cannot
|
|
# write to it until we hand it over. Cheap to repeat on every start.
|
|
if [ "$(id -u)" = '0' ]; then
|
|
mkdir -p "${UPLOAD_FOLDER:-/data/uploads}" "${BACKUP_FOLDER:-/data/backups}"
|
|
chown -R "${APP_USER}:${APP_USER}" /data
|
|
fi
|
|
|
|
# --- wait for the database --------------------------------------------------
|
|
if [ "${DATABASE_URL:-}" != "" ] && [ "${SKIP_DB_WAIT:-0}" != "1" ]; then
|
|
log "Waiting for database..."
|
|
for attempt in $(seq 1 60); do
|
|
if python - <<'PY'
|
|
import os, sys
|
|
from sqlalchemy import create_engine, text
|
|
try:
|
|
engine = create_engine(os.environ['DATABASE_URL'], pool_pre_ping=True)
|
|
with engine.connect() as conn:
|
|
conn.execute(text('SELECT 1'))
|
|
except Exception as exc:
|
|
print(f' not ready: {exc.__class__.__name__}', file=sys.stderr)
|
|
sys.exit(1)
|
|
PY
|
|
then
|
|
log "Database is up."
|
|
break
|
|
fi
|
|
if [ "$attempt" = "60" ]; then
|
|
log "ERROR: database did not become available in time."
|
|
exit 1
|
|
fi
|
|
sleep 2
|
|
done
|
|
fi
|
|
|
|
# --- schema -----------------------------------------------------------------
|
|
# db.create_all() is additive: it creates missing tables and leaves existing
|
|
# ones untouched. It does NOT alter columns, so a schema change still needs a
|
|
# migration — see docs/DOCKER_DEPLOYMENT.md.
|
|
if [ "${SKIP_DB_INIT:-0}" != "1" ]; then
|
|
log "Ensuring schema exists..."
|
|
# No fetcher thread in this short-lived bootstrap process.
|
|
INSTRUCTION_FETCH_WORKER=0 python - <<'PY'
|
|
import os
|
|
from app import create_app, db
|
|
app = create_app(os.getenv('FLASK_ENV', 'production'))
|
|
with app.app_context():
|
|
db.create_all()
|
|
print('[entrypoint] Schema ready.')
|
|
PY
|
|
fi
|
|
|
|
# --- hand over --------------------------------------------------------------
|
|
# Re-chown: create_app() ran as root above and will have created the
|
|
# uploads subdirectories (pdfs/, images/, covers/, thumbnails/, extra_files/)
|
|
# owned by root. Without this the app user cannot write uploads.
|
|
if [ "$(id -u)" = '0' ]; then
|
|
chown -R "${APP_USER}:${APP_USER}" /data
|
|
log "Starting as ${APP_USER}: $*"
|
|
if command -v runuser >/dev/null 2>&1; then
|
|
exec runuser -u "${APP_USER}" -- "$@"
|
|
else
|
|
exec su -s /bin/bash -c 'exec "$0" "$@"' "${APP_USER}" -- "$@"
|
|
fi
|
|
fi
|
|
|
|
log "Starting: $*"
|
|
exec "$@"
|