New destination type 'nfs' alongside SFTP and S3. The StackPilot container needs no mount privileges: the Docker daemon mounts the export as a named volume (stackpilot-nfs-dest-<id>, driver local/type nfs, recreated whenever server/path/options change) and all file I/O runs through throwaway helper containers (BACKUP_HELPER_IMAGE) — upload via put_archive, list via stat, download via get_archive, delete/test via short-lived runs. Config: server, export path, mount options (default rw), optional subdirectory (sanitized; shell-safe charset). Mount failures surface as clean destination errors. Settings UI gains the NFS form + summary; works everywhere destinations are used (push, restore-from, scheduled backups incl. retention). Verified live against a real kernel NFS server: test, push (file on the export), list, restore-from incl. volume data, remote delete, config change recreates the mount volume, unreachable server fails cleanly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
49 lines
1.1 KiB
Python
49 lines
1.1 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", "nfs"]
|
|
|
|
# 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, S3-compatible or NFS)."""
|
|
|
|
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
|