#!/usr/bin/env bash set -Eeuo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" CFG="$SCRIPT_DIR/config.sh" [[ -f "$CFG" ]] || { echo "ERROR: config.sh fehlt: $CFG (erst 01_setup.sh ausführen)"; exit 1; } # shellcheck disable=SC1090 source "$CFG" # User, der das Script gestartet hat (auch wenn via sudo) RUN_USER="${SUDO_USER:-$USER}" # --- helpers --- LOG_FILE="$SCRIPT_DIR/raspi-backup.log" LOCK_FILE="$SCRIPT_DIR/.raspi-backup.lock" ts_now() { date '+%Y-%m-%d %H:%M:%S'; } log() { echo "[$(ts_now)] $*" | tee -a "$LOG_FILE" >/dev/null; } die() { log "ERROR: $*"; echo "ERROR: $*" >&2; exit 1; } need_cmd(){ command -v "$1" >/dev/null 2>&1 || die "Fehlt: $1"; } need_root(){ [[ "${EUID:-$(id -u)}" -eq 0 ]] || die "Bitte mit sudo starten: sudo $SCRIPT_DIR/04_run_backup.sh"; } host_short(){ hostname -s 2>/dev/null || hostname 2>/dev/null || echo "raspi"; } acquire_lock() { if command -v flock >/dev/null 2>&1; then exec 9>"$LOCK_FILE" flock -n 9 || die "Schon ein Lauf aktiv (Lock: $LOCK_FILE)" else [[ -e "$LOCK_FILE" ]] && die "Schon ein Lauf aktiv (Lock: $LOCK_FILE)" echo "$$" >"$LOCK_FILE" trap 'rm -f "$LOCK_FILE" >/dev/null 2>&1 || true' EXIT fi } run_logged() { "$@" 2>&1 | tee -a "$LOG_FILE" } need_root need_cmd rsync need_cmd ssh need_cmd tee # validate config vars (OHNE SSH_USER) : "${NAS_HOST:?}" "${NAS_USER:?}" "${NAS_PORT:?}" "${NAS_BACKUP_BASE:?}" "${KEEP_DAYS:?}" "${KEY_TYPE:?}" hn="$(host_short)" ssh_user="$RUN_USER" # Keypfad: gehört RUN_USER key_path="$(eval echo "~${ssh_user}/.ssh/id_${KEY_TYPE}_${hn}")" [[ -f "$key_path" ]] || die "Key fehlt: $key_path (erst 02_setup_ssh.sh ausführen)" run_ts="$(date '+%Y-%m-%d_%H-%M')" remote_root="${NAS_BACKUP_BASE%/}/${hn}" remote_dest="${remote_root}/${run_ts}" log "============================================================" log "BACKUP START" log "Run user : ${ssh_user}" log "Host : ${hn}" log "NAS : ${NAS_USER}@${NAS_HOST}:${NAS_PORT}" log "Key (local): ${key_path}" log "Remote root: ${remote_root}" log "Remote dest: ${remote_dest}" log "Keep days : ${KEEP_DAYS}" log "Logfile : ${LOG_FILE}" log "============================================================" acquire_lock start_epoch="$(date +%s)" # ---- Performance / SSH tuning ---- SSH_CIPHER="chacha20-poly1305@openssh.com" SSH_COMPRESS_OPT="-o Compression=no" if [[ "${BACKUP_SSH_COMPRESS:-no}" == "yes" ]]; then SSH_COMPRESS_OPT="-o Compression=yes -o CompressionLevel=3" fi SSH_BASE=(ssh -p "$NAS_PORT" -i "$key_path" -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=yes -o ConnectTimeout=20 -o Ciphers="$SSH_CIPHER" $SSH_COMPRESS_OPT ) log "Phase 1/5: Remote Ordner anlegen..." run_logged sudo -u "$ssh_user" "${SSH_BASE[@]}" "${NAS_USER}@${NAS_HOST}" \ "mkdir -p '$remote_dest' '$remote_root'" \ || die "Remote Ordner konnte nicht erstellt werden" # Build rsync excludes/extras exclude_args=() if declare -p EXCLUDES >/dev/null 2>&1; then for e in "${EXCLUDES[@]}"; do exclude_args+=( "--exclude=$e" ); done fi extra_args=() if declare -p RSYNC_EXTRA >/dev/null 2>&1; then for o in "${RSYNC_EXTRA[@]}"; do extra_args+=( "$o" ); done fi # ---- Fix: --inplace kollidiert mit --partial-dir / --partial ---- use_inplace=1 for o in "${extra_args[@]}"; do case "$o" in --partial-dir*|--partial) use_inplace=0 ;; esac done inplace_args=() if [[ "$use_inplace" -eq 1 ]]; then inplace_args=(--inplace) log "rsync: --inplace aktiv" else log "rsync: --inplace deaktiviert (Konflikt mit --partial/--partial-dir aus RSYNC_EXTRA)" fi ssh_cmd="ssh -p ${NAS_PORT} -i ${key_path} \ -o IdentitiesOnly=yes \ -o BatchMode=yes \ -o StrictHostKeyChecking=yes \ -o ConnectTimeout=20 \ -o Ciphers=${SSH_CIPHER} \ ${SSH_COMPRESS_OPT} " log "Phase 2/5: rsync START (Live-Progress sichtbar)..." log "Hinweis: Das kann je nach Datenmenge sehr lange dauern." rsync -aAXH --numeric-ids \ --omit-dir-times \ --no-inc-recursive \ "${inplace_args[@]}" \ --info=progress2,stats2 \ "${exclude_args[@]}" \ "${extra_args[@]}" \ -e "$ssh_cmd" \ / \ "${NAS_USER}@${NAS_HOST}:${remote_dest}/" \ 2>&1 | tee -a "$LOG_FILE" \ || die "rsync fehlgeschlagen" log "Phase 3/5: mtime sichern (touch), damit Retention den frischen Lauf nicht killt..." run_logged sudo -u "$ssh_user" "${SSH_BASE[@]}" "${NAS_USER}@${NAS_HOST}" \ "touch '$remote_dest' '$remote_root'" \ || die "touch auf NAS fehlgeschlagen" log "Phase 4/5: latest Symlink setzen..." run_logged sudo -u "$ssh_user" "${SSH_BASE[@]}" "${NAS_USER}@${NAS_HOST}" \ "cd '$remote_root' && ln -sfn '${run_ts}' latest" \ || die "latest Symlink fehlgeschlagen" log "Phase 5/5: Retention (> ${KEEP_DAYS} Tage) aufräumen..." run_logged sudo -u "$ssh_user" "${SSH_BASE[@]}" "${NAS_USER}@${NAS_HOST}" \ "find '$remote_root' -maxdepth 1 -type d -name '20??-??-??_??-??' -mtime +${KEEP_DAYS} -print -exec rm -rf {} \;" \ || die "Retention Cleanup fehlgeschlagen" end_epoch="$(date +%s)" dur_sec="$((end_epoch - start_epoch))" dur_min="$((dur_sec / 60))" dur_rem="$((dur_sec % 60))" log "============================================================" log "BACKUP OK" log "Dauer: ${dur_min}m ${dur_rem}s" log "Ziel : ${NAS_HOST}:${remote_dest}" log "============================================================" echo echo "Backup fertig → ${NAS_HOST}:${remote_dest}" echo "Dauer: ${dur_min}m ${dur_rem}s" echo "Log: $LOG_FILE"