Add daily backup scripts for PostgreSQL, MariaDB, and Gitea.
Enable scheduled backups of ncue.net databases and git.ncue.net repository mirrors via .env-driven shell scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
34
.env.example
Normal file
34
.env.example
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
## Database (PostgreSQL)
|
||||||
|
PG_DB_HOST=ncue.net
|
||||||
|
PG_DB_PORT=5432
|
||||||
|
PG_DB_NAME=ncue
|
||||||
|
PG_DB_USER=ncue
|
||||||
|
PG_DB_PASSWORD=your_pg_password
|
||||||
|
|
||||||
|
## Database (MariaDB)
|
||||||
|
MR_DB_HOST=ncue.net
|
||||||
|
MR_DB_PORT=3306
|
||||||
|
MR_DB_NAME=wordpress
|
||||||
|
MR_DB_USER=ncue
|
||||||
|
MR_DB_PASSWORD=your_mr_password
|
||||||
|
|
||||||
|
## Gitea
|
||||||
|
GIT_BASE_URL=https://git.ncue.net
|
||||||
|
GIT_USER=your_gitea_user
|
||||||
|
GIT_TOKEN=your_gitea_token
|
||||||
|
|
||||||
|
## Backup
|
||||||
|
BACKUP_DIR=/Users/dsyoon/workspace/backup/ncue
|
||||||
|
BACKUP_RETENTION_DAYS=30
|
||||||
|
|
||||||
|
## PostgreSQL backup options
|
||||||
|
PG_BACKUP_SCOPE=all
|
||||||
|
PG_BACKUP_GLOBALS=1
|
||||||
|
PG_BACKUP_SUPERUSER=postgres
|
||||||
|
PG_BACKUP_SUPERUSER_PASSWORD=
|
||||||
|
|
||||||
|
## MariaDB backup options
|
||||||
|
MR_BACKUP_SCOPE=all
|
||||||
|
|
||||||
|
## Gitea backup options
|
||||||
|
GITEA_REPO_LIMIT=100
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.env
|
||||||
|
backup/
|
||||||
|
*.log
|
||||||
100
README.md
Normal file
100
README.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# ncue_backup
|
||||||
|
|
||||||
|
ncue.net PostgreSQL, MariaDB, Gitea 저장소를 매일 백업하는 스크립트 모음입니다.
|
||||||
|
백업 방식은 [ai_platform](https://git.xavis.co.kr/AI_Innovation_Team/ai_platform)의 `scripts/pg-backup.sh` 패턴을 따릅니다.
|
||||||
|
|
||||||
|
## 백업 대상
|
||||||
|
|
||||||
|
| 구분 | 대상 | 스크립트 |
|
||||||
|
|------|------|----------|
|
||||||
|
| PostgreSQL | 연결 가능한 모든 DB (`ai_web_platform`, `meeting_ai`, `ncue`, `tts` 등) | `scripts/pg-backup.sh` |
|
||||||
|
| MariaDB | 사용자 DB 전체 (`giteadb`, `wordpress`, `roundcube` 등) | `scripts/mr-backup.sh` |
|
||||||
|
| Gitea | 접근 가능한 모든 Git 저장소 mirror | `scripts/gitea-backup.sh` |
|
||||||
|
|
||||||
|
Gitea 메타데이터 DB(`giteadb`)는 MariaDB 백업에 포함됩니다. Git 소스는 mirror 백업으로 별도 보관합니다.
|
||||||
|
|
||||||
|
## 사전 요구사항
|
||||||
|
|
||||||
|
- `pg_dump`, `psql` (PostgreSQL 클라이언트)
|
||||||
|
- `mysqldump`, `mysql` (MariaDB 클라이언트)
|
||||||
|
- `git`, `curl`, `python3`, `gzip`
|
||||||
|
- `pymysql` (MariaDB 클라이언트 미설치 시 Python 폴백용, `pip install -r requirements.txt`)
|
||||||
|
|
||||||
|
macOS 예시:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install libpq mariadb
|
||||||
|
echo 'export PATH="/opt/homebrew/opt/libpq/bin:$PATH"' >> ~/.zshrc
|
||||||
|
```
|
||||||
|
|
||||||
|
## 설정
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# .env 편집 (DB 접속 정보, Gitea 토큰, BACKUP_DIR)
|
||||||
|
```
|
||||||
|
|
||||||
|
주요 환경 변수:
|
||||||
|
|
||||||
|
| 변수 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| `PG_DB_*` | PostgreSQL 접속 정보 |
|
||||||
|
| `MR_DB_*` | MariaDB 접속 정보 |
|
||||||
|
| `GIT_BASE_URL` | Gitea URL (기본 `https://git.ncue.net`) |
|
||||||
|
| `GIT_USER`, `GIT_TOKEN` | Gitea mirror 인증 |
|
||||||
|
| `BACKUP_DIR` | 백업 루트 (기본 `프로젝트/backup`) |
|
||||||
|
| `BACKUP_RETENTION_DAYS` | 보관 일수 (기본 30) |
|
||||||
|
|
||||||
|
## 실행
|
||||||
|
|
||||||
|
통합 백업 (권장):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/daily-backup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
개별 백업:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/pg-backup.sh
|
||||||
|
bash scripts/mr-backup.sh
|
||||||
|
bash scripts/gitea-backup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 백업 결과 구조
|
||||||
|
|
||||||
|
```text
|
||||||
|
backup/
|
||||||
|
├─ 20260526/
|
||||||
|
│ ├─ postgresql/
|
||||||
|
│ │ ├─ 00_manifest.txt
|
||||||
|
│ │ ├─ ncue.dump
|
||||||
|
│ │ └─ ...
|
||||||
|
│ ├─ mariadb/
|
||||||
|
│ │ ├─ 00_manifest.txt
|
||||||
|
│ │ ├─ giteadb.sql.gz
|
||||||
|
│ │ └─ ...
|
||||||
|
│ └─ gitea/
|
||||||
|
│ ├─ 00_manifest.txt
|
||||||
|
│ ├─ 00_repos.json
|
||||||
|
│ └─ mirrors -> ../../_gitea_mirrors/
|
||||||
|
├─ _gitea_mirrors/ # Gitea mirror 영구 저장 (증분 업데이트)
|
||||||
|
│ └─ owner__repo.git/
|
||||||
|
└─ latest -> 20260526/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 매일 cron 등록
|
||||||
|
|
||||||
|
```cron
|
||||||
|
0 2 * * * cd /Users/dsyoon/workspace/ncue_backup && /usr/bin/bash scripts/daily-backup.sh >> /var/log/ncue-backup.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 복원 참고
|
||||||
|
|
||||||
|
- PostgreSQL: `pg_restore -d dbname file.dump`
|
||||||
|
- MariaDB: `gunzip -c file.sql.gz | mysql -u user -p dbname`
|
||||||
|
- Gitea mirror: `git clone /path/to/mirrors/owner__repo.git restored-repo`
|
||||||
|
|
||||||
|
## 변경 이력
|
||||||
|
|
||||||
|
- 2026-05-26: PostgreSQL/MariaDB/Gitea 일일 백업 스크립트 초기 구성
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pymysql>=1.1.0
|
||||||
50
scripts/daily-backup.sh
Executable file
50
scripts/daily-backup.sh
Executable file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# PostgreSQL + MariaDB + Gitea 저장소 일일 통합 백업
|
||||||
|
# 운영 cron 예: 0 2 * * * cd /path/ncue_backup && bash scripts/daily-backup.sh >> /var/log/ncue-backup.log 2>&1
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
# shellcheck source=scripts/lib/load-env.sh
|
||||||
|
source "$REPO_ROOT/scripts/lib/load-env.sh"
|
||||||
|
# shellcheck source=scripts/lib/common.sh
|
||||||
|
source "$REPO_ROOT/scripts/lib/common.sh"
|
||||||
|
|
||||||
|
load_project_env "$REPO_ROOT/.env"
|
||||||
|
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-$REPO_ROOT/backup}"
|
||||||
|
BACKUP_STAMP="${BACKUP_STAMP:-$(date +%Y%m%d)}"
|
||||||
|
BACKUP_RUN_DIR="$BACKUP_DIR/$BACKUP_STAMP"
|
||||||
|
export BACKUP_DIR BACKUP_STAMP BACKUP_RUN_DIR
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_RUN_DIR" "$BACKUP_DIR/latest"
|
||||||
|
|
||||||
|
echo "[$(log_ts)] daily-backup start → $BACKUP_RUN_DIR"
|
||||||
|
|
||||||
|
failures=0
|
||||||
|
|
||||||
|
run_step() {
|
||||||
|
local name="$1"
|
||||||
|
local script="$2"
|
||||||
|
echo "[$(log_ts)] daily-backup step: $name"
|
||||||
|
if bash "$script"; then
|
||||||
|
echo "[$(log_ts)] daily-backup step ok: $name"
|
||||||
|
else
|
||||||
|
echo "[$(log_ts)] daily-backup step failed: $name" >&2
|
||||||
|
failures=$((failures + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_step "postgresql" "$REPO_ROOT/scripts/pg-backup.sh"
|
||||||
|
run_step "mariadb" "$REPO_ROOT/scripts/mr-backup.sh"
|
||||||
|
run_step "gitea" "$REPO_ROOT/scripts/gitea-backup.sh"
|
||||||
|
|
||||||
|
ln -sfn "$BACKUP_RUN_DIR" "$BACKUP_DIR/latest"
|
||||||
|
|
||||||
|
prune_old_backups "$BACKUP_DIR" "${BACKUP_RETENTION_DAYS:-30}"
|
||||||
|
|
||||||
|
if [[ "$failures" -gt 0 ]]; then
|
||||||
|
echo "[$(log_ts)] daily-backup finished with ${failures} failure(s)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[$(log_ts)] daily-backup done → $BACKUP_DIR/latest"
|
||||||
145
scripts/gitea-backup.sh
Executable file
145
scripts/gitea-backup.sh
Executable file
@@ -0,0 +1,145 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Gitea 저장소 mirror 백업. .env의 GIT_* 및 GITEA_* 사용.
|
||||||
|
# Gitea 메타 DB(giteadb 등)는 scripts/mr-backup.sh(MariaDB 전체 백업)에 포함됩니다.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
# shellcheck source=scripts/lib/load-env.sh
|
||||||
|
source "$REPO_ROOT/scripts/lib/load-env.sh"
|
||||||
|
# shellcheck source=scripts/lib/common.sh
|
||||||
|
source "$REPO_ROOT/scripts/lib/common.sh"
|
||||||
|
|
||||||
|
load_project_env "$REPO_ROOT/.env"
|
||||||
|
|
||||||
|
: "${GIT_USER:?GIT_USER is required in .env}"
|
||||||
|
: "${GIT_TOKEN:?GIT_TOKEN is required in .env}"
|
||||||
|
|
||||||
|
GIT_BASE_URL="${GIT_BASE_URL:-https://git.ncue.net}"
|
||||||
|
GITEA_API_URL="${GITEA_API_URL:-${GIT_BASE_URL%/}/api/v1}"
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-$REPO_ROOT/backup}"
|
||||||
|
BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-30}"
|
||||||
|
GITEA_REPO_LIMIT="${GITEA_REPO_LIMIT:-100}"
|
||||||
|
|
||||||
|
if ! command -v git >/dev/null 2>&1; then
|
||||||
|
echo "gitea-backup: git not found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! command -v python3 >/dev/null 2>&1; then
|
||||||
|
echo "gitea-backup: python3 not found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
resolve_run_dir "gitea"
|
||||||
|
mkdir -p "$BACKUP_DIR/latest"
|
||||||
|
PERSISTENT_MIRROR_ROOT="${GITEA_MIRROR_DIR:-$BACKUP_DIR/_gitea_mirrors}"
|
||||||
|
MIRROR_ROOT="$PERSISTENT_MIRROR_ROOT"
|
||||||
|
mkdir -p "$MIRROR_ROOT"
|
||||||
|
|
||||||
|
echo "[$(log_ts)] gitea-backup start → $RUN_DIR (mirrors=$MIRROR_ROOT, api=${GITEA_API_URL})"
|
||||||
|
|
||||||
|
REPO_LIST_FILE="$RUN_DIR/00_repos.json"
|
||||||
|
MANIFEST="$RUN_DIR/00_manifest.txt"
|
||||||
|
|
||||||
|
mirror_result="$(python3 - "$GITEA_API_URL" "$GIT_TOKEN" "$GITEA_REPO_LIMIT" "$REPO_LIST_FILE" "$MIRROR_ROOT" "$GIT_USER" "$GIT_TOKEN" <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
api_base, token, limit_str, repo_list_file, mirror_root, git_user, git_token = sys.argv[1:8]
|
||||||
|
limit = int(limit_str)
|
||||||
|
headers = {"Authorization": f"token {token}"}
|
||||||
|
|
||||||
|
def fetch_json(url: str):
|
||||||
|
req = urllib.request.Request(url, headers=headers)
|
||||||
|
with urllib.request.urlopen(req, timeout=120) as resp:
|
||||||
|
return json.load(resp)
|
||||||
|
|
||||||
|
repos = []
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
url = f"{api_base.rstrip('/')}/user/repos?limit={limit}&page={page}"
|
||||||
|
try:
|
||||||
|
batch = fetch_json(url)
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
print(f"API error {exc.code} for {url}", file=sys.stderr)
|
||||||
|
raise SystemExit(1) from exc
|
||||||
|
if not batch:
|
||||||
|
break
|
||||||
|
repos.extend(batch)
|
||||||
|
if len(batch) < limit:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
with open(repo_list_file, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(repos, fh, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
ok = 0
|
||||||
|
fail = 0
|
||||||
|
manifest_lines = []
|
||||||
|
|
||||||
|
for repo in repos:
|
||||||
|
full_name = repo["full_name"]
|
||||||
|
clone_url = repo["clone_url"]
|
||||||
|
safe_name = full_name.replace("/", "__")
|
||||||
|
mirror_path = os.path.join(mirror_root, f"{safe_name}.git")
|
||||||
|
auth_url = clone_url.replace("https://", f"https://{git_user}:{git_token}@", 1)
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["GIT_TERMINAL_PROMPT"] = "0"
|
||||||
|
|
||||||
|
if os.path.isdir(mirror_path):
|
||||||
|
subprocess.run(["git", "-C", mirror_path, "remote", "set-url", "origin", auth_url], check=True)
|
||||||
|
proc = subprocess.run(["git", "-C", mirror_path, "remote", "update", "--prune"], env=env)
|
||||||
|
action = "updated"
|
||||||
|
else:
|
||||||
|
proc = subprocess.run(["git", "clone", "--mirror", auth_url, mirror_path], env=env)
|
||||||
|
action = "cloned"
|
||||||
|
|
||||||
|
if proc.returncode == 0:
|
||||||
|
size_kb = max(1, sum(
|
||||||
|
os.path.getsize(os.path.join(root, name))
|
||||||
|
for root, _, files in os.walk(mirror_path)
|
||||||
|
for name in files
|
||||||
|
) // 1024)
|
||||||
|
manifest_lines.append(f"{full_name} {action} {size_kb}KB")
|
||||||
|
ok += 1
|
||||||
|
else:
|
||||||
|
manifest_lines.append(f"{full_name} failed")
|
||||||
|
fail += 1
|
||||||
|
|
||||||
|
print(json.dumps({"ok": ok, "fail": fail, "count": len(repos), "manifest": manifest_lines}))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
repo_count="$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['count'])" "$mirror_result")"
|
||||||
|
mirror_ok="$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['ok'])" "$mirror_result")"
|
||||||
|
mirror_fail="$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['fail'])" "$mirror_result")"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# gitea-backup manifest $(log_ts)"
|
||||||
|
echo "api=${GITEA_API_URL}"
|
||||||
|
echo "user=${GIT_USER}"
|
||||||
|
echo "repo_count=${repo_count}"
|
||||||
|
python3 -c "import json,sys; [print(line) for line in json.loads(sys.argv[1])['manifest']]" "$mirror_result"
|
||||||
|
} > "$MANIFEST"
|
||||||
|
|
||||||
|
if [[ "$mirror_ok" -eq 0 ]]; then
|
||||||
|
echo "gitea-backup: all repository mirrors failed." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$mirror_fail" -gt 0 ]]; then
|
||||||
|
echo "[$(log_ts)] warning: ${mirror_fail} mirror(s) failed, ${mirror_ok} succeeded." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
ln -sfn "$MIRROR_ROOT" "$RUN_DIR/mirrors"
|
||||||
|
|
||||||
|
update_latest_link "gitea"
|
||||||
|
|
||||||
|
if [[ -z "${BACKUP_RUN_DIR:-}" ]]; then
|
||||||
|
prune_old_backups "$BACKUP_DIR" "$BACKUP_RETENTION_DAYS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[$(log_ts)] gitea-backup done: ${mirror_ok} repository mirror(s), latest → $BACKUP_DIR/latest/gitea"
|
||||||
66
scripts/lib/common.sh
Executable file
66
scripts/lib/common.sh
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 백업 스크립트 공통 유틸
|
||||||
|
|
||||||
|
log_ts() { date '+%Y-%m-%dT%H:%M:%S%z'; }
|
||||||
|
|
||||||
|
# 오래된 YYYYMMDD 백업 디렉터리 삭제
|
||||||
|
# 사용: prune_old_backups "$BACKUP_ROOT" "$RETENTION_DAYS"
|
||||||
|
prune_old_backups() {
|
||||||
|
local backup_root="$1"
|
||||||
|
local retention_days="$2"
|
||||||
|
local cutoff removed=0 base day entry
|
||||||
|
|
||||||
|
[[ "$retention_days" =~ ^[0-9]+$ ]] || return 0
|
||||||
|
[[ "$retention_days" -le 0 ]] && return 0
|
||||||
|
|
||||||
|
if date -d "1 day ago" +%Y%m%d >/dev/null 2>&1; then
|
||||||
|
cutoff=$(date -d "${retention_days} days ago" +%Y%m%d)
|
||||||
|
else
|
||||||
|
cutoff=$(date -v-"${retention_days}"d +%Y%m%d)
|
||||||
|
fi
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
for entry in "$backup_root"/*; do
|
||||||
|
[[ -d "$entry" ]] || continue
|
||||||
|
base=$(basename "$entry")
|
||||||
|
[[ "$base" == "latest" ]] && continue
|
||||||
|
if [[ "$base" =~ ^([0-9]{8}) ]]; then
|
||||||
|
day="${BASH_REMATCH[1]}"
|
||||||
|
if [[ "$day" < "$cutoff" ]]; then
|
||||||
|
rm -rf "$entry"
|
||||||
|
removed=$((removed + 1))
|
||||||
|
echo "[$(log_ts)] retention: removed $entry (backup date $day < cutoff $cutoff)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
shopt -u nullglob
|
||||||
|
echo "[$(log_ts)] retention: pruned ${removed} dir(s); keeping backups from ${cutoff} onward (${retention_days} days)"
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_run_dir() {
|
||||||
|
local component="$1"
|
||||||
|
local stamp="${BACKUP_STAMP:-$(date +%Y%m%d)}"
|
||||||
|
local root="${BACKUP_DIR:-$REPO_ROOT/backup}"
|
||||||
|
|
||||||
|
if [[ -n "${BACKUP_RUN_DIR:-}" ]]; then
|
||||||
|
RUN_DIR="$BACKUP_RUN_DIR/$component"
|
||||||
|
else
|
||||||
|
RUN_DIR="$root/$stamp/$component"
|
||||||
|
fi
|
||||||
|
mkdir -p "$RUN_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
update_latest_link() {
|
||||||
|
local component="$1"
|
||||||
|
local root="${BACKUP_DIR:-$REPO_ROOT/backup}"
|
||||||
|
local stamp="${BACKUP_STAMP:-$(date +%Y%m%d)}"
|
||||||
|
local target
|
||||||
|
|
||||||
|
if [[ -n "${BACKUP_RUN_DIR:-}" ]]; then
|
||||||
|
target="$BACKUP_RUN_DIR/$component"
|
||||||
|
else
|
||||||
|
target="$root/$stamp/$component"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ln -sfn "$target" "$root/latest/$component"
|
||||||
|
}
|
||||||
24
scripts/lib/load-env.sh
Executable file
24
scripts/lib/load-env.sh
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 프로젝트 루트 .env에서 KEY=VALUE 줄만 export (섹션 헤더 [..]·주석 무시)
|
||||||
|
load_project_env() {
|
||||||
|
local env_file="$1"
|
||||||
|
if [[ ! -f "$env_file" ]]; then
|
||||||
|
echo "load_project_env: .env not found: $env_file" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
line="${line#"${line%%[![:space:]]*}"}"
|
||||||
|
[[ -z "$line" || "$line" == \#* ]] && continue
|
||||||
|
[[ "$line" == \[* ]] && continue
|
||||||
|
if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
|
||||||
|
local key="${BASH_REMATCH[1]}"
|
||||||
|
local val="${BASH_REMATCH[2]}"
|
||||||
|
if [[ "$val" =~ ^\"(.*)\"$ ]]; then
|
||||||
|
val="${BASH_REMATCH[1]}"
|
||||||
|
elif [[ "$val" =~ ^\'(.*)\'$ ]]; then
|
||||||
|
val="${BASH_REMATCH[1]}"
|
||||||
|
fi
|
||||||
|
export "$key=$val"
|
||||||
|
fi
|
||||||
|
done < "$env_file"
|
||||||
|
}
|
||||||
130
scripts/lib/mr_dump.py
Normal file
130
scripts/lib/mr_dump.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""MariaDB logical dump fallback when mysqldump is unavailable."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import gzip
|
||||||
|
import sys
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
import pymysql
|
||||||
|
|
||||||
|
|
||||||
|
SYSTEM_SCHEMAS = {"information_schema", "performance_schema", "sys", "mysql"}
|
||||||
|
|
||||||
|
|
||||||
|
def sql_quote(value) -> str:
|
||||||
|
"""Return a SQL literal for a Python value."""
|
||||||
|
if value is None:
|
||||||
|
return "NULL"
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return str(value)
|
||||||
|
if isinstance(value, (bytes, bytearray)):
|
||||||
|
return "0x" + value.hex()
|
||||||
|
escaped = (
|
||||||
|
str(value)
|
||||||
|
.replace("\\", "\\\\")
|
||||||
|
.replace("\0", "\\0")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "\\r")
|
||||||
|
.replace("\t", "\\t")
|
||||||
|
.replace("'", "\\'")
|
||||||
|
.replace('"', '\\"')
|
||||||
|
)
|
||||||
|
return f"'{escaped}'"
|
||||||
|
|
||||||
|
|
||||||
|
def list_databases(conn) -> list[str]:
|
||||||
|
"""List user databases excluding system schemas."""
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT schema_name
|
||||||
|
FROM information_schema.schemata
|
||||||
|
WHERE schema_name NOT IN (%s, %s, %s, %s)
|
||||||
|
ORDER BY schema_name
|
||||||
|
""",
|
||||||
|
tuple(SYSTEM_SCHEMAS),
|
||||||
|
)
|
||||||
|
return [row[0] for row in cur.fetchall()]
|
||||||
|
|
||||||
|
|
||||||
|
def dump_database(host: str, port: int, user: str, password: str, database: str, output_path: str) -> None:
|
||||||
|
"""Dump one database to a gzip SQL file."""
|
||||||
|
conn = pymysql.connect(
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
user=user,
|
||||||
|
password=password,
|
||||||
|
database=database,
|
||||||
|
charset="utf8mb4",
|
||||||
|
connect_timeout=30,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
lines: list[str] = [
|
||||||
|
"-- MariaDB dump via ncue_backup Python fallback",
|
||||||
|
"SET NAMES utf8mb4;",
|
||||||
|
"SET FOREIGN_KEY_CHECKS=0;",
|
||||||
|
f"CREATE DATABASE IF NOT EXISTS `{database}`;",
|
||||||
|
f"USE `{database}`;",
|
||||||
|
]
|
||||||
|
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'")
|
||||||
|
tables = [row[0] for row in cur.fetchall()]
|
||||||
|
|
||||||
|
for table in tables:
|
||||||
|
cur.execute(f"SHOW CREATE TABLE `{table}`")
|
||||||
|
create_sql = cur.fetchone()[1]
|
||||||
|
lines.append(f"DROP TABLE IF EXISTS `{table}`;")
|
||||||
|
lines.append(f"{create_sql};")
|
||||||
|
|
||||||
|
cur.execute(f"SELECT * FROM `{table}`")
|
||||||
|
columns = [desc[0] for desc in cur.description]
|
||||||
|
col_list = ", ".join(f"`{col}`" for col in columns)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
rows = cur.fetchmany(500)
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
for row in rows:
|
||||||
|
values = ", ".join(sql_quote(value) for value in row)
|
||||||
|
lines.append(f"INSERT INTO `{table}` ({col_list}) VALUES ({values});")
|
||||||
|
|
||||||
|
lines.append("SET FOREIGN_KEY_CHECKS=1;")
|
||||||
|
payload = "\n".join(lines) + "\n"
|
||||||
|
with gzip.open(output_path, "wt", encoding="utf-8") as fh:
|
||||||
|
fh.write(payload)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Iterable[str] | None = None) -> int:
|
||||||
|
"""CLI entrypoint."""
|
||||||
|
parser = argparse.ArgumentParser(description="MariaDB dump fallback")
|
||||||
|
parser.add_argument("--host", required=True)
|
||||||
|
parser.add_argument("--port", type=int, default=3306)
|
||||||
|
parser.add_argument("--user", required=True)
|
||||||
|
parser.add_argument("--password", required=True)
|
||||||
|
parser.add_argument("--database", required=True)
|
||||||
|
parser.add_argument("--output", required=True)
|
||||||
|
args = parser.parse_args(list(argv) if argv is not None else None)
|
||||||
|
|
||||||
|
dump_database(
|
||||||
|
host=args.host,
|
||||||
|
port=args.port,
|
||||||
|
user=args.user,
|
||||||
|
password=args.password,
|
||||||
|
database=args.database,
|
||||||
|
output_path=args.output,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
raise SystemExit(main())
|
||||||
|
except pymysql.MySQLError as exc:
|
||||||
|
print(f"mr_dump.py: database error: {exc}", file=sys.stderr)
|
||||||
|
raise SystemExit(1) from exc
|
||||||
160
scripts/mr-backup.sh
Executable file
160
scripts/mr-backup.sh
Executable file
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# MariaDB/MySQL 논리 백업 (mysqldump). .env의 MR_DB_* 및 MR_BACKUP_* 사용.
|
||||||
|
# MR_BACKUP_SCOPE=all 이면 접근 가능한 사용자 DB 전체를 각각 .sql.gz 로 저장.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
# shellcheck source=scripts/lib/load-env.sh
|
||||||
|
source "$REPO_ROOT/scripts/lib/load-env.sh"
|
||||||
|
# shellcheck source=scripts/lib/common.sh
|
||||||
|
source "$REPO_ROOT/scripts/lib/common.sh"
|
||||||
|
|
||||||
|
load_project_env "$REPO_ROOT/.env"
|
||||||
|
|
||||||
|
: "${MR_DB_HOST:?MR_DB_HOST is required in .env}"
|
||||||
|
: "${MR_DB_USER:?MR_DB_USER is required in .env}"
|
||||||
|
: "${MR_DB_PASSWORD:?MR_DB_PASSWORD is required in .env}"
|
||||||
|
|
||||||
|
MR_DB_PORT="${MR_DB_PORT:-3306}"
|
||||||
|
MR_DB_NAME="${MR_DB_NAME:-mysql}"
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-$REPO_ROOT/backup}"
|
||||||
|
BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-30}"
|
||||||
|
MR_BACKUP_SCOPE="${MR_BACKUP_SCOPE:-all}"
|
||||||
|
|
||||||
|
MYSQL_BIN="${MYSQL_BIN:-mysql}"
|
||||||
|
MYSQLDUMP_BIN="${MYSQLDUMP_BIN:-mysqldump}"
|
||||||
|
MR_DUMP_PY="$REPO_ROOT/scripts/lib/mr_dump.py"
|
||||||
|
|
||||||
|
use_python_fallback=0
|
||||||
|
if ! command -v "$MYSQLDUMP_BIN" >/dev/null 2>&1; then
|
||||||
|
if [[ -f "$MR_DUMP_PY" ]] && python3 -c "import pymysql" >/dev/null 2>&1; then
|
||||||
|
echo "[$(log_ts)] mr-backup: mysqldump not found; using Python fallback (pymysql)." >&2
|
||||||
|
use_python_fallback=1
|
||||||
|
else
|
||||||
|
echo "mr-backup: mysqldump not found. Install mariadb-client (apt) or mariadb (brew), or install pymysql." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ "$use_python_fallback" -eq 0 ]] && ! command -v "$MYSQL_BIN" >/dev/null 2>&1; then
|
||||||
|
echo "mr-backup: mysql client not found. Install mariadb-client." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mysql_common_args=(
|
||||||
|
-h "$MR_DB_HOST"
|
||||||
|
-P "$MR_DB_PORT"
|
||||||
|
-u "$MR_DB_USER"
|
||||||
|
--password="$MR_DB_PASSWORD"
|
||||||
|
--batch
|
||||||
|
--skip-column-names
|
||||||
|
)
|
||||||
|
|
||||||
|
list_all_databases() {
|
||||||
|
if [[ "$use_python_fallback" -eq 1 ]]; then
|
||||||
|
python3 - "$MR_DB_HOST" "$MR_DB_PORT" "$MR_DB_USER" "$MR_DB_PASSWORD" <<'PY'
|
||||||
|
import pymysql, sys
|
||||||
|
host, port, user, password = sys.argv[1:5]
|
||||||
|
conn = pymysql.connect(host=host, port=int(port), user=user, password=password, connect_timeout=30)
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT schema_name FROM information_schema.schemata "
|
||||||
|
"WHERE schema_name NOT IN ('information_schema','performance_schema','sys','mysql') "
|
||||||
|
"ORDER BY schema_name"
|
||||||
|
)
|
||||||
|
for row in cur.fetchall():
|
||||||
|
print(row[0])
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
PY
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
"$MYSQL_BIN" "${mysql_common_args[@]}" -e \
|
||||||
|
"SELECT schema_name FROM information_schema.schemata
|
||||||
|
WHERE schema_name NOT IN ('information_schema','performance_schema','sys','mysql')
|
||||||
|
ORDER BY schema_name;"
|
||||||
|
}
|
||||||
|
|
||||||
|
dump_database() {
|
||||||
|
local db_name="$1"
|
||||||
|
local dump_file="$2"
|
||||||
|
if [[ "$use_python_fallback" -eq 1 ]]; then
|
||||||
|
python3 "$MR_DUMP_PY" \
|
||||||
|
--host "$MR_DB_HOST" \
|
||||||
|
--port "$MR_DB_PORT" \
|
||||||
|
--user "$MR_DB_USER" \
|
||||||
|
--password "$MR_DB_PASSWORD" \
|
||||||
|
--database "$db_name" \
|
||||||
|
--output "$dump_file"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
"$MYSQLDUMP_BIN" \
|
||||||
|
-h "$MR_DB_HOST" \
|
||||||
|
-P "$MR_DB_PORT" \
|
||||||
|
-u "$MR_DB_USER" \
|
||||||
|
--password="$MR_DB_PASSWORD" \
|
||||||
|
--single-transaction \
|
||||||
|
--routines \
|
||||||
|
--triggers \
|
||||||
|
--events \
|
||||||
|
--databases "$db_name" \
|
||||||
|
| gzip -c > "$dump_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_run_dir "mariadb"
|
||||||
|
mkdir -p "$BACKUP_DIR/latest"
|
||||||
|
|
||||||
|
echo "[$(log_ts)] mr-backup start → $RUN_DIR (scope=${MR_BACKUP_SCOPE}, retention ${BACKUP_RETENTION_DAYS} days)"
|
||||||
|
|
||||||
|
declare -a TARGET_DBS=()
|
||||||
|
if [[ "$MR_BACKUP_SCOPE" == "single" ]]; then
|
||||||
|
TARGET_DBS=("$MR_DB_NAME")
|
||||||
|
else
|
||||||
|
while IFS= read -r db; do
|
||||||
|
[[ -n "$db" ]] && TARGET_DBS+=("$db")
|
||||||
|
done < <(list_all_databases)
|
||||||
|
if [[ ${#TARGET_DBS[@]} -eq 0 ]]; then
|
||||||
|
echo "mr-backup: no databases found to backup." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
MANIFEST="$RUN_DIR/00_manifest.txt"
|
||||||
|
{
|
||||||
|
echo "# mr-backup manifest $(log_ts)"
|
||||||
|
echo "scope=${MR_BACKUP_SCOPE}"
|
||||||
|
echo "host=${MR_DB_HOST}:${MR_DB_PORT}"
|
||||||
|
echo "user=${MR_DB_USER}"
|
||||||
|
} > "$MANIFEST"
|
||||||
|
|
||||||
|
dump_ok=0
|
||||||
|
dump_fail=0
|
||||||
|
for db_name in "${TARGET_DBS[@]}"; do
|
||||||
|
dump_file="$RUN_DIR/${db_name}.sql.gz"
|
||||||
|
echo "[$(log_ts)] dumping database: $db_name"
|
||||||
|
if dump_database "$db_name" "$dump_file"; then
|
||||||
|
bytes="$(wc -c < "$dump_file" | tr -d ' ')"
|
||||||
|
echo "[$(log_ts)] dump saved: $dump_file (${bytes} bytes)"
|
||||||
|
echo "${db_name}.sql.gz ${bytes}" >> "$MANIFEST"
|
||||||
|
dump_ok=$((dump_ok + 1))
|
||||||
|
else
|
||||||
|
echo "[$(log_ts)] dump failed: $db_name" >&2
|
||||||
|
dump_fail=$((dump_fail + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$dump_ok" -eq 0 ]]; then
|
||||||
|
echo "mr-backup: all database dumps failed." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$dump_fail" -gt 0 ]]; then
|
||||||
|
echo "[$(log_ts)] warning: ${dump_fail} database dump(s) failed, ${dump_ok} succeeded." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
update_latest_link "mariadb"
|
||||||
|
|
||||||
|
if [[ -z "${BACKUP_RUN_DIR:-}" ]]; then
|
||||||
|
prune_old_backups "$BACKUP_DIR" "$BACKUP_RETENTION_DAYS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[$(log_ts)] mr-backup done: ${dump_ok} database(s), latest → $BACKUP_DIR/latest/mariadb"
|
||||||
153
scripts/pg-backup.sh
Executable file
153
scripts/pg-backup.sh
Executable file
@@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# PostgreSQL 논리 백업 (pg_dump -Fc). .env의 PG_DB_* 및 PG_BACKUP_* 사용.
|
||||||
|
# PG_BACKUP_SCOPE=all 이면 서버의 연결 가능·비템플릿 DB 전체를 각각 .dump 로 저장.
|
||||||
|
# 운영 cron 예: 0 2 * * * cd /path/ncue_backup && bash scripts/daily-backup.sh >> /var/log/ncue-backup.log 2>&1
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
# shellcheck source=scripts/lib/load-env.sh
|
||||||
|
source "$REPO_ROOT/scripts/lib/load-env.sh"
|
||||||
|
# shellcheck source=scripts/lib/common.sh
|
||||||
|
source "$REPO_ROOT/scripts/lib/common.sh"
|
||||||
|
|
||||||
|
load_project_env "$REPO_ROOT/.env"
|
||||||
|
|
||||||
|
: "${PG_DB_HOST:?PG_DB_HOST is required in .env}"
|
||||||
|
: "${PG_DB_USER:?PG_DB_USER is required in .env}"
|
||||||
|
: "${PG_DB_PASSWORD:?PG_DB_PASSWORD is required in .env}"
|
||||||
|
|
||||||
|
PG_DB_PORT="${PG_DB_PORT:-5432}"
|
||||||
|
PG_DB_NAME="${PG_DB_NAME:-postgres}"
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-$REPO_ROOT/backup}"
|
||||||
|
BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-30}"
|
||||||
|
PG_BACKUP_SCOPE="${PG_BACKUP_SCOPE:-all}"
|
||||||
|
PG_BACKUP_GLOBALS="${PG_BACKUP_GLOBALS:-1}"
|
||||||
|
PG_BACKUP_SUPERUSER="${PG_BACKUP_SUPERUSER:-postgres}"
|
||||||
|
|
||||||
|
export PGHOST="$PG_DB_HOST"
|
||||||
|
export PGPORT="$PG_DB_PORT"
|
||||||
|
|
||||||
|
if ! command -v pg_dump >/dev/null 2>&1; then
|
||||||
|
echo "pg-backup: pg_dump not found. Install postgresql-client (apt) or postgresql (brew)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! command -v psql >/dev/null 2>&1; then
|
||||||
|
echo "pg-backup: psql not found. Install postgresql-client." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set_backup_credentials() {
|
||||||
|
if [[ -n "${PG_BACKUP_SUPERUSER_PASSWORD:-}" ]]; then
|
||||||
|
export PGUSER="$PG_BACKUP_SUPERUSER"
|
||||||
|
export PGPASSWORD="$PG_BACKUP_SUPERUSER_PASSWORD"
|
||||||
|
else
|
||||||
|
export PGUSER="$PG_DB_USER"
|
||||||
|
export PGPASSWORD="$PG_DB_PASSWORD"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
list_all_databases() {
|
||||||
|
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -v ON_ERROR_STOP=1 -tAc \
|
||||||
|
"SELECT datname FROM pg_database WHERE datallowconn AND NOT datistemplate ORDER BY datname;"
|
||||||
|
}
|
||||||
|
|
||||||
|
dump_database() {
|
||||||
|
local db_name="$1"
|
||||||
|
local dump_file="$2"
|
||||||
|
pg_dump \
|
||||||
|
-h "$PGHOST" \
|
||||||
|
-p "$PGPORT" \
|
||||||
|
-U "$PGUSER" \
|
||||||
|
-d "$db_name" \
|
||||||
|
-Fc \
|
||||||
|
--no-password \
|
||||||
|
-f "$dump_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_globals() {
|
||||||
|
if [[ "$PG_BACKUP_GLOBALS" != "1" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ -z "${PG_BACKUP_SUPERUSER_PASSWORD:-}" ]]; then
|
||||||
|
echo "[$(log_ts)] globals skipped: set PG_BACKUP_SUPERUSER_PASSWORD for role/global backup." >&2
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local prev_user="$PGUSER" prev_pass="$PGPASSWORD"
|
||||||
|
export PGUSER="$PG_BACKUP_SUPERUSER"
|
||||||
|
export PGPASSWORD="$PG_BACKUP_SUPERUSER_PASSWORD"
|
||||||
|
pg_dumpall \
|
||||||
|
-h "$PGHOST" \
|
||||||
|
-p "$PGPORT" \
|
||||||
|
-U "$PGUSER" \
|
||||||
|
--globals-only \
|
||||||
|
--no-password \
|
||||||
|
-f "$RUN_DIR/00_globals.sql"
|
||||||
|
export PGUSER="$prev_user"
|
||||||
|
export PGPASSWORD="$prev_pass"
|
||||||
|
echo "[$(log_ts)] globals saved: $RUN_DIR/00_globals.sql"
|
||||||
|
}
|
||||||
|
|
||||||
|
set_backup_credentials
|
||||||
|
resolve_run_dir "postgresql"
|
||||||
|
mkdir -p "$BACKUP_DIR/latest"
|
||||||
|
|
||||||
|
echo "[$(log_ts)] pg-backup start → $RUN_DIR (scope=${PG_BACKUP_SCOPE}, retention ${BACKUP_RETENTION_DAYS} days)"
|
||||||
|
|
||||||
|
if [[ -z "${PG_BACKUP_SUPERUSER_PASSWORD:-}" && "$PG_BACKUP_SCOPE" == "all" ]]; then
|
||||||
|
echo "[$(log_ts)] note: PG_BACKUP_SUPERUSER_PASSWORD not set; backing up only databases visible to ${PG_DB_USER}." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
backup_globals
|
||||||
|
|
||||||
|
declare -a TARGET_DBS=()
|
||||||
|
if [[ "$PG_BACKUP_SCOPE" == "single" ]]; then
|
||||||
|
TARGET_DBS=("$PG_DB_NAME")
|
||||||
|
else
|
||||||
|
while IFS= read -r db; do
|
||||||
|
[[ -n "$db" ]] && TARGET_DBS+=("$db")
|
||||||
|
done < <(list_all_databases)
|
||||||
|
if [[ ${#TARGET_DBS[@]} -eq 0 ]]; then
|
||||||
|
echo "pg-backup: no databases found to backup." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
MANIFEST="$RUN_DIR/00_manifest.txt"
|
||||||
|
{
|
||||||
|
echo "# pg-backup manifest $(log_ts)"
|
||||||
|
echo "scope=${PG_BACKUP_SCOPE}"
|
||||||
|
echo "host=${PGHOST}:${PGPORT}"
|
||||||
|
echo "user=${PGUSER}"
|
||||||
|
} > "$MANIFEST"
|
||||||
|
|
||||||
|
dump_ok=0
|
||||||
|
dump_fail=0
|
||||||
|
for db_name in "${TARGET_DBS[@]}"; do
|
||||||
|
dump_file="$RUN_DIR/${db_name}.dump"
|
||||||
|
echo "[$(log_ts)] dumping database: $db_name"
|
||||||
|
if dump_database "$db_name" "$dump_file"; then
|
||||||
|
bytes="$(wc -c < "$dump_file" | tr -d ' ')"
|
||||||
|
echo "[$(log_ts)] dump saved: $dump_file (${bytes} bytes)"
|
||||||
|
echo "${db_name}.dump ${bytes}" >> "$MANIFEST"
|
||||||
|
dump_ok=$((dump_ok + 1))
|
||||||
|
else
|
||||||
|
echo "[$(log_ts)] dump failed: $db_name" >&2
|
||||||
|
dump_fail=$((dump_fail + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$dump_ok" -eq 0 ]]; then
|
||||||
|
echo "pg-backup: all database dumps failed." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$dump_fail" -gt 0 ]]; then
|
||||||
|
echo "[$(log_ts)] warning: ${dump_fail} database dump(s) failed, ${dump_ok} succeeded." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
update_latest_link "postgresql"
|
||||||
|
|
||||||
|
if [[ -z "${BACKUP_RUN_DIR:-}" ]]; then
|
||||||
|
prune_old_backups "$BACKUP_DIR" "$BACKUP_RETENTION_DAYS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[$(log_ts)] pg-backup done: ${dump_ok} database(s), latest → $BACKUP_DIR/latest/postgresql"
|
||||||
Reference in New Issue
Block a user