Inkrementelles Backup mit rsync

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