175 lines
5.6 KiB
Bash
Executable File
175 lines
5.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -Eeuo pipefail
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
ROOT_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
|
PROD_COMPOSE="${ROOT_DIR}/compose.prod.yaml"
|
|
ENV_FILE="${MOONWELL_ENV_FILE:-${ROOT_DIR}/.env}"
|
|
BACKUP_WORK_DIR="${ROOT_DIR}/storage/app/private/database-backups"
|
|
LOCK_FILE="${BACKUP_WORK_DIR}/backup.lock"
|
|
RUN_DIR=''
|
|
|
|
log() {
|
|
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S %Z')" "$*"
|
|
}
|
|
|
|
fail() {
|
|
log "ERROR: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
env_value() {
|
|
local key="$1"
|
|
local value
|
|
|
|
value="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2-)"
|
|
if [[ "$value" == \"*\" && "$value" == *\" ]]; then
|
|
value="${value:1:${#value}-2}"
|
|
elif [[ "$value" == \'*\' && "$value" == *\' ]]; then
|
|
value="${value:1:${#value}-2}"
|
|
fi
|
|
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
validate_database_name() {
|
|
[[ "$1" =~ ^[A-Za-z0-9_-]+$ ]] || fail "Недопустимое имя базы: $1"
|
|
}
|
|
|
|
prod() {
|
|
docker compose -f "$PROD_COMPOSE" "$@"
|
|
}
|
|
|
|
cleanup() {
|
|
if [[ -n "$RUN_DIR" && -d "$RUN_DIR" && "$RUN_DIR" == "${BACKUP_WORK_DIR}/"* ]]; then
|
|
find "$RUN_DIR" -maxdepth 1 -type f -delete
|
|
rmdir "$RUN_DIR" 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
dump_web_database() {
|
|
local database="$1"
|
|
local output="$2"
|
|
|
|
log "Создаю дамп локальной базы ${database}"
|
|
prod exec -T mysql sh -c '
|
|
export MYSQL_PWD="$MYSQL_ROOT_PASSWORD"
|
|
exec mysqldump \
|
|
--user=root \
|
|
--single-transaction \
|
|
--quick \
|
|
--hex-blob \
|
|
--triggers \
|
|
--no-tablespaces \
|
|
--set-gtid-purged=OFF \
|
|
--databases "$1"
|
|
' sh "$database" | gzip > "$output"
|
|
}
|
|
|
|
dump_remote_database() {
|
|
local database="$1"
|
|
local output="$2"
|
|
local host="$3"
|
|
local port="$4"
|
|
local username="$5"
|
|
local password="$6"
|
|
|
|
log "Создаю дамп AzerothCore ${database}"
|
|
docker run --rm --network host \
|
|
--env MYSQL_PWD="$password" \
|
|
mysql:8.4 \
|
|
mysqldump \
|
|
--host="$host" \
|
|
--port="$port" \
|
|
--user="$username" \
|
|
--single-transaction \
|
|
--quick \
|
|
--hex-blob \
|
|
--triggers \
|
|
--no-tablespaces \
|
|
--set-gtid-purged=OFF \
|
|
--databases "$database" | gzip > "$output"
|
|
}
|
|
|
|
upload_backup() {
|
|
local source="$1"
|
|
local destination="$2"
|
|
local app_uid app_gid
|
|
|
|
app_uid="$(env_value WWWUSER)"
|
|
app_gid="$(env_value WWWGROUP)"
|
|
app_uid="${app_uid:-1000}"
|
|
app_gid="${app_gid:-1000}"
|
|
|
|
chown "${app_uid}:${app_gid}" "$RUN_DIR" "$source"
|
|
chmod 0750 "$RUN_DIR"
|
|
chmod 0640 "$source"
|
|
prod exec -T --user "${app_uid}:${app_gid}" app \
|
|
php artisan backup:upload "/var/www/html/storage/app/private/database-backups/${RUN_DIR##*/}/${source##*/}" "$destination"
|
|
}
|
|
|
|
main() {
|
|
command -v docker >/dev/null 2>&1 || fail 'Не найден Docker'
|
|
command -v gzip >/dev/null 2>&1 || fail 'Не найден gzip'
|
|
command -v flock >/dev/null 2>&1 || fail 'Не найден flock'
|
|
docker compose version >/dev/null 2>&1 || fail 'Не найден Docker Compose Plugin'
|
|
[[ -f "$ENV_FILE" ]] || fail "Не найден ${ENV_FILE}"
|
|
[[ -f "$PROD_COMPOSE" ]] || fail "Не найден ${PROD_COMPOSE}"
|
|
|
|
mkdir -p "$BACKUP_WORK_DIR"
|
|
exec 9>"$LOCK_FILE"
|
|
if ! flock -n 9; then
|
|
log 'Предыдущий запуск ещё выполняется, новый запуск пропущен'
|
|
return 0
|
|
fi
|
|
|
|
local timestamp object_prefix web_database
|
|
local acore_host acore_port acore_username acore_password acore_databases
|
|
local database filename
|
|
local -a databases=() files=()
|
|
|
|
timestamp="$(date -u '+%Y-%m-%dT%H-%M-%SZ')"
|
|
object_prefix="$(env_value DATABASE_BACKUP_PREFIX)"
|
|
object_prefix="${object_prefix:-backups/databases}"
|
|
object_prefix="${object_prefix%/}/$(date -u '+%Y/%m/%d')/${timestamp}"
|
|
web_database="$(env_value DB_DATABASE)"
|
|
acore_host="$(env_value AZEROTHCORE_AUTH_DB_HOST)"
|
|
acore_port="$(env_value AZEROTHCORE_AUTH_DB_PORT)"
|
|
acore_username="$(env_value AZEROTHCORE_AUTH_DB_USERNAME)"
|
|
acore_password="$(env_value AZEROTHCORE_AUTH_DB_PASSWORD)"
|
|
acore_databases="$(env_value DATABASE_BACKUP_ACORE_DATABASES)"
|
|
acore_databases="${acore_databases:-acore_auth acore_characters acore_world acore_playerbots store}"
|
|
|
|
[[ -n "$web_database" ]] || fail 'DB_DATABASE не задан'
|
|
[[ -n "$acore_host" && -n "$acore_port" && -n "$acore_username" ]] || fail 'Не настроено подключение к AzerothCore'
|
|
validate_database_name "$web_database"
|
|
|
|
RUN_DIR="${BACKUP_WORK_DIR}/${timestamp}"
|
|
mkdir -m 0700 "$RUN_DIR"
|
|
trap cleanup EXIT
|
|
|
|
filename="${web_database}.sql.gz"
|
|
dump_web_database "$web_database" "${RUN_DIR}/${filename}"
|
|
databases+=("$web_database")
|
|
files+=("$filename")
|
|
|
|
for database in $acore_databases; do
|
|
validate_database_name "$database"
|
|
filename="${database}.sql.gz"
|
|
dump_remote_database "$database" "${RUN_DIR}/${filename}" "$acore_host" "$acore_port" "$acore_username" "$acore_password"
|
|
databases+=("$database")
|
|
files+=("$filename")
|
|
done
|
|
|
|
log "Все дампы созданы, загружаю набор ${timestamp} в S3"
|
|
for filename in "${files[@]}"; do
|
|
[[ -s "${RUN_DIR}/${filename}" ]] || fail "Пустой файл дампа: ${filename}"
|
|
upload_backup "${RUN_DIR}/${filename}" "${object_prefix}/${filename}"
|
|
done
|
|
|
|
log "Бэкап завершён: ${#databases[@]} баз, s3://${object_prefix}/"
|
|
}
|
|
|
|
main "$@"
|