Add gitea-host backup scripts
This commit is contained in:
1 parent
a6ad39e605
commit
8a8edf9fcb
2 files changed
+503
-30
No files matched your search
@@ -0,0 +1,413 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# backup_create.sh
|
||||||
|
#
|
||||||
|
# Companion to setup-git-server.sh: creates ONE zip file that contains
|
||||||
|
# everything needed to bring a Gitea instance installed by that script back
|
||||||
|
# with backup_restore.sh:
|
||||||
|
#
|
||||||
|
# - the database (as a portable SQL dump, SQLite)
|
||||||
|
# - all repositories
|
||||||
|
# - data/ (LFS objects, attachments, avatars, packages, ...)
|
||||||
|
# - custom/ (templates, logos, ...)
|
||||||
|
# - app.ini (including SECRET_KEY / JWT secrets -- needed for 2FA etc.)
|
||||||
|
# - gitea-backup-info.txt (Gitea version, date, host -- lets the restore
|
||||||
|
# refuse a too-old Gitea *before* it touches anything)
|
||||||
|
#
|
||||||
|
# It uses Gitea's own `gitea dump`, so the archive is also a perfectly normal
|
||||||
|
# dump that `gitea doctor`/manual restores understand.
|
||||||
|
#
|
||||||
|
# By default Gitea is stopped while the dump runs, so the database and the
|
||||||
|
# repositories are consistent with each other, and started again right after
|
||||||
|
# (also if something goes wrong). Use --no-stop to back up a running server.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# sudo ./backup_create.sh [options]
|
||||||
|
#
|
||||||
|
# Run with --help for the full list of options.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Defaults (same variables as setup-git-server.sh, overridable via env vars)
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
GITEA_USER="${GITEA_USER:-git}"
|
||||||
|
GITEA_HOME="${GITEA_HOME:-/home/${GITEA_USER}}"
|
||||||
|
GITEA_WORK_DIR="${GITEA_WORK_DIR:-/var/lib/gitea}"
|
||||||
|
GITEA_CONFIG_DIR="${GITEA_CONFIG_DIR:-/etc/gitea}"
|
||||||
|
GITEA_CONFIG="${GITEA_CONFIG_DIR}/app.ini"
|
||||||
|
GITEA_BIN="/usr/local/bin/gitea"
|
||||||
|
|
||||||
|
OUTPUT=""
|
||||||
|
KEEP=""
|
||||||
|
STOP_SERVICE="true"
|
||||||
|
STAGE_BASE="${STAGE_BASE:-}"
|
||||||
|
|
||||||
|
# Filled in while the script runs
|
||||||
|
DISTRO_FAMILY=""
|
||||||
|
GITEA_GROUP=""
|
||||||
|
OUTPUT_DIR=""
|
||||||
|
OUTPUT_FILE=""
|
||||||
|
FINAL=""
|
||||||
|
PARTIAL=""
|
||||||
|
STAGE=""
|
||||||
|
STOPPED="false"
|
||||||
|
BACKUP_VERSION=""
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Small helpers
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
C_GREEN='\033[0;32m'; C_YELLOW='\033[0;33m'; C_RED='\033[0;31m'; C_RESET='\033[0m'
|
||||||
|
log() { printf '%b\n' "${C_GREEN}[+]${C_RESET} $*"; }
|
||||||
|
warn() { printf '%b\n' "${C_YELLOW}[!]${C_RESET} $*" >&2; }
|
||||||
|
error_exit() { printf '%b\n' "${C_RED}[x]${C_RESET} $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
start_service_if_stopped() {
|
||||||
|
# Idempotent: only acts if *this script* stopped the service.
|
||||||
|
if [[ "$STOPPED" == "true" ]]; then
|
||||||
|
STOPPED="false"
|
||||||
|
log "Starting Gitea again..."
|
||||||
|
systemctl start gitea \
|
||||||
|
|| warn "Could not start Gitea -- start it by hand: systemctl start gitea"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
local rc=$?
|
||||||
|
# Whatever happened, don't leave the server down.
|
||||||
|
start_service_if_stopped || true
|
||||||
|
if [[ -n "${STAGE:-}" && -d "$STAGE" ]]; then
|
||||||
|
rm -rf -- "$STAGE"
|
||||||
|
fi
|
||||||
|
if [[ -n "${PARTIAL:-}" ]]; then
|
||||||
|
rm -f -- "$PARTIAL"
|
||||||
|
fi
|
||||||
|
if [[ $rc -ne 0 ]]; then
|
||||||
|
warn "The backup did not complete; no backup file was written."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
print_help() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: sudo ./backup_create.sh [options]
|
||||||
|
|
||||||
|
Creates a single zip file with the whole Gitea instance (database,
|
||||||
|
repositories, data, custom files, app.ini + secrets). Restore it with
|
||||||
|
backup_restore.sh.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-o, --output <path> Where to put the backup. A folder gets a file named
|
||||||
|
gitea-backup-<host>-<date>-<time>.zip inside it; a path
|
||||||
|
ending in .zip is used as the exact file name.
|
||||||
|
(default: the current directory)
|
||||||
|
--keep <n> After a successful backup, delete all but the <n> newest
|
||||||
|
gitea-backup-*.zip files in the output folder (for cron).
|
||||||
|
--no-stop Don't stop Gitea during the dump. No downtime, but the
|
||||||
|
database and repositories may differ slightly if
|
||||||
|
someone pushes while the dump runs.
|
||||||
|
--tmp-dir <dir> Where the archive is built before it is moved to its
|
||||||
|
final place (default: the Gitea work dir). Needs free
|
||||||
|
space of about the size of the backup and must be
|
||||||
|
reachable by the Gitea service user.
|
||||||
|
-h, --help Show this help and exit
|
||||||
|
|
||||||
|
The environment variables of setup-git-server.sh (GITEA_USER, GITEA_HOME,
|
||||||
|
GITEA_WORK_DIR, GITEA_CONFIG_DIR) are honoured if you used non-default ones.
|
||||||
|
|
||||||
|
The backup contains secrets (app.ini, SECRET_KEY, ...). It is written with
|
||||||
|
mode 600; keep it somewhere private.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
sudo ./backup_create.sh
|
||||||
|
sudo ./backup_create.sh -o /mnt/usb/backups --keep 7
|
||||||
|
sudo ./backup_create.sh -o /root/gitea-before-upgrade.zip
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
require_root() {
|
||||||
|
if [[ "${EUID}" -ne 0 ]]; then
|
||||||
|
error_exit "Please run this script as root, e.g.: sudo $0"
|
||||||
|
fi
|
||||||
|
command -v systemctl &>/dev/null || error_exit "This script requires systemd."
|
||||||
|
[[ -d /run/systemd/system ]] \
|
||||||
|
|| error_exit "systemd is not running as init here (container/chroot/WSL?). Run this on a real VM or host."
|
||||||
|
}
|
||||||
|
|
||||||
|
need_arg() { # usage: need_arg "$@" -- an option that takes a value must have one
|
||||||
|
[[ $# -ge 2 ]] || error_exit "Option $1 needs a value (see --help)"
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-o|--output) need_arg "$@"; OUTPUT="$2"; shift 2 ;;
|
||||||
|
--keep) need_arg "$@"; KEEP="$2"; shift 2 ;;
|
||||||
|
--no-stop) STOP_SERVICE="false"; shift ;;
|
||||||
|
--tmp-dir) need_arg "$@"; STAGE_BASE="$(readlink -f -- "$2")"; shift 2 ;;
|
||||||
|
-h|--help) print_help; exit 0 ;;
|
||||||
|
*) error_exit "Unknown option: $1 (see --help)" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [[ -n "$KEEP" ]]; then
|
||||||
|
[[ "$KEEP" =~ ^[0-9]+$ ]] && (( 10#$KEEP >= 1 )) \
|
||||||
|
|| error_exit "Invalid --keep value '${KEEP}' (expected a number >= 1)."
|
||||||
|
KEEP=$((10#$KEEP))
|
||||||
|
[[ "${OUTPUT,,}" != *.zip ]] \
|
||||||
|
|| error_exit "--keep needs an output *folder*, not a fixed .zip file name."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# app.ini helper (section-aware)
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
ini_get() { # usage: ini_get FILE SECTION KEY (SECTION "" = top of the file)
|
||||||
|
local file="$1" section="$2" key="$3"
|
||||||
|
[[ -r "$file" ]] || return 0
|
||||||
|
awk -v want="$section" -v key="$key" '
|
||||||
|
/^[[:space:]]*[;#]/ { next }
|
||||||
|
/^[[:space:]]*\[/ { s = $0; gsub(/^[[:space:]]*\[|\][[:space:]]*$/, "", s); cur = s; next }
|
||||||
|
tolower(cur) == tolower(want) {
|
||||||
|
i = index($0, "=")
|
||||||
|
if (!i) next
|
||||||
|
k = substr($0, 1, i - 1); gsub(/[[:space:]]/, "", k)
|
||||||
|
if (k == key) {
|
||||||
|
v = substr($0, i + 1)
|
||||||
|
sub(/^[[:space:]]+/, "", v); sub(/[[:space:]]+$/, "", v)
|
||||||
|
print v; exit
|
||||||
|
}
|
||||||
|
}' "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Detection helpers / dependencies
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
detect_distro_family() {
|
||||||
|
[[ -r /etc/os-release ]] || error_exit "Cannot read /etc/os-release; unsupported system."
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. /etc/os-release
|
||||||
|
case "${ID:-}" in
|
||||||
|
ubuntu|debian|linuxmint|pop|raspbian) DISTRO_FAMILY="debian" ;;
|
||||||
|
fedora|rhel|centos|rocky|almalinux|nobara) DISTRO_FAMILY="fedora" ;;
|
||||||
|
arch|archarm|manjaro|endeavouros) DISTRO_FAMILY="arch" ;;
|
||||||
|
*)
|
||||||
|
case "${ID_LIKE:-}" in
|
||||||
|
*debian*) DISTRO_FAMILY="debian" ;;
|
||||||
|
*fedora*|*rhel*) DISTRO_FAMILY="fedora" ;;
|
||||||
|
*arch*) DISTRO_FAMILY="arch" ;;
|
||||||
|
*) error_exit "Unsupported distro: ${PRETTY_NAME:-${ID:-unknown}}. This script supports Arch, Debian, Ubuntu and Fedora." ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
install_dependencies() {
|
||||||
|
# Only what's missing: zip (to add the info file) and unzip (to verify).
|
||||||
|
local pkgs=()
|
||||||
|
command -v zip &>/dev/null || pkgs+=(zip)
|
||||||
|
command -v unzip &>/dev/null || pkgs+=(unzip)
|
||||||
|
(( ${#pkgs[@]} )) || return 0
|
||||||
|
|
||||||
|
detect_distro_family
|
||||||
|
log "Installing dependencies: ${pkgs[*]}..."
|
||||||
|
case "$DISTRO_FAMILY" in
|
||||||
|
debian)
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y "${pkgs[@]}"
|
||||||
|
;;
|
||||||
|
fedora) dnf install -y "${pkgs[@]}" ;;
|
||||||
|
# Never do a partial upgrade (-Sy without -u) on Arch.
|
||||||
|
arch) pacman -Syu --noconfirm --needed "${pkgs[@]}" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
run_as_gitea() {
|
||||||
|
# Run a command as the service user, from a directory it can read, with the
|
||||||
|
# same work dir the service uses.
|
||||||
|
( cd "$GITEA_WORK_DIR" && sudo -H -u "$GITEA_USER" env GITEA_WORK_DIR="$GITEA_WORK_DIR" "$@" )
|
||||||
|
}
|
||||||
|
|
||||||
|
gitea_version() {
|
||||||
|
"$GITEA_BIN" --version 2>/dev/null \
|
||||||
|
| sed -nE 's/^Gitea version v?([0-9]+(\.[0-9]+)+).*/\1/p' \
|
||||||
|
| head -n1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Backup steps
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
check_installation() {
|
||||||
|
[[ -x "$GITEA_BIN" ]] \
|
||||||
|
|| error_exit "Gitea is not installed at ${GITEA_BIN}. Nothing to back up."
|
||||||
|
[[ -r "$GITEA_CONFIG" ]] \
|
||||||
|
|| error_exit "${GITEA_CONFIG} not found. Nothing to back up."
|
||||||
|
id -u "$GITEA_USER" &>/dev/null \
|
||||||
|
|| error_exit "User '${GITEA_USER}' does not exist."
|
||||||
|
command -v sudo &>/dev/null || error_exit "sudo is required (setup-git-server.sh installs it)."
|
||||||
|
|
||||||
|
GITEA_HOME="$(getent passwd "$GITEA_USER" | cut -d: -f6)"
|
||||||
|
GITEA_GROUP="$(id -gn "$GITEA_USER")"
|
||||||
|
|
||||||
|
local db_type
|
||||||
|
db_type="$(ini_get "$GITEA_CONFIG" database DB_TYPE | tr -d '"`')"
|
||||||
|
if [[ "${db_type,,}" != "sqlite3" ]]; then
|
||||||
|
warn "DB_TYPE is '${db_type}': the backup will work, but backup_restore.sh can only restore SQLite instances."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_output() {
|
||||||
|
local ts host
|
||||||
|
ts="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
host="$(printf '%s' "$(uname -n)" | tr -c 'A-Za-z0-9._-' '_')"
|
||||||
|
|
||||||
|
if [[ -z "$OUTPUT" ]]; then
|
||||||
|
OUTPUT_DIR="$PWD"
|
||||||
|
OUTPUT_FILE="gitea-backup-${host}-${ts}.zip"
|
||||||
|
elif [[ "${OUTPUT,,}" == *.zip ]]; then
|
||||||
|
OUTPUT_DIR="$(dirname -- "$OUTPUT")"
|
||||||
|
OUTPUT_FILE="$(basename -- "$OUTPUT")"
|
||||||
|
else
|
||||||
|
OUTPUT_DIR="$OUTPUT"
|
||||||
|
OUTPUT_FILE="gitea-backup-${host}-${ts}.zip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p -- "$OUTPUT_DIR"
|
||||||
|
OUTPUT_DIR="$(cd -- "$OUTPUT_DIR" && pwd -P)"
|
||||||
|
FINAL="${OUTPUT_DIR}/${OUTPUT_FILE}"
|
||||||
|
[[ ! -e "$FINAL" ]] || error_exit "${FINAL} already exists; refusing to overwrite it."
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_service() {
|
||||||
|
if [[ "$STOP_SERVICE" != "true" ]]; then
|
||||||
|
warn "Backing up while Gitea keeps running (--no-stop)."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if systemctl is-active --quiet gitea 2>/dev/null; then
|
||||||
|
log "Stopping Gitea so the backup is consistent..."
|
||||||
|
systemctl stop gitea
|
||||||
|
STOPPED="true"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
create_dump() {
|
||||||
|
: "${STAGE_BASE:=$GITEA_WORK_DIR}"
|
||||||
|
mkdir -p -- "$STAGE_BASE"
|
||||||
|
STAGE="$(mktemp -d -p "$STAGE_BASE" .backup.XXXXXX)"
|
||||||
|
# The dump runs as the service user (it reads app.ini via the group), so it
|
||||||
|
# must own the folder it writes into.
|
||||||
|
chown "${GITEA_USER}:${GITEA_GROUP}" "$STAGE"
|
||||||
|
chmod 700 "$STAGE"
|
||||||
|
|
||||||
|
log "Running 'gitea dump' (this can take a while for big instances)..."
|
||||||
|
run_as_gitea "$GITEA_BIN" --config "$GITEA_CONFIG" dump \
|
||||||
|
--file "$STAGE/dump.zip" --type zip --tempdir "$STAGE" --skip-log \
|
||||||
|
|| error_exit "'gitea dump' failed -- see the error above."
|
||||||
|
[[ -s "$STAGE/dump.zip" ]] || error_exit "'gitea dump' did not produce a file."
|
||||||
|
}
|
||||||
|
|
||||||
|
add_manifest() {
|
||||||
|
local info="${STAGE}/gitea-backup-info.txt"
|
||||||
|
BACKUP_VERSION="$(gitea_version)"
|
||||||
|
|
||||||
|
# Plain KEY=VALUE, read back (never sourced) by backup_restore.sh.
|
||||||
|
cat > "$info" <<EOF
|
||||||
|
# Written by backup_create.sh -- read by backup_restore.sh
|
||||||
|
FORMAT=1
|
||||||
|
CREATED_UTC=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
HOSTNAME=$(uname -n)
|
||||||
|
GITEA_VERSION=${BACKUP_VERSION}
|
||||||
|
DOMAIN=$(ini_get "$GITEA_CONFIG" server DOMAIN)
|
||||||
|
DB_TYPE=$(ini_get "$GITEA_CONFIG" database DB_TYPE)
|
||||||
|
EOF
|
||||||
|
(cd "$STAGE" && zip -q -g dump.zip gitea-backup-info.txt) \
|
||||||
|
|| error_exit "Could not add the info file to the archive."
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_archive() {
|
||||||
|
log "Verifying the archive..."
|
||||||
|
unzip -tq "$STAGE/dump.zip" >/dev/null \
|
||||||
|
|| error_exit "The archive failed its integrity test."
|
||||||
|
# (awk reads everything; 'grep -q' could end the pipe early and trip pipefail.)
|
||||||
|
unzip -Z1 "$STAGE/dump.zip" \
|
||||||
|
| awk '$0 == "gitea-db.sql" { found = 1 } END { exit !found }' \
|
||||||
|
|| error_exit "The archive contains no gitea-db.sql -- the dump is incomplete."
|
||||||
|
}
|
||||||
|
|
||||||
|
deliver_archive() {
|
||||||
|
# Move to the destination under a temporary name first, so a half-copied
|
||||||
|
# file never looks like a finished backup.
|
||||||
|
PARTIAL="${FINAL}.partial"
|
||||||
|
mv -- "$STAGE/dump.zip" "$PARTIAL"
|
||||||
|
chown root:root "$PARTIAL"
|
||||||
|
chmod 600 "$PARTIAL"
|
||||||
|
# When started through sudo, hand the file to the person who ran it.
|
||||||
|
if [[ -n "${SUDO_USER:-}" && "$SUDO_USER" != "root" ]] && id -u "$SUDO_USER" &>/dev/null; then
|
||||||
|
chown -- "$SUDO_USER" "$PARTIAL"
|
||||||
|
fi
|
||||||
|
mv -- "$PARTIAL" "$FINAL"
|
||||||
|
PARTIAL=""
|
||||||
|
}
|
||||||
|
|
||||||
|
prune_old() {
|
||||||
|
[[ -n "$KEEP" ]] || return 0
|
||||||
|
local entry n=0
|
||||||
|
while IFS= read -r -d '' entry; do
|
||||||
|
n=$((n + 1))
|
||||||
|
(( n > KEEP )) || continue
|
||||||
|
rm -f -- "${entry#* }"
|
||||||
|
log "Removed old backup: ${entry#* }"
|
||||||
|
done < <(find "$OUTPUT_DIR" -maxdepth 1 -type f -name 'gitea-backup-*.zip' -printf '%T@ %p\0' | sort -z -rn)
|
||||||
|
}
|
||||||
|
|
||||||
|
print_summary() {
|
||||||
|
local size
|
||||||
|
size="$(du -h -- "$FINAL" | awk '{print $1}')"
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
============================================================================
|
||||||
|
Backup created
|
||||||
|
============================================================================
|
||||||
|
|
||||||
|
File ${FINAL}
|
||||||
|
Size ${size}
|
||||||
|
Gitea ${BACKUP_VERSION:-unknown}
|
||||||
|
|
||||||
|
Contains the database, all repositories, data (LFS, attachments, avatars),
|
||||||
|
custom files and app.ini with its secrets -- keep it private.
|
||||||
|
|
||||||
|
Restore it with:
|
||||||
|
sudo ./backup_restore.sh ${FINAL}
|
||||||
|
|
||||||
|
On a new machine, first install the same (or a newer) Gitea:
|
||||||
|
sudo ./setup-git-server.sh --version ${BACKUP_VERSION:-<x.y.z>}
|
||||||
|
============================================================================
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
main() {
|
||||||
|
require_root
|
||||||
|
install_dependencies
|
||||||
|
check_installation
|
||||||
|
resolve_output
|
||||||
|
|
||||||
|
log "Backing up to ${FINAL}"
|
||||||
|
stop_service
|
||||||
|
create_dump
|
||||||
|
start_service_if_stopped # downtime ends here; the rest doesn't need it down
|
||||||
|
|
||||||
|
add_manifest
|
||||||
|
verify_archive
|
||||||
|
deliver_archive
|
||||||
|
prune_old
|
||||||
|
print_summary
|
||||||
|
}
|
||||||
|
|
||||||
|
# Only run when executed, not when sourced (handy for testing single functions).
|
||||||
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
|
parse_args "$@"
|
||||||
|
main
|
||||||
|
fi
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# restore-gitea-dump.sh
|
# backup_restore.sh
|
||||||
#
|
#
|
||||||
# Companion to setup-git-server.sh: restores a backup made with `gitea dump`
|
# Companion to setup-git-server.sh and backup_create.sh: restores a backup zip
|
||||||
# into a Gitea instance that was installed by that script, keeping its layout:
|
# made by backup_create.sh (or any plain `gitea dump` archive) into a Gitea
|
||||||
|
# instance that was installed by setup-git-server.sh, keeping its layout:
|
||||||
#
|
#
|
||||||
# binary /usr/local/bin/gitea
|
# binary /usr/local/bin/gitea
|
||||||
# config /etc/gitea/app.ini
|
# config /etc/gitea/app.ini
|
||||||
@@ -12,23 +13,25 @@
|
|||||||
# database SQLite
|
# database SQLite
|
||||||
#
|
#
|
||||||
# What it does:
|
# What it does:
|
||||||
# 1. unpacks the dump into a staging directory and imports its SQL into a
|
# 1. unpacks the backup into a staging directory, checks that the installed
|
||||||
|
# Gitea is not older than the one that made it, and imports the SQL into a
|
||||||
# brand-new SQLite file there -- nothing on the server is touched until
|
# brand-new SQLite file there -- nothing on the server is touched until
|
||||||
# that has worked;
|
# all of that has worked;
|
||||||
# 2. stops Gitea and moves the current data / custom / repositories aside
|
# 2. stops Gitea and moves the current data / custom / repositories aside
|
||||||
# (as <name>.bak.<timestamp>);
|
# (as <name>.bak.<timestamp>);
|
||||||
# 3. puts the dump's data, custom files, repositories and database in place;
|
# 3. puts the backup's data, custom files, repositories and database in place;
|
||||||
# 4. copies the secrets (SECRET_KEY, JWT secrets, ...) from the dump's app.ini
|
# 4. copies the secrets (SECRET_KEY, JWT secrets, ...) from the backup's
|
||||||
# into the current one, so 2FA, OAuth2 and tokens keep working, while the
|
# app.ini into the current one, so 2FA, OAuth2 and tokens keep working,
|
||||||
# domain / ports / paths of this server stay as they are;
|
# while the domain / ports / paths of this server stay as they are;
|
||||||
# 5. runs 'gitea migrate', regenerates the repo hooks and authorized_keys,
|
# 5. runs 'gitea migrate', regenerates the repo hooks and authorized_keys,
|
||||||
# and starts Gitea again.
|
# and starts Gitea again.
|
||||||
#
|
#
|
||||||
# Install a Gitea version equal to (or newer than) the one that made the dump:
|
# On a fresh machine, install a Gitea version equal to (or newer than) the one
|
||||||
|
# that made the backup first (backup_create.sh prints the exact command):
|
||||||
# sudo ./setup-git-server.sh --version <x.y.z>
|
# sudo ./setup-git-server.sh --version <x.y.z>
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# sudo ./restore-gitea-dump.sh [options] <gitea-dump-file>
|
# sudo ./backup_restore.sh [options] <backup-file>
|
||||||
#
|
#
|
||||||
# Run with --help for the full list of options.
|
# Run with --help for the full list of options.
|
||||||
|
|
||||||
@@ -56,6 +59,10 @@ DB_PATH=""
|
|||||||
STAGE=""
|
STAGE=""
|
||||||
DUMP_ROOT=""
|
DUMP_ROOT=""
|
||||||
DUMP_INI=""
|
DUMP_INI=""
|
||||||
|
MANIFEST=""
|
||||||
|
BACKUP_VERSION=""
|
||||||
|
BACKUP_CREATED=""
|
||||||
|
BACKUP_HOST=""
|
||||||
DUMP_USERS="?"
|
DUMP_USERS="?"
|
||||||
DUMP_REPOS="?"
|
DUMP_REPOS="?"
|
||||||
TS=""
|
TS=""
|
||||||
@@ -87,21 +94,22 @@ trap cleanup EXIT
|
|||||||
|
|
||||||
print_help() {
|
print_help() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
Usage: sudo ./restore-gitea-dump.sh [options] <gitea-dump-file>
|
Usage: sudo ./backup_restore.sh [options] <backup-file>
|
||||||
|
|
||||||
Restores a `gitea dump` archive (.zip, .tar, .tar.gz, .tar.bz2, .tar.xz,
|
Restores a backup made by backup_create.sh (a .zip; plain `gitea dump`
|
||||||
.tar.zst) into a Gitea instance installed by setup-git-server.sh.
|
archives in .zip, .tar, .tar.gz, .tar.bz2, .tar.xz or .tar.zst work too)
|
||||||
|
into a Gitea instance installed by setup-git-server.sh.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-y, --yes Don't ask for confirmation
|
-y, --yes Don't ask for confirmation
|
||||||
--tmp-dir <dir> Where the dump is unpacked (default: the Gitea work
|
--tmp-dir <dir> Where the backup is unpacked (default: the Gitea work
|
||||||
dir; needs free space of about twice the dump size)
|
dir; needs free space of about twice the backup size)
|
||||||
-h, --help Show this help and exit
|
-h, --help Show this help and exit
|
||||||
|
|
||||||
The environment variables of setup-git-server.sh (GITEA_USER, GITEA_HOME,
|
The environment variables of setup-git-server.sh (GITEA_USER, GITEA_HOME,
|
||||||
GITEA_WORK_DIR, GITEA_CONFIG_DIR) are honoured if you used non-default ones.
|
GITEA_WORK_DIR, GITEA_CONFIG_DIR) are honoured if you used non-default ones.
|
||||||
|
|
||||||
Only SQLite dumps can be restored (that is what setup-git-server.sh uses).
|
Only SQLite backups can be restored (that is what setup-git-server.sh uses).
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +134,7 @@ parse_args() {
|
|||||||
-h|--help) print_help; exit 0 ;;
|
-h|--help) print_help; exit 0 ;;
|
||||||
-*) error_exit "Unknown option: $1 (see --help)" ;;
|
-*) error_exit "Unknown option: $1 (see --help)" ;;
|
||||||
*)
|
*)
|
||||||
[[ -z "$DUMP_FILE" ]] || error_exit "Only one dump file can be given (got '${DUMP_FILE}' and '$1')."
|
[[ -z "$DUMP_FILE" ]] || error_exit "Only one backup file can be given (got '${DUMP_FILE}' and '$1')."
|
||||||
DUMP_FILE="$1"; shift ;;
|
DUMP_FILE="$1"; shift ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@@ -134,7 +142,7 @@ parse_args() {
|
|||||||
print_help >&2
|
print_help >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
[[ -f "$DUMP_FILE" && -r "$DUMP_FILE" ]] || error_exit "Cannot read dump file: ${DUMP_FILE}"
|
[[ -f "$DUMP_FILE" && -r "$DUMP_FILE" ]] || error_exit "Cannot read backup file: ${DUMP_FILE}"
|
||||||
DUMP_FILE="$(readlink -f -- "$DUMP_FILE")"
|
DUMP_FILE="$(readlink -f -- "$DUMP_FILE")"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +189,24 @@ ini_set() { # usage: ini_set FILE SECTION KEY VALUE -- fails if KEY isn't in
|
|||||||
rm -f "$out"
|
rm -f "$out"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Backup info file / version helpers
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
manifest_get() { # usage: manifest_get KEY -- read from gitea-backup-info.txt, never sourced
|
||||||
|
[[ -n "$MANIFEST" && -r "$MANIFEST" ]] || return 0
|
||||||
|
sed -nE "s/^$1=(.*)\$/\\1/p" "$MANIFEST" | head -n1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
installed_version() {
|
||||||
|
"$GITEA_BIN" --version 2>/dev/null \
|
||||||
|
| sed -nE 's/^Gitea version v?([0-9]+(\.[0-9]+)+).*/\1/p' \
|
||||||
|
| head -n1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
version_lt() { # usage: version_lt A B -> true if A is an older version than B
|
||||||
|
[[ "$1" != "$2" && "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" == "$1" ]]
|
||||||
|
}
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# Detection helpers / dependencies
|
# Detection helpers / dependencies
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
@@ -270,7 +296,7 @@ extract_dump() {
|
|||||||
STAGE="$(mktemp -d -p "$STAGE_BASE" .restore.XXXXXX)"
|
STAGE="$(mktemp -d -p "$STAGE_BASE" .restore.XXXXXX)"
|
||||||
mkdir -p -- "$STAGE/dump"
|
mkdir -p -- "$STAGE/dump"
|
||||||
|
|
||||||
log "Unpacking $(basename -- "$DUMP_FILE") (this can take a while for big dumps)..."
|
log "Unpacking $(basename -- "$DUMP_FILE") (this can take a while for big backups)..."
|
||||||
case "${DUMP_FILE,,}" in
|
case "${DUMP_FILE,,}" in
|
||||||
*.zip)
|
*.zip)
|
||||||
unzip -q -o "$DUMP_FILE" -d "$STAGE/dump" ;;
|
unzip -q -o "$DUMP_FILE" -d "$STAGE/dump" ;;
|
||||||
@@ -284,10 +310,38 @@ extract_dump() {
|
|||||||
local sql
|
local sql
|
||||||
sql="$(find "$STAGE/dump" -maxdepth 2 -name gitea-db.sql -print -quit)"
|
sql="$(find "$STAGE/dump" -maxdepth 2 -name gitea-db.sql -print -quit)"
|
||||||
[[ -n "$sql" ]] \
|
[[ -n "$sql" ]] \
|
||||||
|| error_exit "No gitea-db.sql in the archive -- is this a 'gitea dump' file (made without --skip-db)?"
|
|| error_exit "No gitea-db.sql in the archive -- is this a backup from backup_create.sh / 'gitea dump' (made without --skip-db)?"
|
||||||
DUMP_ROOT="$(dirname -- "$sql")"
|
DUMP_ROOT="$(dirname -- "$sql")"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_backup_version() {
|
||||||
|
# Gitea can migrate a database forward, never backward -- so find out now,
|
||||||
|
# not after the current data has been moved aside.
|
||||||
|
if [[ ! -f "$DUMP_ROOT/gitea-backup-info.txt" ]]; then
|
||||||
|
warn "No gitea-backup-info.txt (not made by backup_create.sh?): can't check the Gitea version up front."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
MANIFEST="$DUMP_ROOT/gitea-backup-info.txt"
|
||||||
|
BACKUP_VERSION="$(manifest_get GITEA_VERSION)"
|
||||||
|
BACKUP_CREATED="$(manifest_get CREATED_UTC)"
|
||||||
|
BACKUP_HOST="$(manifest_get HOSTNAME)"
|
||||||
|
|
||||||
|
local current
|
||||||
|
current="$(installed_version)"
|
||||||
|
if [[ -z "$BACKUP_VERSION" || -z "$current" ]]; then
|
||||||
|
warn "Could not determine the Gitea versions to compare (backup: '${BACKUP_VERSION:-?}', installed: '${current:-?}')."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if version_lt "$current" "$BACKUP_VERSION"; then
|
||||||
|
error_exit "This backup was made with Gitea ${BACKUP_VERSION}, but ${current} is installed, and a database can't be migrated to an older release.
|
||||||
|
Install the same or a newer version first: sudo ./setup-git-server.sh --version ${BACKUP_VERSION}
|
||||||
|
Nothing was changed."
|
||||||
|
fi
|
||||||
|
if version_lt "$BACKUP_VERSION" "$current"; then
|
||||||
|
log "Backup is from Gitea ${BACKUP_VERSION}; its database will be migrated to ${current}."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
inspect_dump() {
|
inspect_dump() {
|
||||||
local f
|
local f
|
||||||
for f in "$DUMP_ROOT/app.ini" "$DUMP_ROOT/data/conf/app.ini"; do
|
for f in "$DUMP_ROOT/app.ini" "$DUMP_ROOT/data/conf/app.ini"; do
|
||||||
@@ -298,10 +352,10 @@ inspect_dump() {
|
|||||||
local src_db_type
|
local src_db_type
|
||||||
src_db_type="$(ini_get "$DUMP_INI" database DB_TYPE | tr -d '"`')"
|
src_db_type="$(ini_get "$DUMP_INI" database DB_TYPE | tr -d '"`')"
|
||||||
if [[ -n "$src_db_type" && "${src_db_type,,}" != "sqlite3" ]]; then
|
if [[ -n "$src_db_type" && "${src_db_type,,}" != "sqlite3" ]]; then
|
||||||
error_exit "The dump comes from a '${src_db_type}' database. Only SQLite dumps can be restored by this script."
|
error_exit "The backup comes from a '${src_db_type}' database. Only SQLite backups can be restored by this script."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
warn "No app.ini in the dump: SECRET_KEY can't be carried over, so 2FA secrets and similar will not work after the restore."
|
warn "No app.ini in the backup: SECRET_KEY can't be carried over, so 2FA secrets and similar will not work after the restore."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Dumps made by old Gitea versions keep the repositories in a nested zip.
|
# Dumps made by old Gitea versions keep the repositories in a nested zip.
|
||||||
@@ -309,7 +363,7 @@ inspect_dump() {
|
|||||||
log "Unpacking the nested gitea-repo.zip..."
|
log "Unpacking the nested gitea-repo.zip..."
|
||||||
unzip -q -o "$DUMP_ROOT/gitea-repo.zip" -d "$DUMP_ROOT/repos"
|
unzip -q -o "$DUMP_ROOT/gitea-repo.zip" -d "$DUMP_ROOT/repos"
|
||||||
fi
|
fi
|
||||||
[[ -d "$DUMP_ROOT/repos" ]] || warn "The dump contains no repositories directory."
|
[[ -d "$DUMP_ROOT/repos" ]] || warn "The backup contains no repositories directory."
|
||||||
}
|
}
|
||||||
|
|
||||||
stage_database() {
|
stage_database() {
|
||||||
@@ -317,14 +371,19 @@ stage_database() {
|
|||||||
sqlite3 -bail "$STAGE/gitea.db" < "$DUMP_ROOT/gitea-db.sql" \
|
sqlite3 -bail "$STAGE/gitea.db" < "$DUMP_ROOT/gitea-db.sql" \
|
||||||
|| error_exit "Importing gitea-db.sql into SQLite failed -- see the error above. Nothing was changed."
|
|| error_exit "Importing gitea-db.sql into SQLite failed -- see the error above. Nothing was changed."
|
||||||
DUMP_USERS="$(sqlite3 "$STAGE/gitea.db" 'SELECT COUNT(*) FROM "user";' 2>/dev/null)" \
|
DUMP_USERS="$(sqlite3 "$STAGE/gitea.db" 'SELECT COUNT(*) FROM "user";' 2>/dev/null)" \
|
||||||
|| error_exit "The imported database has no 'user' table -- this doesn't look like a Gitea dump. Nothing was changed."
|
|| error_exit "The imported database has no 'user' table -- this doesn't look like a Gitea backup. Nothing was changed."
|
||||||
DUMP_REPOS="$(sqlite3 "$STAGE/gitea.db" 'SELECT COUNT(*) FROM repository;' 2>/dev/null || echo '?')"
|
DUMP_REPOS="$(sqlite3 "$STAGE/gitea.db" 'SELECT COUNT(*) FROM repository;' 2>/dev/null || echo '?')"
|
||||||
}
|
}
|
||||||
|
|
||||||
confirm_restore() {
|
confirm_restore() {
|
||||||
|
local origin=""
|
||||||
|
if [[ -n "$BACKUP_CREATED" || -n "$BACKUP_HOST" ]]; then
|
||||||
|
origin="Made on ${BACKUP_HOST:-?} at ${BACKUP_CREATED:-?} (Gitea ${BACKUP_VERSION:-?})."
|
||||||
|
fi
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|
||||||
The dump contains ${DUMP_USERS} user account(s) and ${DUMP_REPOS} repositories.
|
The backup contains ${DUMP_USERS} user account(s) and ${DUMP_REPOS} repositories.
|
||||||
|
${origin}
|
||||||
|
|
||||||
This will REPLACE the current Gitea data on this machine:
|
This will REPLACE the current Gitea data on this machine:
|
||||||
database ${DB_PATH}
|
database ${DB_PATH}
|
||||||
@@ -334,7 +393,7 @@ This will REPLACE the current Gitea data on this machine:
|
|||||||
(the current ones are kept next to them as <name>.bak.<timestamp>)
|
(the current ones are kept next to them as <name>.bak.<timestamp>)
|
||||||
|
|
||||||
The accounts made by setup-git-server.sh (e.g. the initial admin) are replaced
|
The accounts made by setup-git-server.sh (e.g. the initial admin) are replaced
|
||||||
by the dump's accounts -- you'll log in with the credentials from the old server.
|
by the backup's accounts -- you'll log in with the credentials from the old server.
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
if [[ "$ASSUME_YES" == "true" ]]; then
|
if [[ "$ASSUME_YES" == "true" ]]; then
|
||||||
@@ -401,13 +460,13 @@ swap_in_files() {
|
|||||||
|
|
||||||
restore_secrets() {
|
restore_secrets() {
|
||||||
[[ -n "$DUMP_INI" ]] || return 0
|
[[ -n "$DUMP_INI" ]] || return 0
|
||||||
log "Carrying the secrets over from the dump's app.ini..."
|
log "Carrying the secrets over from the backup's app.ini..."
|
||||||
|
|
||||||
# Keep the old config around for reference (mailer, OAuth, etc. settings).
|
# Keep the old config around for reference (mailer, OAuth, etc. settings).
|
||||||
install -m 640 -o root -g "$GITEA_GROUP" "$DUMP_INI" "${GITEA_CONFIG}.from-dump"
|
install -m 640 -o root -g "$GITEA_GROUP" "$DUMP_INI" "${GITEA_CONFIG}.from-dump"
|
||||||
|
|
||||||
# SECRET_KEY encrypts 2FA secrets and other values stored in the database,
|
# SECRET_KEY encrypts 2FA secrets and other values stored in the database,
|
||||||
# so it must be the one the dump was made with. The JWT secrets and
|
# so it must be the one the backup was made with. The JWT secrets and
|
||||||
# INTERNAL_TOKEN are carried over too so tokens issued before keep working.
|
# INTERNAL_TOKEN are carried over too so tokens issued before keep working.
|
||||||
local pair section key value
|
local pair section key value
|
||||||
for pair in security:SECRET_KEY security:INTERNAL_TOKEN server:LFS_JWT_SECRET oauth2:JWT_SECRET; do
|
for pair in security:SECRET_KEY security:INTERNAL_TOKEN server:LFS_JWT_SECRET oauth2:JWT_SECRET; do
|
||||||
@@ -415,7 +474,7 @@ restore_secrets() {
|
|||||||
key="${pair##*:}"
|
key="${pair##*:}"
|
||||||
value="$(ini_get "$DUMP_INI" "$section" "$key")"
|
value="$(ini_get "$DUMP_INI" "$section" "$key")"
|
||||||
if [[ -z "$value" ]]; then
|
if [[ -z "$value" ]]; then
|
||||||
warn "No ${key} in the dump's app.ini (maybe set through a *_URI option); keeping the current value."
|
warn "No ${key} in the backup's app.ini (maybe set through a *_URI option); keeping the current value."
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
ini_set "$GITEA_CONFIG" "$section" "$key" "$value" \
|
ini_set "$GITEA_CONFIG" "$section" "$key" "$value" \
|
||||||
@@ -511,6 +570,7 @@ main() {
|
|||||||
check_installation
|
check_installation
|
||||||
|
|
||||||
extract_dump
|
extract_dump
|
||||||
|
check_backup_version
|
||||||
inspect_dump
|
inspect_dump
|
||||||
stage_database
|
stage_database
|
||||||
confirm_restore
|
confirm_restore
|
||||||
Reference in new issue
Block a user