Files
gitea-ops/gitea-backups/bin/backup.sh
T
prajwal a181625c89 Initial commit: backup + mirror automation for self-hosted Gitea
Includes:
- gitea-backups/bin/backup.sh (per-push bundle + DB snapshot to local + S3)
- gitea-backups/bin/install-hooks.sh (idempotent post-receive shim installer)
- gitea-backups/bin/retention.sh (count-based retention: keep newest 7 dates)
- gitea-mirror/bin/auto-mirror.sh (Gitea -> GitHub push mirror automation,
  hardened against Gitea outages)
- crontab.txt (reference for the 3 cron entries)
- README.md (architecture, layout, bootstrap)
2026-05-09 06:02:09 +00:00

114 lines
4.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Gitea per-push backup: bundles the pushed repo + snapshots SQLite DB.
# Invoked from each repo's hooks/post-receive.d/zzz-backup shim.
# Failures NEVER block the push — git push has already succeeded by the time we run.
set -u
# Git hooks run with $HOME pointing oddly (the repo dir or similar), so tools
# like rclone can't find ~/.config/rclone/rclone.conf. Force it.
export HOME="/home/ubuntu"
BACKUP_ROOT="/home/ubuntu/gitea-backups"
GITEA_REPOS="/home/ubuntu/gitea/data/gitea-repositories"
GITEA_DB="/home/ubuntu/gitea/data/gitea.db"
RETENTION_DAYS=7
LOG="${BACKUP_ROOT}/logs/backup.log"
STATUS="${BACKUP_ROOT}/logs/last-status"
# S3 (offsite). Empty S3_BUCKET disables the upload step entirely.
S3_BUCKET="toqqer-gitea-backup"
S3_REMOTE="s3" # rclone remote name (configured in ~/.config/rclone/rclone.conf)
ts="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
day="$(date -u +%Y-%m-%d)"
log() { printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >> "$LOG"; }
# git invokes the hook with $GIT_DIR set to the bare repo path
repo_path="${GIT_DIR:-$(pwd)}"
repo_path="$(cd "$repo_path" && pwd)"
# derive owner/name from the path: .../gitea-repositories/<owner>/<name>.git
rel="${repo_path#${GITEA_REPOS}/}"
owner="${rel%%/*}"
name="${rel#*/}"
name="${name%.git}"
if [[ -z "$owner" || -z "$name" || "$owner" == "$rel" ]]; then
log "SKIP: could not parse owner/name from $repo_path"
echo "FAIL ${ts} parse-error ${repo_path}" > "$STATUS"
exit 0 # never block the push
fi
log "START ${owner}/${name}"
# ---- 1) repo bundle ---------------------------------------------------------
bundle_dir="${BACKUP_ROOT}/repos/${owner}/${name}"
mkdir -p "$bundle_dir"
bundle_file="${bundle_dir}/${ts}.bundle"
if git -C "$repo_path" bundle create "$bundle_file" --all 2>>"$LOG"; then
bundle_size=$(stat -c %s "$bundle_file" 2>/dev/null || echo 0)
log "OK bundle ${owner}/${name} -> ${bundle_file} (${bundle_size} bytes)"
else
log "FAIL bundle ${owner}/${name}"
echo "FAIL ${ts} bundle ${owner}/${name}" > "$STATUS"
# continue to DB backup anyway
fi
# ---- 2) SQLite hot backup (via python3, avoids sqlite3 CLI dependency) ----
db_file="${BACKUP_ROOT}/db/${ts}-gitea.db"
if python3 -c "
import sqlite3, sys
src = sqlite3.connect('${GITEA_DB}')
dst = sqlite3.connect('${db_file}')
src.backup(dst)
dst.close(); src.close()
" 2>>"$LOG"; then
if gzip -f "$db_file" 2>>"$LOG"; then
db_size=$(stat -c %s "${db_file}.gz" 2>/dev/null || echo 0)
log "OK db -> ${db_file}.gz (${db_size} bytes)"
else
log "FAIL gzip db ${db_file}"
fi
else
log "FAIL sqlite .backup -> ${db_file}"
fi
# ---- 3) S3 offsite upload --------------------------------------------------
# Layout: s3://<bucket>/YYYY-MM-DD/repos/<owner>/<name>/<ts>.bundle
# s3://<bucket>/YYYY-MM-DD/db/<ts>-gitea.db.gz
# 7-day retention enforced by the bucket's lifecycle policy, NOT here.
s3_status="skipped"
if [[ -n "$S3_BUCKET" ]] && command -v rclone >/dev/null 2>&1; then
s3_bundle_target="${S3_REMOTE}:${S3_BUCKET}/${day}/repos/${owner}/${name}/${ts}.bundle"
s3_db_target="${S3_REMOTE}:${S3_BUCKET}/${day}/db/${ts}-gitea.db.gz"
s3_ok=1
if [[ -f "$bundle_file" ]]; then
if rclone copyto --no-traverse "$bundle_file" "$s3_bundle_target" 2>>"$LOG"; then
log "OK s3 bundle -> ${s3_bundle_target}"
else
log "FAIL s3 bundle -> ${s3_bundle_target}"
s3_ok=0
fi
fi
if [[ -f "${db_file}.gz" ]]; then
if rclone copyto --no-traverse "${db_file}.gz" "$s3_db_target" 2>>"$LOG"; then
log "OK s3 db -> ${s3_db_target}"
else
log "FAIL s3 db -> ${s3_db_target}"
s3_ok=0
fi
fi
s3_status=$([[ $s3_ok -eq 1 ]] && echo "ok" || echo "fail")
fi
# ---- 4) cleanup is handled by retention.sh (daily cron, "keep newest N dates"). ----
# Push-triggered cleanup was removed because age-based cleanup would empty the
# bucket during quiet periods. retention.sh keeps the most-recent N date-folders
# regardless of how old they are.
echo "OK ${ts} ${owner}/${name} s3=${s3_status}" > "$STATUS"
log "END ${owner}/${name} s3=${s3_status}"
exit 0