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>
146 lines
4.7 KiB
Bash
Executable File
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"
|