Compare commits

...

2 Commits

Author SHA1 Message Date
jisangs fcc8926172 마에스트로 DB 데이터 백업+삭제, 복원 스크립트 2026-09-07 22:18:08 +09:00
jisangs b363f9bcac 운영 DB 백업 스크립트
- 백업용 DB 계정 생성: backup
2026-09-07 18:32:54 +09:00
3 changed files with 627 additions and 0 deletions
@@ -0,0 +1,308 @@
#!/usr/bin/env bash
#
# backup_maestro.sh
# ------------------------------------------------------------
# 지정한 MaestroID 와 관련된 12개 테이블의 데이터를
# 1) mariadb-dump(mysqldump) 로 백업하고
# 2) (재확인 후) 운영 DB 에서 실제 삭제까지 수행하는 스크립트.
#
# 실행 위치: Synology NAS (운영 서버가 아님)
# - NAS에 설치된 MariaDB client 로 AWS 운영 DB 서버에
# 원격 접속해서 덤프/삭제를 수행합니다.
# - 운영 DB 서버의 보안 그룹/방화벽에서 NAS 의 공인 IP
# (또는 VPN 대역)에 대해 3306 포트가 열려 있어야 합니다.
#
# 결과물은 "복원 가능한 INSERT 문(.sql)" 한 개 파일로 생성됩니다.
# (테이블 구조는 포함하지 않음 — 복원 대상 DB에 스키마가 이미
# 존재한다고 가정. 스키마까지 필요하면 아래 USE_NO_CREATE_INFO
# 를 false 로 바꾸세요.)
#
# 삭제 단계는:
# - 백업이 모든 테이블에서 성공했을 때만 진행합니다.
# - MaestroID 를 다시 한번 입력해야만 실행됩니다.
# - 자식 테이블 → 부모(maestro) 순으로 하나의 트랜잭션에서 삭제합니다.
# - 문제 발생 시 방금 만든 백업 파일로 restore_maestro.sh 로 복원 가능.
#
# 사용법:
# chmod +x backup_maestro.sh
# ./backup_maestro.sh <MaestroID> # 운영 DB (기본)
# ./backup_maestro.sh --test <MaestroID> # 테스트 DB
# 예) ./backup_maestro.sh 317
# ./backup_maestro.sh --test 317
# ------------------------------------------------------------
set -euo pipefail
# Synology NAS 의 MariaDB10 패키지 바이너리 경로.
# 기본 PATH 에 포함되지 않는 경우가 많아 명시적으로 추가합니다.
export PATH="$PATH:/var/packages/MariaDB10/target/usr/local/mariadb10.11/bin"
# ===================== DB 접속 정보 =====================
# 운영 DB (기본)
PROD_DB_HOST="chocomae.jinaju.com"
PROD_DB_PORT="3306"
PROD_DB_USER="chocomae"
PROD_DB_NAME="chocomae"
# 테스트 DB (--test 옵션으로 활성화)
TEST_DB_HOST="mariadb.jisangs.com"
TEST_DB_PORT="30001"
TEST_DB_USER="chocomae"
TEST_DB_NAME="chocomae"
# true : 데이터(INSERT)만 백업 (기본, 스키마는 이미 있다고 가정)
# false : CREATE TABLE 구조까지 포함해서 백업
USE_NO_CREATE_INFO=true
OUTPUT_DIR="./backup"
# =========================================================
# ---- 인자 파싱 (순서 자유: --test 는 앞/뒤 어디에 와도 됨) ----
USE_TEST=false
MAESTRO_ID=""
while [ $# -gt 0 ]; do
case "$1" in
-t|--test)
USE_TEST=true
shift
;;
-h|--help)
echo "사용법: $0 [--test] <MaestroID>"
exit 0
;;
-*)
echo "알 수 없는 옵션: $1"
exit 1
;;
*)
if [ -z "$MAESTRO_ID" ]; then
MAESTRO_ID="$1"
else
echo "인자가 너무 많습니다: $1"
exit 1
fi
shift
;;
esac
done
if [ -z "$MAESTRO_ID" ]; then
echo "사용법: $0 [--test] <MaestroID>"
echo "예) $0 317"
echo " $0 --test 317"
exit 1
fi
if ! [[ "$MAESTRO_ID" =~ ^[0-9]+$ ]]; then
echo "MaestroID 는 정수여야 합니다: '${MAESTRO_ID}'"
exit 1
fi
# ---- 대상 DB 확정 ----
if [ "$USE_TEST" = true ]; then
DB_HOST="$TEST_DB_HOST"
DB_PORT="$TEST_DB_PORT"
DB_USER="$TEST_DB_USER"
DB_NAME="$TEST_DB_NAME"
TARGET_LABEL="TEST"
else
DB_HOST="$PROD_DB_HOST"
DB_PORT="$PROD_DB_PORT"
DB_USER="$PROD_DB_USER"
DB_NAME="$PROD_DB_NAME"
TARGET_LABEL="PROD"
fi
echo "접속 대상: [${TARGET_LABEL}] ${DB_USER}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
# 운영 서버 대상일 경우 실수 방지용 재확인.
if [ "$TARGET_LABEL" = "PROD" ]; then
echo
echo "!! 대상은 운영(PROD) DB 입니다. 백업 후 삭제까지 실제로 수행됩니다."
read -r -p "계속하려면 'PROD' 를 입력하세요: " PROD_CONFIRM
if [ "$PROD_CONFIRM" != "PROD" ]; then
echo "취소되었습니다."
exit 0
fi
fi
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTPUT_FILE="${OUTPUT_DIR}/maestro_${MAESTRO_ID}_backup_${TIMESTAMP}.sql"
# 복원 편의를 위해 부모 테이블(maestro)을 가장 먼저 나열
TABLES=(
maestro
app_highest_record
best_record
license_maestro_password
license_score
license_time
maestro_extension
maestro_log
maestro_upgrade
player
typing_exam_highest_record
typing_exam_record
)
# Synology NAS 에는 mariadb-dump / mariadb 로 설치되는 경우가 많고,
# 배포판에 따라 mysqldump / mysql 심볼릭 링크만 있는 경우도 있어 둘 다 지원합니다.
if command -v mariadb-dump >/dev/null 2>&1; then
DUMP_CMD="mariadb-dump"
elif command -v mysqldump >/dev/null 2>&1; then
DUMP_CMD="mysqldump"
else
echo "mariadb-dump / mysqldump 명령을 찾을 수 없습니다. NAS 에 MariaDB client 를 먼저 설치하세요."
exit 1
fi
if command -v mariadb >/dev/null 2>&1; then
MYSQL_CMD="mariadb"
elif command -v mysql >/dev/null 2>&1; then
MYSQL_CMD="mysql"
else
echo "mariadb / mysql 명령을 찾을 수 없습니다. NAS 에 MariaDB client 를 먼저 설치하세요."
exit 1
fi
mkdir -p "$OUTPUT_DIR"
# 비밀번호는 명령행 인자로 넘기지 않고(ps 로 노출되는 것을 방지)
# 프롬프트로 입력받아 MYSQL_PWD 환경변수로 전달합니다.
read -r -s -p "DB(${DB_USER}@${DB_HOST}) 비밀번호: " DB_PASS
echo
export MYSQL_PWD="$DB_PASS"
# ---- MaestroID → Name 조회로 대상 계정 확인 ----
if ! MAESTRO_NAME=$("$MYSQL_CMD" --protocol=TCP --default-character-set=utf8mb4 \
-h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" "$DB_NAME" \
-sN -e "SELECT \`Name\` FROM \`maestro\` WHERE MaestroID=${MAESTRO_ID};" \
2>/tmp/mysql_err.$$); then
echo "DB 조회에 실패했습니다: $(cat /tmp/mysql_err.$$)"
rm -f /tmp/mysql_err.$$
unset MYSQL_PWD
exit 1
fi
rm -f /tmp/mysql_err.$$
if [ -z "$MAESTRO_NAME" ]; then
echo "MaestroID=${MAESTRO_ID} 에 해당하는 계정이 [${TARGET_LABEL}] DB 에 없습니다."
unset MYSQL_PWD
exit 1
fi
echo
echo "대상 계정: MaestroID=${MAESTRO_ID} / Name='${MAESTRO_NAME}'"
read -r -p "이 계정이 맞으면 진행합니다. 계속하시겠습니까? (y/N) " NAME_CONFIRM
if [[ "$NAME_CONFIRM" != "y" && "$NAME_CONFIRM" != "Y" ]]; then
echo "취소되었습니다."
unset MYSQL_PWD
exit 0
fi
# 원격(AWS) DB 로 접속하므로 네트워크 관련 옵션을 추가합니다.
# --protocol=TCP : 로컬 소켓이 아닌 TCP 로 강제 접속
# --default-character-set : 한글 데이터 안전하게 덤프
# --set-gtid-purged=OFF : (MySQL 계열에서만 유효, MariaDB 는 무시)
DUMP_OPTS=(
--protocol=TCP
--default-character-set=utf8mb4
--no-create-info
--compact
--complete-insert
--skip-lock-tables
)
if [ "$USE_NO_CREATE_INFO" = false ]; then
DUMP_OPTS=(
--protocol=TCP
--default-character-set=utf8mb4
--compact
--complete-insert
--skip-lock-tables
)
fi
{
echo "-- MaestroID=${MAESTRO_ID} 데이터 백업"
echo "-- 생성 시각: $(date '+%Y-%m-%d %H:%M:%S')"
echo "SET FOREIGN_KEY_CHECKS=0;"
echo "SET AUTOCOMMIT=0;"
echo "START TRANSACTION;"
echo
} > "$OUTPUT_FILE"
FAILED_TABLES=()
for TABLE in "${TABLES[@]}"; do
echo ">> ${TABLE} 백업 중..."
{
echo "-- ---------------------------------------------"
echo "-- ${TABLE}"
echo "-- ---------------------------------------------"
} >> "$OUTPUT_FILE"
if ! "$DUMP_CMD" -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" \
"${DUMP_OPTS[@]}" \
--where="MaestroID=${MAESTRO_ID}" \
"$DB_NAME" "$TABLE" >> "$OUTPUT_FILE" 2>/tmp/mysqldump_err.$$; then
echo " 경고: ${TABLE} 덤프 실패 - $(cat /tmp/mysqldump_err.$$)"
FAILED_TABLES+=("$TABLE")
fi
rm -f /tmp/mysqldump_err.$$
echo >> "$OUTPUT_FILE"
done
{
echo "COMMIT;"
echo "SET FOREIGN_KEY_CHECKS=1;"
} >> "$OUTPUT_FILE"
echo
echo "백업 완료: ${OUTPUT_FILE}"
echo "총 라인 수: $(wc -l < "$OUTPUT_FILE")"
# ---------------- 원본 데이터 삭제 ----------------
# 백업 도중 실패가 있었다면 삭제를 절대 진행하지 않습니다.
if [ ${#FAILED_TABLES[@]} -gt 0 ]; then
echo "다음 테이블에서 문제가 발생했습니다 (확인 필요): ${FAILED_TABLES[*]}"
echo "백업이 불완전하므로 원본 데이터 삭제를 건너뜁니다."
unset MYSQL_PWD
exit 1
fi
echo
echo "===================================================="
echo " WARNING: 이제 [${TARGET_LABEL}] DB(${DB_HOST}) 에서"
echo " MaestroID=${MAESTRO_ID} 관련 모든 데이터를 삭제합니다."
echo " 되돌리려면 방금 만든 백업 파일로 restore 해야 합니다."
echo "===================================================="
read -r -p "정말 삭제하려면 MaestroID 값(${MAESTRO_ID}) 숫자를 다시 입력하세요: " CONFIRM_ID
if [ "$CONFIRM_ID" != "$MAESTRO_ID" ]; then
echo "취소되었습니다. (원본 데이터는 그대로 유지됨)"
unset MYSQL_PWD
exit 0
fi
# 자식 테이블부터 삭제 → 마지막에 maestro(부모).
# 단일 트랜잭션으로 원자성 보장, FK 는 잠시 꺼두어 순서 의존성 회피.
DELETE_SQL="SET FOREIGN_KEY_CHECKS=0;
START TRANSACTION;
"
for (( i=${#TABLES[@]}-1; i>=0; i-- )); do
DELETE_SQL+="DELETE FROM \`${TABLES[$i]}\` WHERE MaestroID=${MAESTRO_ID};
SELECT '${TABLES[$i]}' AS tbl, ROW_COUNT() AS deleted;
"
done
DELETE_SQL+="COMMIT;
SET FOREIGN_KEY_CHECKS=1;"
echo
echo ">> 삭제 실행 중..."
if printf '%s\n' "$DELETE_SQL" | "$MYSQL_CMD" \
--protocol=TCP --default-character-set=utf8mb4 \
-h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" "$DB_NAME"; then
echo "삭제 완료."
else
echo "삭제 중 오류가 발생했습니다. 트랜잭션이 롤백되었을 수 있으니 DB 상태를 반드시 확인하세요."
unset MYSQL_PWD
exit 1
fi
unset MYSQL_PWD
@@ -0,0 +1,149 @@
#!/usr/bin/env bash
#
# restore_maestro.sh
# ------------------------------------------------------------
# backup_maestro.sh 로 만든 백업(.sql) 파일을 지정한 DB에
# 복원하는 스크립트.
#
# 사용법:
# chmod +x restore_maestro.sh
# ./restore_maestro.sh <backup_file.sql> # 운영 DB (기본)
# ./restore_maestro.sh --test <backup_file.sql> # 테스트 DB
# ------------------------------------------------------------
set -euo pipefail
# Synology NAS 의 MariaDB10 패키지 바이너리 경로.
# 기본 PATH 에 포함되지 않는 경우가 많아 명시적으로 추가합니다.
export PATH="$PATH:/var/packages/MariaDB10/target/usr/local/mariadb10.11/bin"
# ===================== DB 접속 정보 =====================
# 운영 DB (기본)
PROD_DB_HOST="chocomae.jinaju.com"
PROD_DB_PORT="3306"
PROD_DB_USER="chocomae"
PROD_DB_NAME="chocomae"
# 테스트 DB (--test 옵션으로 활성화)
TEST_DB_HOST="mariadb.jisangs.com"
TEST_DB_PORT="30001"
TEST_DB_USER="chocomae"
TEST_DB_NAME="chocomae"
# =========================================================
# ---- 인자 파싱 (순서 자유: --test 는 앞/뒤 어디에 와도 됨) ----
USE_TEST=false
BACKUP_FILE=""
while [ $# -gt 0 ]; do
case "$1" in
-t|--test)
USE_TEST=true
shift
;;
-h|--help)
echo "사용법: $0 [--test] <backup_file.sql>"
exit 0
;;
-*)
echo "알 수 없는 옵션: $1"
exit 1
;;
*)
if [ -z "$BACKUP_FILE" ]; then
BACKUP_FILE="$1"
else
echo "인자가 너무 많습니다: $1"
exit 1
fi
shift
;;
esac
done
if [ -z "$BACKUP_FILE" ]; then
echo "사용법: $0 [--test] <backup_file.sql>"
exit 1
fi
if [ ! -f "$BACKUP_FILE" ]; then
echo "파일을 찾을 수 없습니다: $BACKUP_FILE"
exit 1
fi
# ---- 대상 DB 확정 ----
if [ "$USE_TEST" = true ]; then
DB_HOST="$TEST_DB_HOST"
DB_PORT="$TEST_DB_PORT"
DB_USER="$TEST_DB_USER"
DB_NAME="$TEST_DB_NAME"
TARGET_LABEL="TEST"
else
DB_HOST="$PROD_DB_HOST"
DB_PORT="$PROD_DB_PORT"
DB_USER="$PROD_DB_USER"
DB_NAME="$PROD_DB_NAME"
TARGET_LABEL="PROD"
fi
# Synology NAS 에서는 mariadb CLI 로 설치되는 경우가 많습니다.
if command -v mariadb >/dev/null 2>&1; then
MYSQL_CMD="mariadb"
elif command -v mysql >/dev/null 2>&1; then
MYSQL_CMD="mysql"
else
echo "mariadb / mysql 명령을 찾을 수 없습니다. NAS 에 MariaDB client 를 먼저 설치하세요."
exit 1
fi
# 파일명 규칙: maestro_<MaestroID>_backup_<타임스탬프>.sql
# 규칙에 맞으면 MaestroID 를 뽑아 안내에 노출합니다. (없어도 복원은 정상 동작)
BACKUP_BASENAME=$(basename "$BACKUP_FILE")
DETECTED_MAESTRO_ID=""
if [[ "$BACKUP_BASENAME" =~ ^maestro_([0-9]+)_backup_ ]]; then
DETECTED_MAESTRO_ID="${BASH_REMATCH[1]}"
fi
echo "복원 대상 : [${TARGET_LABEL}] ${DB_USER}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
echo "백업 파일 : ${BACKUP_FILE}"
if [ -n "$DETECTED_MAESTRO_ID" ]; then
echo "MaestroID : ${DETECTED_MAESTRO_ID} (파일명에서 추출)"
else
echo "MaestroID : (파일명 규칙과 달라 확인 불가)"
fi
echo
echo "주의: 이미 동일한 MaestroID 데이터가 대상 DB에 존재하면"
echo " Primary/Unique Key 충돌로 실패할 수 있습니다."
echo " (신규 서버 또는 데이터가 없는 상태에서 복원을 권장)"
echo
read -r -p "계속 진행하시겠습니까? (y/N) " CONFIRM
if [[ "$CONFIRM" != "y" && "$CONFIRM" != "Y" ]]; then
echo "취소되었습니다."
exit 0
fi
# 운영 서버 대상일 경우 실수 방지용 재확인.
if [ "$TARGET_LABEL" = "PROD" ]; then
echo
echo "!! 대상은 운영(PROD) DB 입니다. 실제 데이터가 변경됩니다."
read -r -p "계속하려면 'PROD' 를 입력하세요: " PROD_CONFIRM
if [ "$PROD_CONFIRM" != "PROD" ]; then
echo "취소되었습니다."
exit 0
fi
fi
read -r -s -p "DB(${DB_USER}@${DB_HOST}) 비밀번호: " DB_PASS
echo
export MYSQL_PWD="$DB_PASS"
echo ">> 복원을 시작합니다..."
if "$MYSQL_CMD" --protocol=TCP --default-character-set=utf8mb4 \
-h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" "$DB_NAME" < "$BACKUP_FILE"; then
echo "복원이 완료되었습니다."
else
echo "복원 중 오류가 발생했습니다. 백업 파일 내용과 대상 DB 스키마를 확인하세요."
unset MYSQL_PWD
exit 1
fi
unset MYSQL_PWD
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/env bash
#
# backup_db.sh
# ------------------------------------------------------------
# chocomae 운영 DB(database 전체) 를 gzip 압축 덤프로 저장하고,
# 4주(28일) 초과 백업 파일은 자동 정리한다.
#
# 실행 위치 : Synology NAS (DSM Task Scheduler)
# 실행 주기 : 매일 1회 (트래픽 낮은 새벽 권장)
#
# 자격증명은 --defaults-extra-file 로 참조하는 옵션 파일에서 읽어옵니다.
# (아래 "1회 초기 설정" 참고)
#
# 1회 초기 설정:
# 1) chmod +x backup_db.sh
# 2) 자격증명 파일 생성 (CREDENTIALS_FILE 경로에):
# [client]
# user=chocomae_backup
# password=여기에_비밀번호
# host=chocomae.jinaju.com
# port=3306
# * 백업 전용 DB 계정을 만들어 그 자격증명만 저장할 것을 권장.
# (필요 권한 예: SELECT, LOCK TABLES, EVENT, TRIGGER, PROCESS, SHOW VIEW)
# 3) chmod 600 <CREDENTIALS_FILE> # 매우 중요 (권한 검사에 걸림)
# 4) DSM > 제어판 > 작업 스케줄러 > 사용자 정의 스크립트로 등록,
# "매일 <원하는 시각>" 스케줄 설정.
#
# 로그: OUTPUT_DIR/backup.log 에 append 됩니다.
# ------------------------------------------------------------
set -euo pipefail
# 배포된 스크립트가 최신인지 로그로 확인할 수 있게 버전 문자열을 남김.
# 코드 변경 시 이 값을 손으로 올린 뒤 배포.
SCRIPT_VERSION="2026-09-07.1"
# 새로 만드는 파일이 세계-읽기가 되지 않도록 umask 를 잠금.
# (Synology 공유 폴더는 상위 디렉터리 ACL 로 777 이 상속되는 경우가 있음)
umask 077
# Synology NAS 의 MariaDB10 패키지 바이너리 경로.
# 배치는 로그인 셸이 아니라 PATH 가 비어 있으므로 명시적으로 추가.
export PATH="$PATH:/var/packages/MariaDB10/target/usr/local/mariadb10.11/bin"
# ===================== 설정 =====================
DB_HOST="chocomae.jinaju.com"
DB_PORT="3306"
DB_USER="backup"
DB_NAME="chocomae"
# MySQL 옵션 파일. [client] 섹션에 user/password/host/port 저장.
# 배치가 실행되는 사용자(보통 root) 홈 아래에 두는 것이 자연스럽다.
CREDENTIALS_FILE="/root/.chocomae_db.cnf"
# 백업 파일 저장 위치: 이 스크립트 파일이 있는 디렉터리.
# (호출 위치와 무관하게 항상 스크립트 옆에 저장되도록 절대 경로로 확정)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUTPUT_DIR="$SCRIPT_DIR"
# 보관 기간(일). 이 값을 초과한 파일은 스크립트가 실행될 때 삭제.
RETENTION_DAYS=28
# 로그 파일 크기 제한. 초과 시 마지막 LOG_KEEP_LINES 줄만 남기고 잘라냄.
LOG_MAX_BYTES=$((1 * 1024 * 1024)) # 1 MB
LOG_KEEP_LINES=500
# =================================================
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTPUT_FILE="${OUTPUT_DIR}/chocomae_${TIMESTAMP}.sql.gz"
LOG_FILE="${OUTPUT_DIR}/backup.log"
mkdir -p "$OUTPUT_DIR"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
# ---- 사전 점검 ----
if [ ! -f "$CREDENTIALS_FILE" ]; then
log "ERROR: 자격증명 파일이 없습니다: ${CREDENTIALS_FILE}"
exit 1
fi
# 권한이 600 또는 400 이 아니면 실행 중단 (평문 비밀번호 파일 보호)
PERM=$(stat -c '%a' "$CREDENTIALS_FILE" 2>/dev/null || echo "unknown")
if [ "$PERM" != "600" ] && [ "$PERM" != "400" ]; then
log "ERROR: 자격증명 파일 권한이 안전하지 않습니다 (현재: ${PERM}). 'chmod 600 ${CREDENTIALS_FILE}' 실행 필요."
exit 1
fi
# ---- 덤프 명령 탐지 ----
if command -v mariadb-dump >/dev/null 2>&1; then
DUMP_CMD="mariadb-dump"
elif command -v mysqldump >/dev/null 2>&1; then
DUMP_CMD="mysqldump"
else
log "ERROR: mariadb-dump / mysqldump 명령을 찾을 수 없습니다."
exit 1
fi
# ---- 백업 실행 ----
log "backup-db.sh 시작 (v${SCRIPT_VERSION})"
log "백업 시작 : ${DB_USER}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
log "출력 파일 : ${OUTPUT_FILE}"
# 옵션 설명:
# --defaults-extra-file : 자격증명 파일 (반드시 다른 옵션보다 먼저 위치)
# --single-transaction : InnoDB 온라인 백업. 락 없이 일관된 스냅샷.
# --routines/--triggers/--events : 스토어드 프로시저/트리거/이벤트 포함.
# --hex-blob : 바이너리 데이터 안전 직렬화.
# --protocol=TCP : NAS→AWS 원격 접속이므로 TCP 강제.
# --default-character-set=utf8mb4 : 한글 안전.
DUMP_ERR=$(mktemp)
if "$DUMP_CMD" \
"--defaults-extra-file=${CREDENTIALS_FILE}" \
--protocol=TCP \
--default-character-set=utf8mb4 \
--single-transaction \
--routines --triggers --events \
--hex-blob \
-h"$DB_HOST" -P"$DB_PORT" \
"$DB_NAME" 2>"$DUMP_ERR" | gzip -c > "$OUTPUT_FILE"; then
# 확장 ACL 이 상속되어 붙어 있을 수 있으므로 명시적으로 제거하고 600 으로 잠금.
if command -v setfacl >/dev/null 2>&1; then
setfacl -b "$OUTPUT_FILE" 2>/dev/null || true
fi
chmod 600 "$OUTPUT_FILE"
SIZE=$(du -h "$OUTPUT_FILE" | cut -f1)
log "백업 성공 (크기: ${SIZE})"
else
log "ERROR: 백업 실패 - $(cat "$DUMP_ERR")"
rm -f "$OUTPUT_FILE" "$DUMP_ERR"
exit 1
fi
rm -f "$DUMP_ERR"
# ---- 오래된 백업 정리 ----
log "${RETENTION_DAYS}일 초과 백업 파일 정리 중..."
DELETED_COUNT=0
while IFS= read -r -d '' OLD_FILE; do
log " 삭제: ${OLD_FILE}"
rm -f "$OLD_FILE"
DELETED_COUNT=$((DELETED_COUNT + 1))
done < <(find "$OUTPUT_DIR" -maxdepth 1 -type f \
-name 'chocomae_*.sql.gz' \
-mtime +"${RETENTION_DAYS}" -print0)
log "정리 완료: ${DELETED_COUNT}개 파일 삭제"
# ---- 로그 크기 제한 (rotate) ----
# LOG_MAX_BYTES 초과 시 마지막 LOG_KEEP_LINES 줄만 남기고 축소.
# 이 스크립트만 로그를 쓰는 구조라 tail | mv 방식으로도 레이스가 없음.
if [ -f "$LOG_FILE" ]; then
LOG_SIZE=$(wc -c < "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$LOG_SIZE" -gt "$LOG_MAX_BYTES" ]; then
tail -n "$LOG_KEEP_LINES" "$LOG_FILE" > "${LOG_FILE}.tmp"
mv "${LOG_FILE}.tmp" "$LOG_FILE"
log "로그 로테이션: ${LOG_SIZE} 바이트 → 최근 ${LOG_KEEP_LINES}줄로 축소"
fi
fi
log "완료"
# 로그 파일 권한 잠금: 상위 디렉터리 ACL 상속으로 777 이 되는 것을 방지.
# 매 실행 끝에 한 번 정규화하므로 첫 실행/로그 로테이션 모든 경로를 커버함.
if [ -f "$LOG_FILE" ]; then
if command -v setfacl >/dev/null 2>&1; then
setfacl -b "$LOG_FILE" 2>/dev/null || true
fi
chmod 640 "$LOG_FILE"
fi