#!/usr/bin/env bash # Import SQLite DB (+ uploads) INTO Docker (production / compose). # Run this on the machine that runs docker compose. # # Usage: # ./scripts/xdocker-db-import.sh ./backups/casegambling-YYYYMMDD-HHMMSS # ./scripts/xdocker-db-import.sh ./backups/casegambling-YYYYMMDD-HHMMSS.tar.gz # ./scripts/xdocker-db-import.sh ./backups/….tar.gz # player-only merge # # 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 MERGE_PLAYER_SCRIPT="$ROOT/scripts/merge-player-db.sh" if [[ -z "${1:-}" || "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then echo "Usage: $0 [username]" >&2 echo "Example: $0 ./backups/casegambling-20260116-203000.tar.gz" >&2 echo " $0 ./backups/casegambling-20260116-203000.tar.gz SomePlayer" >&2 echo "Create a backup first: ./scripts/docker-db-export.sh (or local-db-export.sh)" >&2 exit 1 fi BACKUP_ARG="$1" USERNAME="${2:-}" # Resolve backup path from the caller's cwd (before any cd) if [[ "$BACKUP_ARG" = /* ]]; then SRC="$BACKUP_ARG" else SRC="$(pwd)/$BACKUP_ARG" 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="" WORK="" cleanup() { if [[ -n "$TMP" && -d "$TMP" ]]; then rm -rf "$TMP" fi if [[ -n "$WORK" && -d "$WORK" ]]; then rm -rf "$WORK" fi } trap cleanup EXIT is_archive() { case "$1" in *.tar.gz|*.tgz) return 0 ;; *) return 1 ;; esac } copy_player_avatar_docker() { local avatar_url="$1" local uploads_src="$2" [[ -z "$avatar_url" || ! -d "$uploads_src" ]] && return 0 local rel="${avatar_url#/}" rel="${rel#uploads/}" rel="${rel#./}" case "$rel" in ''|http:*|https:*|*..*) return 0 ;; esac local src_file="$uploads_src/$rel" if [[ -f "$src_file" ]]; then "${COMPOSE[@]}" run --rm --no-deps --entrypoint sh "$SERVICE" -c \ "mkdir -p /app/uploads/$(dirname "$rel")" >/dev/null "${COMPOSE[@]}" cp "$src_file" "$SERVICE:/app/uploads/$rel" echo "==> Copied avatar upload: $rel" fi } 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 # ── Player-only merge into existing Docker DB ──────────────────────────────── if [[ -n "$USERNAME" ]]; then if [[ ! -x "$MERGE_PLAYER_SCRIPT" ]]; then echo "Error: merge helper missing or not executable: $MERGE_PLAYER_SCRIPT" >&2 exit 1 fi if ! command -v sqlite3 >/dev/null 2>&1; then echo "Error: sqlite3 is required on the host for player-only import" >&2 exit 1 fi echo "==> Stopping $SERVICE (safe SQLite merge)" "${COMPOSE[@]}" stop "$SERVICE" >/dev/null 2>&1 || true WORK="$(mktemp -d)" TARGET_DB="$WORK/prod.db" echo "==> Fetching current /data/prod.db" if ! "${COMPOSE[@]}" cp "$SERVICE:/data/prod.db" "$TARGET_DB" 2>/dev/null; then echo "Error: could not copy /data/prod.db from $SERVICE" >&2 echo "Tip: run a full import once first so the volume has a database." >&2 exit 1 fi "${COMPOSE[@]}" run --rm --no-deps --entrypoint sh "$SERVICE" -c \ 'rm -f /data/prod.db-wal /data/prod.db-shm' >/dev/null "$MERGE_PLAYER_SCRIPT" "$DB" "$TARGET_DB" "$USERNAME" echo "==> Writing merged database → /data/prod.db" "${COMPOSE[@]}" cp "$TARGET_DB" "$SERVICE:/data/prod.db" AVATAR_URL="$(sqlite3 "$DB" "SELECT avatarUrl FROM User WHERE username = '${USERNAME//\'/\'\'}' LIMIT 1;" 2>/dev/null || true)" if [[ -d "$SRC/uploads" ]]; then copy_player_avatar_docker "${AVATAR_URL:-}" "$SRC/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 (player-only)." echo " App: http://localhost:${HOST_PORT_VAL}" echo " Logs: ${COMPOSE[*]} logs -f --tail=50" exit 0 fi # ── Full import (replace Docker DB + uploads) ──────────────────────────────── 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"