269 lines
9.3 KiB
Bash
Executable File
269 lines
9.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Create emergency Git bundle backups from Gitea over a non-interactive Git remote.
|
|
# This is a repository-history fallback only. It does not replace `gitea dump`,
|
|
# does not back up Gitea DB rows, settings, issues, packages, secrets, or LFS.
|
|
|
|
set -euo pipefail
|
|
|
|
GITEA_BASE_URL="${GITEA_BASE_URL:-http://192.168.0.110:3001}"
|
|
GIT_REMOTE_BASE="${GIT_REMOTE_BASE:-}"
|
|
LOCAL_GITEA_CONTAINER="${LOCAL_GITEA_CONTAINER:-}"
|
|
LOCAL_REPO_ROOT="${LOCAL_REPO_ROOT:-/data/git/repositories}"
|
|
OUTPUT_ROOT="${OUTPUT_ROOT:-/home/ollama/backup/110/gitea/git-bundles}"
|
|
TIMEOUT_LS_REMOTE_SECONDS="${TIMEOUT_LS_REMOTE_SECONDS:-60}"
|
|
TIMEOUT_CLONE_SECONDS="${TIMEOUT_CLONE_SECONDS:-180}"
|
|
TIMEOUT_BUNDLE_SECONDS="${TIMEOUT_BUNDLE_SECONDS:-180}"
|
|
TIMEOUT_VERIFY_SECONDS="${TIMEOUT_VERIFY_SECONDS:-60}"
|
|
STAMP="${STAMP:-$(date +%Y%m%d-%H%M%S)}"
|
|
DRY_RUN=0
|
|
REPOS=()
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage: scripts/backup/gitea-repo-bundle-backup.sh [options]
|
|
|
|
Options:
|
|
--repo OWNER/NAME Repository to bundle. May be passed more than once.
|
|
--output-root PATH Bundle root. Default: /home/ollama/backup/110/gitea/git-bundles
|
|
--gitea-base URL Gitea base URL. Default: http://192.168.0.110:3001
|
|
--git-remote-base URL
|
|
Git remote base, such as ssh://git@192.168.0.110:2222.
|
|
When set, it is used instead of --gitea-base.
|
|
--local-container NAME
|
|
Read bare repos directly from a local Gitea container.
|
|
This avoids tokens and SSH keys on the Gitea host.
|
|
--local-repo-root PATH
|
|
Repo root inside the local container.
|
|
--stamp STAMP Output directory stamp. Default: current local timestamp.
|
|
--dry-run Print target URLs without cloning or writing bundles.
|
|
-h, --help Show this help.
|
|
|
|
This script uses GIT_TERMINAL_PROMPT=0 and never asks for, reads, or prints
|
|
tokens or passwords. Private repositories without an existing non-interactive
|
|
credential or SSH key fail
|
|
closed in the manifest.
|
|
USAGE
|
|
}
|
|
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--repo)
|
|
shift
|
|
REPOS+=("${1:?--repo requires OWNER/NAME}")
|
|
;;
|
|
--output-root)
|
|
shift
|
|
OUTPUT_ROOT="${1:?--output-root requires PATH}"
|
|
;;
|
|
--gitea-base)
|
|
shift
|
|
GITEA_BASE_URL="${1:?--gitea-base requires URL}"
|
|
;;
|
|
--git-remote-base)
|
|
shift
|
|
GIT_REMOTE_BASE="${1:?--git-remote-base requires URL}"
|
|
;;
|
|
--local-container)
|
|
shift
|
|
LOCAL_GITEA_CONTAINER="${1:?--local-container requires NAME}"
|
|
;;
|
|
--local-repo-root)
|
|
shift
|
|
LOCAL_REPO_ROOT="${1:?--local-repo-root requires PATH}"
|
|
;;
|
|
--stamp)
|
|
shift
|
|
STAMP="${1:?--stamp requires value}"
|
|
;;
|
|
--dry-run)
|
|
DRY_RUN=1
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown argument: $1" >&2
|
|
usage >&2
|
|
exit 64
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [ "${#REPOS[@]}" -eq 0 ]; then
|
|
REPOS=(
|
|
"wooo/awoooi"
|
|
"wooo/ewoooc"
|
|
"wooo/2026FIFAWorldCup"
|
|
"wooo/agent-bounty-protocol"
|
|
"wooo/AwoooGo"
|
|
"wooo/stockplatform-v2"
|
|
"wooo/vibework"
|
|
"wooo/momo-pro-system"
|
|
"wooo/tsenyang-website"
|
|
"wooo/clawbot-v5"
|
|
"wooo/bitan-pharmacy"
|
|
"wooo/vtuber"
|
|
)
|
|
fi
|
|
|
|
repo_remote_url() {
|
|
repo="$1"
|
|
if [ -n "$GIT_REMOTE_BASE" ]; then
|
|
printf '%s/%s.git\n' "${GIT_REMOTE_BASE%/}" "$repo"
|
|
return
|
|
fi
|
|
printf '%s/%s.git\n' "${GITEA_BASE_URL%/}" "$repo"
|
|
}
|
|
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
for repo in "${REPOS[@]}"; do
|
|
if [ -n "$LOCAL_GITEA_CONTAINER" ]; then
|
|
printf 'DRY_RUN repo=%s transport=container container=%s\n' "$repo" "$LOCAL_GITEA_CONTAINER"
|
|
else
|
|
printf 'DRY_RUN repo=%s url=%s\n' "$repo" "$(repo_remote_url "$repo")"
|
|
fi
|
|
done
|
|
exit 0
|
|
fi
|
|
|
|
backup_root="${OUTPUT_ROOT%/}/${STAMP}"
|
|
tmp_root="$(mktemp -d "${TMPDIR:-/tmp}/gitea-bundle.XXXXXX")"
|
|
manifest="${backup_root}/manifest.tsv"
|
|
mkdir -p "$backup_root"
|
|
trap 'rm -rf "$tmp_root"' EXIT
|
|
|
|
printf 'repo\tstatus\thead_count\tbundle\tchecksum\n' > "$manifest"
|
|
failed_count=0
|
|
success_count=0
|
|
|
|
for repo in "${REPOS[@]}"; do
|
|
slug="${repo//\//__}"
|
|
refs_file="${backup_root}/${slug}.refs"
|
|
err_file="${backup_root}/${slug}.err"
|
|
bundle_file="${backup_root}/${slug}.bundle"
|
|
checksum_file="${bundle_file}.sha256"
|
|
repo_tmp="${tmp_root}/${slug}.git"
|
|
|
|
if [ -n "$LOCAL_GITEA_CONTAINER" ]; then
|
|
container_repo="${LOCAL_REPO_ROOT%/}/${repo}.git"
|
|
container_bundle="/tmp/awoooi-${slug}-${STAMP}.bundle"
|
|
if ! docker exec -u git "$LOCAL_GITEA_CONTAINER" test -d "$container_repo" >/dev/null 2>&1; then
|
|
owner="${repo%%/*}"
|
|
name="${repo#*/}"
|
|
resolved_paths="$(docker exec -u git "$LOCAL_GITEA_CONTAINER" sh -c \
|
|
'find "$1/$2" -mindepth 1 -maxdepth 1 -type d -iname "$3.git" -print' \
|
|
sh "${LOCAL_REPO_ROOT%/}" "$owner" "$name" 2>>"$err_file" || true)"
|
|
resolved_count="$(printf '%s\n' "$resolved_paths" | awk 'NF {count += 1} END {print count + 0}')"
|
|
if [ "$resolved_count" -ne 1 ]; then
|
|
printf '%s\tlocal_repo_read_failed\t0\t\t\n' "$repo" >> "$manifest"
|
|
failed_count=$((failed_count + 1))
|
|
continue
|
|
fi
|
|
container_repo="$resolved_paths"
|
|
fi
|
|
if ! timeout "$TIMEOUT_LS_REMOTE_SECONDS" docker exec -u git "$LOCAL_GITEA_CONTAINER" \
|
|
git -C "$container_repo" for-each-ref --format='%(objectname)%09%(refname)' \
|
|
>"$refs_file" 2>"$err_file"; then
|
|
printf '%s\tlocal_repo_read_failed\t0\t\t\n' "$repo" >> "$manifest"
|
|
failed_count=$((failed_count + 1))
|
|
continue
|
|
fi
|
|
|
|
head_count="$(wc -l < "$refs_file" | tr -d ' ')"
|
|
if ! timeout "$TIMEOUT_BUNDLE_SECONDS" docker exec -u git "$LOCAL_GITEA_CONTAINER" \
|
|
git -C "$container_repo" bundle create "$container_bundle" --all \
|
|
>/dev/null 2>>"$err_file"; then
|
|
docker exec -u git "$LOCAL_GITEA_CONTAINER" rm -f "$container_bundle" >/dev/null 2>&1 || true
|
|
printf '%s\tbundle_failed\t%s\t\t\n' "$repo" "$head_count" >> "$manifest"
|
|
failed_count=$((failed_count + 1))
|
|
continue
|
|
fi
|
|
|
|
if ! timeout "$TIMEOUT_VERIFY_SECONDS" docker exec -u git "$LOCAL_GITEA_CONTAINER" \
|
|
git -C "$container_repo" bundle verify "$container_bundle" \
|
|
>/dev/null 2>>"$err_file"; then
|
|
docker exec -u git "$LOCAL_GITEA_CONTAINER" rm -f "$container_bundle" >/dev/null 2>&1 || true
|
|
printf '%s\tverify_failed\t%s\t\t\n' "$repo" "$head_count" >> "$manifest"
|
|
failed_count=$((failed_count + 1))
|
|
continue
|
|
fi
|
|
|
|
if ! docker cp "${LOCAL_GITEA_CONTAINER}:${container_bundle}" "$bundle_file" 2>>"$err_file"; then
|
|
docker exec -u git "$LOCAL_GITEA_CONTAINER" rm -f "$container_bundle" >/dev/null 2>&1 || true
|
|
printf '%s\tcopy_failed\t%s\t\t\n' "$repo" "$head_count" >> "$manifest"
|
|
failed_count=$((failed_count + 1))
|
|
continue
|
|
fi
|
|
docker exec -u git "$LOCAL_GITEA_CONTAINER" rm -f "$container_bundle" >/dev/null 2>&1 || true
|
|
(sha256sum "$bundle_file" 2>/dev/null || shasum -a 256 "$bundle_file") > "$checksum_file"
|
|
printf '%s\tok\t%s\t%s\t%s\n' "$repo" "$head_count" "$bundle_file" "$checksum_file" >> "$manifest"
|
|
success_count=$((success_count + 1))
|
|
continue
|
|
fi
|
|
|
|
url="$(repo_remote_url "$repo")"
|
|
|
|
if ! GIT_TERMINAL_PROMPT=0 timeout "$TIMEOUT_LS_REMOTE_SECONDS" \
|
|
git ls-remote --heads "$url" >"$refs_file" 2>"$err_file"; then
|
|
printf '%s\tls_remote_failed\t0\t\t\n' "$repo" >> "$manifest"
|
|
failed_count=$((failed_count + 1))
|
|
continue
|
|
fi
|
|
|
|
head_count="$(wc -l < "$refs_file" | tr -d ' ')"
|
|
rm -rf "$repo_tmp"
|
|
if ! GIT_TERMINAL_PROMPT=0 timeout "$TIMEOUT_CLONE_SECONDS" \
|
|
git clone --mirror --quiet "$url" "$repo_tmp" 2>>"$err_file"; then
|
|
printf '%s\tclone_failed\t%s\t\t\n' "$repo" "$head_count" >> "$manifest"
|
|
failed_count=$((failed_count + 1))
|
|
continue
|
|
fi
|
|
|
|
if ! timeout "$TIMEOUT_BUNDLE_SECONDS" \
|
|
git -C "$repo_tmp" bundle create "$bundle_file" --all >/dev/null 2>>"$err_file"; then
|
|
printf '%s\tbundle_failed\t%s\t\t\n' "$repo" "$head_count" >> "$manifest"
|
|
failed_count=$((failed_count + 1))
|
|
continue
|
|
fi
|
|
|
|
if ! timeout "$TIMEOUT_VERIFY_SECONDS" \
|
|
git -C "$repo_tmp" bundle verify "$bundle_file" >/dev/null 2>>"$err_file"; then
|
|
printf '%s\tverify_failed\t%s\t%s\t\n' "$repo" "$head_count" "$bundle_file" >> "$manifest"
|
|
failed_count=$((failed_count + 1))
|
|
continue
|
|
fi
|
|
|
|
(sha256sum "$bundle_file" 2>/dev/null || shasum -a 256 "$bundle_file") > "$checksum_file"
|
|
rm -rf "$repo_tmp"
|
|
printf '%s\tok\t%s\t%s\t%s\n' "$repo" "$head_count" "$bundle_file" "$checksum_file" >> "$manifest"
|
|
success_count=$((success_count + 1))
|
|
done
|
|
|
|
expected_count="${#REPOS[@]}"
|
|
cat "$manifest"
|
|
|
|
if [ "$failed_count" -ne 0 ] || [ "$success_count" -ne "$expected_count" ]; then
|
|
printf 'ERROR bundle backup incomplete success=%s expected=%s failed=%s; complete links unchanged\n' \
|
|
"$success_count" "$expected_count" "$failed_count" >&2
|
|
exit 1
|
|
fi
|
|
|
|
promotion_path_ready() {
|
|
link="$1"
|
|
if [ ! -e "$link" ] || [ -L "$link" ]; then
|
|
return 0
|
|
fi
|
|
printf 'ERROR promotion path exists and is not a symlink: %s\n' "$link" >&2
|
|
return 1
|
|
}
|
|
|
|
latest_link="${OUTPUT_ROOT%/}/latest"
|
|
complete_link="${OUTPUT_ROOT%/}/latest-private-complete"
|
|
promotion_path_ready "$latest_link"
|
|
promotion_path_ready "$complete_link"
|
|
ln -sfn "$backup_root" "$latest_link"
|
|
ln -sfn "$backup_root" "$complete_link"
|
|
printf 'BUNDLE_BACKUP_COMPLETE=1 success=%s expected=%s root=%s\n' "$success_count" "$expected_count" "$backup_root"
|