Updating app-backup.sh and app-restore.sh to handle nextcloud data correctly.

This commit is contained in:
2026-02-08 21:49:45 +01:00
parent 144de4f393
commit 77b3b65bfb
2 changed files with 266 additions and 190 deletions

View File

@@ -53,6 +53,10 @@ fi
: "${MAIL_SUBJECT_PREFIX:=[app-backup]}" : "${MAIL_SUBJECT_PREFIX:=[app-backup]}"
: "${MAIL_INCLUDE_LOG_TAIL_LINES:=200}" : "${MAIL_INCLUDE_LOG_TAIL_LINES:=200}"
# OPTIONAL: allow wp excludes via config, e.g. WP_EXCLUDES=("nextcloud/" "foo/")
# If unset, we compute a safe default for your setup.
: "${WP_EXCLUDES_MODE:=auto}" # auto|manual
# ---------- State for report ---------- # ---------- State for report ----------
START_EPOCH="$(date +%s)" START_EPOCH="$(date +%s)"
STATUS="SUCCESS" STATUS="SUCCESS"
@@ -95,10 +99,7 @@ cleanup_old_local_archives() {
DELETED_LOCAL_LIST_FILE="$list_file" DELETED_LOCAL_LIST_FILE="$list_file"
: > "$list_file" : > "$list_file"
# collect files older than retention
# shellcheck disable=SC2016
while IFS= read -r -d '' f; do while IFS= read -r -d '' f; do
# best-effort size
local sz local sz
sz="$(stat -c '%s' "$f" 2>/dev/null || echo 0)" sz="$(stat -c '%s' "$f" 2>/dev/null || echo 0)"
DELETED_LOCAL_BYTES=$((DELETED_LOCAL_BYTES + sz)) DELETED_LOCAL_BYTES=$((DELETED_LOCAL_BYTES + sz))
@@ -108,7 +109,6 @@ cleanup_old_local_archives() {
if [[ "$DELETED_LOCAL_COUNT" -gt 0 ]]; then if [[ "$DELETED_LOCAL_COUNT" -gt 0 ]]; then
echo "-- Local retention: deleting ${DELETED_LOCAL_COUNT} archive(s) older than ${LOCAL_RETENTION_DAYS} day(s) from ${ARCHIVE_DIR}" echo "-- Local retention: deleting ${DELETED_LOCAL_COUNT} archive(s) older than ${LOCAL_RETENTION_DAYS} day(s) from ${ARCHIVE_DIR}"
# delete using the collected list to ensure count/bytes match what we report
while IFS= read -r f; do while IFS= read -r f; do
rm -f -- "$f" || true rm -f -- "$f" || true
done < "$list_file" done < "$list_file"
@@ -356,14 +356,55 @@ echo "-- Collecting files via rsync..."
rsync_dir() { rsync_dir() {
local src="$1" local src="$1"
local dst="$2" local dst="$2"
shift 2 || true
[[ -d "$src" ]] || die "Source directory missing: $src" [[ -d "$src" ]] || die "Source directory missing: $src"
mkdir -p "$dst" mkdir -p "$dst"
rsync -aHAX --numeric-ids --delete --info=stats2 "$src"/ "$dst"/
# Remaining args are exclude patterns like "nextcloud/"
local excludes=()
while [[ $# -gt 0 ]]; do
excludes+=("--exclude=$1")
shift
done
rsync -aHAX --numeric-ids --delete --info=stats2 \
"${excludes[@]}" \
"$src"/ "$dst"/
}
compute_wp_excludes() {
# Returns exclude patterns via stdout, one per line
if [[ "${WP_EXCLUDES_MODE}" == "manual" ]]; then
if declare -p WP_EXCLUDES >/dev/null 2>&1; then
for e in "${WP_EXCLUDES[@]}"; do
echo "$e"
done
fi
return 0
fi
# auto mode:
# If Nextcloud is enabled and sits inside WP_DIR (your setup), exclude "nextcloud/" from WP sync
if [[ "${ENABLE_NEXTCLOUD}" == "true" ]]; then
local wp="${WP_DIR%/}"
local nc="${NC_DIR%/}"
if [[ "$nc" == "$wp/nextcloud" ]]; then
echo "nextcloud/"
fi
fi
} }
if [[ "${ENABLE_WORDPRESS}" == "true" ]]; then if [[ "${ENABLE_WORDPRESS}" == "true" ]]; then
echo "-- WordPress files: ${WP_DIR}" echo "-- WordPress files: ${WP_DIR}"
rsync_dir "${WP_DIR}" "$STAGING_DIR/files/wordpress"
mapfile -t _wp_excludes < <(compute_wp_excludes || true)
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 fi
if [[ "${ENABLE_NEXTCLOUD}" == "true" ]]; then if [[ "${ENABLE_NEXTCLOUD}" == "true" ]]; then

View File

@@ -2,14 +2,14 @@
set -Eeuo pipefail set -Eeuo pipefail
umask 027 umask 027
# ---------------- Logging ---------------- # ---------- Logging ----------
LOG_DIR="/var/log/app-backup" LOG_DIR="/var/log/app-backup"
mkdir -p "$LOG_DIR" mkdir -p "$LOG_DIR"
ts="$(date '+%Y-%m-%d_%H-%M-%S')" ts="$(date '+%Y-%m-%d_%H-%M-%S')"
LOG_FILE="${LOG_DIR}/app-restore_${ts}.log" LOG_FILE="${LOG_DIR}/app-restore_${ts}.log"
exec > >(tee -a "$LOG_FILE" | systemd-cat -t app-restore -p info) 2>&1 exec > >(tee -a "$LOG_FILE" | systemd-cat -t app-restore -p info) 2>&1
# ---------------- Config ---------------- # ---------- Config ----------
CONFIG_FILE="/etc/app-backup/app-backup.conf" CONFIG_FILE="/etc/app-backup/app-backup.conf"
if [[ -r "$CONFIG_FILE" ]]; then if [[ -r "$CONFIG_FILE" ]]; then
# shellcheck disable=SC1090 # shellcheck disable=SC1090
@@ -19,225 +19,260 @@ else
exit 2 exit 2
fi fi
# ---------- Defaults ----------
: "${WORKDIR:=/var/backups/app-backup}" : "${WORKDIR:=/var/backups/app-backup}"
: "${STAGING_ROOT:=${WORKDIR}/restore-staging}" : "${RESTORE_ROOT:=${WORKDIR}/restore}"
: "${RCLONE_REMOTE:=onedrive:Sicherung}" : "${RCLONE_REMOTE:=onedrive:Sicherung}"
: "${RCLONE_BIN:=rclone}" : "${RCLONE_BIN:=rclone}"
: "${DRY_RUN:=false}" # true = show what would happen
: "${RESTORE_DB:=true}" # true/false
: "${RESTORE_FILES:=true}" # true/false
: "${RESTORE_STRICT_DELETE:=false}" # true = rsync --delete on restore
: "${ENABLE_NEXTCLOUD_MAINTENANCE:=true}"
: "${NC_OCC_USER:=apache}" : "${NC_OCC_USER:=apache}"
: "${NC_FILES_SCAN_AFTER_RESTORE:=false}"
ARCHIVE_PATH="" die() { echo "ERROR: $*"; exit 1; }
REMOTE_NAME="" have() { command -v "$1" >/dev/null 2>&1; }
DRY_RUN=false
FORCE=false
DO_WORDPRESS=true run_cmd() {
DO_NEXTCLOUD=true if [[ "${DRY_RUN}" == "true" ]]; then
DO_NEXTCLOUD_DATA=true echo "[DRY_RUN] $*"
DO_MAIL=true
DO_DB=true
usage() {
cat <<EOF
Usage:
app-restore.sh --archive /path/to/appbackup_....tar.(zst|gz)
app-restore.sh --remote-file appbackup_....tar.(zst|gz)
Options:
--archive <path> Local archive path.
--remote-file <name> Download that file name from rclone remote (${RCLONE_REMOTE}) into ${WORKDIR}/restore-downloads/.
--dry-run No writes (rsync --dry-run; DB import skipped; services not restarted).
--force Skip some safety checks.
--only <list> Comma-separated: wordpress,nextcloud,nextcloud-data,mail,db
--skip <list> Comma-separated: wordpress,nextcloud,nextcloud-data,mail,db
-h|--help Help.
EOF
}
set_all_false() { DO_WORDPRESS=false; DO_NEXTCLOUD=false; DO_NEXTCLOUD_DATA=false; DO_MAIL=false; DO_DB=false; }
parse_list_flags() {
local mode="$1" list="$2"
IFS=',' read -r -a items <<< "$list"
for it in "${items[@]}"; do
it="$(echo "$it" | tr '[:upper:]' '[:lower:]' | xargs)"
case "$it" in
wordpress) [[ "$mode" == "only" ]] && DO_WORDPRESS=true || DO_WORDPRESS=false ;;
nextcloud) [[ "$mode" == "only" ]] && DO_NEXTCLOUD=true || DO_NEXTCLOUD=false ;;
nextcloud-data) [[ "$mode" == "only" ]] && DO_NEXTCLOUD_DATA=true || DO_NEXTCLOUD_DATA=false ;;
mail) [[ "$mode" == "only" ]] && DO_MAIL=true || DO_MAIL=false ;;
db) [[ "$mode" == "only" ]] && DO_DB=true || DO_DB=false ;;
"" ) : ;;
*) echo "WARN: unknown component: $it" ;;
esac
done
}
while [[ $# -gt 0 ]]; do
case "$1" in
--archive) ARCHIVE_PATH="${2:-}"; shift 2 ;;
--remote-file) REMOTE_NAME="${2:-}"; shift 2 ;;
--dry-run) DRY_RUN=true; shift ;;
--force) FORCE=true; shift ;;
--only) set_all_false; parse_list_flags "only" "${2:-}"; shift 2 ;;
--skip) parse_list_flags "skip" "${2:-}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "ERROR: unknown arg: $1"; usage; exit 2 ;;
esac
done
die(){ echo "ERROR: $*"; exit 1; }
have(){ command -v "$1" >/dev/null 2>&1; }
service_is_active(){ systemctl is-active --quiet "$1"; }
STOPPED_SERVICES=()
stop_service_if_active() {
local s="$1"
if service_is_active "$s"; then
echo "-- Stopping service: $s"
[[ "$DRY_RUN" == "true" ]] || systemctl stop "$s"
STOPPED_SERVICES+=("$s")
fi
}
start_stopped_services() {
if [[ "$DRY_RUN" == "true" ]]; then
echo "-- DRY RUN: not starting services"
return 0
fi
for s in "${STOPPED_SERVICES[@]}"; do
echo "-- Starting service: $s"
systemctl start "$s" || echo "WARN: failed to start $s"
done
}
integrity_check() {
local f="$1"
if [[ "$f" == *.zst ]]; then
have zstd || die "zstd missing but archive is .zst"
zstd -t "$f"
elif [[ "$f" == *.gz ]]; then
have gzip || die "gzip missing but archive is .gz"
gzip -t "$f"
else else
die "Unsupported archive extension: $f" "$@"
fi fi
} }
extract_archive() { # Nextcloud maintenance-mode safety trap
local f="$1" dest="$2"
mkdir -p "$dest"
if [[ "$f" == *.zst ]]; then
tar --use-compress-program=unzstd -xf "$f" -C "$dest"
else
tar -xzf "$f" -C "$dest"
fi
}
rsync_restore_dir() {
local src="$1" dst="$2"
[[ -d "$src" ]] || die "Staging source missing: $src"
mkdir -p "$dst"
local args=(-aHAX --numeric-ids --delete)
[[ "$DRY_RUN" == "true" ]] && args+=(--dry-run)
echo "-- Restoring: $src => $dst"
rsync "${args[@]}" "$src"/ "$dst"/
}
mysql_import() {
local cnf="$1" db="$2" sql="$3"
[[ -r "$cnf" ]] || die "DB cnf not readable: $cnf"
[[ -r "$sql" ]] || die "SQL dump not readable: $sql"
have mysql || die "mysql client missing"
if [[ "$DRY_RUN" == "true" ]]; then
echo "-- DRY RUN: skipping DB import for $db"
return 0
fi
mysql --defaults-extra-file="$cnf" -e "CREATE DATABASE IF NOT EXISTS \`${db}\`;"
mysql --defaults-extra-file="$cnf" "$db" < "$sql"
}
NC_MAINTENANCE_ON=false NC_MAINTENANCE_ON=false
nc_maintenance_on() {
if [[ "$DRY_RUN" == "true" ]]; then
echo "-- DRY RUN: not toggling Nextcloud maintenance mode"
return 0
fi
if [[ -f "${NC_DIR}/occ" ]]; then
sudo -u "${NC_OCC_USER}" php "${NC_DIR}/occ" maintenance:mode --on
NC_MAINTENANCE_ON=true
fi
}
nc_maintenance_off() { nc_maintenance_off() {
if [[ "${NC_MAINTENANCE_ON}" == "true" ]]; then if [[ "${NC_MAINTENANCE_ON}" == "true" ]]; then
sudo -u "${NC_OCC_USER}" php "${NC_DIR}/occ" maintenance:mode --off || true echo "-- Nextcloud maintenance mode OFF (trap)..."
run_cmd sudo -u "${NC_OCC_USER}" php "${NC_DIR}/occ" maintenance:mode --off || true
NC_MAINTENANCE_ON=false NC_MAINTENANCE_ON=false
fi fi
} }
echo "== app-restore start: ${ts} ==" on_exit() {
echo "-- Log: ${LOG_FILE}" local exit_code=$?
nc_maintenance_off
exit "${exit_code}"
}
trap on_exit EXIT
# ---------- Preconditions ----------
[[ $EUID -eq 0 ]] || die "Must run as root." [[ $EUID -eq 0 ]] || die "Must run as root."
LOCKFILE="/run/app-restore.lock" for t in tar rsync flock df find stat; do
have "$t" || die "Missing required tool: $t"
done
mkdir -p "$WORKDIR" "$RESTORE_ROOT" "$LOG_DIR"
# ---------- Locking ----------
LOCKFILE="/run/app-backup.lock"
exec 9>"$LOCKFILE" exec 9>"$LOCKFILE"
flock -n 9 || die "Another restore is already running (lock: $LOCKFILE)" if ! flock -n 9; then
die "Another backup/restore already running (lock: $LOCKFILE)"
fi
DOWNLOAD_DIR="${WORKDIR}/restore-downloads" # ---------- Input ----------
mkdir -p "$DOWNLOAD_DIR" # Usage:
# app-restore.sh /path/to/appbackup_YYYY-mm-dd_HH-MM-SS.tar.zst
# or
# app-restore.sh /path/to/appbackup_YYYY-mm-dd_HH-MM-SS.tar.gz
# or
# app-restore.sh --remote appbackup_YYYY-mm-dd_HH-MM-SS.tar.zst
# (copies from RCLONE_REMOTE to RESTORE_ROOT first)
ARCHIVE_PATH=""
REMOTE_NAME=""
if [[ "${1:-}" == "--remote" ]]; then
REMOTE_NAME="${2:-}"
[[ -n "$REMOTE_NAME" ]] || die "Usage: $0 --remote <archive_filename>"
have "$RCLONE_BIN" || die "rclone missing but --remote used"
else
ARCHIVE_PATH="${1:-}"
[[ -n "$ARCHIVE_PATH" ]] || die "Usage: $0 <archive_file.tar.zst|tar.gz> OR $0 --remote <archive_filename>"
fi
echo "== app-restore start: ${ts} =="
echo "-- Config: ${CONFIG_FILE}"
echo "-- Log: ${LOG_FILE}"
echo "-- DRY_RUN: ${DRY_RUN}"
echo "-- RESTORE_FILES: ${RESTORE_FILES}"
echo "-- RESTORE_DB: ${RESTORE_DB}"
echo "-- STRICT_DELETE: ${RESTORE_STRICT_DELETE}"
# ---------- Fetch from remote if requested ----------
if [[ -n "$REMOTE_NAME" ]]; then if [[ -n "$REMOTE_NAME" ]]; then
have "$RCLONE_BIN" || die "rclone required for --remote-file" ARCHIVE_PATH="${RESTORE_ROOT}/${REMOTE_NAME}"
ARCHIVE_PATH="${DOWNLOAD_DIR}/${REMOTE_NAME}" echo "-- Fetching from remote: ${RCLONE_REMOTE}/${REMOTE_NAME} -> ${ARCHIVE_PATH}"
if [[ "$DRY_RUN" == "true" ]]; then run_cmd "$RCLONE_BIN" copy "${RCLONE_REMOTE}/${REMOTE_NAME}" "${RESTORE_ROOT}" --checksum --log-level INFO
echo "-- DRY RUN: would download ${RCLONE_REMOTE}/${REMOTE_NAME} to ${ARCHIVE_PATH}" fi
[[ -f "$ARCHIVE_PATH" ]] || die "Archive not found: $ARCHIVE_PATH"
# ---------- Detect compression ----------
ARCHIVE_BASENAME="$(basename "$ARCHIVE_PATH")"
IS_ZSTD=false
IS_GZIP=false
case "$ARCHIVE_BASENAME" in
*.tar.zst) IS_ZSTD=true ;;
*.tar.gz) IS_GZIP=true ;;
*)
# fallback: try file(1)
if have file; then
ftype="$(file -b "$ARCHIVE_PATH" || true)"
if echo "$ftype" | grep -qi zstd; then
IS_ZSTD=true
elif echo "$ftype" | grep -qi gzip; then
IS_GZIP=true
else
die "Cannot detect archive compression for: $ARCHIVE_PATH"
fi
else
die "Unknown archive extension and file(1) not available: $ARCHIVE_PATH"
fi
;;
esac
if [[ "$IS_ZSTD" == "true" ]]; then
have zstd || die "zstd archive but zstd missing"
elif [[ "$IS_GZIP" == "true" ]]; then
have gzip || die "gzip archive but gzip missing"
fi
# ---------- Extract ----------
RUN_DIR="${RESTORE_ROOT}/run_${ts}"
STAGING_DIR="${RUN_DIR}/staging"
mkdir -p "$STAGING_DIR"
echo "-- Extracting archive to: ${STAGING_DIR}"
if [[ "$IS_ZSTD" == "true" ]]; then
run_cmd tar --zstd -xf "$ARCHIVE_PATH" -C "$STAGING_DIR"
elif [[ "$IS_GZIP" == "true" ]]; then
run_cmd tar -xzf "$ARCHIVE_PATH" -C "$STAGING_DIR"
fi
[[ -d "$STAGING_DIR/files" ]] || die "Invalid archive content: missing files/ in extracted staging"
# ---------- Maintenance mode (Nextcloud) ----------
if [[ "${ENABLE_NEXTCLOUD}" == "true" && "${ENABLE_NEXTCLOUD_MAINTENANCE}" == "true" ]]; then
if [[ -d "${NC_DIR}" && -f "${NC_DIR}/occ" ]]; then
echo "-- Nextcloud maintenance mode ON..."
run_cmd sudo -u "${NC_OCC_USER}" php "${NC_DIR}/occ" maintenance:mode --on
NC_MAINTENANCE_ON=true
else else
"$RCLONE_BIN" copy "${RCLONE_REMOTE}/${REMOTE_NAME}" "$DOWNLOAD_DIR" --log-level INFO echo "WARN: Nextcloud directory/occ not found at NC_DIR=${NC_DIR} - cannot toggle maintenance mode"
fi fi
fi fi
[[ -n "$ARCHIVE_PATH" ]] || { usage; die "Provide --archive or --remote-file"; } # ---------- Restore files ----------
[[ -f "$ARCHIVE_PATH" ]] || die "Archive not found: $ARCHIVE_PATH" rsync_restore_dir() {
local src="$1"
local dst="$2"
echo "-- Selected archive: $ARCHIVE_PATH" [[ -d "$src" ]] || die "Restore source missing: $src"
integrity_check "$ARCHIVE_PATH" mkdir -p "$dst"
STAGING_DIR="${STAGING_ROOT}/run_${ts}" local delete_flag=()
trap 'nc_maintenance_off; start_stopped_services; rm -rf "${STAGING_DIR:?}"' EXIT if [[ "${RESTORE_STRICT_DELETE}" == "true" ]]; then
extract_archive "$ARCHIVE_PATH" "$STAGING_DIR" delete_flag=(--delete)
fi
[[ -d "$STAGING_DIR/files" ]] || die "Invalid archive: missing 'files' dir" run_cmd rsync -aHAX --numeric-ids --info=stats2 \
"${delete_flag[@]}" \
"$src"/ "$dst"/
}
# stop services to prevent writes if [[ "${RESTORE_FILES}" == "true" ]]; then
if systemctl list-unit-files | grep -q '^httpd\.service'; then stop_service_if_active httpd; fi echo "-- Restoring files..."
if [[ "$DO_MAIL" == "true" ]]; then
if systemctl list-unit-files | grep -q '^postfix\.service'; then stop_service_if_active postfix; fi if [[ "${ENABLE_WORDPRESS}" == "true" ]]; then
if systemctl list-unit-files | grep -q '^dovecot\.service'; then stop_service_if_active dovecot; fi echo "-- Restore WordPress (webroot) to: ${WP_DIR}"
# Backup excluded nextcloud/ automatically, so this should not overwrite Nextcloud.
rsync_restore_dir "$STAGING_DIR/files/wordpress" "${WP_DIR}"
fi
if [[ "${ENABLE_NEXTCLOUD}" == "true" ]]; then
echo "-- Restore Nextcloud code to: ${NC_DIR}"
rsync_restore_dir "$STAGING_DIR/files/nextcloud" "${NC_DIR}"
if [[ "${ENABLE_NEXTCLOUD_DATA}" == "true" ]]; then
echo "-- Restore Nextcloud data to: ${NC_DATA_DIR}"
rsync_restore_dir "$STAGING_DIR/files/nextcloud-data" "${NC_DATA_DIR}"
fi
fi
if [[ "${ENABLE_MAIL}" == "true" ]]; then
echo "-- Restore mail files..."
[[ -d "$STAGING_DIR/files/mail" && -n "${MAIL_DIR:-}" ]] && rsync_restore_dir "$STAGING_DIR/files/mail" "${MAIL_DIR}" || true
[[ -d "$STAGING_DIR/files/postfix" && -n "${POSTFIX_DIR:-}" ]] && rsync_restore_dir "$STAGING_DIR/files/postfix" "${POSTFIX_DIR}" || true
[[ -d "$STAGING_DIR/files/dovecot" && -n "${DOVECOT_DIR:-}" ]] && rsync_restore_dir "$STAGING_DIR/files/dovecot" "${DOVECOT_DIR}" || true
fi
else
echo "-- RESTORE_FILES=false (skipping file restore)"
fi fi
# nextcloud maintenance for restore # ---------- Restore databases ----------
if [[ "$DO_NEXTCLOUD" == "true" || "$DO_NEXTCLOUD_DATA" == "true" || "$DO_DB" == "true" ]]; then mysql_restore_sql() {
nc_maintenance_on local cnf="$1"
local db="$2"
local sql_file="$3"
[[ -r "$cnf" ]] || die "DB CNF not readable: $cnf"
[[ -r "$sql_file" ]] || die "SQL file not readable: $sql_file"
echo "-- Import DB: ${db} from ${sql_file}"
run_cmd mysql --defaults-extra-file="$cnf" "$db" < "$sql_file"
}
if [[ "${RESTORE_DB}" == "true" ]]; then
echo "-- Restoring databases..."
if [[ -n "${WP_DB_NAME:-}" ]]; then
wp_sql="$(ls -1 "$STAGING_DIR/db"/wordpress_*.sql 2>/dev/null | tail -n 1 || true)"
if [[ -n "$wp_sql" ]]; then
mysql_restore_sql "${WP_DB_CNF}" "${WP_DB_NAME}" "$wp_sql"
else
echo "WARN: No WordPress SQL dump found in archive."
fi
fi
if [[ -n "${NC_DB_NAME:-}" ]]; then
nc_sql="$(ls -1 "$STAGING_DIR/db"/nextcloud_*.sql 2>/dev/null | tail -n 1 || true)"
if [[ -n "$nc_sql" ]]; then
mysql_restore_sql "${NC_DB_CNF}" "${NC_DB_NAME}" "$nc_sql"
else
echo "WARN: No Nextcloud SQL dump found in archive."
fi
fi
else
echo "-- RESTORE_DB=false (skipping DB restore)"
fi fi
# restore files # ---------- Post-restore Nextcloud steps ----------
[[ "$DO_WORDPRESS" == "true" && -d "$STAGING_DIR/files/wordpress" ]] && rsync_restore_dir "$STAGING_DIR/files/wordpress" "$WP_DIR" || true if [[ "${ENABLE_NEXTCLOUD}" == "true" && -d "${NC_DIR}" && -f "${NC_DIR}/occ" ]]; then
[[ "$DO_NEXTCLOUD" == "true" && -d "$STAGING_DIR/files/nextcloud" ]] && rsync_restore_dir "$STAGING_DIR/files/nextcloud" "$NC_DIR" || true echo "-- Nextcloud post-restore: maintenance:repair"
[[ "$DO_NEXTCLOUD_DATA" == "true" && -d "$STAGING_DIR/files/nextcloud-data" ]] && rsync_restore_dir "$STAGING_DIR/files/nextcloud-data" "$NC_DATA_DIR" || true run_cmd sudo -u "${NC_OCC_USER}" php "${NC_DIR}/occ" maintenance:repair || true
if [[ "$DO_MAIL" == "true" ]]; then echo "-- Nextcloud post-restore: files:scan (optional, can be slow)"
[[ -d "$STAGING_DIR/files/mail" && -n "${MAIL_DIR:-}" ]] && rsync_restore_dir "$STAGING_DIR/files/mail" "$MAIL_DIR" || true if [[ "${NC_FILES_SCAN_AFTER_RESTORE}" == "true" ]]; then
[[ -d "$STAGING_DIR/files/postfix" && -n "${POSTFIX_DIR:-}" ]] && rsync_restore_dir "$STAGING_DIR/files/postfix" "$POSTFIX_DIR" || true run_cmd sudo -u "${NC_OCC_USER}" php "${NC_DIR}/occ" files:scan --all || true
[[ -d "$STAGING_DIR/files/dovecot" && -n "${DOVECOT_DIR:-}" ]] && rsync_restore_dir "$STAGING_DIR/files/dovecot" "$DOVECOT_DIR" || true else
echo "-- Skipping files:scan (set NC_FILES_SCAN_AFTER_RESTORE=true to enable)"
fi
if [[ "${ENABLE_NEXTCLOUD_MAINTENANCE}" == "true" ]]; then
echo "-- Nextcloud maintenance mode OFF..."
run_cmd sudo -u "${NC_OCC_USER}" php "${NC_DIR}/occ" maintenance:mode --off
NC_MAINTENANCE_ON=false
fi
fi fi
# restore DBs
if [[ "$DO_DB" == "true" && "${ENABLE_DB_DUMPS:-false}" == "true" ]]; then
WP_SQL="$(ls -1 "$STAGING_DIR/db"/wordpress_*.sql 2>/dev/null | tail -n 1 || true)"
NC_SQL="$(ls -1 "$STAGING_DIR/db"/nextcloud_*.sql 2>/dev/null | tail -n 1 || true)"
[[ "$DO_WORDPRESS" == "true" && -n "${WP_DB_NAME:-}" && -n "$WP_SQL" ]] && mysql_import "$WP_DB_CNF" "$WP_DB_NAME" "$WP_SQL" || true
[[ ( "$DO_NEXTCLOUD" == "true" || "$DO_NEXTCLOUD_DATA" == "true" ) && -n "${NC_DB_NAME:-}" && -n "$NC_SQL" ]] && mysql_import "$NC_DB_CNF" "$NC_DB_NAME" "$NC_SQL" || true
fi
nc_maintenance_off
start_stopped_services
echo "== app-restore done: ${ts} ==" echo "== app-restore done: ${ts} =="
echo "-- Extracted staging kept at: ${STAGING_DIR}"
echo "-- Log: ${LOG_FILE}" echo "-- Log: ${LOG_FILE}"