Docker Compose mit drei Services (nginx auf Port 8000), Migrationsmechanik, Update- und Backup-Skript. Anwendung: Router mit CSRF-Prüfung, Anmeldung und Ersteinrichtung, zentraler Einstellungsdialog mit Reitern, ein- und ausschaltbares Debug-Log mit Maskierung von Zugangsdaten. Layout nach Vorbild des FinanzManagers: feste Navigation links, Werkzeugleiste oben, Tab-Leiste; Farbschema hell/dunkel/System. Tests: 51 PHP-Prüfungen und 7 Playwright-Tests, alle grün.
77 lines
2.2 KiB
Bash
Executable file
77 lines
2.2 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
#
|
||
# Sichert Datenbank und Nutzdaten nach backups/.
|
||
#
|
||
# Rotation: 7 Tages-, 4 Wochen-, 12 Monats-Backups. Ein flock verhindert, dass
|
||
# sich zwei Läufe überholen.
|
||
#
|
||
# ./scripts/backup.sh
|
||
|
||
set -euo pipefail
|
||
|
||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||
|
||
ZIEL="backups"
|
||
mkdir -p "$ZIEL"
|
||
|
||
exec 9>"$ZIEL/.lock"
|
||
if ! flock -n 9; then
|
||
echo "Ein Backup läuft bereits." >&2
|
||
exit 0
|
||
fi
|
||
|
||
STEMPEL="$(date +%Y%m%d_%H%M%S)"
|
||
DB_PASS="${DB_PASS:-finanzmanager}"
|
||
|
||
echo "Sichere Datenbank …"
|
||
docker compose exec -T db mariadb-dump \
|
||
-u finanzmanager -p"$DB_PASS" \
|
||
--single-transaction --routines --events \
|
||
finanzmanager > "$ZIEL/db_${STEMPEL}.sql"
|
||
|
||
echo "Sichere Nutzdaten …"
|
||
# data/mariadb bleibt außen vor – die Datenbank steckt bereits im Dump.
|
||
tar czf "$ZIEL/daten_${STEMPEL}.tar.gz" \
|
||
--exclude='data/mariadb' \
|
||
data 2>/dev/null || true
|
||
|
||
# ── Rotation ────────────────────────────────────────────────────────────────
|
||
# Behalten: die letzten 7 Tage vollständig, danach je ein Backup pro Woche
|
||
# (4 Wochen) und pro Monat (12 Monate).
|
||
python3 - "$ZIEL" <<'PY'
|
||
import os, re, sys, time
|
||
from datetime import datetime
|
||
|
||
ziel = sys.argv[1]
|
||
muster = re.compile(r'^(db|daten)_(\d{8})_(\d{6})\.(sql|tar\.gz)$')
|
||
|
||
dateien = []
|
||
for name in os.listdir(ziel):
|
||
treffer = muster.match(name)
|
||
if treffer:
|
||
stempel = datetime.strptime(treffer.group(2) + treffer.group(3), '%Y%m%d%H%M%S')
|
||
dateien.append((stempel, name))
|
||
|
||
dateien.sort(reverse=True)
|
||
jetzt = datetime.now()
|
||
|
||
behalten, wochen, monate = set(), set(), set()
|
||
for stempel, name in dateien:
|
||
alter = (jetzt - stempel).days
|
||
if alter <= 7:
|
||
behalten.add(name)
|
||
continue
|
||
woche = stempel.isocalendar()[:2]
|
||
monat = (stempel.year, stempel.month)
|
||
if alter <= 35 and woche not in wochen:
|
||
wochen.add(woche); behalten.add(name)
|
||
elif alter <= 400 and monat not in monate:
|
||
monate.add(monat); behalten.add(name)
|
||
|
||
for _, name in dateien:
|
||
if name not in behalten:
|
||
os.remove(os.path.join(ziel, name))
|
||
print(f"Entfernt: {name}")
|
||
PY
|
||
|
||
echo "Backup abgeschlossen: $ZIEL/db_${STEMPEL}.sql"
|