Compare commits
67 commits
main
...
tvarohohla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb6bbcfd74 | ||
|
|
1d49f59ebc | ||
|
|
99ccfa4305 | ||
|
|
64b7b93831 | ||
|
|
3da653d5ad | ||
|
|
3f550678db | ||
|
|
081a10afeb | ||
|
|
20c3b8156d | ||
|
|
b84ae02ec6 | ||
|
|
378878f439 | ||
|
|
0859fba431 | ||
|
|
4090d5caa6 | ||
|
|
ce714f7bfc | ||
|
|
e15fff2c23 | ||
|
|
375cd10fea | ||
|
|
62df1b1241 | ||
|
|
dc5c88efcd | ||
|
|
630b34ee10 | ||
|
|
535e5c7e7d | ||
|
|
1421085660 | ||
|
|
83dfbf3212 | ||
|
|
512bee535a | ||
|
|
20861cda70 | ||
|
|
34b9e4f6fb | ||
|
|
1beaf406cc | ||
|
|
6ad6044a76 | ||
|
|
764b64cc25 | ||
|
|
b55bebf8f4 | ||
|
|
ded9d935ce | ||
|
|
66f8ff0e50 | ||
|
|
63f67b3b30 | ||
|
|
bf36f9aa56 | ||
|
|
692dd5aa21 | ||
|
|
a7e36e98d7 | ||
|
|
9f1a7694a3 | ||
|
|
2dfa15a83a | ||
|
|
13883f3053 | ||
|
|
94712bb7b8 | ||
|
|
fd4f031963 | ||
|
|
f1e59e5931 | ||
|
|
2b6cd7af2c | ||
|
|
7dab73e9bd | ||
|
|
eaca5abde5 | ||
|
|
7f966b504b | ||
|
|
709a08cfe8 | ||
|
|
c736657ffe | ||
|
|
30047cbcf0 | ||
|
|
ced7a08672 | ||
|
|
5093699568 | ||
|
|
c65d84ee9a | ||
|
|
11646b6187 | ||
|
|
a9ac6eec20 | ||
|
|
2306df316e | ||
|
|
07f7c5e025 | ||
|
|
4536f8a613 | ||
|
|
f452d72050 | ||
|
|
82f632f63a | ||
|
|
95342f6ec9 | ||
|
|
1b874727e3 | ||
|
|
c5c5da78bd | ||
|
|
0d339d973f | ||
|
|
d72975e9e2 | ||
|
|
8e7e1694d5 | ||
|
|
55e8f2e2d3 | ||
|
|
94342cb48d | ||
|
|
3df892455f | ||
|
|
2be65f6069 |
14 changed files with 493 additions and 26 deletions
|
|
@ -50,7 +50,7 @@ services:
|
||||||
devices:
|
devices:
|
||||||
- /dev/fuse:/dev/fuse
|
- /dev/fuse:/dev/fuse
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Paris # Set your timezone here
|
- TZ=Europe/Paris # Set your timezone here
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- /var/lib/zerobyte:/var/lib/zerobyte
|
- /var/lib/zerobyte:/var/lib/zerobyte
|
||||||
|
|
@ -83,7 +83,7 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- "4096:4096"
|
- "4096:4096"
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Paris # Set your timezone here
|
- TZ=Europe/Paris # Set your timezone here
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- /var/lib/zerobyte:/var/lib/zerobyte
|
- /var/lib/zerobyte:/var/lib/zerobyte
|
||||||
|
|
@ -91,6 +91,7 @@ services:
|
||||||
```
|
```
|
||||||
|
|
||||||
**Trade-offs:**
|
**Trade-offs:**
|
||||||
|
|
||||||
- ✅ Improved security by reducing container capabilities
|
- ✅ Improved security by reducing container capabilities
|
||||||
- ✅ Support for local directories
|
- ✅ Support for local directories
|
||||||
- ✅ Keep support all repository types (local, S3, GCS, Azure, rclone)
|
- ✅ Keep support all repository types (local, S3, GCS, Azure, rclone)
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
import { type DefaultError, queryOptions, type UseMutationOptions } from '@tanstack/react-query';
|
import { type DefaultError, queryOptions, type UseMutationOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { client } from '../client.gen';
|
import { client } from '../client.gen';
|
||||||
import { browseFilesystem, changePassword, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteSnapshots, deleteVolume, doctorRepository, downloadResticPassword, getBackupSchedule, getBackupScheduleForVolume, getMe, getMirrorCompatibility, getNotificationDestination, getRepository, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getStatus, getSystemInfo, getUpdates, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, login, logout, mountVolume, type Options, register, reorderBackupSchedules, restoreSnapshot, runBackupNow, runForget, stopBackup, tagSnapshots, testConnection, testNotificationDestination, unmountVolume, updateBackupSchedule, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateVolume } from '../sdk.gen';
|
import { browseFilesystem, changePassword, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteSnapshots, deleteVolume, doctorRepository, downloadResticPassword, exportFullConfig, getBackupSchedule, getBackupScheduleForVolume, getMe, getMirrorCompatibility, getNotificationDestination, getRepository, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getStatus, getSystemInfo, getUpdates, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, login, logout, mountVolume, type Options, register, reorderBackupSchedules, restoreSnapshot, runBackupNow, runForget, stopBackup, tagSnapshots, testConnection, testNotificationDestination, unmountVolume, updateBackupSchedule, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateVolume } from '../sdk.gen';
|
||||||
import type { BrowseFilesystemData, BrowseFilesystemResponse, ChangePasswordData, ChangePasswordResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteSnapshotsData, DeleteSnapshotsResponse, DeleteVolumeData, DeleteVolumeResponse, DoctorRepositoryData, DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetMeData, GetMeResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetRepositoryData, GetRepositoryResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetUpdatesData, GetUpdatesResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, LoginData, LoginResponse, LogoutData, LogoutResponse, MountVolumeData, MountVolumeResponse, RegisterData, RegisterResponse, ReorderBackupSchedulesData, ReorderBackupSchedulesResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, StopBackupData, StopBackupResponse, TagSnapshotsData, TagSnapshotsResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
|
import type { BrowseFilesystemData, BrowseFilesystemResponse, ChangePasswordData, ChangePasswordResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteSnapshotsData, DeleteSnapshotsResponse, DeleteVolumeData, DeleteVolumeResponse, DoctorRepositoryData, DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, ExportFullConfigData, ExportFullConfigResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetMeData, GetMeResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetRepositoryData, GetRepositoryResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetUpdatesData, GetUpdatesResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, LoginData, LoginResponse, LogoutData, LogoutResponse, MountVolumeData, MountVolumeResponse, RegisterData, RegisterResponse, ReorderBackupSchedulesData, ReorderBackupSchedulesResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, StopBackupData, StopBackupResponse, TagSnapshotsData, TagSnapshotsResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new user
|
* Register a new user
|
||||||
|
|
@ -995,3 +995,20 @@ export const downloadResticPasswordMutation = (options?: Partial<Options<Downloa
|
||||||
};
|
};
|
||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export full configuration including all volumes, repositories, backup schedules, and notifications
|
||||||
|
*/
|
||||||
|
export const exportFullConfigMutation = (options?: Partial<Options<ExportFullConfigData>>): UseMutationOptions<ExportFullConfigResponse, DefaultError, Options<ExportFullConfigData>> => {
|
||||||
|
const mutationOptions: UseMutationOptions<ExportFullConfigResponse, DefaultError, Options<ExportFullConfigData>> = {
|
||||||
|
mutationFn: async (fnOptions) => {
|
||||||
|
const { data } = await exportFullConfig({
|
||||||
|
...options,
|
||||||
|
...fnOptions,
|
||||||
|
throwOnError: true
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return mutationOptions;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import type { Client, Options as Options2, TDataShape } from './client';
|
import type { Client, Options as Options2, TDataShape } from './client';
|
||||||
import { client } from './client.gen';
|
import { client } from './client.gen';
|
||||||
import type { BrowseFilesystemData, BrowseFilesystemResponses, ChangePasswordData, ChangePasswordResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteSnapshotsData, DeleteSnapshotsResponses, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetMeData, GetMeResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetRepositoryData, GetRepositoryResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetUpdatesData, GetUpdatesResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, LoginData, LoginResponses, LogoutData, LogoutResponses, MountVolumeData, MountVolumeResponses, RegisterData, RegisterResponses, ReorderBackupSchedulesData, ReorderBackupSchedulesResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TagSnapshotsData, TagSnapshotsResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
|
import type { BrowseFilesystemData, BrowseFilesystemResponses, ChangePasswordData, ChangePasswordResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteSnapshotsData, DeleteSnapshotsResponses, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, ExportFullConfigData, ExportFullConfigResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetMeData, GetMeResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetRepositoryData, GetRepositoryResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetUpdatesData, GetUpdatesResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, LoginData, LoginResponses, LogoutData, LogoutResponses, MountVolumeData, MountVolumeResponses, RegisterData, RegisterResponses, ReorderBackupSchedulesData, ReorderBackupSchedulesResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TagSnapshotsData, TagSnapshotsResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
|
||||||
|
|
||||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
||||||
/**
|
/**
|
||||||
|
|
@ -425,3 +425,15 @@ export const downloadResticPassword = <ThrowOnError extends boolean = false>(opt
|
||||||
...options?.headers
|
...options?.headers
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export full configuration including all volumes, repositories, backup schedules, and notifications
|
||||||
|
*/
|
||||||
|
export const exportFullConfig = <ThrowOnError extends boolean = false>(options?: Options<ExportFullConfigData, ThrowOnError>) => (options?.client ?? client).post<ExportFullConfigResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/system/export',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options?.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -3337,3 +3337,37 @@ export type DownloadResticPasswordResponses = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DownloadResticPasswordResponse = DownloadResticPasswordResponses[keyof DownloadResticPasswordResponses];
|
export type DownloadResticPasswordResponse = DownloadResticPasswordResponses[keyof DownloadResticPasswordResponses];
|
||||||
|
|
||||||
|
export type ExportFullConfigData = {
|
||||||
|
body?: {
|
||||||
|
password: string;
|
||||||
|
includeMetadata?: boolean;
|
||||||
|
};
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: '/api/v1/system/export';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExportFullConfigResponses = {
|
||||||
|
/**
|
||||||
|
* Full configuration export
|
||||||
|
*/
|
||||||
|
200: {
|
||||||
|
version: number;
|
||||||
|
backupSchedules?: Array<unknown>;
|
||||||
|
exportedAt?: string;
|
||||||
|
notificationDestinations?: Array<unknown>;
|
||||||
|
recoveryKey?: string;
|
||||||
|
repositories?: Array<unknown>;
|
||||||
|
users?: Array<{
|
||||||
|
username: string;
|
||||||
|
createdAt?: number;
|
||||||
|
hasDownloadedResticPassword?: boolean;
|
||||||
|
id?: number;
|
||||||
|
updatedAt?: number;
|
||||||
|
}>;
|
||||||
|
volumes?: Array<unknown>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExportFullConfigResponse = ExportFullConfigResponses[keyof ExportFullConfigResponses];
|
||||||
|
|
|
||||||
125
app/client/components/export-dialog.tsx
Normal file
125
app/client/components/export-dialog.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { Download } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { exportFullConfigMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
import { Button } from "~/client/components/ui/button";
|
||||||
|
import { Checkbox } from "~/client/components/ui/checkbox";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "~/client/components/ui/dialog";
|
||||||
|
import { Input } from "~/client/components/ui/input";
|
||||||
|
import { Label } from "~/client/components/ui/label";
|
||||||
|
import { parseError } from "../lib/errors";
|
||||||
|
import { downloadFile } from "../lib/utils";
|
||||||
|
|
||||||
|
const DEFAULT_EXPORT_FILENAME = "zerobyte-full-config";
|
||||||
|
|
||||||
|
export const ExportDialog = () => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [includeMetadata, setIncludeMetadata] = useState(false);
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
const exportMutation = useMutation({
|
||||||
|
...exportFullConfigMutation(),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
downloadFile(data, `${DEFAULT_EXPORT_FILENAME}.json`, "application/json");
|
||||||
|
toast.success("Configuration exported successfully");
|
||||||
|
setOpen(false);
|
||||||
|
setPassword("");
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast.error("Export failed", {
|
||||||
|
description: parseError(e)?.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleExport = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!password) {
|
||||||
|
toast.error("Password is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
exportMutation.mutate({
|
||||||
|
body: {
|
||||||
|
password,
|
||||||
|
includeMetadata,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDialogChange = (isOpen: boolean) => {
|
||||||
|
setOpen(isOpen);
|
||||||
|
if (!isOpen) {
|
||||||
|
setPassword("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={handleDialogChange}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button>
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Export configuration
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<form onSubmit={handleExport}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Export Full Configuration</DialogTitle>
|
||||||
|
<DialogDescription>Export the complete Zerobyte configuration.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Checkbox
|
||||||
|
id="includeMetadata"
|
||||||
|
checked={includeMetadata}
|
||||||
|
onCheckedChange={(checked) => setIncludeMetadata(checked === true)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="includeMetadata" className="cursor-pointer">
|
||||||
|
Include metadata
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground ml-7">
|
||||||
|
Include timestamps and runtime state (status, health checks, last backup info).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-2 pt-2 border-t">
|
||||||
|
<Label htmlFor="export-password">Your Password</Label>
|
||||||
|
<Input
|
||||||
|
id="export-password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="Enter your password to export"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Password is required to verify your identity before exporting configuration.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" loading={exportMutation.isPending}>
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -28,3 +28,20 @@ export function slugify(input: string): string {
|
||||||
.replace(/[_]{2,}/g, "_")
|
.replace(/[_]{2,}/g, "_")
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DownloadFileMimeType = "text/plain" | "application/json";
|
||||||
|
export const downloadFile = (data: unknown, filename: string, mimeType: DownloadFileMimeType = "text/plain") => {
|
||||||
|
const content = mimeType === "application/json" && typeof data !== "string" ? JSON.stringify(data, null, 2) : data;
|
||||||
|
|
||||||
|
const blob = new Blob([content as BlobPart], { type: mimeType });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { Label } from "~/client/components/ui/label";
|
||||||
import { authMiddleware } from "~/middleware/auth";
|
import { authMiddleware } from "~/middleware/auth";
|
||||||
import type { Route } from "./+types/download-recovery-key";
|
import type { Route } from "./+types/download-recovery-key";
|
||||||
import { downloadResticPasswordMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
import { downloadResticPasswordMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
import { downloadFile } from "~/client/lib/utils";
|
||||||
|
|
||||||
export const clientMiddleware = [authMiddleware];
|
export const clientMiddleware = [authMiddleware];
|
||||||
|
|
||||||
|
|
@ -31,16 +32,7 @@ export default function DownloadRecoveryKeyPage() {
|
||||||
const downloadResticPassword = useMutation({
|
const downloadResticPassword = useMutation({
|
||||||
...downloadResticPasswordMutation(),
|
...downloadResticPasswordMutation(),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
const blob = new Blob([data], { type: "text/plain" });
|
downloadFile(data, "restic.pass");
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = url;
|
|
||||||
a.download = "restic.pass";
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
toast.success("Recovery key downloaded successfully!");
|
toast.success("Recovery key downloaded successfully!");
|
||||||
navigate("/volumes", { replace: true });
|
navigate("/volumes", { replace: true });
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Download, KeyRound, User, X } from "lucide-react";
|
import { Download, KeyRound, User, X } from "lucide-react";
|
||||||
|
import { ExportDialog } from "~/client/components/export-dialog";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
@ -23,6 +24,7 @@ import {
|
||||||
downloadResticPasswordMutation,
|
downloadResticPasswordMutation,
|
||||||
logoutMutation,
|
logoutMutation,
|
||||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
import { downloadFile } from "~/client/lib/utils";
|
||||||
|
|
||||||
export const handle = {
|
export const handle = {
|
||||||
breadcrumb: () => [{ label: "Settings" }],
|
breadcrumb: () => [{ label: "Settings" }],
|
||||||
|
|
@ -78,16 +80,7 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
|
||||||
const downloadResticPassword = useMutation({
|
const downloadResticPassword = useMutation({
|
||||||
...downloadResticPasswordMutation(),
|
...downloadResticPasswordMutation(),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
const blob = new Blob([data], { type: "text/plain" });
|
downloadFile(data, "restic.pass");
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = url;
|
|
||||||
a.download = "restic.pass";
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
toast.success("Restic password file downloaded successfully");
|
toast.success("Restic password file downloaded successfully");
|
||||||
setDownloadDialogOpen(false);
|
setDownloadDialogOpen(false);
|
||||||
setDownloadPassword("");
|
setDownloadPassword("");
|
||||||
|
|
@ -265,6 +258,21 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
<div className="border-t border-border/50 bg-card-header p-6">
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Download className="size-5" />
|
||||||
|
Export Configuration
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="mt-1.5">Export your Zerobyte configuration for backup or migration</CardDescription>
|
||||||
|
</div>
|
||||||
|
<CardContent className="p-6 space-y-4">
|
||||||
|
<p className="text-sm text-muted-foreground max-w-2xl">
|
||||||
|
Export all your volumes, repositories, backup schedules, and notification settings to a JSON file. This can be
|
||||||
|
used to restore your configuration on a new instance or as a backup of your settings.
|
||||||
|
</p>
|
||||||
|
<ExportDialog />
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,19 @@ export class AuthService {
|
||||||
|
|
||||||
logger.info(`Password changed for user: ${user.username}`);
|
logger.info(`Password changed for user: ${user.username}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify password for a user (for re-authentication purposes)
|
||||||
|
*/
|
||||||
|
async verifyPassword(userId: number, password: string): Promise<boolean> {
|
||||||
|
const [user] = await db.select().from(usersTable).where(eq(usersTable.id, userId));
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Bun.password.verify(password, user.passwordHash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authService = new AuthService();
|
export const authService = new AuthService();
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ describe("system security", () => {
|
||||||
const endpoints: { method: string; path: string }[] = [
|
const endpoints: { method: string; path: string }[] = [
|
||||||
{ method: "GET", path: "/api/v1/system/info" },
|
{ method: "GET", path: "/api/v1/system/info" },
|
||||||
{ method: "POST", path: "/api/v1/system/restic-password" },
|
{ method: "POST", path: "/api/v1/system/restic-password" },
|
||||||
|
{ method: "POST", path: "/api/v1/system/export" },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const { method, path } of endpoints) {
|
for (const { method, path } of endpoints) {
|
||||||
|
|
@ -86,4 +87,60 @@ describe("system security", () => {
|
||||||
expect(body.message).toBe("Incorrect password");
|
expect(body.message).toBe("Incorrect password");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should return 400 for invalid payload on full export", async () => {
|
||||||
|
const { sessionId } = await createTestSession();
|
||||||
|
const res = await app.request("/api/v1/system/export", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Cookie: `session_id=${sessionId}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return 401 for incorrect password on full export", async () => {
|
||||||
|
const { sessionId } = await createTestSession();
|
||||||
|
const res = await app.request("/api/v1/system/export", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Cookie: `session_id=${sessionId}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
includeMetadata: true,
|
||||||
|
password: "wrong-password",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.message).toBe("Incorrect password");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("full export never exposes password hashes", async () => {
|
||||||
|
const { sessionId } = await createTestSession();
|
||||||
|
|
||||||
|
const res = await app.request("/api/v1/system/export", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Cookie: `session_id=${sessionId}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
includeMetadata: true,
|
||||||
|
password: "testpassword",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const body = (await res.json()) as { users?: Array<Record<string, unknown>> };
|
||||||
|
expect(Array.isArray(body.users)).toBe(true);
|
||||||
|
for (const user of body.users ?? []) {
|
||||||
|
expect(user.passwordHash).toBeUndefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { validator } from "hono-openapi";
|
||||||
import {
|
import {
|
||||||
downloadResticPasswordBodySchema,
|
downloadResticPasswordBodySchema,
|
||||||
downloadResticPasswordDto,
|
downloadResticPasswordDto,
|
||||||
|
fullExportBodySchema,
|
||||||
|
fullExportDto,
|
||||||
getUpdatesDto,
|
getUpdatesDto,
|
||||||
systemInfoDto,
|
systemInfoDto,
|
||||||
type SystemInfoDto,
|
type SystemInfoDto,
|
||||||
|
|
@ -61,4 +63,24 @@ export const systemController = new Hono()
|
||||||
return c.json({ message: "Failed to read Restic password file" }, 500);
|
return c.json({ message: "Failed to read Restic password file" }, 500);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
.post("/export", fullExportDto, validator("json", fullExportBodySchema), async (c) => {
|
||||||
|
const user = c.get("user");
|
||||||
|
const { password, ...body } = c.req.valid("json");
|
||||||
|
|
||||||
|
const [dbUser] = await db.select().from(usersTable).where(eq(usersTable.id, user.id));
|
||||||
|
|
||||||
|
if (!dbUser) {
|
||||||
|
return c.json({ message: "User not found" }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await Bun.password.verify(password, dbUser.passwordHash);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return c.json({ message: "Incorrect password" }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await systemService.exportConfig(body);
|
||||||
|
|
||||||
|
return c.json(res);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -79,3 +79,47 @@ export const downloadResticPasswordDto = describeRoute({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const fullExportBodySchema = type({
|
||||||
|
includeMetadata: "boolean = false",
|
||||||
|
password: "string",
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FullExportBody = typeof fullExportBodySchema.infer;
|
||||||
|
|
||||||
|
const exportResponseSchema = type({
|
||||||
|
version: "number",
|
||||||
|
exportedAt: "string?",
|
||||||
|
recoveryKey: "string?",
|
||||||
|
volumes: "unknown[]?",
|
||||||
|
repositories: "unknown[]?",
|
||||||
|
backupSchedules: "unknown[]?",
|
||||||
|
notificationDestinations: "unknown[]?",
|
||||||
|
users: type({
|
||||||
|
id: "number?",
|
||||||
|
username: "string",
|
||||||
|
createdAt: "number?",
|
||||||
|
updatedAt: "number?",
|
||||||
|
hasDownloadedResticPassword: "boolean?",
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ExportFullConfigResponse = typeof exportResponseSchema.infer;
|
||||||
|
|
||||||
|
export const fullExportDto = describeRoute({
|
||||||
|
description: "Export full configuration including all volumes, repositories, backup schedules, and notifications",
|
||||||
|
operationId: "exportFullConfig",
|
||||||
|
tags: ["Config Export"],
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Full configuration export",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(exportResponseSchema),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,23 @@ import type { UpdateInfoDto } from "./system.dto";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { cache } from "../../utils/cache";
|
import { cache } from "../../utils/cache";
|
||||||
import { logger } from "~/server/utils/logger";
|
import { logger } from "~/server/utils/logger";
|
||||||
|
import { db } from "~/server/db/db";
|
||||||
|
import {
|
||||||
|
backupScheduleMirrorsTable,
|
||||||
|
backupScheduleNotificationsTable,
|
||||||
|
backupSchedulesTable,
|
||||||
|
notificationDestinationsTable,
|
||||||
|
repositoriesTable,
|
||||||
|
usersTable,
|
||||||
|
volumesTable,
|
||||||
|
type BackupScheduleMirror,
|
||||||
|
type BackupScheduleNotification,
|
||||||
|
type BackupSchedule,
|
||||||
|
} from "~/server/db/schema";
|
||||||
|
|
||||||
|
type ExportParams = {
|
||||||
|
includeMetadata: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const CACHE_TTL = 60 * 60;
|
const CACHE_TTL = 60 * 60;
|
||||||
|
|
||||||
|
|
@ -90,7 +107,113 @@ const getUpdates = async (): Promise<UpdateInfoDto> => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const METADATA_KEYS = {
|
||||||
|
timestamps: [
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
"lastBackupAt",
|
||||||
|
"nextBackupAt",
|
||||||
|
"lastHealthCheck",
|
||||||
|
"lastChecked",
|
||||||
|
"lastCopyAt",
|
||||||
|
],
|
||||||
|
runtimeState: [
|
||||||
|
"status",
|
||||||
|
"lastError",
|
||||||
|
"lastBackupStatus",
|
||||||
|
"lastBackupError",
|
||||||
|
"hasDownloadedResticPassword",
|
||||||
|
"lastCopyStatus",
|
||||||
|
"lastCopyError",
|
||||||
|
"sortOrder",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const ALL_METADATA_KEYS = [...METADATA_KEYS.timestamps, ...METADATA_KEYS.runtimeState];
|
||||||
|
|
||||||
|
function filterMetadataOut<T extends Record<string, unknown>>(obj: T, includeMetadata: boolean): Partial<T> {
|
||||||
|
if (includeMetadata) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
const result = { ...obj };
|
||||||
|
for (const key of ALL_METADATA_KEYS) {
|
||||||
|
delete result[key as keyof T];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportEntity(entity: Record<string, unknown>, params: ExportParams) {
|
||||||
|
return filterMetadataOut(entity, params.includeMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportEntities<T extends Record<string, unknown>>(entities: T[], params: ExportParams) {
|
||||||
|
return Promise.all(entities.map((e) => exportEntity(e, params)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformBackupSchedules = (
|
||||||
|
schedules: BackupSchedule[],
|
||||||
|
scheduleNotifications: BackupScheduleNotification[],
|
||||||
|
scheduleMirrors: BackupScheduleMirror[],
|
||||||
|
params: ExportParams,
|
||||||
|
) => {
|
||||||
|
return schedules.map((schedule) => {
|
||||||
|
const assignments = scheduleNotifications
|
||||||
|
.filter((sn) => sn.scheduleId === schedule.id)
|
||||||
|
.map((sn) => filterMetadataOut(sn, params.includeMetadata));
|
||||||
|
|
||||||
|
const mirrors = scheduleMirrors
|
||||||
|
.filter((sm) => sm.scheduleId === schedule.id)
|
||||||
|
.map((sm) => filterMetadataOut(sm, params.includeMetadata));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...filterMetadataOut(schedule, params.includeMetadata),
|
||||||
|
notifications: assignments,
|
||||||
|
mirrors,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportConfig = async (params: ExportParams) => {
|
||||||
|
const [volumes, repositories, backupSchedulesRaw, notifications, scheduleNotifications, scheduleMirrors, users] =
|
||||||
|
await Promise.all([
|
||||||
|
db.select().from(volumesTable),
|
||||||
|
db.select().from(repositoriesTable),
|
||||||
|
db.select().from(backupSchedulesTable),
|
||||||
|
db.select().from(notificationDestinationsTable),
|
||||||
|
db.select().from(backupScheduleNotificationsTable),
|
||||||
|
db.select().from(backupScheduleMirrorsTable),
|
||||||
|
db.select().from(usersTable),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const backupSchedules = transformBackupSchedules(backupSchedulesRaw, scheduleNotifications, scheduleMirrors, params);
|
||||||
|
|
||||||
|
const [exportVolumes, exportRepositories, exportNotifications, exportedUsersWithHash] = await Promise.all([
|
||||||
|
exportEntities(volumes, params) as Promise<typeof volumes>,
|
||||||
|
exportEntities(repositories, params) as Promise<typeof repositories>,
|
||||||
|
exportEntities(notifications, params) as Promise<typeof notifications>,
|
||||||
|
exportEntities(users, params) as Promise<typeof users>,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const exportUsers = exportedUsersWithHash.map((user) => {
|
||||||
|
const sanitizedUser = { ...user } as Record<string, unknown>;
|
||||||
|
delete sanitizedUser.passwordHash;
|
||||||
|
sanitizedUser.password = `\${USER_${user.username.toUpperCase()}_PASSWORD}`;
|
||||||
|
return sanitizedUser;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
volumes: exportVolumes,
|
||||||
|
repositories: exportRepositories,
|
||||||
|
backupSchedules,
|
||||||
|
notificationDestinations: exportNotifications,
|
||||||
|
users: exportUsers,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const systemService = {
|
export const systemService = {
|
||||||
getSystemInfo,
|
getSystemInfo,
|
||||||
getUpdates,
|
getUpdates,
|
||||||
|
exportConfig,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -186,4 +186,6 @@ const sealSecret = async (value: string): Promise<string> => {
|
||||||
export const cryptoUtils = {
|
export const cryptoUtils = {
|
||||||
resolveSecret,
|
resolveSecret,
|
||||||
sealSecret,
|
sealSecret,
|
||||||
|
isEncrypted,
|
||||||
|
decrypt,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue