This commit is contained in:
gpatruno
2026-07-18 16:26:08 +02:00
parent 49c444e8b9
commit 2614d679c8
85 changed files with 8074 additions and 710 deletions
+9 -64
View File
@@ -1,67 +1,12 @@
#!/usr/bin/env bash
# Export local project SQLite DB (+ uploads) for backup / Docker import
# Usage:
# ./scripts/db-export.sh
# ./scripts/db-export.sh ./backups/my-backup
#
# Sources (local):
# server/prisma/dev.db (+ -wal / -shm if present)
# server/uploads/
#
# Restore into Docker with:
# ./scripts/db-import.sh ./backups/...
# Deprecated alias — use docker-db-export.sh on prod, or local-db-export.sh on your laptop.
# This wrapper keeps the old name for existing docs / muscle memory.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
LOCAL_DB="${LOCAL_DB:-$ROOT/server/prisma/dev.db}"
LOCAL_UPLOADS="${LOCAL_UPLOADS:-$ROOT/server/uploads}"
STAMP="$(date +%Y%m%d-%H%M%S)"
OUT="${1:-$ROOT/backups/casegambling-$STAMP}"
if [[ ! -f "$LOCAL_DB" ]]; then
echo "Error: local database not found: $LOCAL_DB" >&2
echo "Tip: run the app once or set LOCAL_DB=/path/to/dev.db" >&2
exit 1
DIR="$(cd "$(dirname "$0")" && pwd)"
if [[ -f "$DIR/../docker-compose.yml" ]] && command -v docker >/dev/null 2>&1 \
&& docker compose -f "$DIR/../docker-compose.yml" ps -q casegambling >/dev/null 2>&1; then
echo "Note: forwarding to docker-db-export.sh (Docker detected)." >&2
exec "$DIR/docker-db-export.sh" "$@"
fi
mkdir -p "$OUT"
echo "==> Exporting local database → $OUT/prod.db"
echo " source: $LOCAL_DB"
cp -f "$LOCAL_DB" "$OUT/prod.db"
# keep a copy under original name too (import accepts both)
cp -f "$LOCAL_DB" "$OUT/dev.db"
if [[ -f "${LOCAL_DB}-wal" ]]; then
cp -f "${LOCAL_DB}-wal" "$OUT/prod.db-wal"
cp -f "${LOCAL_DB}-wal" "$OUT/dev.db-wal"
fi
if [[ -f "${LOCAL_DB}-shm" ]]; then
cp -f "${LOCAL_DB}-shm" "$OUT/prod.db-shm"
cp -f "${LOCAL_DB}-shm" "$OUT/dev.db-shm"
fi
echo "==> Exporting local uploads → $OUT/uploads"
mkdir -p "$OUT/uploads"
if [[ -d "$LOCAL_UPLOADS" ]]; then
# copy contents; ignore empty/.gitkeep-only quietly
cp -a "$LOCAL_UPLOADS"/. "$OUT/uploads/" 2>/dev/null || true
else
echo " (no uploads dir at $LOCAL_UPLOADS)"
fi
{
echo "exported_at=$STAMP"
echo "source=local"
echo "local_db=$LOCAL_DB"
echo "local_uploads=$LOCAL_UPLOADS"
echo "database=prod.db"
echo "uploads=uploads/"
} > "$OUT/manifest.txt"
TAR="$OUT.tar.gz"
tar -czf "$TAR" -C "$(dirname "$OUT")" "$(basename "$OUT")"
echo "==> Archive: $TAR"
echo "==> Done. Import to Docker: ./scripts/db-import.sh $OUT"
echo "Note: forwarding to local-db-export.sh." >&2
exec "$DIR/local-db-export.sh" "$@"
+8 -112
View File
@@ -1,115 +1,11 @@
#!/usr/bin/env bash
# Import SQLite DB (+ uploads) into the CaseGambling Docker container
# Usage:
# ./scripts/db-import.sh ./backups/casegambling-20260116-203000
# ./db-import.sh ./casegambling-20260116-203000.tar.gz
# Deprecated alias — use docker-db-import.sh (Docker) or local-db-import.sh (laptop).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Project root: folder that contains docker-compose.yml
if [[ -f "$SCRIPT_DIR/docker-compose.yml" ]]; then
ROOT="$SCRIPT_DIR"
elif [[ -f "$SCRIPT_DIR/../docker-compose.yml" ]]; then
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
else
ROOT="$SCRIPT_DIR"
DIR="$(cd "$(dirname "$0")" && pwd)"
if [[ -f "$DIR/../docker-compose.yml" ]] && command -v docker >/dev/null 2>&1 \
&& docker compose -f "$DIR/../docker-compose.yml" ps -aq casegambling >/dev/null 2>&1; then
echo "Note: forwarding to docker-db-import.sh (Docker detected)." >&2
exec "$DIR/docker-db-import.sh" "$@"
fi
# Resolve backup path from the caller's working directory (before any cd)
if [[ -z "${1:-}" ]]; then
echo "Usage: $0 <backup-dir-or-tar.gz>" >&2
echo "Example: $0 ./backups/casegambling-20260116-203000.tar.gz" >&2
exit 1
fi
if [[ "$1" = /* ]]; then
SRC="$1"
else
SRC="$(pwd)/$1"
fi
cd "$ROOT"
ENV_FILE="${ENV_FILE:-.env.prod}"
COMPOSE=(docker compose)
if [[ -f "$ENV_FILE" ]]; then
COMPOSE=(docker compose --env-file "$ENV_FILE")
fi
SERVICE="${SERVICE:-casegambling}"
TMP=""
cleanup() {
if [[ -n "$TMP" && -d "$TMP" ]]; then
rm -rf "$TMP"
fi
}
trap cleanup EXIT
is_archive() {
case "$1" in
*.tar.gz|*.tgz) return 0 ;;
*) return 1 ;;
esac
}
if [[ -f "$SRC" ]] && is_archive "$SRC"; then
echo "==> Extracting $(basename "$SRC")"
TMP="$(mktemp -d)"
tar -xzf "$SRC" -C "$TMP"
# Prefer a single top-level directory from the archive
mapfile -t DIRS < <(find "$TMP" -mindepth 1 -maxdepth 1 -type d | sort)
if [[ ${#DIRS[@]} -ge 1 ]]; then
SRC="${DIRS[0]}"
else
SRC="$TMP"
fi
elif [[ -f "$SRC" ]]; then
echo "Error: not a directory or .tar.gz archive: $SRC" >&2
exit 1
fi
if [[ ! -d "$SRC" ]]; then
echo "Error: backup not found: $SRC" >&2
echo "Pass a folder or a .tar.gz produced by ./scripts/db-export.sh" >&2
exit 1
fi
DB="$SRC/prod.db"
if [[ ! -f "$DB" ]]; then
if [[ -f "$SRC/dev.db" ]]; then
DB="$SRC/dev.db"
else
echo "Error: no prod.db (or dev.db) in $SRC" >&2
ls -la "$SRC" >&2 || true
exit 1
fi
fi
echo "==> Stopping $SERVICE (safe SQLite replace)"
"${COMPOSE[@]}" stop "$SERVICE" 2>/dev/null || docker stop "$SERVICE" 2>/dev/null || true
echo "==> Clearing old SQLite sidecars on volume"
"${COMPOSE[@]}" run --rm --no-deps --entrypoint sh "$SERVICE" -c \
'rm -f /data/prod.db-wal /data/prod.db-shm' >/dev/null
echo "==> Importing database ← $DB"
"${COMPOSE[@]}" cp "$DB" "$SERVICE:/data/prod.db"
if [[ -f "$SRC/prod.db-wal" ]]; then
"${COMPOSE[@]}" cp "$SRC/prod.db-wal" "$SERVICE:/data/prod.db-wal"
fi
if [[ -f "$SRC/prod.db-shm" ]]; then
"${COMPOSE[@]}" cp "$SRC/prod.db-shm" "$SERVICE:/data/prod.db-shm"
fi
if [[ -d "$SRC/uploads" ]]; then
echo "==> Importing uploads"
"${COMPOSE[@]}" run --rm --no-deps --entrypoint sh "$SERVICE" -c \
'rm -rf /app/uploads/* /app/uploads/.[!.]* 2>/dev/null; mkdir -p /app/uploads' >/dev/null
"${COMPOSE[@]}" cp "$SRC/uploads/." "$SERVICE:/app/uploads/"
fi
echo "==> Starting $SERVICE"
"${COMPOSE[@]}" start "$SERVICE" 2>/dev/null || "${COMPOSE[@]}" up -d "$SERVICE"
echo "==> Done. Check: ${COMPOSE[*]} logs -f --tail=50"
echo "Note: forwarding to local-db-import.sh." >&2
exec "$DIR/local-db-import.sh" "$@"
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Export local project SQLite DB (+ uploads) for backup / Docker import.
#
# Usage:
# ./scripts/local-db-export.sh
# ./scripts/local-db-export.sh ./backups/my-backup
#
# Sources (local):
# server/prisma/dev.db (+ -wal / -shm if present)
# server/uploads/
#
# Restore locally:
# ./scripts/local-db-import.sh ./backups/casegambling-….tar.gz
# Restore into Docker (prod):
# ./scripts/docker-db-import.sh ./backups/casegambling-….tar.gz
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
LOCAL_DB="${LOCAL_DB:-$ROOT/server/prisma/dev.db}"
LOCAL_UPLOADS="${LOCAL_UPLOADS:-$ROOT/server/uploads}"
STAMP="$(date +%Y%m%d-%H%M%S)"
OUT="${1:-$ROOT/backups/casegambling-$STAMP}"
if [[ ! -f "$LOCAL_DB" ]]; then
echo "Error: local database not found: $LOCAL_DB" >&2
echo "Tip: run the app once, or set LOCAL_DB=/path/to/dev.db" >&2
exit 1
fi
mkdir -p "$OUT"
echo "==> Exporting local database → $OUT/dev.db"
echo " source: $LOCAL_DB"
# Prefer a consistent SQLite snapshot when sqlite3 is available
if command -v sqlite3 >/dev/null 2>&1; then
sqlite3 "$LOCAL_DB" ".backup '$OUT/dev.db'"
cp -f "$OUT/dev.db" "$OUT/prod.db"
else
cp -f "$LOCAL_DB" "$OUT/dev.db"
cp -f "$LOCAL_DB" "$OUT/prod.db"
if [[ -f "${LOCAL_DB}-wal" ]]; then
cp -f "${LOCAL_DB}-wal" "$OUT/dev.db-wal"
cp -f "${LOCAL_DB}-wal" "$OUT/prod.db-wal"
fi
if [[ -f "${LOCAL_DB}-shm" ]]; then
cp -f "${LOCAL_DB}-shm" "$OUT/dev.db-shm"
cp -f "${LOCAL_DB}-shm" "$OUT/prod.db-shm"
fi
fi
echo "==> Exporting local uploads → $OUT/uploads"
mkdir -p "$OUT/uploads"
if [[ -d "$LOCAL_UPLOADS" ]]; then
cp -a "$LOCAL_UPLOADS"/. "$OUT/uploads/" 2>/dev/null || true
else
echo " (no uploads dir at $LOCAL_UPLOADS)"
fi
{
echo "exported_at=$STAMP"
echo "source=local"
echo "local_db=$LOCAL_DB"
echo "local_uploads=$LOCAL_UPLOADS"
echo "database=dev.db"
echo "uploads=uploads/"
} > "$OUT/manifest.txt"
TAR="$OUT.tar.gz"
tar -czf "$TAR" -C "$(dirname "$OUT")" "$(basename "$OUT")"
DB_SIZE="$(du -h "$OUT/dev.db" | awk '{print $1}')"
UPLOAD_COUNT="$(find "$OUT/uploads" -type f 2>/dev/null | wc -l | tr -d ' ')"
echo "==> Archive: $TAR"
echo " db size: $DB_SIZE · uploads files: $UPLOAD_COUNT"
echo "==> Done."
echo " Local restore: ./scripts/local-db-import.sh $TAR"
echo " Docker restore: ./scripts/docker-db-import.sh $TAR"
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env bash
# Import SQLite DB (+ uploads) into the LOCAL project
# (server/prisma/dev.db + server/uploads/).
#
# Usage:
# ./scripts/local-db-import.sh ./backups/casegambling-YYYYMMDD-HHMMSS
# ./scripts/local-db-import.sh ./backups/casegambling-YYYYMMDD-HHMMSS.tar.gz
#
# Typical flow (prod → laptop):
# # on prod:
# ./scripts/docker-db-export.sh
# # copy the .tar.gz here, then:
# ./scripts/local-db-import.sh ./backups/casegambling-….tar.gz
#
# Stop `npm run dev` before importing so SQLite is not locked.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
if [[ -z "${1:-}" ]]; then
echo "Usage: $0 <backup-dir-or-tar.gz>" >&2
echo "Example: $0 ./backups/casegambling-20260116-203000.tar.gz" >&2
echo "Create a backup from Docker: ./scripts/docker-db-export.sh" >&2
exit 1
fi
# Resolve backup path from the caller's cwd
if [[ "$1" = /* ]]; then
SRC="$1"
else
SRC="$(pwd)/$1"
fi
LOCAL_DB="${LOCAL_DB:-$ROOT/server/prisma/dev.db}"
LOCAL_UPLOADS="${LOCAL_UPLOADS:-$ROOT/server/uploads}"
TMP=""
cleanup() {
if [[ -n "$TMP" && -d "$TMP" ]]; then
rm -rf "$TMP"
fi
}
trap cleanup EXIT
is_archive() {
case "$1" in
*.tar.gz|*.tgz) return 0 ;;
*) return 1 ;;
esac
}
if [[ -f "$SRC" ]] && is_archive "$SRC"; then
echo "==> Extracting $(basename "$SRC")"
TMP="$(mktemp -d)"
tar -xzf "$SRC" -C "$TMP"
mapfile -t DIRS < <(find "$TMP" -mindepth 1 -maxdepth 1 -type d | sort)
if [[ ${#DIRS[@]} -ge 1 ]]; then
SRC="${DIRS[0]}"
else
SRC="$TMP"
fi
elif [[ -f "$SRC" ]]; then
echo "Error: not a directory or .tar.gz archive: $SRC" >&2
exit 1
fi
if [[ ! -d "$SRC" ]]; then
echo "Error: backup not found: $SRC" >&2
echo "Pass a folder or a .tar.gz from docker-db-export.sh / local-db-export.sh" >&2
exit 1
fi
DB=""
for candidate in "$SRC/dev.db" "$SRC/prod.db"; do
if [[ -f "$candidate" ]]; then
DB="$candidate"
break
fi
done
if [[ -z "$DB" ]]; then
echo "Error: no dev.db or prod.db in $SRC" >&2
ls -la "$SRC" >&2 || true
exit 1
fi
mkdir -p "$(dirname "$LOCAL_DB")"
echo "==> Importing database → $LOCAL_DB"
echo " source: $DB"
# Drop local WAL/SHM so the new file is the sole source of truth
rm -f "${LOCAL_DB}-wal" "${LOCAL_DB}-shm"
if command -v sqlite3 >/dev/null 2>&1; then
# Normalize into a clean single-file DB
sqlite3 "$DB" ".backup '$LOCAL_DB'"
else
cp -f "$DB" "$LOCAL_DB"
WAL=""
SHM=""
[[ -f "$SRC/dev.db-wal" ]] && WAL="$SRC/dev.db-wal"
[[ -z "$WAL" && -f "$SRC/prod.db-wal" ]] && WAL="$SRC/prod.db-wal"
[[ -f "$SRC/dev.db-shm" ]] && SHM="$SRC/dev.db-shm"
[[ -z "$SHM" && -f "$SRC/prod.db-shm" ]] && SHM="$SRC/prod.db-shm"
[[ -n "$WAL" ]] && cp -f "$WAL" "${LOCAL_DB}-wal"
[[ -n "$SHM" ]] && cp -f "$SHM" "${LOCAL_DB}-shm"
fi
if [[ -d "$SRC/uploads" ]]; then
echo "==> Importing uploads → $LOCAL_UPLOADS"
mkdir -p "$LOCAL_UPLOADS"
# Keep .gitkeep if present; replace other files
find "$LOCAL_UPLOADS" -mindepth 1 ! -name '.gitkeep' -exec rm -rf {} + 2>/dev/null || true
cp -a "$SRC/uploads"/. "$LOCAL_UPLOADS/" 2>/dev/null || true
fi
DB_SIZE="$(du -h "$LOCAL_DB" | awk '{print $1}')"
UPLOAD_COUNT="$(find "$LOCAL_UPLOADS" -type f ! -name '.gitkeep' 2>/dev/null | wc -l | tr -d ' ')"
echo "==> Done."
echo " db: $LOCAL_DB ($DB_SIZE)"
echo " uploads files: $UPLOAD_COUNT"
echo " Tip: stop npm run dev before import if the DB was locked; then restart:"
echo " npm run db:push && npm run dev"
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
# Export CaseGambling database (+ uploads) FROM Docker (production).
# Run this on the prod server next to docker-compose.yml.
#
# Usage:
# ./scripts/docker-db-export.sh
# ./scripts/docker-db-export.sh ./backups/my-backup
#
# Output:
# backups/casegambling-YYYYMMDD-HHMMSS/
# prod.db (+ optional -wal/-shm)
# uploads/
# manifest.txt
# backups/casegambling-YYYYMMDD-HHMMSS.tar.gz
#
# Then copy the .tar.gz to your machine and import locally:
# ./scripts/local-db-import.sh ./backups/casegambling-….tar.gz
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [[ -f "$SCRIPT_DIR/../docker-compose.yml" ]]; then
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
elif [[ -f "$SCRIPT_DIR/docker-compose.yml" ]]; then
ROOT="$SCRIPT_DIR"
else
echo "Error: docker-compose.yml not found near $SCRIPT_DIR" >&2
exit 1
fi
cd "$ROOT"
ENV_FILE="${ENV_FILE:-.env.prod}"
COMPOSE=(docker compose)
if [[ -f "$ENV_FILE" ]]; then
COMPOSE=(docker compose --env-file "$ENV_FILE")
fi
SERVICE="${SERVICE:-casegambling}"
STAMP="$(date +%Y%m%d-%H%M%S)"
OUT="${1:-$ROOT/backups/casegambling-$STAMP}"
if ! command -v docker >/dev/null 2>&1; then
echo "Error: docker not found" >&2
exit 1
fi
if ! "${COMPOSE[@]}" ps -q "$SERVICE" >/dev/null 2>&1; then
echo "Error: compose service '$SERVICE' not found" >&2
echo "Tip: run from the folder that has docker-compose.yml" >&2
exit 1
fi
mkdir -p "$OUT"
echo "==> Exporting Docker database → $OUT/prod.db"
echo " service: $SERVICE · volume path: /data/prod.db"
TMP_UPLOADS="$(mktemp -d)"
cleanup() {
rm -rf "$TMP_UPLOADS"
}
trap cleanup EXIT
WAS_RUNNING=0
if [[ -n "$("${COMPOSE[@]}" ps -q --status running "$SERVICE" 2>/dev/null || true)" ]]; then
WAS_RUNNING=1
fi
# Prefer a consistent SQLite snapshot inside a running container
if [[ "$WAS_RUNNING" -eq 1 ]] \
&& "${COMPOSE[@]}" exec -T "$SERVICE" sh -c 'command -v sqlite3 >/dev/null && test -f /data/prod.db'; then
"${COMPOSE[@]}" exec -T "$SERVICE" sh -c "sqlite3 /data/prod.db \".backup '/tmp/prod-export.db'\""
"${COMPOSE[@]}" cp "$SERVICE:/tmp/prod-export.db" "$OUT/prod.db"
"${COMPOSE[@]}" exec -T "$SERVICE" rm -f /tmp/prod-export.db >/dev/null 2>&1 || true
else
# Safe file copy: stop if running so WAL is flushed
if [[ "$WAS_RUNNING" -eq 1 ]]; then
echo "==> Stopping $SERVICE for a consistent copy"
"${COMPOSE[@]}" stop "$SERVICE" >/dev/null
fi
"${COMPOSE[@]}" cp "$SERVICE:/data/prod.db" "$OUT/prod.db"
"${COMPOSE[@]}" cp "$SERVICE:/data/prod.db-wal" "$OUT/prod.db-wal" 2>/dev/null || true
"${COMPOSE[@]}" cp "$SERVICE:/data/prod.db-shm" "$OUT/prod.db-shm" 2>/dev/null || true
if [[ "$WAS_RUNNING" -eq 1 ]]; then
echo "==> Starting $SERVICE"
"${COMPOSE[@]}" start "$SERVICE" >/dev/null 2>&1 || "${COMPOSE[@]}" up -d "$SERVICE" >/dev/null
fi
fi
# Also write as dev.db so local import can use either name
cp -f "$OUT/prod.db" "$OUT/dev.db"
[[ -f "$OUT/prod.db-wal" ]] && cp -f "$OUT/prod.db-wal" "$OUT/dev.db-wal"
[[ -f "$OUT/prod.db-shm" ]] && cp -f "$OUT/prod.db-shm" "$OUT/dev.db-shm"
echo "==> Exporting Docker uploads → $OUT/uploads"
mkdir -p "$OUT/uploads"
if "${COMPOSE[@]}" cp "$SERVICE:/app/uploads/." "$TMP_UPLOADS/" 2>/dev/null; then
cp -a "$TMP_UPLOADS"/. "$OUT/uploads/" 2>/dev/null || true
else
echo " (no uploads or empty volume)"
fi
{
echo "exported_at=$STAMP"
echo "source=docker"
echo "service=$SERVICE"
echo "database=prod.db"
echo "uploads=uploads/"
} > "$OUT/manifest.txt"
TAR="$OUT.tar.gz"
tar -czf "$TAR" -C "$(dirname "$OUT")" "$(basename "$OUT")"
DB_SIZE="$(du -h "$OUT/prod.db" | awk '{print $1}')"
UPLOAD_COUNT="$(find "$OUT/uploads" -type f 2>/dev/null | wc -l | tr -d ' ')"
echo "==> Archive: $TAR"
echo " db size: $DB_SIZE · uploads files: $UPLOAD_COUNT"
echo "==> Done. On your laptop:"
echo " scp user@prod:$TAR ./backups/"
echo " ./scripts/local-db-import.sh ./backups/$(basename "$TAR")"
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env bash
# Import SQLite DB (+ uploads) INTO Docker (production / compose).
# Run this on the machine that runs docker compose.
#
# Usage:
# ./scripts/docker-db-import.sh ./backups/casegambling-YYYYMMDD-HHMMSS
# ./scripts/docker-db-import.sh ./backups/casegambling-YYYYMMDD-HHMMSS.tar.gz
#
# Expects a backup from docker-db-export.sh or local-db-export.sh
# (prod.db or dev.db + uploads/).
#
# Docker paths (see docker-compose.yml):
# DATABASE_URL=file:/data/prod.db
# uploads → /app/uploads
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [[ -f "$SCRIPT_DIR/../docker-compose.yml" ]]; then
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
elif [[ -f "$SCRIPT_DIR/docker-compose.yml" ]]; then
ROOT="$SCRIPT_DIR"
else
echo "Error: docker-compose.yml not found near $SCRIPT_DIR" >&2
exit 1
fi
if [[ -z "${1:-}" ]]; then
echo "Usage: $0 <backup-dir-or-tar.gz>" >&2
echo "Example: $0 ./backups/casegambling-20260116-203000.tar.gz" >&2
echo "Create a backup first: ./scripts/docker-db-export.sh (or local-db-export.sh)" >&2
exit 1
fi
# Resolve backup path from the caller's cwd (before any cd)
if [[ "$1" = /* ]]; then
SRC="$1"
else
SRC="$(pwd)/$1"
fi
cd "$ROOT"
ENV_FILE="${ENV_FILE:-.env.prod}"
COMPOSE=(docker compose)
if [[ -f "$ENV_FILE" ]]; then
COMPOSE=(docker compose --env-file "$ENV_FILE")
elif [[ -f ".env.prod.example" ]]; then
echo "Warning: $ENV_FILE missing — using compose defaults." >&2
echo " Copy .env.prod.example → .env.prod for production settings." >&2
fi
SERVICE="${SERVICE:-casegambling}"
TMP=""
cleanup() {
if [[ -n "$TMP" && -d "$TMP" ]]; then
rm -rf "$TMP"
fi
}
trap cleanup EXIT
is_archive() {
case "$1" in
*.tar.gz|*.tgz) return 0 ;;
*) return 1 ;;
esac
}
if ! command -v docker >/dev/null 2>&1; then
echo "Error: docker not found" >&2
exit 1
fi
if [[ -f "$SRC" ]] && is_archive "$SRC"; then
echo "==> Extracting $(basename "$SRC")"
TMP="$(mktemp -d)"
tar -xzf "$SRC" -C "$TMP"
mapfile -t DIRS < <(find "$TMP" -mindepth 1 -maxdepth 1 -type d | sort)
if [[ ${#DIRS[@]} -ge 1 ]]; then
SRC="${DIRS[0]}"
else
SRC="$TMP"
fi
elif [[ -f "$SRC" ]]; then
echo "Error: not a directory or .tar.gz archive: $SRC" >&2
exit 1
fi
if [[ ! -d "$SRC" ]]; then
echo "Error: backup not found: $SRC" >&2
echo "Pass a folder or a .tar.gz produced by docker-db-export.sh / local-db-export.sh" >&2
exit 1
fi
DB=""
for candidate in "$SRC/prod.db" "$SRC/dev.db"; do
if [[ -f "$candidate" ]]; then
DB="$candidate"
break
fi
done
if [[ -z "$DB" ]]; then
echo "Error: no prod.db or dev.db in $SRC" >&2
ls -la "$SRC" >&2 || true
exit 1
fi
echo "==> Ensuring compose service exists"
"${COMPOSE[@]}" up -d --no-start "$SERVICE" >/dev/null
echo "==> Stopping $SERVICE (safe SQLite replace)"
"${COMPOSE[@]}" stop "$SERVICE" >/dev/null 2>&1 || true
echo "==> Clearing old SQLite sidecars on volume"
"${COMPOSE[@]}" run --rm --no-deps --entrypoint sh "$SERVICE" -c \
'rm -f /data/prod.db-wal /data/prod.db-shm' >/dev/null
echo "==> Importing database ← $DB"
"${COMPOSE[@]}" cp "$DB" "$SERVICE:/data/prod.db"
WAL=""
SHM=""
[[ -f "$SRC/prod.db-wal" ]] && WAL="$SRC/prod.db-wal"
[[ -z "$WAL" && -f "$SRC/dev.db-wal" ]] && WAL="$SRC/dev.db-wal"
[[ -f "$SRC/prod.db-shm" ]] && SHM="$SRC/prod.db-shm"
[[ -z "$SHM" && -f "$SRC/dev.db-shm" ]] && SHM="$SRC/dev.db-shm"
if [[ -n "$WAL" ]]; then
"${COMPOSE[@]}" cp "$WAL" "$SERVICE:/data/prod.db-wal"
fi
if [[ -n "$SHM" ]]; then
"${COMPOSE[@]}" cp "$SHM" "$SERVICE:/data/prod.db-shm"
fi
if [[ -d "$SRC/uploads" ]]; then
echo "==> Importing uploads"
"${COMPOSE[@]}" run --rm --no-deps --entrypoint sh "$SERVICE" -c \
'rm -rf /app/uploads/* /app/uploads/.[!.]* 2>/dev/null; mkdir -p /app/uploads' >/dev/null
"${COMPOSE[@]}" cp "$SRC/uploads/." "$SERVICE:/app/uploads/"
fi
echo "==> Starting $SERVICE"
"${COMPOSE[@]}" start "$SERVICE" 2>/dev/null || "${COMPOSE[@]}" up -d "$SERVICE"
HOST_PORT_VAL="${HOST_PORT:-}"
if [[ -z "$HOST_PORT_VAL" && -f "$ENV_FILE" ]]; then
HOST_PORT_VAL="$(grep -E '^HOST_PORT=' "$ENV_FILE" 2>/dev/null | cut -d= -f2- | tr -d '"' || true)"
fi
HOST_PORT_VAL="${HOST_PORT_VAL:-3009}"
echo "==> Done."
echo " App: http://localhost:${HOST_PORT_VAL}"
echo " Logs: ${COMPOSE[*]} logs -f --tail=50"
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env bash
# Delete a user by username from the Docker (production) SQLite DB.
# Run this on the prod server next to docker-compose.yml.
#
# Usage:
# ./scripts/xdocker-delete-user.sh <username>
# ./scripts/xdocker-delete-user.sh <username> --yes # skip confirmation
#
# Removes the User and related data (inventory, transactions, deposits,
# case-key progress, duel rooms they created). Nulls jackpot/duel winner
# pointers that referenced them.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [[ -f "$SCRIPT_DIR/../docker-compose.yml" ]]; then
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
elif [[ -f "$SCRIPT_DIR/docker-compose.yml" ]]; then
ROOT="$SCRIPT_DIR"
else
echo "Error: docker-compose.yml not found near $SCRIPT_DIR" >&2
exit 1
fi
cd "$ROOT"
ENV_FILE="${ENV_FILE:-.env.prod}"
COMPOSE=(docker compose)
if [[ -f "$ENV_FILE" ]]; then
COMPOSE=(docker compose --env-file "$ENV_FILE")
fi
SERVICE="${SERVICE:-casegambling}"
USERNAME="${1:-}"
ASSUME_YES=0
if [[ -z "$USERNAME" || "$USERNAME" == "-h" || "$USERNAME" == "--help" ]]; then
echo "Usage: $0 <username> [--yes]" >&2
echo "Example: $0 badplayer" >&2
echo " $0 badplayer --yes" >&2
exit 1
fi
shift || true
for arg in "$@"; do
case "$arg" in
--yes|-y) ASSUME_YES=1 ;;
*)
echo "Error: unknown argument: $arg" >&2
exit 1
;;
esac
done
if ! command -v docker >/dev/null 2>&1; then
echo "Error: docker not found" >&2
exit 1
fi
if [[ -z "$("${COMPOSE[@]}" ps -q --status running "$SERVICE" 2>/dev/null || true)" ]]; then
echo "Error: compose service '$SERVICE' is not running" >&2
echo "Tip: docker compose --env-file .env.prod up -d" >&2
exit 1
fi
run_node() {
local mode="$1"
"${COMPOSE[@]}" exec -T \
-e DEL_USERNAME="$USERNAME" \
-e DEL_MODE="$mode" \
"$SERVICE" node <<'NODE'
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
(async () => {
const username = process.env.DEL_USERNAME;
const mode = process.env.DEL_MODE;
const user = await prisma.user.findUnique({
where: { username },
select: {
id: true,
username: true,
role: true,
balance: true,
osuBalance: true,
createdAt: true,
_count: {
select: {
inventory: true,
transactions: true,
jackpotDeposits: true,
duelDeposits: true,
duelRoomsCreated: true,
caseKeyProgress: true,
},
},
},
});
if (!user) {
console.error(`Error: no user with username '${username}'`);
process.exit(2);
}
if (mode === 'info') {
console.log(`id: ${user.id}`);
console.log(`username: ${user.username}`);
console.log(`role: ${user.role}`);
console.log(`balance: ${user.balance.toString()}`);
console.log(`osuBalance: ${user.osuBalance}`);
console.log(`createdAt: ${user.createdAt.toISOString()}`);
console.log(`inventory: ${user._count.inventory}`);
console.log(`transactions: ${user._count.transactions}`);
console.log(`jackpot deps: ${user._count.jackpotDeposits}`);
console.log(`duel deps: ${user._count.duelDeposits}`);
console.log(`duel rooms: ${user._count.duelRoomsCreated}`);
console.log(`case keys: ${user._count.caseKeyProgress}`);
if (user.role === 'admin') {
console.log('WARNING: role=admin');
}
process.exit(0);
}
if (mode !== 'delete') {
console.error(`Unknown mode: ${mode}`);
process.exit(1);
}
await prisma.$transaction(async (tx) => {
// Optional FKs without onDelete Cascade
await tx.jackpotRound.updateMany({
where: { winnerUserId: user.id },
data: { winnerUserId: null },
});
await tx.duelRoom.updateMany({
where: { winnerUserId: user.id },
data: { winnerUserId: null },
});
// Deposits reference InventoryItem — remove before user/inventory cascade
await tx.jackpotDeposit.deleteMany({ where: { userId: user.id } });
await tx.duelDeposit.deleteMany({ where: { userId: user.id } });
// Rooms created by this user (other deposits in those rooms cascade with room)
await tx.duelRoom.deleteMany({ where: { creatorId: user.id } });
await tx.user.delete({ where: { id: user.id } });
});
console.log(`Deleted user id=${user.id} username=${user.username}`);
})().catch((e) => {
console.error(e);
process.exit(1);
}).finally(() => prisma.$disconnect());
NODE
}
echo "==> Looking up user '$USERNAME' …"
set +e
INFO_OUT="$(run_node info 2>&1)"
INFO_RC=$?
set -e
echo "$INFO_OUT"
if [[ "$INFO_RC" -ne 0 ]]; then
exit "$INFO_RC"
fi
if [[ "$ASSUME_YES" -ne 1 ]]; then
echo ""
printf "Delete user '%s' permanently? [y/N] " "$USERNAME"
read -r REPLY
case "$REPLY" in
y|Y|yes|YES) ;;
*)
echo "Aborted."
exit 0
;;
esac
fi
echo "==> Deleting …"
run_node delete
echo "==> Done."
+215
View File
@@ -0,0 +1,215 @@
#!/usr/bin/env bash
# Update a user's username and/or password in the Docker (production) SQLite DB.
# Run this on the prod server next to docker-compose.yml.
#
# Usage:
# ./scripts/xdocker-update-user.sh <current-username> --username <new>
# ./scripts/xdocker-update-user.sh <current-username> --password <new>
# ./scripts/xdocker-update-user.sh <current-username> --username <new> --password <new>
# ./scripts/xdocker-update-user.sh <current-username> --username <new> --password <new> --yes
#
# Password is hashed with bcrypt (cost 10), same as the app.
# Username rules match registration: 332 chars, [a-zA-Z0-9_], unique.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [[ -f "$SCRIPT_DIR/../docker-compose.yml" ]]; then
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
elif [[ -f "$SCRIPT_DIR/docker-compose.yml" ]]; then
ROOT="$SCRIPT_DIR"
else
echo "Error: docker-compose.yml not found near $SCRIPT_DIR" >&2
exit 1
fi
cd "$ROOT"
ENV_FILE="${ENV_FILE:-.env.prod}"
COMPOSE=(docker compose)
if [[ -f "$ENV_FILE" ]]; then
COMPOSE=(docker compose --env-file "$ENV_FILE")
fi
SERVICE="${SERVICE:-casegambling}"
CURRENT_USERNAME="${1:-}"
NEW_USERNAME=""
NEW_PASSWORD=""
ASSUME_YES=0
usage() {
echo "Usage: $0 <current-username> [--username <new>] [--password <new>] [--yes]" >&2
echo "Example: $0 oldname --username newname --password 's3cret'" >&2
echo " $0 oldname --password 's3cret' --yes" >&2
exit 1
}
if [[ -z "$CURRENT_USERNAME" || "$CURRENT_USERNAME" == "-h" || "$CURRENT_USERNAME" == "--help" ]]; then
usage
fi
shift || true
while [[ $# -gt 0 ]]; do
case "$1" in
--username|-u)
[[ $# -ge 2 ]] || { echo "Error: $1 needs a value" >&2; exit 1; }
NEW_USERNAME="$2"
shift 2
;;
--password|-p)
[[ $# -ge 2 ]] || { echo "Error: $1 needs a value" >&2; exit 1; }
NEW_PASSWORD="$2"
shift 2
;;
--yes|-y)
ASSUME_YES=1
shift
;;
*)
echo "Error: unknown argument: $1" >&2
usage
;;
esac
done
if [[ -z "$NEW_USERNAME" && -z "$NEW_PASSWORD" ]]; then
echo "Error: provide at least --username and/or --password" >&2
usage
fi
if ! command -v docker >/dev/null 2>&1; then
echo "Error: docker not found" >&2
exit 1
fi
if [[ -z "$("${COMPOSE[@]}" ps -q --status running "$SERVICE" 2>/dev/null || true)" ]]; then
echo "Error: compose service '$SERVICE' is not running" >&2
echo "Tip: docker compose --env-file .env.prod up -d" >&2
exit 1
fi
run_node() {
local mode="$1"
"${COMPOSE[@]}" exec -T \
-e UPD_CURRENT="$CURRENT_USERNAME" \
-e UPD_NEW_USERNAME="$NEW_USERNAME" \
-e UPD_NEW_PASSWORD="$NEW_PASSWORD" \
-e UPD_MODE="$mode" \
"$SERVICE" node <<'NODE'
const { PrismaClient } = require('@prisma/client');
const bcrypt = require('bcrypt');
const prisma = new PrismaClient();
(async () => {
const currentUsername = process.env.UPD_CURRENT;
const newUsername = (process.env.UPD_NEW_USERNAME || '').trim();
const newPassword = process.env.UPD_NEW_PASSWORD || '';
const mode = process.env.UPD_MODE;
const user = await prisma.user.findUnique({
where: { username: currentUsername },
select: {
id: true,
username: true,
role: true,
balance: true,
createdAt: true,
},
});
if (!user) {
console.error(`Error: no user with username '${currentUsername}'`);
process.exit(2);
}
if (mode === 'info') {
console.log(`id: ${user.id}`);
console.log(`username: ${user.username}`);
console.log(`role: ${user.role}`);
console.log(`balance: ${user.balance.toString()}`);
console.log(`createdAt: ${user.createdAt.toISOString()}`);
if (newUsername) console.log(`new user: ${newUsername}`);
if (newPassword) console.log(`password: (will be changed)`);
process.exit(0);
}
if (mode !== 'update') {
console.error(`Unknown mode: ${mode}`);
process.exit(1);
}
const data = {};
if (newUsername) {
if (newUsername.length < 3 || newUsername.length > 32) {
console.error('Error: username must be 332 characters');
process.exit(1);
}
if (!/^[a-zA-Z0-9_]+$/.test(newUsername)) {
console.error('Error: username may only contain letters, numbers, and underscores');
process.exit(1);
}
if (newUsername !== user.username) {
const taken = await prisma.user.findUnique({ where: { username: newUsername } });
if (taken) {
console.error(`Error: username '${newUsername}' already taken`);
process.exit(1);
}
data.username = newUsername;
}
}
if (newPassword) {
if (newPassword.length < 6) {
console.error('Error: password must be at least 6 characters');
process.exit(1);
}
data.passwordHash = await bcrypt.hash(newPassword, 10);
}
if (Object.keys(data).length === 0) {
console.log('Nothing to change (same username, no password).');
process.exit(0);
}
const updated = await prisma.user.update({
where: { id: user.id },
data,
select: { id: true, username: true },
});
const parts = [];
if (data.username) parts.push(`username → ${updated.username}`);
if (data.passwordHash) parts.push('password updated');
console.log(`Updated user id=${updated.id}: ${parts.join(', ')}`);
})().catch((e) => {
console.error(e);
process.exit(1);
}).finally(() => prisma.$disconnect());
NODE
}
echo "==> Looking up user '$CURRENT_USERNAME' …"
set +e
INFO_OUT="$(run_node info 2>&1)"
INFO_RC=$?
set -e
echo "$INFO_OUT"
if [[ "$INFO_RC" -ne 0 ]]; then
exit "$INFO_RC"
fi
if [[ "$ASSUME_YES" -ne 1 ]]; then
echo ""
printf "Apply changes to '%s'? [y/N] " "$CURRENT_USERNAME"
read -r REPLY
case "$REPLY" in
y|Y|yes|YES) ;;
*)
echo "Aborted."
exit 0
;;
esac
fi
echo "==> Updating …"
run_node update
echo "==> Done."