- BackupDestination model + backup_destination_service (SFTP via paramiko,
S3-compatible via boto3): upload/list/download/delete/test.
- routers/destinations.py: destinations CRUD (secrets masked, merge-on-update),
test, list/delete remote backups. backups.py: POST /{id}/backup/push and
POST /restore-from (download from a destination + restore, volumes included).
- Frontend: Settings → Backup destinations (SFTP/S3 forms + test); Backup dialog
can push to a destination; Restore dialog can pick a destination + backup.
- deps: paramiko 3.5.0, boto3 1.35.99.
Verified end-to-end against live MinIO + atmoz/sftp: create/test destinations,
push (incl. volumes), list, restore-from to a fresh stack (volume data intact),
delete remote backup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
49 lines
1.0 KiB
Python
49 lines
1.0 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
|
|
from sqlmodel import Field, SQLModel
|
|
|
|
|
|
def _now() -> datetime:
|
|
return datetime.now(timezone.utc)
|
|
|
|
|
|
DESTINATION_TYPES = ["sftp", "s3"]
|
|
|
|
# config keys that hold secrets — masked in API responses.
|
|
SECRET_KEYS = {"password", "private_key", "secret_key"}
|
|
|
|
|
|
class BackupDestination(SQLModel, table=True):
|
|
"""A remote target for stack backups (SFTP or S3-compatible)."""
|
|
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
name: str
|
|
type: str # one of DESTINATION_TYPES
|
|
config: str = "{}" # JSON-encoded, type-specific (incl. secrets)
|
|
created_at: datetime = Field(default_factory=_now)
|
|
|
|
|
|
# --- API schemas ---
|
|
|
|
|
|
class DestinationCreate(SQLModel):
|
|
name: str
|
|
type: str
|
|
config: dict
|
|
|
|
|
|
class DestinationUpdate(SQLModel):
|
|
name: Optional[str] = None
|
|
config: Optional[dict] = None
|
|
|
|
|
|
class DestinationRead(SQLModel):
|
|
id: int
|
|
name: str
|
|
type: str
|
|
config: dict # secrets masked
|
|
created_at: datetime
|