diff --git a/backup/backup_primary.sh b/backup/backup_primary.sh new file mode 100755 index 0000000..227aeb7 --- /dev/null +++ b/backup/backup_primary.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# backup/backup_primary.sh — Create a full Gitea backup (dump) and store on Fedora +# Depends on: Phase 1 complete (Gitea running on Unraid) +# +# What's in the dump: +# - SQLite database (users, tokens, SSH keys, OAuth, webhooks, org/team, issues) +# - All git repositories +# - app.ini config +# +# Steps: +# 1. Run `gitea dump` inside the container to create a zip archive +# 2. SCP the dump from Unraid to Fedora (offsite storage) +# 3. Clean up the dump from Unraid /tmp +# 4. Prune old backups beyond retention count +# 5. Print backup summary +# ============================================================================= + +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +source "${SCRIPT_DIR}/lib/common.sh" + +load_env +require_vars UNRAID_IP UNRAID_SSH_USER UNRAID_GITEA_DATA_PATH \ + FEDORA_IP FEDORA_SSH_USER \ + BACKUP_STORAGE_PATH BACKUP_RETENTION_COUNT + +log_info "=== Gitea Primary Backup ===" + +# --------------------------------------------------------------------------- +# Step 1: Run gitea dump inside the container +# The -u git flag is important — gitea dump must run as the git user who +# owns the repository files. The dump is created in /tmp inside the container +# which maps to /tmp on the host via the default Docker tmpfs mount. +# --------------------------------------------------------------------------- +TIMESTAMP=$(date +%Y%m%d-%H%M%S) +DUMP_FILENAME="gitea-dump-${TIMESTAMP}.zip" +DUMP_REMOTE_PATH="/tmp/${DUMP_FILENAME}" + +log_info "Creating Gitea dump on Unraid..." +ssh_exec UNRAID "docker exec -u git gitea gitea dump \ + -c /data/gitea/conf/app.ini \ + -f '${DUMP_REMOTE_PATH}'" +log_success "Dump created: ${DUMP_FILENAME}" + +# --------------------------------------------------------------------------- +# Step 2: Create backup storage directory on Fedora and transfer dump +# The dump goes from Unraid → local machine → Fedora because direct +# Unraid→Fedora SCP may not have SSH keys set up. Using the MacBook as +# a relay is more reliable with our existing SSH config. +# --------------------------------------------------------------------------- +log_info "Transferring dump to Fedora backup storage..." +ssh_exec FEDORA "mkdir -p '${BACKUP_STORAGE_PATH}'" + +# SCP from Unraid to local temp, then to Fedora +LOCAL_TMP=$(mktemp -d) +scp_to_local() { + local ip_var="UNRAID_IP" user_var="UNRAID_SSH_USER" port_var="UNRAID_SSH_PORT" + local ip="${!ip_var:-}" user="${!user_var:-}" port="${!port_var:-22}" + scp -o ConnectTimeout=10 -o BatchMode=yes -P "$port" \ + "${user}@${ip}:${DUMP_REMOTE_PATH}" "${LOCAL_TMP}/${DUMP_FILENAME}" +} +scp_to_local +scp_to FEDORA "${LOCAL_TMP}/${DUMP_FILENAME}" "${BACKUP_STORAGE_PATH}/${DUMP_FILENAME}" +rm -rf "$LOCAL_TMP" +log_success "Dump transferred to Fedora: ${BACKUP_STORAGE_PATH}/${DUMP_FILENAME}" + +# --------------------------------------------------------------------------- +# Step 3: Clean up dump from Unraid /tmp +# No reason to keep the dump on Unraid — it's on Fedora now. +# --------------------------------------------------------------------------- +ssh_exec UNRAID "rm -f '${DUMP_REMOTE_PATH}'" +log_info "Cleaned up dump from Unraid" + +# --------------------------------------------------------------------------- +# Step 4: Prune old backups beyond retention count +# Lists all gitea-dump-*.zip files sorted by time (newest first), then +# removes everything beyond BACKUP_RETENTION_COUNT. +# --------------------------------------------------------------------------- +log_info "Pruning old backups (keeping ${BACKUP_RETENTION_COUNT})..." +ssh_exec FEDORA "cd '${BACKUP_STORAGE_PATH}' && ls -t gitea-dump-*.zip 2>/dev/null | tail -n +\$((${BACKUP_RETENTION_COUNT}+1)) | xargs -r rm -f" + +REMAINING=$(ssh_exec FEDORA "ls -1 '${BACKUP_STORAGE_PATH}'/gitea-dump-*.zip 2>/dev/null | wc -l" | xargs) +log_info "Backups remaining: ${REMAINING}" + +# --------------------------------------------------------------------------- +# Step 5: Summary +# --------------------------------------------------------------------------- +DUMP_SIZE=$(ssh_exec FEDORA "du -h '${BACKUP_STORAGE_PATH}/${DUMP_FILENAME}'" | awk '{print $1}') + +printf '\n' +log_success "Backup complete" +log_info " File: ${DUMP_FILENAME}" +log_info " Size: ${DUMP_SIZE}" +log_info " Path: ${BACKUP_STORAGE_PATH}/${DUMP_FILENAME} (on Fedora)" +log_info " Total backups: ${REMAINING}" diff --git a/backup/restore_to_primary.sh b/backup/restore_to_primary.sh new file mode 100755 index 0000000..f345835 --- /dev/null +++ b/backup/restore_to_primary.sh @@ -0,0 +1,195 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# backup/restore_to_primary.sh — Restore a Gitea backup to Unraid primary +# DESTRUCTIVE: Replaces all Gitea data on Unraid with the backup contents. +# +# Usage: ./backup/restore_to_primary.sh --archive +# can be: +# - A path on Fedora (e.g. /mnt/nvme/gitea-backups/gitea-dump-*.zip) +# - A local path on this machine +# +# Steps: +# 1. Stop Gitea container +# 2. Back up current data as safety net (data.pre-restore) +# 3. Extract archive to Gitea data directory +# 4. Restart Gitea container +# 5. Wait for Gitea to be ready +# 6. Verify admin login +# 7. Regenerate API token (old token from dump may be stale) +# ============================================================================= + +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +source "${SCRIPT_DIR}/lib/common.sh" + +load_env +require_vars UNRAID_IP UNRAID_SSH_USER UNRAID_GITEA_DATA_PATH \ + GITEA_INTERNAL_URL GITEA_ADMIN_USER GITEA_ADMIN_PASSWORD + +DATA_PATH="$UNRAID_GITEA_DATA_PATH" + +# --------------------------------------------------------------------------- +# Parse arguments +# --------------------------------------------------------------------------- +ARCHIVE_PATH="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --archive) + if [[ $# -lt 2 ]]; then log_error "--archive requires a path"; exit 1; fi + ARCHIVE_PATH="$2"; shift 2 ;; + --help|-h) + echo "Usage: $(basename "$0") --archive " + exit 0 ;; + *) log_error "Unknown argument: $1"; exit 1 ;; + esac +done + +if [[ -z "$ARCHIVE_PATH" ]]; then + log_error "Missing required --archive " + echo "Usage: $(basename "$0") --archive " + exit 1 +fi + +log_warn "=== Gitea Restore to Primary ===" +log_warn "This will REPLACE all Gitea data on Unraid with the backup." + +printf 'Continue? This is IRREVERSIBLE (current data will be backed up first). [y/N] ' +read -r confirm +if [[ ! "$confirm" =~ ^[Yy]$ ]]; then + log_info "Restore cancelled" + exit 0 +fi + +# --------------------------------------------------------------------------- +# Step 1: Transfer archive to Unraid /tmp if needed +# If the archive is a local file, SCP it directly. If it's on Fedora, +# we relay through the local machine. +# --------------------------------------------------------------------------- +log_step 1 "Preparing archive..." +ARCHIVE_NAME=$(basename "$ARCHIVE_PATH") +UNRAID_ARCHIVE="/tmp/${ARCHIVE_NAME}" + +if [[ -f "$ARCHIVE_PATH" ]]; then + # Local file — SCP to Unraid + log_info "Uploading local archive to Unraid..." + scp_to UNRAID "$ARCHIVE_PATH" "$UNRAID_ARCHIVE" +else + # Assume path is on Fedora — relay through local machine + log_info "Transferring archive from Fedora to Unraid..." + LOCAL_TMP=$(mktemp -d) + # SCP from Fedora to local + ip="${FEDORA_IP:-}" user="${FEDORA_SSH_USER:-}" port="${FEDORA_SSH_PORT:-22}" + scp -o ConnectTimeout=10 -o BatchMode=yes -P "$port" \ + "${user}@${ip}:${ARCHIVE_PATH}" "${LOCAL_TMP}/${ARCHIVE_NAME}" + # SCP from local to Unraid + scp_to UNRAID "${LOCAL_TMP}/${ARCHIVE_NAME}" "$UNRAID_ARCHIVE" + rm -rf "$LOCAL_TMP" +fi +log_success "Archive ready on Unraid: ${UNRAID_ARCHIVE}" + +# --------------------------------------------------------------------------- +# Step 2: Stop Gitea container +# --------------------------------------------------------------------------- +log_step 2 "Stopping Gitea container..." +ssh_exec UNRAID "cd '${DATA_PATH}' && docker compose down 2>/dev/null || docker-compose down" || true +log_success "Gitea container stopped" + +# --------------------------------------------------------------------------- +# Step 3: Back up current data as safety net +# Rename data/ → data.pre-restore/ so we can roll back if needed. +# If a pre-restore backup already exists, append a timestamp. +# --------------------------------------------------------------------------- +log_step 3 "Backing up current data..." +if ssh_exec UNRAID "test -d '${DATA_PATH}/data'" 2>/dev/null; then + BACKUP_SUFFIX="pre-restore-$(date +%Y%m%d-%H%M%S)" + ssh_exec UNRAID "mv '${DATA_PATH}/data' '${DATA_PATH}/data.${BACKUP_SUFFIX}'" + log_success "Current data backed up to data.${BACKUP_SUFFIX}" +else + log_info "No existing data directory to back up" +fi + +# --------------------------------------------------------------------------- +# Step 4: Extract archive +# gitea dump creates a zip with: gitea-db.sql (or gitea.db), repos/, app.ini +# We extract to a temp dir, then move files to the correct locations. +# --------------------------------------------------------------------------- +log_step 4 "Extracting archive..." +ssh_exec UNRAID "mkdir -p '${DATA_PATH}/data' && cd '${DATA_PATH}' && unzip -o '${UNRAID_ARCHIVE}' -d restore-tmp" + +# Move restored files into place +# The dump structure varies by Gitea version — handle common layouts +ssh_exec UNRAID " + cd '${DATA_PATH}/restore-tmp' + # Move repos if present + if [ -d repos ]; then mv repos '${DATA_PATH}/data/'; fi + # Move gitea.db (SQLite database) + if [ -f gitea.db ]; then mv gitea.db '${DATA_PATH}/data/'; fi + if [ -f gitea-db.sql ]; then mv gitea-db.sql '${DATA_PATH}/data/'; fi + # Move config if present + if [ -f app.ini ]; then + mkdir -p '${DATA_PATH}/config' + mv app.ini '${DATA_PATH}/config/' + fi + # Move any remaining data files + if [ -d data ]; then cp -r data/* '${DATA_PATH}/data/' 2>/dev/null || true; fi + # Clean up temp + cd '${DATA_PATH}' && rm -rf restore-tmp +" +log_success "Archive extracted and files placed" + +# Clean up archive from Unraid /tmp +ssh_exec UNRAID "rm -f '${UNRAID_ARCHIVE}'" + +# --------------------------------------------------------------------------- +# Step 5: Restart Gitea container +# --------------------------------------------------------------------------- +log_step 5 "Starting Gitea container..." +ssh_exec UNRAID "cd '${DATA_PATH}' && docker compose up -d 2>/dev/null || docker-compose up -d" +log_success "Gitea container started" + +# --------------------------------------------------------------------------- +# Step 6: Wait for Gitea to be ready +# --------------------------------------------------------------------------- +log_step 6 "Waiting for Gitea to be ready..." +wait_for_http "${GITEA_INTERNAL_URL}/api/v1/version" 120 + +# --------------------------------------------------------------------------- +# Step 7: Verify admin login +# --------------------------------------------------------------------------- +log_step 7 "Verifying admin login..." +if curl -sf -u "${GITEA_ADMIN_USER}:${GITEA_ADMIN_PASSWORD}" "${GITEA_INTERNAL_URL}/api/v1/user" -o /dev/null 2>/dev/null; then + log_success "Admin login verified" +else + log_error "Admin login failed — check credentials or backup integrity" + exit 1 +fi + +# --------------------------------------------------------------------------- +# Step 8: Regenerate API token +# Old tokens from the dump may conflict or be stale. Generate a fresh one. +# --------------------------------------------------------------------------- +log_step 8 "Regenerating API token..." +TOKEN_RESPONSE=$(curl -sf -u "${GITEA_ADMIN_USER}:${GITEA_ADMIN_PASSWORD}" \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"name":"migration-token-restored","scopes":["all"]}' \ + "${GITEA_INTERNAL_URL}/api/v1/users/${GITEA_ADMIN_USER}/tokens") + +NEW_TOKEN=$(printf '%s' "$TOKEN_RESPONSE" | jq -r '.sha1') + +if [[ -z "$NEW_TOKEN" ]] || [[ "$NEW_TOKEN" == "null" ]]; then + log_warn "Could not generate new API token — may need manual regeneration" +else + save_env_var "GITEA_ADMIN_TOKEN" "$NEW_TOKEN" + log_success "New API token generated and saved to .env" +fi + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +printf '\n' +log_success "Restore complete — Gitea is running with restored data" +log_info "Pre-restore data preserved at: ${DATA_PATH}/data.${BACKUP_SUFFIX:-unknown}" +log_info "Verify all repos and users are accessible before deleting the pre-restore backup."