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:
Stefan Hacker
2026-04-11 18:07:28 +02:00
parent c6fe2c590f
commit d42d6d5d96
8 changed files with 817 additions and 1 deletions
+1
View File
@@ -5,6 +5,7 @@ from app.models.contact import AddressBook, Contact, AddressBookShare
from app.models.email_account import EmailAccount
from app.models.password_vault import PasswordFolder, PasswordEntry, PasswordShare
from app.models.settings import AppSettings
from app.models.backup_target import BackupTarget
__all__ = [
'User',
+42
View File
@@ -0,0 +1,42 @@
from datetime import datetime, timezone
from app.extensions import db
class BackupTarget(db.Model):
__tablename__ = 'backup_targets'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
host = db.Column(db.String(255), nullable=False)
port = db.Column(db.Integer, default=22)
username = db.Column(db.String(100), nullable=False)
password_encrypted = db.Column(db.LargeBinary, nullable=True)
private_key_encrypted = db.Column(db.LargeBinary, nullable=True)
remote_path = db.Column(db.String(500), default='/backups/minicloud')
is_active = db.Column(db.Boolean, default=True)
backup_interval_minutes = db.Column(db.Integer, default=1440) # Default: daily
max_versions = db.Column(db.Integer, default=10)
last_backup_at = db.Column(db.DateTime, nullable=True)
last_backup_status = db.Column(db.String(20), nullable=True) # 'success', 'error'
last_backup_message = db.Column(db.Text, nullable=True)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'host': self.host,
'port': self.port,
'username': self.username,
'has_password': bool(self.password_encrypted),
'has_private_key': bool(self.private_key_encrypted),
'remote_path': self.remote_path,
'is_active': self.is_active,
'backup_interval_minutes': self.backup_interval_minutes,
'max_versions': self.max_versions,
'last_backup_at': self.last_backup_at.isoformat() if self.last_backup_at else None,
'last_backup_status': self.last_backup_status,
'last_backup_message': self.last_backup_message,
'created_at': self.created_at.isoformat() if self.created_at else None,
}