7177cdb9ab
Adds a /help page served from the dashboard navigation, containing the full
Romanian user manual for operators: dashboard, playlist management, adding
media, removing files from playlists, player management, and the advanced
edited-media view.
Implementation notes
- Manual is pre-rendered to HTML with pandoc and shipped as a static asset
(app/static/help/_manual_body.html) rather than parsed at runtime. This
keeps the container free of a Markdown dependency and makes the page render
identically regardless of what is installed.
- /help is login-protected; the sidebar table of contents is derived from the
level-2 headings at request time.
- Screenshots are served from app/static/help/screenshots/ (32 images).
- Regenerate after editing the manual:
documentatie/convert_help_page.sh
docker compose up -d --build
Also adds
- backup_players_playlists.sh / restore_database.sh for DB backup and restore.
- .gitignore rules so local database backups (real production data), generated
.docx files and duplicate screenshot copies are never committed.
241 lines
8.9 KiB
Bash
Executable File
241 lines
8.9 KiB
Bash
Executable File
#!/bin/bash
|
|
# ============================================================================
|
|
# DigiServer - Database Backup (restore-ready for Docker image / host upgrades)
|
|
# ----------------------------------------------------------------------------
|
|
# Produces a timestamped, self-contained bundle under ./backups/<timestamp>/:
|
|
#
|
|
# dashboard.db.gz - CONSISTENT full snapshot of the SQLite database
|
|
# (player, playlist, playlist_content,
|
|
# player_feedback and all other tables)
|
|
# players_playlists.sql.gz - logical dump of player/playlist tables
|
|
# (secondary safety net; human-readable)
|
|
# SHA256SUMS - integrity check
|
|
# MANIFEST.txt - row counts, sizes, checksums, app version
|
|
# RESTORE.md - copy-paste restore instructions for this bundle
|
|
#
|
|
# The bundle is designed so that after a `docker compose build` + host upgrade
|
|
# you can run ./restore_database.sh <timestamp> and be back exactly as before.
|
|
#
|
|
# Usage:
|
|
# ./backup_players_playlists.sh # full restore-ready backup
|
|
# ./backup_players_playlists.sh --logical-only # SQL dump only (no 2.2GB copy)
|
|
# ============================================================================
|
|
set -euo pipefail
|
|
|
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
COMPOSE="docker compose -f $PROJECT_DIR/docker-compose.yml"
|
|
CONTAINER="digiserver-v2"
|
|
DB_HOST="$PROJECT_DIR/data/instance/dashboard.db"
|
|
DB_CTR="/app/instance/dashboard.db"
|
|
SNAP_CTR="/app/instance/__snapshot.db"
|
|
BACKUP_ROOT="$PROJECT_DIR/backups"
|
|
TS="$(date +%Y%m%d_%H%M%S)"
|
|
DEST="$BACKUP_ROOT/$TS"
|
|
|
|
ALL_DUMP_TABLES=(player playlist playlist_content player_user player_edit content group group_content)
|
|
|
|
LOGICAL_ONLY=0
|
|
[ "${1:-}" = "--logical-only" ] && LOGICAL_ONLY=1
|
|
|
|
mkdir -p "$DEST"
|
|
echo "============================================================"
|
|
echo " DigiServer restore-ready backup"
|
|
echo " Target: $DEST"
|
|
echo "============================================================"
|
|
|
|
if [ ! -f "$DB_HOST" ]; then
|
|
echo "ERROR: database not found at $DB_HOST"
|
|
exit 1
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Consistent snapshot via SQLite online backup API (safe while app runs)
|
|
# ---------------------------------------------------------------------------
|
|
if [ "$LOGICAL_ONLY" -eq 0 ]; then
|
|
echo
|
|
echo "[1/5] Creating consistent DB snapshot (online backup API)..."
|
|
$COMPOSE exec -T digiserver-app python - <<'PYEOF'
|
|
import sqlite3
|
|
src = sqlite3.connect('/app/instance/dashboard.db')
|
|
dst = sqlite3.connect('/app/instance/__snapshot.db')
|
|
with dst:
|
|
src.backup(dst)
|
|
dst.close(); src.close()
|
|
print(" snapshot created")
|
|
PYEOF
|
|
|
|
docker cp "$CONTAINER:$SNAP_CTR" "$DEST/dashboard.db"
|
|
$COMPOSE exec -T digiserver-app rm -f "$SNAP_CTR"
|
|
echo " -> size: $(du -h "$DEST/dashboard.db" | cut -f1)"
|
|
|
|
echo
|
|
echo "[2/5] Compressing snapshot..."
|
|
gzip -f "$DEST/dashboard.db"
|
|
echo " -> $(ls -lh "$DEST/dashboard.db.gz" | awk '{print $5}')"
|
|
else
|
|
echo
|
|
echo "[1/5] Skipping full snapshot (--logical-only)"
|
|
echo "[2/5] Skipping compression"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. Logical SQL dump (secondary safety net)
|
|
# ---------------------------------------------------------------------------
|
|
echo
|
|
echo "[3/5] Creating logical SQL dump of player/playlist tables..."
|
|
TABLE_LIST=$(printf '"%s",' "${ALL_DUMP_TABLES[@]}")
|
|
TABLE_LIST="[${TABLE_LIST%,}]"
|
|
|
|
$COMPOSE exec -T digiserver-app python - "$TABLE_LIST" > "$DEST/players_playlists.sql" <<'PYEOF'
|
|
import sqlite3, sys
|
|
tables = eval(sys.argv[1])
|
|
con = sqlite3.connect('/app/instance/dashboard.db')
|
|
o = sys.stdout
|
|
o.write("-- DigiServer logical backup: players & playlists\n")
|
|
o.write("-- Tables: %s\n\n" % ", ".join(tables))
|
|
o.write("PRAGMA foreign_keys=OFF;\nBEGIN TRANSACTION;\n\n")
|
|
for t in tables:
|
|
row = con.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (t,)).fetchone()
|
|
if not row or not row[0]:
|
|
continue
|
|
o.write("DROP TABLE IF EXISTS \"%s\";\n" % t)
|
|
o.write(row[0].rstrip() + ";\n")
|
|
cur = con.execute('SELECT * FROM "%s"' % t)
|
|
cols = [d[0] for d in cur.description]
|
|
collist = ",".join('"%s"' % c for c in cols)
|
|
batch = []
|
|
def flush(b):
|
|
if b:
|
|
o.write("INSERT INTO \"%s\" (%s) VALUES\n%s;\n" % (t, collist, ",\n".join(b)))
|
|
for rec in cur:
|
|
vals = []
|
|
for v in rec:
|
|
if v is None: vals.append("NULL")
|
|
elif isinstance(v, (int, float)): vals.append(str(v))
|
|
elif isinstance(v, bytes): vals.append("X'%s'" % v.hex())
|
|
else: vals.append("'" + str(v).replace("'", "''") + "'")
|
|
batch.append("(%s)" % ",".join(vals))
|
|
if len(batch) >= 500:
|
|
flush(batch); batch = []
|
|
flush(batch)
|
|
o.write("\n")
|
|
o.write("COMMIT;\n")
|
|
PYEOF
|
|
|
|
gzip -f "$DEST/players_playlists.sql"
|
|
echo " -> $(ls -lh "$DEST/players_playlists.sql.gz" | awk '{print $5}')"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. Checksums
|
|
# ---------------------------------------------------------------------------
|
|
echo
|
|
echo "[4/5] Writing checksums..."
|
|
( cd "$DEST" && sha256sum *.gz > SHA256SUMS )
|
|
sed 's/^/ /' "$DEST/SHA256SUMS"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. Manifest + restore instructions
|
|
# ---------------------------------------------------------------------------
|
|
echo
|
|
echo "[5/5] Writing MANIFEST.txt and RESTORE.md..."
|
|
{
|
|
echo "DigiServer backup manifest"
|
|
echo "=========================="
|
|
echo "Created: $(date -Iseconds)"
|
|
echo "Host: $(hostname)"
|
|
echo "Backup ID: $TS"
|
|
echo "Source DB: $DB_HOST"
|
|
echo "Source DB size: $(du -h "$DB_HOST" | cut -f1)"
|
|
echo ""
|
|
echo "Row counts at backup time:"
|
|
$COMPOSE exec -T digiserver-app python - <<'PYEOF' 2>/dev/null || echo " (unavailable)"
|
|
import sqlite3
|
|
con = sqlite3.connect('/app/instance/dashboard.db')
|
|
for t in ['player','playlist','playlist_content','player_user','player_edit',
|
|
'player_feedback','content','group','group_content','user']:
|
|
try:
|
|
n = con.execute('SELECT COUNT(*) FROM "%s"' % t).fetchone()[0]
|
|
print(' %-20s %9d' % (t, n))
|
|
except Exception as e:
|
|
print(' %-20s ERROR %s' % (t, e))
|
|
PYEOF
|
|
echo ""
|
|
echo "Files:"
|
|
( cd "$DEST" && ls -lh | tail -n +2 | sed 's/^/ /' )
|
|
} > "$DEST/MANIFEST.txt"
|
|
|
|
cat > "$DEST/RESTORE.md" <<EOF
|
|
# Restore this DigiServer backup
|
|
|
|
Backup ID: **$TS**
|
|
Created: $(date -Iseconds)
|
|
|
|
## What's in this bundle
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| \`dashboard.db.gz\` | Full consistent snapshot (all tables) - primary restore source |
|
|
| \`players_playlists.sql.gz\` | Logical dump of player/playlist tables - secondary source |
|
|
| \`SHA256SUMS\` | Integrity check |
|
|
| \`MANIFEST.txt\` | Row counts + metadata |
|
|
|
|
## Restore procedure (after Docker image / host upgrade)
|
|
|
|
Run from the project root (\`$PROJECT_DIR\`):
|
|
|
|
\`\`\`bash
|
|
./restore_database.sh $TS
|
|
\`\`\`
|
|
|
|
Or do it manually:
|
|
|
|
\`\`\`bash
|
|
# 1. Stop the app so nothing writes to the DB
|
|
docker compose stop digiserver-app
|
|
|
|
# 2. Verify the backup is intact
|
|
( cd backups/$TS && sha256sum -c SHA256SUMS )
|
|
|
|
# 3. Decompress and swap in the database
|
|
gunzip -c backups/$TS/dashboard.db.gz > data/instance/dashboard.db
|
|
|
|
# 4. Fix ownership (container runs as uid 1000)
|
|
sudo chown 1000:1000 data/instance/dashboard.db
|
|
chmod 644 data/instance/dashboard.db
|
|
|
|
# 5. Remove any stale journal file
|
|
rm -f data/instance/dashboard.db-journal
|
|
|
|
# 6. Rebuild/start the upgraded image
|
|
docker compose up -d --build
|
|
|
|
# 7. Re-run migrations (new schema may have been added)
|
|
docker compose exec -T digiserver-app python /app/migrations/add_https_config_table.py
|
|
docker compose exec -T digiserver-app python /app/migrations/add_player_user_table.py
|
|
docker compose exec -T digiserver-app python /app/migrations/add_email_to_https_config.py
|
|
docker compose exec -T digiserver-app python /app/migrations/migrate_player_user_global.py
|
|
|
|
# 8. Verify
|
|
docker compose exec -T digiserver-app python -c "
|
|
import sqlite3
|
|
c=sqlite3.connect('/app/instance/dashboard.db')
|
|
print('players: ', c.execute('SELECT COUNT(*) FROM player').fetchone()[0])
|
|
print('playlists:', c.execute('SELECT COUNT(*) FROM playlist').fetchone()[0])
|
|
print('items: ', c.execute('SELECT COUNT(*) FROM playlist_content').fetchone()[0])
|
|
"
|
|
\`\`\`
|
|
|
|
## Important notes
|
|
|
|
- The container's **entrypoint only creates a DB if one is missing** - dropping in this
|
|
file bypasses re-initialisation, which is what we want.
|
|
- Ownership **must be uid/gid 1000** (host \`pi:pi\` == container \`appuser\`).
|
|
- If the new app version changed the schema, the migration step (7) is mandatory.
|
|
EOF
|
|
|
|
echo
|
|
echo "============================================================"
|
|
echo " Backup complete and restore-ready: $DEST"
|
|
echo " To restore later: ./restore_database.sh $TS"
|
|
echo "============================================================"
|