diff --git a/.env.example b/.env.example index 4e3262a..0df8b59 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,10 @@ POSTGRES_PASSWORD=StrongPasswordForDatabase # Локальный порт веб-интерфейса GM-Relay GMRELAY_WEB_PORT=8080 + +# === Backup === +# Сколько дней хранить дампы PostgreSQL (default: 7) +BACKUP_RETENTION_DAYS=7 + +# Имя Docker volume для резервных копий БД +BACKUP_VOLUME_NAME=game_pgbackups diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 932e3fa..ba256c5 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -6,7 +6,7 @@ on: - main env: - VERSION: 1.14.0 + VERSION: 1.15.0 jobs: # ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами) diff --git a/Directory.Build.props b/Directory.Build.props index 7a5fd28..29c24cc 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 1.14.0 + 1.15.0 net10.0 preview enable diff --git a/README.md b/README.md index f9ada98..40892c3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Проект разработан с упором на производительность, архитектуру Vertical Slice, Native AOT (для бота) и удобство развертывания с использованием .NET Aspire. -**Текущая версия:** `v1.14.0`. +**Текущая версия:** `v1.15.0`. --- @@ -105,6 +105,44 @@ docker compose up -d 2. Создайте группу через `/newgroup`. 3. Откройте Mini App или Web Dashboard для расширенного управления. +## 💾 Backup и восстановление + +Проект включает автоматический ежедневный backup PostgreSQL через сервис `db-backup` в Docker Compose. + +### Как это работает +- **Каждый день в 03:00** выполняется `pg_dump` базы `gmrelay_db`. +- Дампы сжимаются (`gzip`) и сохраняются в volume `pgbackups` (`/backups`). +- Формат имени: `gmrelay_db_YYYYMMDD_HHMMSS.sql.gz`. +- Ротация: по умолчанию хранятся последние **7 дней** (настраивается через `BACKUP_RETENTION_DAYS`). + +### Проверка бэкапов +```bash +docker compose exec db-backup ls -la /backups +``` + +### Ручное создание дампа +```bash +docker compose exec db-backup sh -c "pg_dump -h db -U gmrelay -d gmrelay_db | gzip > /backups/gmrelay_db_manual.sql.gz" +``` + +### Восстановление из бэкапа +```bash +# Использовать последний автоматический бэкап +./scripts/restore.sh + +# Или указать конкретный файл +./scripts/restore.sh backups/gmrelay_db_20260512_030000.sql.gz +``` + +> [!WARNING] +> Восстановление **перезаписывает текущую базу данных**. Убедитесь, что вы понимаете последствия, прежде чем запускать `restore.sh`. + +### Переменные окружения (опциональные) +```env +BACKUP_RETENTION_DAYS=7 +BACKUP_VOLUME_NAME=game_pgbackups +``` + --- ## 🗂 Структура репозитория diff --git a/compose.yaml b/compose.yaml index 3f17b13..bb12187 100644 --- a/compose.yaml +++ b/compose.yaml @@ -16,8 +16,30 @@ services: timeout: 3s retries: 10 + db-backup: + image: postgres:17-alpine + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + POSTGRES_USER: gmrelay + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env} + POSTGRES_DB: gmrelay_db + PGPASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env} + BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7} + volumes: + - pgbackups:/backups + networks: + - gmrelay + entrypoint: ["sh", "-c"] + command: + - | + echo "0 3 * * * pg_dump -h db -U gmrelay -d gmrelay_db | gzip > /backups/gmrelay_db_\$$(date +\%Y\%m\%d_\%H\%M\%S).sql.gz && find /backups -name 'gmrelay_db_*.sql.gz' -type f -mtime +\$${BACKUP_RETENTION_DAYS} -delete" | crontab - + crond -f + bot: - image: git.codeanddice.ru/toutsu/gmrelay-bot:1.14.0 + image: git.codeanddice.ru/toutsu/gmrelay-bot:1.15.0 restart: always depends_on: db: @@ -30,7 +52,7 @@ services: - gmrelay web: - image: git.codeanddice.ru/toutsu/gmrelay-web:1.14.0 + image: git.codeanddice.ru/toutsu/gmrelay-web:1.15.0 restart: always depends_on: db: @@ -52,6 +74,8 @@ volumes: name: ${POSTGRES_VOLUME_NAME:-game_pgdata} web_keys: name: ${WEB_KEYS_VOLUME_NAME:-gmrelay_web_keys} + pgbackups: + name: ${BACKUP_VOLUME_NAME:-game_pgbackups} networks: gmrelay: diff --git a/scripts/restore.sh b/scripts/restore.sh new file mode 100644 index 0000000..b771cb1 --- /dev/null +++ b/scripts/restore.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# GM-Relay PostgreSQL Backup Restore Script +# Usage: ./scripts/restore.sh [backup_file] +# If no file is provided, uses the most recent backup. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +# Check required env +if [ -z "${POSTGRES_PASSWORD:-}" ]; then + if [ -f "${PROJECT_ROOT}/.env" ]; then + export $(grep -v '^#' "${PROJECT_ROOT}/.env" | xargs) 2>/dev/null || true + fi +fi + +if [ -z "${POSTGRES_PASSWORD:-}" ]; then + echo "ERROR: POSTGRES_PASSWORD is not set. Please set it in your environment or .env file." + exit 1 +fi + +BACKUP_DIR="${PROJECT_ROOT}/backups" + +# Determine backup file +if [ $# -ge 1 ]; then + BACKUP_FILE="$1" +else + BACKUP_FILE=$(find "${BACKUP_DIR}" -name 'gmrelay_db_*.sql.gz' -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n1 | cut -d' ' -f2-) + if [ -z "${BACKUP_FILE}" ]; then + echo "ERROR: No backup files found in ${BACKUP_DIR}." + exit 1 + fi +fi + +if [ ! -f "${BACKUP_FILE}" ]; then + echo "ERROR: Backup file not found: ${BACKUP_FILE}" + exit 1 +fi + +echo "==================================================" +echo " GM-Relay PostgreSQL Restore" +echo "==================================================" +echo "" +echo "Backup file: ${BACKUP_FILE}" +echo "Database: gmrelay_db" +echo "User: gmrelay" +echo "" +read -p "This will OVERWRITE the current database. Are you sure? [y/N] " CONFIRM + +if [[ ! "${CONFIRM}" =~ ^[Yy]$ ]]; then + echo "Restore cancelled." + exit 0 +fi + +echo "" +echo "Restoring database from ${BACKUP_FILE}..." + +# Restore using docker compose exec to leverage the running postgres container +docker compose -f "${PROJECT_ROOT}/compose.yaml" exec -T db psql \ + -U gmrelay \ + -d gmrelay_db \ + -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" 2>/dev/null || true + +gunzip -c "${BACKUP_FILE}" | docker compose -f "${PROJECT_ROOT}/compose.yaml" exec -T db psql \ + -U gmrelay \ + -d gmrelay_db + +echo "" +echo "==================================================" +echo " Restore completed successfully!" +echo "==================================================" diff --git a/src/GmRelay.AppHost/packages.lock.json b/src/GmRelay.AppHost/packages.lock.json index 8e0fb8f..3f74298 100644 --- a/src/GmRelay.AppHost/packages.lock.json +++ b/src/GmRelay.AppHost/packages.lock.json @@ -83,6 +83,12 @@ "System.IO.Hashing": "10.0.3" } }, + "SecurityCodeScan.VS2019": { + "type": "Direct", + "requested": "[5.6.7, )", + "resolved": "5.6.7", + "contentHash": "WIE9RJswdSc2j+rLz2gW6U+gMUjMHzY2j7C/CL8/R2olXNM/+twarfMnWqm+rZodDBvaYDApJyxM8mVYf9FGrQ==" + }, "Aspire.Hosting": { "type": "Transitive", "resolved": "13.2.1", diff --git a/src/GmRelay.Bot/packages.lock.json b/src/GmRelay.Bot/packages.lock.json index 3f68185..2b3b5c5 100644 --- a/src/GmRelay.Bot/packages.lock.json +++ b/src/GmRelay.Bot/packages.lock.json @@ -95,6 +95,12 @@ "Microsoft.Extensions.Logging.Abstractions": "10.0.0" } }, + "SecurityCodeScan.VS2019": { + "type": "Direct", + "requested": "[5.6.7, )", + "resolved": "5.6.7", + "contentHash": "WIE9RJswdSc2j+rLz2gW6U+gMUjMHzY2j7C/CL8/R2olXNM/+twarfMnWqm+rZodDBvaYDApJyxM8mVYf9FGrQ==" + }, "Telegram.Bot": { "type": "Direct", "requested": "[22.9.5.3, )", diff --git a/src/GmRelay.ServiceDefaults/packages.lock.json b/src/GmRelay.ServiceDefaults/packages.lock.json index 65d1c84..6a2c51d 100644 --- a/src/GmRelay.ServiceDefaults/packages.lock.json +++ b/src/GmRelay.ServiceDefaults/packages.lock.json @@ -66,6 +66,12 @@ "OpenTelemetry.Api": "[1.15.3, 2.0.0)" } }, + "SecurityCodeScan.VS2019": { + "type": "Direct", + "requested": "[5.6.7, )", + "resolved": "5.6.7", + "contentHash": "WIE9RJswdSc2j+rLz2gW6U+gMUjMHzY2j7C/CL8/R2olXNM/+twarfMnWqm+rZodDBvaYDApJyxM8mVYf9FGrQ==" + }, "Microsoft.Extensions.AmbientMetadata.Application": { "type": "Transitive", "resolved": "10.2.0", diff --git a/src/GmRelay.Shared/packages.lock.json b/src/GmRelay.Shared/packages.lock.json index 4a91a8c..36d6a25 100644 --- a/src/GmRelay.Shared/packages.lock.json +++ b/src/GmRelay.Shared/packages.lock.json @@ -1,6 +1,13 @@ { "version": 1, "dependencies": { - "net10.0": {} + "net10.0": { + "SecurityCodeScan.VS2019": { + "type": "Direct", + "requested": "[5.6.7, )", + "resolved": "5.6.7", + "contentHash": "WIE9RJswdSc2j+rLz2gW6U+gMUjMHzY2j7C/CL8/R2olXNM/+twarfMnWqm+rZodDBvaYDApJyxM8mVYf9FGrQ==" + } + } } } \ No newline at end of file diff --git a/src/GmRelay.Web/Components/Layout/NavMenu.razor b/src/GmRelay.Web/Components/Layout/NavMenu.razor index b32e0d7..408fdb2 100644 --- a/src/GmRelay.Web/Components/Layout/NavMenu.razor +++ b/src/GmRelay.Web/Components/Layout/NavMenu.razor @@ -56,7 +56,7 @@ - + diff --git a/src/GmRelay.Web/packages.lock.json b/src/GmRelay.Web/packages.lock.json index d634357..8c9ff76 100644 --- a/src/GmRelay.Web/packages.lock.json +++ b/src/GmRelay.Web/packages.lock.json @@ -32,6 +32,12 @@ "resolved": "10.0.2", "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==" }, + "SecurityCodeScan.VS2019": { + "type": "Direct", + "requested": "[5.6.7, )", + "resolved": "5.6.7", + "contentHash": "WIE9RJswdSc2j+rLz2gW6U+gMUjMHzY2j7C/CL8/R2olXNM/+twarfMnWqm+rZodDBvaYDApJyxM8mVYf9FGrQ==" + }, "Telegram.Bot": { "type": "Direct", "requested": "[22.9.6.1, )", diff --git a/tests/GmRelay.Bot.Tests/packages.lock.json b/tests/GmRelay.Bot.Tests/packages.lock.json index 743677d..46df037 100644 --- a/tests/GmRelay.Bot.Tests/packages.lock.json +++ b/tests/GmRelay.Bot.Tests/packages.lock.json @@ -18,6 +18,12 @@ "Microsoft.TestPlatform.TestHost": "17.14.1" } }, + "SecurityCodeScan.VS2019": { + "type": "Direct", + "requested": "[5.6.7, )", + "resolved": "5.6.7", + "contentHash": "WIE9RJswdSc2j+rLz2gW6U+gMUjMHzY2j7C/CL8/R2olXNM/+twarfMnWqm+rZodDBvaYDApJyxM8mVYf9FGrQ==" + }, "xunit": { "type": "Direct", "requested": "[2.9.3, )", @@ -726,8 +732,8 @@ "Aspire.Npgsql": "[13.2.2, )", "Dapper": "[2.1.72, )", "Dapper.AOT": "[1.0.48, )", - "GmRelay.ServiceDefaults": "[1.14.0, )", - "GmRelay.Shared": "[1.14.0, )", + "GmRelay.ServiceDefaults": "[1.15.0, )", + "GmRelay.Shared": "[1.15.0, )", "Microsoft.Extensions.Hosting": "[10.0.5, )", "Npgsql": "[10.0.2, )", "Telegram.Bot": "[22.9.5.3, )", @@ -754,8 +760,8 @@ "dependencies": { "Aspire.Npgsql": "[13.2.2, )", "Dapper": "[2.1.72, )", - "GmRelay.ServiceDefaults": "[1.14.0, )", - "GmRelay.Shared": "[1.14.0, )", + "GmRelay.ServiceDefaults": "[1.15.0, )", + "GmRelay.Shared": "[1.15.0, )", "Npgsql": "[10.0.2, )", "Telegram.Bot": "[22.9.6.1, )" }