179 lines
5.4 KiB
Bash
179 lines
5.4 KiB
Bash
#!/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"
|