557 lines
21 KiB
Bash
Executable File
557 lines
21 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
umask 027
|
|
|
|
# ==============================================================================
|
|
# app-backup.sh
|
|
# - Separate archives per component (db / wordpress / nextcloud-code / nextcloud-data / gitea)
|
|
# - rclone tuned for large files (OneDrive chunking + timeouts + conservative concurrency)
|
|
# - Nextcloud "data" excluded from code backup (layout: ${NC_DIR}/data)
|
|
# - Gitea native install (systemd), data path configurable (default: /var/lib/gitea/data)
|
|
# ==============================================================================
|
|
|
|
# ---------- Logging ----------
|
|
LOG_DIR="/var/log/app-backup"
|
|
mkdir -p "$LOG_DIR"
|
|
ts="$(date '+%Y-%m-%d_%H-%M-%S')"
|
|
LOG_FILE="${LOG_DIR}/app-backup_${ts}.log"
|
|
exec > >(tee -a "$LOG_FILE" | systemd-cat -t app-backup -p info) 2>&1
|
|
|
|
# ---------- Config ----------
|
|
CONFIG_FILE="/etc/app-backup/app-backup.conf"
|
|
if [[ -r "$CONFIG_FILE" ]]; then
|
|
# shellcheck disable=SC1090
|
|
source "$CONFIG_FILE"
|
|
else
|
|
echo "ERROR: Config not found/readable: $CONFIG_FILE"
|
|
exit 2
|
|
fi
|
|
|
|
# ---------- Defaults (if not set in conf) ----------
|
|
: "${ENABLE_MAIL:=false}"
|
|
: "${MAIL_BACKUP_MODE:=live}" # live|stop (stop briefly stops postfix/dovecot)
|
|
: "${MAIL_DIR:=/var/vmail}"
|
|
: "${POSTFIX_DIR:=/etc/postfix}"
|
|
: "${DOVECOT_DIR:=/etc/dovecot}"
|
|
: "${ENABLE_MAIL_HOME_DIRS:=true}"
|
|
: "${MAIL_HOME_DIRS:=/home/johannes /home/luciana}"
|
|
: "${MAIL_HOME_EXCLUDES:=--exclude=OneDrive/ --exclude=.cache/ --exclude=.ccache/ --exclude=.local/share/Trash/}"
|
|
|
|
: "${WORKDIR:=/var/backups/app-backup}"
|
|
: "${STAGING_ROOT:=${WORKDIR}/staging}"
|
|
: "${ARCHIVE_DIR:=${WORKDIR}/archives}"
|
|
: "${LOCAL_RETENTION_DAYS:=7}"
|
|
|
|
: "${COMPRESSOR:=zstd}" # zstd|gzip
|
|
: "${ARCHIVE_PREFIX:=appbackup}" # file prefix
|
|
|
|
# rclone
|
|
: "${RCLONE_BIN:=rclone}"
|
|
: "${RCLONE_REMOTE_BASE:=OneDrive:Sicherung/JRITServerBackups/$(hostname -s)}" # remote folder
|
|
: "${RCLONE_RETRIES:=10}"
|
|
: "${RCLONE_LOW_LEVEL_RETRIES:=40}"
|
|
: "${RCLONE_RETRIES_SLEEP:=30s}"
|
|
: "${RCLONE_STATS:=1m}"
|
|
: "${RCLONE_BWLIMIT:=0}" # "0" = no limit
|
|
: "${ENABLE_UPLOAD:=true}"
|
|
|
|
# large-file stability (OneDrive)
|
|
: "${RCLONE_ONEDRIVE_CHUNK_SIZE:=80Mi}"
|
|
: "${RCLONE_TIMEOUT:=1h}"
|
|
: "${RCLONE_CONTIMEOUT:=30s}"
|
|
: "${RCLONE_TRANSFERS:=2}"
|
|
: "${RCLONE_CHECKERS:=4}"
|
|
|
|
: "${REMOTE_RETENTION_DAYS:=30}"
|
|
: "${ENABLE_REMOTE_RETENTION:=true}"
|
|
|
|
# Disk-space safety
|
|
: "${MIN_FREE_GB:=12}"
|
|
|
|
# Process niceness
|
|
: "${NICE_LEVEL:=10}"
|
|
: "${IONICE_CLASS:=2}"
|
|
: "${IONICE_LEVEL:=6}"
|
|
|
|
# Mail reporting
|
|
: "${ENABLE_MAIL_REPORT:=true}"
|
|
: "${MAIL_TO:=johannes}"
|
|
: "${MAIL_FROM:=app-backup@$(hostname -f 2>/dev/null || hostname)}"
|
|
: "${MAIL_SUBJECT_PREFIX:=[app-backup]}"
|
|
: "${MAIL_INCLUDE_LOG_TAIL_LINES:=200}"
|
|
|
|
# ---------- State for report ----------
|
|
START_EPOCH="$(date +%s)"
|
|
STATUS="SUCCESS"
|
|
ERROR_SUMMARY=""
|
|
RCLONE_STATUS="SKIPPED"
|
|
RCLONE_OUTPUT_FILE=""
|
|
SIZES_FILE=""
|
|
|
|
# ---------- Helpers ----------
|
|
die() { STATUS="FAIL"; ERROR_SUMMARY="${1:-Unknown error}"; echo "ERROR: $*"; exit 1; }
|
|
have() { command -v "$1" >/dev/null 2>&1; }
|
|
|
|
require_crypt_remote() {
|
|
local remote_name="$1"
|
|
local cfg
|
|
cfg="$("$RCLONE_BIN" config show "$remote_name" 2>/dev/null || true)"
|
|
[[ -n "$cfg" ]] || die "rclone remote \"$remote_name\" not found in config"
|
|
echo "$cfg" | grep -Eq "^type\s*=\s*crypt\s*$" || die "Remote \"$remote_name\" is not a crypt remote (type!=crypt). Refusing to upload unencrypted."
|
|
# These options may be omitted if defaults are used. If present and set to disable encryption, fail.
|
|
if echo "$cfg" | grep -Eq "^filename_encryption\s*=\s*off\s*$"; then
|
|
die "Remote \"$remote_name\" has filename_encryption=off. Filenames would be visible in OneDrive."
|
|
fi
|
|
if echo "$cfg" | grep -Eq "^directory_name_encryption\s*=\s*(false|off)\s*$"; then
|
|
die "Remote \"$remote_name\" has directory_name_encryption disabled. Directory names would be visible in OneDrive."
|
|
fi
|
|
}
|
|
|
|
|
|
human_bytes() { local b="${1:-0}"; if have numfmt; then numfmt --to=iec-i --suffix=B "$b"; else echo "${b}B"; fi; }
|
|
bytes_of_path() { local p="$1"; [[ -e "$p" ]] && (du -sb "$p" 2>/dev/null | awk '{print $1}' || du -sB1 "$p" | awk '{print $1}') || echo 0; }
|
|
free_bytes_workdir_fs() { df -PB1 "$WORKDIR" | awk 'NR==2{print $4}'; }
|
|
|
|
ensure_min_free_space() {
|
|
local min_bytes=$((MIN_FREE_GB * 1024 * 1024 * 1024))
|
|
local avail; avail="$(free_bytes_workdir_fs)"
|
|
echo "-- Free space on WORKDIR filesystem: $(human_bytes "$avail") (min required: ${MIN_FREE_GB}GiB)"
|
|
[[ "$avail" -ge "$min_bytes" ]] || die "Not enough free space on WORKDIR filesystem (need >= ${MIN_FREE_GB}GiB)."
|
|
}
|
|
|
|
cleanup_old_local_archives() {
|
|
mkdir -p "$ARCHIVE_DIR"
|
|
echo "-- Local retention: deleting archives older than ${LOCAL_RETENTION_DAYS} day(s) from ${ARCHIVE_DIR}"
|
|
find "$ARCHIVE_DIR" -type f -name "${ARCHIVE_PREFIX}_*.tar.*" -mtime "+${LOCAL_RETENTION_DAYS}" -print -delete 2>/dev/null || true
|
|
}
|
|
|
|
send_report_mail() {
|
|
[[ "${ENABLE_MAIL_REPORT}" == "true" ]] || return 0
|
|
local SENDMAIL_BIN="/usr/sbin/sendmail"
|
|
[[ -x "$SENDMAIL_BIN" ]] || { echo "WARN: sendmail missing at $SENDMAIL_BIN"; return 0; }
|
|
|
|
local end_epoch now duration subject host
|
|
end_epoch="$(date +%s)"
|
|
now="$(date -Is)"
|
|
duration="$((end_epoch - START_EPOCH))"
|
|
host="$(hostname -f 2>/dev/null || hostname)"
|
|
subject="${MAIL_SUBJECT_PREFIX} ${STATUS} ${host} ${ts}"
|
|
|
|
{
|
|
echo "From: ${MAIL_FROM}"
|
|
echo "To: ${MAIL_TO}"
|
|
echo "Subject: ${subject}"
|
|
echo "Date: $(date -R)"
|
|
echo "MIME-Version: 1.0"
|
|
echo "Content-Type: text/plain; charset=UTF-8"
|
|
echo
|
|
echo "Backup Report"
|
|
echo "============="
|
|
echo "Zeit: ${now}"
|
|
echo "Host: ${host}"
|
|
echo "Status: ${STATUS}"
|
|
[[ -n "${ERROR_SUMMARY}" ]] && echo "Fehler: ${ERROR_SUMMARY}"
|
|
echo "Dauer: ${duration}s"
|
|
echo
|
|
echo "Config: ${CONFIG_FILE}"
|
|
echo "Log: ${LOG_FILE}"
|
|
echo "Workdir: ${WORKDIR}"
|
|
echo "Archive dir: ${ARCHIVE_DIR}"
|
|
echo "Kompression: ${COMPRESSOR}"
|
|
echo
|
|
echo "Remote"
|
|
echo "------"
|
|
echo "Upload: ${ENABLE_UPLOAD}"
|
|
echo "Remote base: ${RCLONE_REMOTE_BASE}"
|
|
echo "Upload Status: ${RCLONE_STATUS}"
|
|
[[ -n "${RCLONE_OUTPUT_FILE}" && -f "${RCLONE_OUTPUT_FILE}" ]] && { echo; echo "rclone Tail:"; tail -n 60 "${RCLONE_OUTPUT_FILE}" || true; }
|
|
echo
|
|
echo "Größen"
|
|
echo "------"
|
|
[[ -n "${SIZES_FILE}" && -f "${SIZES_FILE}" ]] && cat "${SIZES_FILE}" || echo "(keine Größeninfos verfügbar)"
|
|
echo
|
|
echo "Log-Auszug (Tail)"
|
|
echo "-----------------"
|
|
tail -n "${MAIL_INCLUDE_LOG_TAIL_LINES}" "${LOG_FILE}" || true
|
|
} | "$SENDMAIL_BIN" -t || echo "WARN: sending mail failed"
|
|
}
|
|
|
|
cleanup_staging() { [[ -n "${STAGING_DIR:-}" && -d "${STAGING_DIR:-}" ]] && rm -rf "${STAGING_DIR:?}"; }
|
|
|
|
# Nextcloud maintenance-mode safety trap
|
|
NC_MAINTENANCE_ON=false
|
|
nc_maintenance_off() {
|
|
if [[ "${NC_MAINTENANCE_ON}" == "true" ]]; then
|
|
echo "-- Nextcloud maintenance mode OFF (trap)..."
|
|
sudo -u "${NC_OCC_USER}" php "${NC_DIR}/occ" maintenance:mode --off || true
|
|
NC_MAINTENANCE_ON=false
|
|
fi
|
|
}
|
|
|
|
# Gitea service safety trap
|
|
GITEA_WAS_STOPPED=false
|
|
gitea_service_start() {
|
|
if [[ "${GITEA_WAS_STOPPED}" == "true" ]]; then
|
|
echo "-- Starting Gitea service (trap)..."
|
|
systemctl start "${GITEA_SERVICE_NAME}" || true
|
|
GITEA_WAS_STOPPED=false
|
|
fi
|
|
}
|
|
|
|
on_error() { local ec=$?; STATUS="FAIL"; ERROR_SUMMARY="Exit code ${ec} (see log)"; return 0; }
|
|
on_exit() { local ec=$?; send_report_mail; nc_maintenance_off; gitea_service_start; cleanup_staging; exit "${ec}"; }
|
|
trap on_error ERR
|
|
trap on_exit EXIT
|
|
|
|
# ---------- Start ----------
|
|
echo "== app-backup start: ${ts} =="
|
|
echo "-- Host: $(hostname -f 2>/dev/null || hostname)"
|
|
echo "-- Config: ${CONFIG_FILE}"
|
|
echo "-- Log: ${LOG_FILE}"
|
|
|
|
# ---------- Preconditions ----------
|
|
[[ $EUID -eq 0 ]] || die "Must run as root."
|
|
mkdir -p "$WORKDIR" "$ARCHIVE_DIR" "$STAGING_ROOT" "$LOG_DIR"
|
|
|
|
# ---------- Locking ----------
|
|
LOCKFILE="/run/app-backup.lock"
|
|
exec 9>"$LOCKFILE"
|
|
if ! flock -n 9; then
|
|
STATUS="FAIL"
|
|
ERROR_SUMMARY="Another backup already running (lock: $LOCKFILE)"
|
|
exit 3
|
|
fi
|
|
|
|
# ---------- Tools ----------
|
|
for t in tar rsync flock df find stat; do have "$t" || die "Missing required tool: $t"; done
|
|
if [[ "$COMPRESSOR" == "zstd" ]]; then have zstd || die "COMPRESSOR=zstd but zstd is missing"
|
|
elif [[ "$COMPRESSOR" == "gzip" ]]; then have gzip || die "COMPRESSOR=gzip but gzip is missing"
|
|
else die "Unsupported COMPRESSOR=$COMPRESSOR (use zstd or gzip)"; fi
|
|
|
|
if [[ "${ENABLE_DB_DUMPS}" == "true" ]]; then
|
|
have mysqldump || die "ENABLE_DB_DUMPS=true but mysqldump missing"
|
|
have mysql || die "ENABLE_DB_DUMPS=true but mysql client missing"
|
|
fi
|
|
have "$RCLONE_BIN" || die "rclone not installed (missing: $RCLONE_BIN)"
|
|
|
|
# ---------- Disk safety ----------
|
|
cleanup_old_local_archives
|
|
ensure_min_free_space
|
|
|
|
# ---------- Staging ----------
|
|
STAGING_DIR="${STAGING_ROOT}/run_${ts}"
|
|
mkdir -p "$STAGING_DIR"/{db,files,meta}
|
|
echo "$(date -Is)" > "$STAGING_DIR/meta/created_at.txt"
|
|
echo "$(hostname -f 2>/dev/null || hostname)" > "$STAGING_DIR/meta/hostname.txt"
|
|
echo "${ts}" > "$STAGING_DIR/meta/timestamp.txt"
|
|
|
|
# ---------- Services consistency ----------
|
|
if [[ "${ENABLE_GITEA:-false}" == "true" && "${ENABLE_GITEA_SERVICE_STOP:-true}" == "true" ]]; then
|
|
if systemctl is-active --quiet "${GITEA_SERVICE_NAME}"; then
|
|
echo "-- Stopping Gitea service for consistent backup: ${GITEA_SERVICE_NAME}"
|
|
systemctl stop "${GITEA_SERVICE_NAME}"
|
|
GITEA_WAS_STOPPED=true
|
|
fi
|
|
fi
|
|
|
|
# ---------- DB Dumps ----------
|
|
if [[ "${ENABLE_DB_DUMPS}" == "true" ]]; then
|
|
echo "-- DB dumps enabled"
|
|
|
|
dump_mysql_db() {
|
|
local cnf="$1" db="$2" out="$3"
|
|
[[ -r "$cnf" ]] || die "DB CNF not readable: $cnf"
|
|
echo "-- Dump MySQL/MariaDB DB: ${db}"
|
|
mysqldump --defaults-extra-file="$cnf" --single-transaction --routines --triggers --hex-blob "$db" > "$out"
|
|
}
|
|
|
|
[[ -n "${WP_DB_NAME:-}" ]] && dump_mysql_db "${WP_DB_CNF}" "${WP_DB_NAME}" "$STAGING_DIR/db/wordpress_${ts}.sql" || true
|
|
|
|
if [[ -n "${NC_DB_NAME:-}" ]]; then
|
|
if [[ "${ENABLE_NEXTCLOUD_MAINTENANCE:-true}" == "true" ]]; then
|
|
echo "-- Nextcloud maintenance mode ON..."
|
|
sudo -u "${NC_OCC_USER}" php "${NC_DIR}/occ" maintenance:mode --on
|
|
NC_MAINTENANCE_ON=true
|
|
fi
|
|
dump_mysql_db "${NC_DB_CNF}" "${NC_DB_NAME}" "$STAGING_DIR/db/nextcloud_${ts}.sql"
|
|
if [[ "${ENABLE_NEXTCLOUD_MAINTENANCE:-true}" == "true" ]]; then
|
|
echo "-- Nextcloud maintenance mode OFF..."
|
|
sudo -u "${NC_OCC_USER}" php "${NC_DIR}/occ" maintenance:mode --off || true
|
|
NC_MAINTENANCE_ON=false
|
|
fi
|
|
fi
|
|
|
|
if [[ "${ENABLE_GITEA:-false}" == "true" ]]; then
|
|
# native gitea with MariaDB
|
|
[[ -n "${GITEA_DB_NAME:-}" ]] && dump_mysql_db "${GITEA_DB_CNF}" "${GITEA_DB_NAME}" "$STAGING_DIR/db/gitea_${ts}.sql" || echo "WARN: ENABLE_GITEA=true but GITEA_DB_NAME empty - skipping gitea DB"
|
|
fi
|
|
else
|
|
echo "-- DB dumps disabled"
|
|
fi
|
|
|
|
# ---------- File copies ----------
|
|
echo "-- Collecting files via rsync..."
|
|
|
|
rsync_dir() {
|
|
local src="$1"
|
|
local dst="$2"
|
|
shift 2 || true
|
|
[[ -d "$src" ]] || die "Source directory missing: $src"
|
|
mkdir -p "$dst"
|
|
local excludes=()
|
|
while [[ $# -gt 0 ]]; do excludes+=("--exclude=$1"); shift; done
|
|
rsync -aHAX --numeric-ids --delete --info=stats2 "${excludes[@]}" "$src"/ "$dst"/
|
|
}
|
|
|
|
# WordPress webroot: exclude nextcloud/ if it lives below WP_DIR
|
|
if [[ "${ENABLE_WORDPRESS:-false}" == "true" ]]; then
|
|
echo "-- WordPress files: ${WP_DIR}"
|
|
wp_excludes=()
|
|
if [[ "${ENABLE_NEXTCLOUD:-false}" == "true" ]]; then
|
|
wp="${WP_DIR%/}"; nc="${NC_DIR%/}"
|
|
if [[ "$nc" == "$wp/nextcloud" ]]; then
|
|
wp_excludes+=("nextcloud/")
|
|
fi
|
|
fi
|
|
if [[ "${#wp_excludes[@]}" -gt 0 ]]; then
|
|
echo "-- WordPress excludes: ${wp_excludes[*]}"
|
|
rsync_dir "${WP_DIR}" "$STAGING_DIR/files/wordpress" "${wp_excludes[@]}"
|
|
else
|
|
rsync_dir "${WP_DIR}" "$STAGING_DIR/files/wordpress"
|
|
fi
|
|
fi
|
|
|
|
# Nextcloud code: exclude data/
|
|
if [[ "${ENABLE_NEXTCLOUD:-false}" == "true" ]]; then
|
|
echo "-- Nextcloud code: ${NC_DIR} (excluding data/)"
|
|
rsync_dir "${NC_DIR}" "$STAGING_DIR/files/nextcloud" "data/"
|
|
|
|
: "${NC_DATA_DIR:=${NC_DIR%/}/data}"
|
|
if [[ "${ENABLE_NEXTCLOUD_DATA:-true}" == "true" ]]; then
|
|
echo "-- Nextcloud data: ${NC_DATA_DIR}"
|
|
rsync_dir "${NC_DATA_DIR}" "$STAGING_DIR/files/nextcloud-data"
|
|
fi
|
|
fi
|
|
|
|
# Gitea files (based on app.ini APP_DATA_PATH)
|
|
if [[ "${ENABLE_GITEA:-false}" == "true" ]]; then
|
|
: "${GITEA_DATA_DIR:=/var/lib/gitea/data}"
|
|
echo "-- Gitea data dir: ${GITEA_DATA_DIR}"
|
|
rsync_dir "${GITEA_DATA_DIR}" "$STAGING_DIR/files/gitea-data"
|
|
|
|
: "${GITEA_ETC_DIR:=/etc/gitea}"
|
|
if [[ -n "${GITEA_ETC_DIR}" && -d "${GITEA_ETC_DIR}" ]]; then
|
|
echo "-- Gitea config dir: ${GITEA_ETC_DIR}"
|
|
rsync_dir "${GITEA_ETC_DIR}" "$STAGING_DIR/files/gitea-etc"
|
|
fi
|
|
fi
|
|
|
|
\
|
|
if [[ "${ENABLE_MAIL}" == "true" ]]; then
|
|
echo "-- Mail files..."
|
|
mkdir -p "$STAGING_DIR/files/mail" "$STAGING_DIR/files/postfix" "$STAGING_DIR/files/dovecot" "$STAGING_DIR/files/mail-home"
|
|
|
|
# Optional short stop for maximum consistency
|
|
if [[ "${MAIL_BACKUP_MODE}" == "stop" ]]; then
|
|
echo "-- Stopping postfix/dovecot for mail backup..."
|
|
systemctl stop postfix || true
|
|
systemctl stop dovecot || true
|
|
fi
|
|
|
|
[[ -n "${MAIL_DIR:-}" && -d "${MAIL_DIR}" ]] && rsync_dir "${MAIL_DIR}" "$STAGING_DIR/files/mail" || true
|
|
[[ -n "${POSTFIX_DIR:-}" && -d "${POSTFIX_DIR}" ]] && rsync_dir "${POSTFIX_DIR}" "$STAGING_DIR/files/postfix" || true
|
|
[[ -n "${DOVECOT_DIR:-}" && -d "${DOVECOT_DIR}" ]] && rsync_dir "${DOVECOT_DIR}" "$STAGING_DIR/files/dovecot" || true
|
|
|
|
# Home mail dirs (per-user mail files under /home)
|
|
if [[ "${ENABLE_MAIL_HOME_DIRS}" == "true" ]]; then
|
|
for h in ${MAIL_HOME_DIRS}; do
|
|
if [[ -d "${h}" ]]; then
|
|
user="$(basename "${h}")"
|
|
echo "-- Mail (home): ${h}"
|
|
dest="$STAGING_DIR/files/mail-home/${user}"
|
|
mkdir -p "$dest"
|
|
|
|
# Some home subdirs (e.g. FUSE mounts like OneDrive) may deny access or cause
|
|
# noise during backup. We keep this resilient:
|
|
# - skip if the user does not exist
|
|
# - run rsync as root (so we can write into the root-owned staging dir)
|
|
# - prevent crossing filesystem boundaries (skip mounts)
|
|
# - exclude noisy/unwanted dirs via MAIL_HOME_EXCLUDES
|
|
if ! id "${user}" >/dev/null 2>&1; then
|
|
echo "WARNING: mail-home user '${user}' does not exist (skipping ${h})" >&2
|
|
continue
|
|
fi
|
|
|
|
chown root:root "$dest" 2>/dev/null || true
|
|
chmod 0750 "$dest" 2>/dev/null || true
|
|
|
|
set +e
|
|
rsync -aHAX --numeric-ids --info=stats2 --one-file-system ${MAIL_HOME_EXCLUDES} "${h}/" "${dest}/"
|
|
rc=$?
|
|
set -e
|
|
|
|
if [[ $rc -ne 0 ]]; then
|
|
echo "WARNING: rsync for ${h} returned code ${rc} (continuing)" >&2
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [[ "${MAIL_BACKUP_MODE}" == "stop" ]]; then
|
|
echo "-- Starting postfix/dovecot after mail backup..."
|
|
systemctl start dovecot || true
|
|
systemctl start postfix || true
|
|
fi
|
|
fi
|
|
|
|
|
|
# ---------- Size summary ----------
|
|
SIZES_FILE="${STAGING_DIR}/meta/sizes.txt"
|
|
{
|
|
echo "Mail-home staged: $(human_bytes "$(bytes_of_path "$STAGING_DIR/files/mail-home")")"
|
|
echo "Mail staged: $(human_bytes "$(bytes_of_path "$STAGING_DIR/files/mail")")"
|
|
echo "Postfix staged: $(human_bytes "$(bytes_of_path "$STAGING_DIR/files/postfix")")"
|
|
echo "Dovecot staged: $(human_bytes "$(bytes_of_path "$STAGING_DIR/files/dovecot")")"
|
|
echo "DB dumps staged: $(human_bytes "$(bytes_of_path "$STAGING_DIR/db")")"
|
|
echo "WordPress staged: $(human_bytes "$(bytes_of_path "$STAGING_DIR/files/wordpress")")"
|
|
echo "Nextcloud code staged: $(human_bytes "$(bytes_of_path "$STAGING_DIR/files/nextcloud")")"
|
|
echo "Nextcloud data staged: $(human_bytes "$(bytes_of_path "$STAGING_DIR/files/nextcloud-data")")"
|
|
echo "Gitea data staged: $(human_bytes "$(bytes_of_path "$STAGING_DIR/files/gitea-data")")"
|
|
echo "Gitea etc staged: $(human_bytes "$(bytes_of_path "$STAGING_DIR/files/gitea-etc")")"
|
|
echo "Staging total: $(human_bytes "$(bytes_of_path "$STAGING_DIR")")"
|
|
} > "$SIZES_FILE" || true
|
|
|
|
ensure_min_free_space
|
|
|
|
# ---------- Create separate archives ----------
|
|
make_archive() {
|
|
local label="$1" src_rel="$2"
|
|
local tar_file="${ARCHIVE_DIR}/${ARCHIVE_PREFIX}_${ts}_${label}.tar"
|
|
local out_file
|
|
echo "-- Creating archive (${label}): ${tar_file}" >&2
|
|
(
|
|
cd "$STAGING_DIR"
|
|
tar --numeric-owner --xattrs --acls -cf "$tar_file" "$src_rel"
|
|
)
|
|
|
|
if [[ "$COMPRESSOR" == "zstd" ]]; then
|
|
out_file="${tar_file}.zst"
|
|
echo "-- Compressing (zstd): ${out_file}" >&2
|
|
ionice -c "${IONICE_CLASS}" -n "${IONICE_LEVEL}" nice -n "${NICE_LEVEL}" zstd -T0 -19 --rm -q "$tar_file" 1>&2
|
|
zstd -t "$out_file" 1>&2
|
|
else
|
|
out_file="${tar_file}.gz"
|
|
echo "-- Compressing (gzip): ${out_file}" >&2
|
|
ionice -c "${IONICE_CLASS}" -n "${IONICE_LEVEL}" nice -n "${NICE_LEVEL}" gzip -9 "$tar_file"
|
|
gzip -t "$out_file"
|
|
fi
|
|
echo "$out_file" >&2
|
|
echo "$out_file"
|
|
}
|
|
|
|
ARCHIVES=()
|
|
ARCHIVES+=("$(make_archive "meta" "meta")")
|
|
|
|
if [[ -d "$STAGING_DIR/db" && -n "$(ls -A "$STAGING_DIR/db" 2>/dev/null || true)" ]]; then
|
|
ARCHIVES+=("$(make_archive "db" "db")")
|
|
fi
|
|
[[ "${ENABLE_WORDPRESS:-false}" == "true" ]] && ARCHIVES+=("$(make_archive "wordpress" "files/wordpress")") || true
|
|
if [[ "${ENABLE_NEXTCLOUD:-false}" == "true" ]]; then
|
|
ARCHIVES+=("$(make_archive "nextcloud" "files/nextcloud")")
|
|
[[ "${ENABLE_NEXTCLOUD_DATA:-true}" == "true" ]] && ARCHIVES+=("$(make_archive "nextcloud-data" "files/nextcloud-data")") || true
|
|
fi
|
|
if [[ "${ENABLE_GITEA:-false}" == "true" ]]; then
|
|
ARCHIVES+=("$(make_archive "gitea" "files/gitea-data")")
|
|
if [[ -d "$STAGING_DIR/files/gitea-etc" && -n "$(ls -A "$STAGING_DIR/files/gitea-etc" 2>/dev/null || true)" ]]; then
|
|
ARCHIVES+=("$(make_archive "gitea-etc" "files/gitea-etc")")
|
|
fi
|
|
fi
|
|
|
|
# Filter empty archive entries
|
|
ARCHIVES=($(for a in "${ARCHIVES[@]:-}"; do [[ -n "$a" ]] && echo "$a"; done))
|
|
|
|
# Mail archives (vmail/postfix/dovecot + optional /home mail dirs)
|
|
if [[ "${ENABLE_MAIL}" == "true" ]]; then
|
|
ARCHIVES+=("$(make_archive "mail" "files/mail")")
|
|
ARCHIVES+=("$(make_archive "mail-home" "files/mail-home")")
|
|
ARCHIVES+=("$(make_archive "postfix" "files/postfix")")
|
|
ARCHIVES+=("$(make_archive "dovecot" "files/dovecot")")
|
|
fi
|
|
|
|
echo "-- Archives created:"
|
|
for f in "${ARCHIVES[@]}"; do
|
|
echo " - $f ($(du -h "$f" | awk '{print $1}'))"
|
|
done
|
|
|
|
# restart gitea before upload
|
|
gitea_service_start
|
|
|
|
# ---------- Upload via rclone ----------
|
|
if [[ "${ENABLE_UPLOAD}" == "true" ]]; then
|
|
RCLONE_OUTPUT_FILE="${LOG_DIR}/rclone_${ts}.log"
|
|
RCLONE_STATUS="RUNNING"
|
|
remote_run="${RCLONE_REMOTE_BASE}/${ARCHIVE_PREFIX}_${ts}"
|
|
|
|
# Split "Remote:path" into remote name + path. Some rclone subcommands require only the remote name.
|
|
[[ "${RCLONE_REMOTE_BASE}" == *:* ]] || die "RCLONE_REMOTE_BASE must be in the form Remote:path (got: ${RCLONE_REMOTE_BASE})"
|
|
remote_name="${RCLONE_REMOTE_BASE%%:*}"
|
|
remote_base_path="${RCLONE_REMOTE_BASE#*:}"
|
|
|
|
echo "-- rclone remote check (crypt enforced): ${remote_name}"
|
|
require_crypt_remote "${remote_name}"
|
|
|
|
# Check remote root reachability (works even if base folder doesn't exist yet)
|
|
"$RCLONE_BIN" lsd "${remote_name}:" --max-depth 1 >/dev/null 2>&1 || die "Remote not reachable: ${remote_name}:"
|
|
|
|
# Ensure base folder exists (best effort)
|
|
echo "-- Ensuring remote base folder exists: ${RCLONE_REMOTE_BASE}"
|
|
"$RCLONE_BIN" mkdir "${RCLONE_REMOTE_BASE}" >/dev/null 2>&1 || true
|
|
|
|
echo "-- Creating remote folder: ${remote_run}"
|
|
"$RCLONE_BIN" mkdir "${remote_run}" >/dev/null 2>&1 || true
|
|
|
|
common_args=(
|
|
"--checksum"
|
|
"--retries" "${RCLONE_RETRIES}"
|
|
"--low-level-retries" "${RCLONE_LOW_LEVEL_RETRIES}"
|
|
"--retries-sleep" "${RCLONE_RETRIES_SLEEP}"
|
|
"--stats" "${RCLONE_STATS}"
|
|
"--stats-one-line"
|
|
"--log-level" "INFO"
|
|
"--transfers" "${RCLONE_TRANSFERS}"
|
|
"--checkers" "${RCLONE_CHECKERS}"
|
|
"--timeout" "${RCLONE_TIMEOUT}"
|
|
"--contimeout" "${RCLONE_CONTIMEOUT}"
|
|
"--onedrive-chunk-size" "${RCLONE_ONEDRIVE_CHUNK_SIZE}"
|
|
)
|
|
[[ "${RCLONE_BWLIMIT}" != "0" ]] && common_args+=("--bwlimit" "${RCLONE_BWLIMIT}") || true
|
|
|
|
echo "-- Uploading archives to: ${remote_run} (log: ${RCLONE_OUTPUT_FILE})"
|
|
for f in "${ARCHIVES[@]}"; do
|
|
[[ -n "$f" ]] || continue
|
|
echo "-- Upload: $(basename "$f")"
|
|
if ionice -c "${IONICE_CLASS}" -n "${IONICE_LEVEL}" nice -n "${NICE_LEVEL}" "$RCLONE_BIN" copy "$f" "${remote_run}" "${common_args[@]}" | tee -a "$RCLONE_OUTPUT_FILE"
|
|
then
|
|
:
|
|
else
|
|
RCLONE_STATUS="FAIL"
|
|
die "Upload failed for $(basename "$f") (see ${RCLONE_OUTPUT_FILE})"
|
|
fi
|
|
done
|
|
RCLONE_STATUS="OK"
|
|
|
|
# Show what landed on the remote for this run (small folder: archives only)
|
|
echo "-- Remote listing (this run): ${remote_run}"
|
|
"$RCLONE_BIN" lsl "${remote_run}" --max-depth 1 --log-level INFO || echo "WARNING: Could not list remote run folder: ${remote_run}"
|
|
|
|
if [[ "${ENABLE_REMOTE_RETENTION}" == "true" ]]; then
|
|
echo "-- Remote retention: delete objects older than ${REMOTE_RETENTION_DAYS}d (best effort)"
|
|
"$RCLONE_BIN" delete "${RCLONE_REMOTE_BASE}" --min-age "${REMOTE_RETENTION_DAYS}d" --log-level INFO || true
|
|
fi
|
|
else
|
|
echo "-- Upload disabled (ENABLE_UPLOAD=false)"
|
|
fi
|
|
|
|
cleanup_old_local_archives
|
|
echo "== app-backup done: ${ts} =="
|