feat: SFTP-Backup mit Scheduler, Versionierung und Multi-Target
Mehrere SFTP-Backup-Ziele konfigurierbar mit:
- Host, Port, Benutzername, Passwort, Remote-Pfad
- Konfigurierbares Intervall (15 Min. bis woechentlich oder deaktiviert)
- Maximale Anzahl aufbewahrter Versionen (aeltere werden automatisch geloescht)
- Aktiv/Inaktiv-Toggle pro Ziel
Features:
- Automatischer Hintergrund-Scheduler prueft alle 60 Sekunden ob
Backups faellig sind und fuehrt sie aus
- Manuelles Backup per Klick ("Jetzt sichern")
- SFTP-Verbindungstest-Button
- Versionen-Dialog: Alle Backup-Versionen auf dem SFTP-Server auflisten
mit Groesse und Datum
- Restore direkt von SFTP: Version auswaehlen -> wird heruntergeladen
und ueber die bestehende DB-Merge-Logik wiederhergestellt
- Chunked Upload zum SFTP in 16MB-Bloecken (fuer grosse Backups)
- Status-Anzeige: Letztes Backup, Erfolg/Fehler, Nachricht
Backend: BackupTarget Model, SFTP-Service (paramiko), Backup-Scheduler
API: /admin/backup/targets CRUD, /test, /run, /versions, /restore
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""Background scheduler for periodic SFTP backups."""
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
_scheduler_thread = None
|
||||
_scheduler_running = False
|
||||
|
||||
|
||||
def start_backup_scheduler(app):
|
||||
"""Start the background backup scheduler."""
|
||||
global _scheduler_thread, _scheduler_running
|
||||
|
||||
if _scheduler_running:
|
||||
return
|
||||
|
||||
_scheduler_running = True
|
||||
|
||||
def scheduler_loop():
|
||||
while _scheduler_running:
|
||||
try:
|
||||
with app.app_context():
|
||||
_check_and_run_backups(app)
|
||||
except Exception as e:
|
||||
print(f'[Backup Scheduler] Error: {e}')
|
||||
|
||||
# Check every 60 seconds
|
||||
for _ in range(60):
|
||||
if not _scheduler_running:
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
_scheduler_thread = threading.Thread(target=scheduler_loop, daemon=True)
|
||||
_scheduler_thread.start()
|
||||
|
||||
|
||||
def stop_backup_scheduler():
|
||||
global _scheduler_running
|
||||
_scheduler_running = False
|
||||
|
||||
|
||||
def _check_and_run_backups(app):
|
||||
"""Check all active backup targets and run if due."""
|
||||
from app.extensions import db
|
||||
from app.models.backup_target import BackupTarget
|
||||
from app.services.sftp_backup import create_backup_zip, upload_backup_to_sftp
|
||||
|
||||
targets = BackupTarget.query.filter_by(is_active=True).all()
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
for target in targets:
|
||||
if not target.backup_interval_minutes or target.backup_interval_minutes <= 0:
|
||||
continue
|
||||
|
||||
# Check if backup is due
|
||||
if target.last_backup_at:
|
||||
next_due = target.last_backup_at + timedelta(minutes=target.backup_interval_minutes)
|
||||
if now < next_due:
|
||||
continue
|
||||
|
||||
# Run backup
|
||||
db_uri = app.config['SQLALCHEMY_DATABASE_URI']
|
||||
db_path = db_uri.replace('sqlite:///', '')
|
||||
upload_path = app.config['UPLOAD_PATH']
|
||||
|
||||
zip_path = None
|
||||
try:
|
||||
zip_path = create_backup_zip(db_path, upload_path)
|
||||
version = upload_backup_to_sftp(target, zip_path, app)
|
||||
|
||||
target.last_backup_at = now
|
||||
target.last_backup_status = 'success'
|
||||
target.last_backup_message = f'Version {version} erfolgreich hochgeladen'
|
||||
db.session.commit()
|
||||
print(f'[Backup] {target.name}: {version} OK')
|
||||
|
||||
except Exception as e:
|
||||
target.last_backup_at = now
|
||||
target.last_backup_status = 'error'
|
||||
target.last_backup_message = str(e)[:500]
|
||||
db.session.commit()
|
||||
print(f'[Backup] {target.name}: FEHLER - {e}')
|
||||
|
||||
finally:
|
||||
if zip_path and os.path.exists(zip_path):
|
||||
os.unlink(zip_path)
|
||||
Reference in New Issue
Block a user