Files
docker-backup-sftp-uploader/docker-backup-sftp-uploader.sh
2026-06-07 20:02:33 +02:00

337 lines
7.2 KiB
Bash

#!/usr/bin/env bash
set -Eeuo pipefail
########################################
# Script- und Konfigurationspfade
########################################
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DIR_NAME="$(basename "$SCRIPT_DIR")"
CONFIG_FILE="${CONFIG_FILE:-${SCRIPT_DIR}/docker-backup-sftp-uploader.conf}"
if [[ -f "$CONFIG_FILE" ]]; then
# shellcheck source=/dev/null
source "$CONFIG_FILE"
else
echo "FEHLER: Konfigurationsdatei nicht gefunden: $CONFIG_FILE" >&2
exit 1
fi
########################################
# Optionale Fallbacks
########################################
: "${SFTP_PORT:=22}"
: "${MAIL_TO:=}"
: "${MAIL_FROM:=}"
: "${BACKUP_DIR:=$(dirname "$SCRIPT_DIR")/${DIR_NAME}_backups}"
: "${LOG_DIR:=${BACKUP_DIR}/logs}"
: "${KEEP_LOCAL_BACKUP:=false}"
: "${MIN_FREE_MB:=1024}"
########################################
# Variablen
########################################
DATE="$(date +"%Y-%m-%d_%H-%M-%S")"
BACKUP_FILE="${DIR_NAME}_${DATE}.tar.gz"
mkdir -p "$BACKUP_DIR" "$LOG_DIR"
BACKUP_DIR="$(cd "$BACKUP_DIR" && pwd)"
LOG_DIR="$(cd "$LOG_DIR" && pwd)"
BACKUP_PATH="${BACKUP_DIR}/${BACKUP_FILE}"
LOGFILE="${LOG_DIR}/${DIR_NAME}_backup_${DATE}.log"
DOCKER_STOPPED=false
SUCCESS=false
########################################
# Logging
########################################
exec > >(tee -a "$LOGFILE")
exec 2>&1
log() {
echo "[$(date +"%Y-%m-%d %H:%M:%S")] $*"
}
log "========================================="
log "Backup gestartet"
log "Projekt: $DIR_NAME"
log "Konfiguration: $CONFIG_FILE"
log "Backup-Datei: $BACKUP_PATH"
log "Log-Datei: $LOGFILE"
log "========================================="
########################################
# Hilfsfunktionen
########################################
require_command() {
local command_name="$1"
if ! command -v "$command_name" >/dev/null 2>&1; then
log "FEHLER: Benötigtes Kommando fehlt: $command_name"
return 1
fi
}
require_config_value() {
local variable_name="$1"
if [[ -z "${!variable_name:-}" ]]; then
log "FEHLER: Pflichtwert fehlt in der Konfiguration: $variable_name"
return 1
fi
}
send_mail() {
local subject="$1"
local body="$2"
if [[ -z "$MAIL_TO" ]]; then
log "Hinweis: MAIL_TO ist leer, Mailversand wird übersprungen."
return 0
fi
if ! command -v mail >/dev/null 2>&1; then
log "WARNUNG: Kommando 'mail' wurde nicht gefunden, Mailversand nicht möglich."
return 0
fi
if [[ -n "$MAIL_FROM" ]]; then
if printf '%s\n' "$body" | mail -r "$MAIL_FROM" -s "$subject" "$MAIL_TO"; then
return 0
fi
log "WARNUNG: Mailversand mit MAIL_FROM fehlgeschlagen, versuche ohne Absenderoption."
fi
if ! printf '%s\n' "$body" | mail -s "$subject" "$MAIL_TO"; then
log "WARNUNG: Mailversand fehlgeschlagen."
fi
}
send_error_mail() {
local exit_code="$1"
local body
body="$(cat <<EOF
Backup fehlgeschlagen.
Projekt:
${DIR_NAME}
Datum:
$(date)
Exit-Code:
${exit_code}
Log-Datei:
${LOGFILE}
Log:
$(cat "$LOGFILE" 2>/dev/null || true)
EOF
)"
send_mail "Backup FEHLGESCHLAGEN - ${DIR_NAME}" "$body"
}
send_success_mail() {
local size
local body
size="$(du -h "$BACKUP_PATH" | awk '{print $1}')"
body="$(cat <<EOF
Backup erfolgreich abgeschlossen.
Projekt:
${DIR_NAME}
Datum:
$(date)
Backup:
${BACKUP_FILE}
Groesse:
${size}
Remote-Ziel:
${SFTP_HOST}:${SFTP_REMOTE_DIR}/${BACKUP_FILE}
Log:
$(cat "$LOGFILE")
EOF
)"
send_mail "Backup erfolgreich - ${DIR_NAME} (${size})" "$body"
}
cleanup_local_backup() {
if [[ "$KEEP_LOCAL_BACKUP" == "true" ]]; then
log "Lokales Backup bleibt erhalten: $BACKUP_PATH"
return 0
fi
if [[ -f "$BACKUP_PATH" ]]; then
log "Entferne lokales Backup: $BACKUP_PATH"
rm -f "$BACKUP_PATH"
fi
}
start_docker_if_needed() {
if [[ "$DOCKER_STOPPED" == "true" ]]; then
log "Starte Docker..."
docker compose up -d
DOCKER_STOPPED=false
fi
}
on_error() {
local exit_code="$?"
log "FEHLER: Backup fehlgeschlagen (Exit-Code: ${exit_code})."
if [[ "$DOCKER_STOPPED" == "true" ]]; then
log "Versuche Docker nach Fehler wieder zu starten..."
if docker compose up -d; then
DOCKER_STOPPED=false
log "Docker wurde nach Fehler wieder gestartet."
else
log "WARNUNG: Docker konnte nach Fehler nicht gestartet werden."
fi
fi
send_error_mail "$exit_code"
cleanup_local_backup
exit "$exit_code"
}
on_exit() {
local exit_code="$?"
if [[ "$SUCCESS" == "true" ]]; then
cleanup_local_backup
fi
exit "$exit_code"
}
check_free_space() {
local source_kb
local required_kb
local free_kb
local min_free_kb
source_kb="$(du -sk "$SCRIPT_DIR" | awk '{print $1}')"
min_free_kb=$((MIN_FREE_MB * 1024))
required_kb=$((source_kb + (source_kb / 10) + min_free_kb))
free_kb="$(df -Pk "$BACKUP_DIR" | awk 'NR == 2 {print $4}')"
log "Quellgroesse: $((source_kb / 1024)) MB"
log "Freier Speicher am Backup-Ziel: $((free_kb / 1024)) MB"
log "Geschaetzter Mindestbedarf: $((required_kb / 1024)) MB"
if (( free_kb < required_kb )); then
log "FEHLER: Zu wenig freier Speicher in $BACKUP_DIR."
return 1
fi
}
check_backup_dir_location() {
case "$BACKUP_DIR" in
"$SCRIPT_DIR"|"$SCRIPT_DIR"/*)
log "FEHLER: BACKUP_DIR darf nicht innerhalb des Projektverzeichnisses liegen."
log "Projektverzeichnis: $SCRIPT_DIR"
log "Backup-Verzeichnis: $BACKUP_DIR"
return 1
;;
esac
}
trap on_error ERR
trap on_exit EXIT
########################################
# Vorbedingungen
########################################
cd "$SCRIPT_DIR"
require_command docker
require_command tar
require_command gzip
require_command sshpass
require_command sftp
require_config_value SFTP_HOST
require_config_value SFTP_USER
require_config_value SFTP_PASS
require_config_value SFTP_REMOTE_DIR
check_backup_dir_location
check_free_space
########################################
# Docker stoppen
########################################
log "Stoppe Docker..."
docker compose down
DOCKER_STOPPED=true
########################################
# Archiv erstellen
########################################
log "Erstelle Backup..."
tar \
--same-owner \
--preserve-permissions \
-czpf "$BACKUP_PATH" \
-C "$(dirname "$SCRIPT_DIR")" \
"$DIR_NAME"
########################################
# Docker starten
########################################
start_docker_if_needed
########################################
# Upload
########################################
log "Upload nach ${SFTP_HOST}:${SFTP_REMOTE_DIR}..."
sshpass -p "$SFTP_PASS" sftp \
-P "$SFTP_PORT" \
-oBatchMode=no \
-oStrictHostKeyChecking=no \
-oServerAliveInterval=30 \
-oServerAliveCountMax=10 \
"${SFTP_USER}@${SFTP_HOST}" << EOF
cd "${SFTP_REMOTE_DIR}"
put "${BACKUP_PATH}" "${BACKUP_FILE}.part"
rename "${BACKUP_FILE}.part" "${BACKUP_FILE}"
bye
EOF
log "Upload erfolgreich."
########################################
# Erfolgsmail
########################################
send_success_mail
SUCCESS=true
log "Backup beendet: $(date)"