Phase 6: remote backup destinations — SFTP & S3 (0.6.0)
- 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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
59037f4287
commit
7bd449101d
@@ -1,5 +1,19 @@
|
||||
import api from "./client";
|
||||
|
||||
export interface BackupDestination {
|
||||
id: number;
|
||||
name: string;
|
||||
type: "sftp" | "s3";
|
||||
config: Record<string, string>;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface RemoteBackup {
|
||||
name: string;
|
||||
size: number;
|
||||
modified: number | null;
|
||||
}
|
||||
|
||||
function triggerDownload(blob: Blob, filename: string) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
@@ -42,4 +56,50 @@ export const backupsApi = {
|
||||
}>("/api/stacks/restore", form);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
push: (
|
||||
stackId: string,
|
||||
body: { destination_id: number; include_volumes: boolean; stop_first: boolean }
|
||||
) =>
|
||||
api
|
||||
.post<{ ok: boolean; destination: string; name: string }>(
|
||||
`/api/stacks/${stackId}/backup/push`,
|
||||
body
|
||||
)
|
||||
.then((r) => r.data),
|
||||
|
||||
restoreFrom: (body: {
|
||||
destination_id: number;
|
||||
name: string;
|
||||
target_id?: string;
|
||||
overwrite: boolean;
|
||||
restore_volumes: boolean;
|
||||
}) =>
|
||||
api
|
||||
.post<{ stack_id: string; name: string; volumes_restored: number }>(
|
||||
"/api/stacks/restore-from",
|
||||
body
|
||||
)
|
||||
.then((r) => r.data),
|
||||
};
|
||||
|
||||
export const destinationsApi = {
|
||||
list: () =>
|
||||
api.get<BackupDestination[]>("/api/backups/destinations").then((r) => r.data),
|
||||
create: (body: { name: string; type: string; config: Record<string, string> }) =>
|
||||
api.post<BackupDestination>("/api/backups/destinations", body).then((r) => r.data),
|
||||
update: (id: number, body: { name?: string; config?: Record<string, string> }) =>
|
||||
api.put<BackupDestination>(`/api/backups/destinations/${id}`, body).then((r) => r.data),
|
||||
remove: (id: number) =>
|
||||
api.delete(`/api/backups/destinations/${id}`).then((r) => r.data),
|
||||
test: (id: number) =>
|
||||
api
|
||||
.post<{ ok: boolean; error?: string }>(`/api/backups/destinations/${id}/test`)
|
||||
.then((r) => r.data),
|
||||
backups: (id: number) =>
|
||||
api.get<RemoteBackup[]>(`/api/backups/destinations/${id}/backups`).then((r) => r.data),
|
||||
deleteBackup: (id: number, name: string) =>
|
||||
api
|
||||
.delete(`/api/backups/destinations/${id}/backups/${encodeURIComponent(name)}`)
|
||||
.then((r) => r.data),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user