Files
ncue_backup/scripts/gitea-backup.sh
dsyoon 3c9d9283bd 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>
2026-05-26 17:02:53 +09:00

146 lines
4.7 KiB
Bash
Executable File

#!/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"