auf eine zweite interne Festplatte, die nur beim Backup eingebunden wird.
Variablen und Funktionen festlegen
#!/bin/bash
# 0_functions.sh
BAK="/BACKUP_EXT"
BASE="00_BASE"
# Timestamp
DATE=$(LC_ALL=C date '+%d. %b %H:%M:%S')
DWE=$(date +%Y-%m-%d) # 2025-11-29
HOMIN=$(date +%H%M) # 1430
# What?
SRC0=(
'/etc'
'/root'
'/var/www'
'/home/user/Desktop'
)
# Check Mount
onmount() {
[ -f "$BAK/.mounted" ] && return 0
echo "$DATE - Mounting $BAK ..."
mount /dev/vdb1 "$BAK" || echo "$DATE ist schon gemounted"
touch "$BAK/.mounted"
}
offmount() {
[ ! -f "$BAK/.mounted" ] && return 0
echo "$DATE - Unmounting $BAK ..."
sleep 2
umount "$BAK" 2>/dev/null || true
rm -f "$BAK/.mounted"
}
# Läuft das Script schon - dann Killyourself
selfchk() {
exec 9<"$0"
flock -n 9 || { echo "$DATE - Bereits am Laufen!"; exit 1; }
}
# Optional SQL Datenbanken sichern
dumpbase() {
local dumpdir="$BAK/SQL" # mit auf die externe Platte in Unterverzeichnis
mkdir -p "$dumpdir"
for db in database_sql0 database_sql1; do
local file="$dumpdir/${db}-$DWE.sql"
echo "$DATE - Dumping $db → $file"
mysqldump "$db" > "$file" || echo "mysqldump for $db failed!"
done
}
Basis erstellen
um von hier ausgehend Hardlinks zu generieren
#!/bin/bash
# 1_base.sh – 00_BASE neu anlegen/aktualisieren, etwa alle zwei Wochen ausführen
# In welchem Verzeichnis befinde ich mich und finde ich hier meine 0_functions??
DEAR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
source "$DEAR/0_functions.sh"
selfchk
onmount
echo "$DATE - Erstelle/aktualisiere die Basis 00_BASE auf externer Platte"
# Alte Basis ggf. sichern (optional)
#[ -d "$BAK/$BASE" ] && mv "$BAK/$BASE" "$BAK/${BASE}_old_$(date +%Y%m%d)" 2>/dev/null
mkdir -p "$BAK/$BASE"
for SRC in "${SRC0[@]}"; do
echo "$DATE - BASE: $SRC → $BAK/$BASE$SRC"
mkdir -p "$BAK/$BASE$SRC"
rsync -aP --delete "$SRC/" "$BAK/$BASE$SRC/"
done
echo "$DATE - BASE fertig. Größe: $(du -sh "$BAK/$BASE")"
offmount
exit 0
Tägliche inkrementelle Backups erstellen
mit zusätzlichen wöchentlichen Tarballs und Aufräumfunktion mit Sicherheit, dass die Platte nicht voll läuft
#!/bin/bash
# 2_daily.sh → tägliches Backup mit Hardlinks zu 00_BASE
# Für stündliche Backups die Datei nach 3_hourly.sh kopieren und DWE durch HOMIN ersetzen
DEAR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
source "$DEAR/0_functions.sh"
selfchk
onmount
# Zielordner
TARGET="$BAK/DAILY/$DWE" # stündlich? Dann: "$BAK/HOURLY/$HOMIN"
mkdir -p "$TARGET"
echo "$DATE - Starte Backup → $TARGET (Hardlinks zu 00_BASE)"
for SRC in "${SRC0[@]}"; do
DEST="$TARGET$SRC"
PREV="$BAK/$BASE$SRC"
mkdir -p "$DEST"
echo "$DATE - $SRC → Hardlinks zur BASE"
rsync -aP --delete --stats --link-dest="$PREV" "$SRC/" "$DEST/"
done
# Rotation: älter als 22 Tage löschen
find "$BAK/DAILY" -maxdepth 1 -type d -mtime +22 -exec rm -rf {} + 2>/dev/null
# SQL Daten dumpen und älter als 22 Tage löschen
dumpbase
find "$BAK/SQL" -maxdepth 1 -type f -mtime +22 -exec rm -rf {} + 2>/dev/null
# Regelmäßige Tarballs
# ────────────────────── Täglicher SQL-Dump-Tarball ──────────────────────
echo "$DATE - Erstelle SQL-Dump-Tarball..."
if [ -d "$BAK/SQL" ] && [ "$(ls -A "$BAK/SQL" 2>/dev/null)" ]; then
tar -czf "$BAK/SQL-$DWE.tar.gz" -C "$BAK/SQL" . \
&& echo "$DATE - SQL-Tarball fertig: $(du -h "$BAK/SQL-$DWE.tar.gz")" \
|| echo "$DATE - SQL-Tarball fehlgeschlagen!"
else
echo "$DATE - Keine SQL-Dumps vorhanden, ergo kein SQL-Tarball"
fi
# ────────────────────── Tarball DAILY ─────────────────
echo "$DATE - Erstelle offsite Tarball von $DWE ..."
tar -czf "$BAK/offsite_daily_$DWE.tar.gz" -C "$BAK/DAILY/$DWE" . \
&& echo "$DATE - Offsite-Tarball fertig: $(du -h "$BAK/offsite_daily_$DWE.tar.gz")" \
|| echo "$DATE - Offsite-Tarball fehlgeschlagen!"
# ────────────────────── Retention: Platz < 45 GB halten ─────────────────
# Nur die letzten 4 täglichen Offsite-Tarballs behalten
find "$BAK" -name "offsite_daily_*.tar.gz" -type f -printf '%T@\t%p\n' 2>/dev/null | \
sort -nr | tail -n +5 | cut -f2- | xargs rm -f 2>/dev/null
# Nur die letzten 10 SQL-Tarballs behalten
find "$BAK" -name "SQL-*.tar.gz" -type f -printf '%T@\t%p\n' 2>/dev/null | \
sort -nr | tail -n +11 | cut -f2- | xargs rm -f 2>/dev/null
# Am 1. monatlich die Tarballs durch Umbenennung sichern
if [ "$(date +%d)" -eq 01 ]; then
[ -f "$BAK/offsite_daily_$DWE.tar.gz" ] && \
mv "$BAK/offsite_daily_$DWE.tar.gz" "$BAK/offsite_MONTHLY_$(date +%Y-%m).tar.gz"
[ -f "$BAK/SQL-$DWE.tar.gz" ] && \
mv "$BAK/SQL-$DWE.tar.gz" "$BAK/SQL_MONTHLY_$(date +%Y-%m).tar.gz"
fi
# Nur letzte 8 Monats-Tarballs (offsite + SQL) behalten
find "$BAK" -name "offsite_MONTHLY_*.tar.gz" -type f -printf '%T@\t%p\n' 2>/dev/null | \
sort -nr | tail -n +9 | cut -f2- | xargs rm -f 2>/dev/null
find "$BAK" -name "SQL_MONTHLY_*.tar.gz" -type f -printf '%T@\t%p\n' 2>/dev/null | \
sort -nr | tail -n +9 | cut -f2- | xargs rm -f 2>/dev/null
# Hardlink-Ordner: letzte 14 Tage behalten (kostet fast keinen Speicherplatz)
find "$BAK/DAILY" -mindepth 1 -maxdepth 1 -type d -mtime +13 -exec rm -rf {} + 2>/dev/null
# NOTFALL: wenn > 90 % der Platte voll sind, älteste daily-Tarballs löschen
while [ "$(df --output=pcent "$BAK" | tail -1 | tr -d ' %')" -gt 90 ]; do
oldest=$(ls -1t "$BAK"/offsite_daily_*.tar.gz "$BAK"/SQL-*.tar.gz 2>/dev/null | tail -1)
[ -z "$oldest" ] && break
echo "$DATE - NOTFALL: Platte voll → lösche $oldest"
rm -f "$oldest"
done
echo "$DATE - Backup + Tarballs + Aufräumen abgeschlossen."
echo "$DATE - Belegung: $(df -h "$BAK" | tail -1)"
# Hardlink-Statistik, Kontrolle, ob tatsächlich Hardlinks und nicht Kopien erstellt werden
echo "=== Hardlink-Statistik $DWE ==="
find "$BAK/$BASE" -type f -printf '%i\n' | sort -u > /tmp/base_inodes
find "$TARGET" -type f -printf '%i\n' | sort -u > /tmp/new_inodes
echo "Hardlinks (gemeinsam): $(comm -12 /tmp/base_inodes /tmp/new_inodes | wc -l)"
echo "Echte Kopien (neu): $(comm -23 /tmp/new_inodes /tmp/base_inodes | wc -l)"
rm -f /tmp/*_inodes
offmount
echo "$DATE - Backup fertig!"
exit 0
Stündlich, so zum Spaß
#!/bin/bash
# 3_hourly.sh → stündliches Backup mit Hardlinks zu 00_BASE
DEAR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
source "$DEAR/0_functions.sh"
selfchk
onmount
# Zielordner stündlich
TARGET="$BAK/HOURLY/$HOMIN"
mkdir -p "$TARGET"
echo "$DATE - Starte Backup → $TARGET (Hardlinks zu 00_BASE)"
for SRC in "${SRC0[@]}"; do
DEST="$TARGET$SRC"
PREV="$BAK/$BASE$SRC"
mkdir -p "$DEST"
echo "$DATE - $SRC → Hardlinks zu BASE"
rsync -avP --delete --stats --link-dest="$PREV" "$SRC/" "$DEST/"
done
# Hardlink-Statistik
echo "=== Hardlink-Statistik $HOMIN ==="
find "$BAK/$BASE" -type f -printf '%i\n' | sort -u > /tmp/base_inodes
find "$TARGET" -type f -printf '%i\n' | sort -u > /tmp/new_inodes
echo "Hardlinks (gemeinsam): $(comm -12 /tmp/base_inodes /tmp/new_inodes | wc -l)"
echo "Echte Kopien (neu): $(comm -23 /tmp/new_inodes /tmp/base_inodes | wc -l)"
rm -f /tmp/*_inodes
offmount
echo "$DATE - Backup fertig!"
exit 0
Zu zu guter Letzt noch ein passender Cronjob
# m h dom mon dow command
# 4:12 Uhr am 3.&17. jeden Monats
# 1:12 Uhr täglich
# zwischen 8:07 und 20:07 Uhr täglich jede Stunde
SHELL=/bin/bash
12 04 3,17 * * /root/scripts/bak/1_base.sh >/dev/null 2>&1
12 01 * * * /root/scripts/bak/2_daily.sh >/dev/null 2>&1
7 8-20 * * * /root/scripts/bak/3_hourly.sh >/dev/null 2>&1
