118 lines
5.5 KiB
Bash
Executable File
118 lines
5.5 KiB
Bash
Executable File
#!/bin/bash
|
||
# Supersam app code backup to S3 (Beget Cloud Storage)
|
||
# Archives /opt/supersam source code (excluding node_modules, .git, dist)
|
||
# and uploads to S3. Retention: keeps last 30 days.
|
||
#
|
||
# Cron: 0 1 * * * /opt/supersam/scripts/backup-code-s3.sh >> /var/log/supersam-backup-code.log 2>&1
|
||
# Also triggered after deploy via deploy.sh
|
||
# Каждая операция (успех или провал) логируется в public.backup_logs.
|
||
|
||
set -euo pipefail
|
||
|
||
# ── Config ────────────────────────────────────────────────────────────────
|
||
AWS_ACCESS_KEY_ID="YG4MQNKAPNL65200MBUY"
|
||
AWS_SECRET_ACCESS_KEY="8mXkFM2VRQ3pN1Nx4mhmJ2jrZoB5YTPUa4CaZh43"
|
||
S3_BUCKET="02f162ff4a18-supersam-s3"
|
||
S3_PREFIX="backups"
|
||
|
||
SOURCE_DIR="/opt/supersam"
|
||
SOURCE_DIR_DEV="/opt/supersam-dev"
|
||
RETENTION_DAYS=30
|
||
|
||
DB_CONTAINER="supabase-db"
|
||
DB_USER="supabase_admin"
|
||
DB_NAME="postgres"
|
||
DB_PASS="${POSTGRES_PASSWORD:-$(grep -m1 '^POSTGRES_PASSWORD=' /opt/supabase/docker/.env 2>/dev/null | cut -d= -f2- || true)}"
|
||
DB_PASS="${DB_PASS:-4fe80bb21c7c3d17a8d8b226adf7a479}"
|
||
|
||
BACKUP_TYPE="code"
|
||
|
||
# ── Logging в public.backup_logs ──────────────────────────────────────────
|
||
LOGGED=0
|
||
log_backup() {
|
||
local status="$1" size="$2" bytes="$3" s3_path="$4" tables="$5"
|
||
printf "INSERT INTO public.backup_logs (backup_type, status, file_size, file_size_bytes, s3_path, tables_included) VALUES ('%s', '%s', '%s', %s, '%s', '%s');\n" \
|
||
"${BACKUP_TYPE}" "${status}" "${size}" "${bytes:-0}" "${s3_path}" "${tables}" \
|
||
| docker exec -i -e PGPASSWORD="${DB_PASS}" "${DB_CONTAINER}" \
|
||
psql -U "${DB_USER}" -d "${DB_NAME}" -q -t -A >/dev/null \
|
||
|| echo "[$(date)] WARN: не удалось записать backup_logs"
|
||
LOGGED=1
|
||
}
|
||
|
||
on_exit() {
|
||
local rc=$?
|
||
if [ "${rc}" -ne 0 ] && [ "${LOGGED}" -eq 0 ]; then
|
||
echo "[$(date)] FAILED: скрипт завершился с кодом ${rc}"
|
||
log_backup "failed" "" 0 "" "exit code ${rc}"
|
||
fi
|
||
exit "${rc}"
|
||
}
|
||
trap on_exit EXIT
|
||
|
||
# ── Runtime ───────────────────────────────────────────────────────────────
|
||
TIMESTAMP=$(date +%Y-%m-%d_%H%M)
|
||
ARCHIVE_FILE="/tmp/supersam-code-${TIMESTAMP}.tar.gz"
|
||
S3_KEY="${S3_PREFIX}/code/supersam-code-${TIMESTAMP}.tar.gz"
|
||
S3_PATH="s3://${S3_BUCKET}/${S3_KEY}"
|
||
|
||
export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY
|
||
|
||
echo "[$(date)] Starting code backup..."
|
||
|
||
# ── Archive ──────────────────────────────────────────────────────────────
|
||
echo "[$(date)] Archiving ${SOURCE_DIR}..."
|
||
# Рантайм-чекаут + актуальный dev-чекаут (источник прод-фронта)
|
||
tar -czf "${ARCHIVE_FILE}" \
|
||
--directory="/" \
|
||
--exclude="node_modules" \
|
||
--exclude=".git" \
|
||
--exclude="dist" \
|
||
--exclude="volumes/db" \
|
||
--exclude="volumes/storage" \
|
||
--exclude="volumes/logs" \
|
||
--exclude="volumes/api" \
|
||
--exclude="volumes/snippets" \
|
||
--exclude="__pycache__" \
|
||
"${SOURCE_DIR#/}" "${SOURCE_DIR_DEV#/}" 2>/dev/null
|
||
|
||
ARCHIVE_SIZE=$(du -h "${ARCHIVE_FILE}" | cut -f1)
|
||
ARCHIVE_BYTES=$(stat -c%s "${ARCHIVE_FILE}")
|
||
echo "[$(date)] Archive created: ${ARCHIVE_FILE} (${ARCHIVE_SIZE}, ${ARCHIVE_BYTES} bytes)"
|
||
|
||
# ── Upload via mc ─────────────────────────────────────────────────────────
|
||
echo "[$(date)] Uploading to S3..."
|
||
mc cp "${ARCHIVE_FILE}" "beget/${S3_BUCKET}/${S3_KEY}" --quiet 2>&1
|
||
|
||
echo "[$(date)] Uploaded to ${S3_PATH}"
|
||
|
||
# ── Verify: объект существует в S3 ────────────────────────────────────────
|
||
S3_BYTES=$(mc stat --json "beget/${S3_BUCKET}/${S3_KEY}" 2>/dev/null | grep -o '"size":[0-9]*' | head -1 | cut -d: -f2 || true)
|
||
if [ -z "${S3_BYTES}" ]; then
|
||
echo "[$(date)] FAILED: объект не найден в S3 после загрузки: ${S3_PATH}"
|
||
log_backup "failed" "${ARCHIVE_SIZE}" "${ARCHIVE_BYTES}" "${S3_PATH}" "upload not verified"
|
||
exit 1
|
||
fi
|
||
if [ "${S3_BYTES}" != "${ARCHIVE_BYTES}" ]; then
|
||
echo "[$(date)] WARN: размер в S3 (${S3_BYTES}) не совпадает с локальным (${ARCHIVE_BYTES})"
|
||
fi
|
||
|
||
# ── Cleanup local ─────────────────────────────────────────────────────────
|
||
rm -f "${ARCHIVE_FILE}"
|
||
echo "[$(date)] Local cleanup done"
|
||
|
||
# ── Retention: delete S3 code backups older than RETENTION_DAYS ───────────
|
||
echo "[$(date)] Cleaning S3 code backups older than ${RETENTION_DAYS} days..."
|
||
CUTOFF=$(date -d "-${RETENTION_DAYS} days" +%Y-%m-%d)
|
||
{ mc ls "beget/${S3_BUCKET}/${S3_PREFIX}/code/" 2>/dev/null || true; } \
|
||
| awk '{print $NF}' \
|
||
| while read -r fname; do
|
||
file_date=$(echo "$fname" | grep -oP '\d{4}-\d{2}-\d{2}' || echo "")
|
||
if [ -n "$file_date" ] && [[ "$file_date" < "$CUTOFF" ]]; then
|
||
mc rm "beget/${S3_BUCKET}/${S3_PREFIX}/code/${fname}" --quiet 2>/dev/null
|
||
echo "[$(date)] Deleted old backup: ${fname}"
|
||
fi
|
||
done
|
||
|
||
log_backup "success" "${ARCHIVE_SIZE}" "${ARCHIVE_BYTES}" "${S3_PATH}" "source code"
|
||
echo "[$(date)] Code backup complete."
|