reduced the scope of PR to MVP - exportAll only
This commit is contained in:
parent
692dd5aa21
commit
bf36f9aa56
16 changed files with 189 additions and 1162 deletions
19
README.md
19
README.md
|
|
@ -198,26 +198,21 @@ Zerobyte allows you to easily restore your data from backups. To restore data, n
|
||||||
|
|
||||||
## Exporting configuration
|
## Exporting configuration
|
||||||
|
|
||||||
Zerobyte allows you to export your configuration for backup, migration, or documentation purposes. You can export:
|
Zerobyte allows you to export your configuration for backup, migration, or documentation purposes.
|
||||||
|
|
||||||
- **Full configuration** - All volumes, repositories, backup schedules, and notification destinations
|
|
||||||
- **Individual entities** - Export specific volumes, repositories, notifications, or backup schedules
|
|
||||||
|
|
||||||
To export, click the "Export" button on any list page or detail page. A dialog will appear with options to:
|
To export, click the "Export" button on any list page or detail page. A dialog will appear with options to:
|
||||||
|
|
||||||
- **Include database IDs** - Useful for debugging or when you need to reference internal identifiers
|
- **Include metadata** - Include IDs, timestamps, and runtime state of entities
|
||||||
- **Include timestamps** - Include createdAt/updatedAt fields in the export
|
- **Secrets handling**:
|
||||||
- **Include runtime state** - Include current status, health checks, and last backup information (usually not needed for migration)
|
|
||||||
- **Secrets handling** (for repositories and notifications):
|
|
||||||
- **Exclude** - Remove sensitive fields like passwords and API keys
|
- **Exclude** - Remove sensitive fields like passwords and API keys
|
||||||
- **Keep encrypted** - Export secrets in encrypted form (requires the same recovery key to decrypt on import)
|
- **Keep encrypted** - Export secrets in encrypted form (requires the same recovery key to decrypt on import)
|
||||||
- **Decrypt** - Export secrets as plaintext (use with caution)
|
- **Decrypt** - Export secrets as plaintext (use with caution)
|
||||||
- **Include recovery key** (full export only) - Include the master encryption key for all repositories
|
- **Include recovery key** - Include the master encryption key for all repositories
|
||||||
- **Include password hash** (full export only) - Include the hashed admin password for seamless migration
|
- **Include password hash** - Include the hashed user passwords for seamless migration
|
||||||
|
|
||||||
All exports require password verification for security. You must enter your password to confirm your identity before any configuration can be exported.
|
Export requires password verification for security. You must enter your password to confirm your identity before any configuration can be exported.
|
||||||
|
|
||||||
Exports are downloaded as JSON files that can be used for reference or future import functionality.
|
Export is downloaded as JSON file that can be used for reference or future import functionality.
|
||||||
|
|
||||||
## Propagating mounts to host
|
## Propagating mounts to host
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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, deleteVolume, doctorRepository, downloadResticPassword, exportBackupSchedules, exportFullConfig, exportNotificationDestinations, exportRepositories, exportVolumes, getBackupSchedule, getBackupScheduleForVolume, getContainersUsingVolume, getMe, getNotificationDestination, getRepository, getScheduleNotifications, getSnapshotDetails, getStatus, getSystemInfo, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, login, logout, mountVolume, type Options, register, restoreSnapshot, runBackupNow, runForget, stopBackup, testConnection, testNotificationDestination, unmountVolume, updateBackupSchedule, updateNotificationDestination, updateRepository, updateScheduleNotifications, updateVolume } from '../sdk.gen';
|
import { browseFilesystem, changePassword, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteVolume, doctorRepository, downloadResticPassword, exportFullConfig, getBackupSchedule, getBackupScheduleForVolume, getContainersUsingVolume, getMe, getNotificationDestination, getRepository, getScheduleNotifications, getSnapshotDetails, getStatus, getSystemInfo, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, login, logout, mountVolume, type Options, register, restoreSnapshot, runBackupNow, runForget, stopBackup, testConnection, testNotificationDestination, unmountVolume, updateBackupSchedule, updateNotificationDestination, updateRepository, 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, DeleteVolumeData, DeleteVolumeResponse, DoctorRepositoryData, DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, ExportBackupSchedulesData, ExportBackupSchedulesError, ExportBackupSchedulesResponse, ExportFullConfigData, ExportFullConfigError, ExportFullConfigResponse, ExportNotificationDestinationsData, ExportNotificationDestinationsError, ExportNotificationDestinationsResponse, ExportRepositoriesData, ExportRepositoriesError, ExportRepositoriesResponse, ExportVolumesData, ExportVolumesError, ExportVolumesResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetContainersUsingVolumeData, GetContainersUsingVolumeResponse, GetMeData, GetMeResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetRepositoryData, GetRepositoryResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, 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, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, StopBackupData, StopBackupResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, 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, DeleteVolumeData, DeleteVolumeResponse, DoctorRepositoryData, DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, ExportFullConfigData, ExportFullConfigError, ExportFullConfigResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetContainersUsingVolumeData, GetContainersUsingVolumeResponse, GetMeData, GetMeResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetRepositoryData, GetRepositoryResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, 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, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, StopBackupData, StopBackupResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new user
|
* Register a new user
|
||||||
|
|
@ -910,71 +910,3 @@ export const exportFullConfigMutation = (options?: Partial<Options<ExportFullCon
|
||||||
};
|
};
|
||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Export volumes configuration
|
|
||||||
*/
|
|
||||||
export const exportVolumesMutation = (options?: Partial<Options<ExportVolumesData>>): UseMutationOptions<ExportVolumesResponse, ExportVolumesError, Options<ExportVolumesData>> => {
|
|
||||||
const mutationOptions: UseMutationOptions<ExportVolumesResponse, ExportVolumesError, Options<ExportVolumesData>> = {
|
|
||||||
mutationFn: async (fnOptions) => {
|
|
||||||
const { data } = await exportVolumes({
|
|
||||||
...options,
|
|
||||||
...fnOptions,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return mutationOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export repositories configuration
|
|
||||||
*/
|
|
||||||
export const exportRepositoriesMutation = (options?: Partial<Options<ExportRepositoriesData>>): UseMutationOptions<ExportRepositoriesResponse, ExportRepositoriesError, Options<ExportRepositoriesData>> => {
|
|
||||||
const mutationOptions: UseMutationOptions<ExportRepositoriesResponse, ExportRepositoriesError, Options<ExportRepositoriesData>> = {
|
|
||||||
mutationFn: async (fnOptions) => {
|
|
||||||
const { data } = await exportRepositories({
|
|
||||||
...options,
|
|
||||||
...fnOptions,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return mutationOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export notification destinations configuration
|
|
||||||
*/
|
|
||||||
export const exportNotificationDestinationsMutation = (options?: Partial<Options<ExportNotificationDestinationsData>>): UseMutationOptions<ExportNotificationDestinationsResponse, ExportNotificationDestinationsError, Options<ExportNotificationDestinationsData>> => {
|
|
||||||
const mutationOptions: UseMutationOptions<ExportNotificationDestinationsResponse, ExportNotificationDestinationsError, Options<ExportNotificationDestinationsData>> = {
|
|
||||||
mutationFn: async (fnOptions) => {
|
|
||||||
const { data } = await exportNotificationDestinations({
|
|
||||||
...options,
|
|
||||||
...fnOptions,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return mutationOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export backup schedules configuration
|
|
||||||
*/
|
|
||||||
export const exportBackupSchedulesMutation = (options?: Partial<Options<ExportBackupSchedulesData>>): UseMutationOptions<ExportBackupSchedulesResponse, ExportBackupSchedulesError, Options<ExportBackupSchedulesData>> => {
|
|
||||||
const mutationOptions: UseMutationOptions<ExportBackupSchedulesResponse, ExportBackupSchedulesError, Options<ExportBackupSchedulesData>> = {
|
|
||||||
mutationFn: async (fnOptions) => {
|
|
||||||
const { data } = await exportBackupSchedules({
|
|
||||||
...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, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, ExportBackupSchedulesData, ExportBackupSchedulesErrors, ExportBackupSchedulesResponses, ExportFullConfigData, ExportFullConfigErrors, ExportFullConfigResponses, ExportNotificationDestinationsData, ExportNotificationDestinationsErrors, ExportNotificationDestinationsResponses, ExportRepositoriesData, ExportRepositoriesErrors, ExportRepositoriesResponses, ExportVolumesData, ExportVolumesErrors, ExportVolumesResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetContainersUsingVolumeData, GetContainersUsingVolumeErrors, GetContainersUsingVolumeResponses, GetMeData, GetMeResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetRepositoryData, GetRepositoryResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, 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, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, 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, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, ExportFullConfigData, ExportFullConfigErrors, ExportFullConfigResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetContainersUsingVolumeData, GetContainersUsingVolumeErrors, GetContainersUsingVolumeResponses, GetMeData, GetMeResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetRepositoryData, GetRepositoryResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, 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, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, 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> & {
|
||||||
/**
|
/**
|
||||||
|
|
@ -581,59 +581,3 @@ export const exportFullConfig = <ThrowOnError extends boolean = false>(options?:
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Export volumes configuration
|
|
||||||
*/
|
|
||||||
export const exportVolumes = <ThrowOnError extends boolean = false>(options?: Options<ExportVolumesData, ThrowOnError>) => {
|
|
||||||
return (options?.client ?? client).post<ExportVolumesResponses, ExportVolumesErrors, ThrowOnError>({
|
|
||||||
url: '/api/v1/config/export/volumes',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options?.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export repositories configuration
|
|
||||||
*/
|
|
||||||
export const exportRepositories = <ThrowOnError extends boolean = false>(options?: Options<ExportRepositoriesData, ThrowOnError>) => {
|
|
||||||
return (options?.client ?? client).post<ExportRepositoriesResponses, ExportRepositoriesErrors, ThrowOnError>({
|
|
||||||
url: '/api/v1/config/export/repositories',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options?.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export notification destinations configuration
|
|
||||||
*/
|
|
||||||
export const exportNotificationDestinations = <ThrowOnError extends boolean = false>(options?: Options<ExportNotificationDestinationsData, ThrowOnError>) => {
|
|
||||||
return (options?.client ?? client).post<ExportNotificationDestinationsResponses, ExportNotificationDestinationsErrors, ThrowOnError>({
|
|
||||||
url: '/api/v1/config/export/notification-destinations',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options?.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export backup schedules configuration
|
|
||||||
*/
|
|
||||||
export const exportBackupSchedules = <ThrowOnError extends boolean = false>(options?: Options<ExportBackupSchedulesData, ThrowOnError>) => {
|
|
||||||
return (options?.client ?? client).post<ExportBackupSchedulesResponses, ExportBackupSchedulesErrors, ThrowOnError>({
|
|
||||||
url: '/api/v1/config/export/backup-schedules',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options?.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -2619,11 +2619,9 @@ export type DownloadResticPasswordResponse = DownloadResticPasswordResponses[key
|
||||||
export type ExportFullConfigData = {
|
export type ExportFullConfigData = {
|
||||||
body?: {
|
body?: {
|
||||||
password: string;
|
password: string;
|
||||||
includeIds?: boolean;
|
includeMetadata?: boolean;
|
||||||
includePasswordHash?: boolean;
|
includePasswordHash?: boolean;
|
||||||
includeRecoveryKey?: boolean;
|
includeRecoveryKey?: boolean;
|
||||||
includeRuntimeState?: boolean;
|
|
||||||
includeTimestamps?: boolean;
|
|
||||||
secretsMode?: 'cleartext' | 'encrypted' | 'exclude';
|
secretsMode?: 'cleartext' | 'encrypted' | 'exclude';
|
||||||
};
|
};
|
||||||
path?: never;
|
path?: never;
|
||||||
|
|
@ -2668,222 +2666,3 @@ export type ExportFullConfigResponses = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExportFullConfigResponse = ExportFullConfigResponses[keyof ExportFullConfigResponses];
|
export type ExportFullConfigResponse = ExportFullConfigResponses[keyof ExportFullConfigResponses];
|
||||||
|
|
||||||
export type ExportVolumesData = {
|
|
||||||
body?: {
|
|
||||||
password: string;
|
|
||||||
id?: number | string;
|
|
||||||
includeIds?: boolean;
|
|
||||||
includeRuntimeState?: boolean;
|
|
||||||
includeTimestamps?: boolean;
|
|
||||||
name?: string;
|
|
||||||
secretsMode?: 'cleartext' | 'encrypted' | 'exclude';
|
|
||||||
};
|
|
||||||
path?: never;
|
|
||||||
query?: never;
|
|
||||||
url: '/api/v1/config/export/volumes';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExportVolumesErrors = {
|
|
||||||
/**
|
|
||||||
* Invalid request
|
|
||||||
*/
|
|
||||||
400: {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Password required for export
|
|
||||||
*/
|
|
||||||
401: {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Volume not found
|
|
||||||
*/
|
|
||||||
404: {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Export failed
|
|
||||||
*/
|
|
||||||
500: {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExportVolumesError = ExportVolumesErrors[keyof ExportVolumesErrors];
|
|
||||||
|
|
||||||
export type ExportVolumesResponses = {
|
|
||||||
/**
|
|
||||||
* Volumes configuration export
|
|
||||||
*/
|
|
||||||
200: {
|
|
||||||
volumes: Array<unknown>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExportVolumesResponse = ExportVolumesResponses[keyof ExportVolumesResponses];
|
|
||||||
|
|
||||||
export type ExportRepositoriesData = {
|
|
||||||
body?: {
|
|
||||||
password: string;
|
|
||||||
id?: number | string;
|
|
||||||
includeIds?: boolean;
|
|
||||||
includeRuntimeState?: boolean;
|
|
||||||
includeTimestamps?: boolean;
|
|
||||||
name?: string;
|
|
||||||
secretsMode?: 'cleartext' | 'encrypted' | 'exclude';
|
|
||||||
};
|
|
||||||
path?: never;
|
|
||||||
query?: never;
|
|
||||||
url: '/api/v1/config/export/repositories';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExportRepositoriesErrors = {
|
|
||||||
/**
|
|
||||||
* Invalid request
|
|
||||||
*/
|
|
||||||
400: {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Password required for sensitive export options
|
|
||||||
*/
|
|
||||||
401: {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Repository not found
|
|
||||||
*/
|
|
||||||
404: {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Export failed
|
|
||||||
*/
|
|
||||||
500: {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExportRepositoriesError = ExportRepositoriesErrors[keyof ExportRepositoriesErrors];
|
|
||||||
|
|
||||||
export type ExportRepositoriesResponses = {
|
|
||||||
/**
|
|
||||||
* Repositories configuration export
|
|
||||||
*/
|
|
||||||
200: {
|
|
||||||
repositories: Array<unknown>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExportRepositoriesResponse = ExportRepositoriesResponses[keyof ExportRepositoriesResponses];
|
|
||||||
|
|
||||||
export type ExportNotificationDestinationsData = {
|
|
||||||
body?: {
|
|
||||||
password: string;
|
|
||||||
id?: number | string;
|
|
||||||
includeIds?: boolean;
|
|
||||||
includeRuntimeState?: boolean;
|
|
||||||
includeTimestamps?: boolean;
|
|
||||||
name?: string;
|
|
||||||
secretsMode?: 'cleartext' | 'encrypted' | 'exclude';
|
|
||||||
};
|
|
||||||
path?: never;
|
|
||||||
query?: never;
|
|
||||||
url: '/api/v1/config/export/notification-destinations';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExportNotificationDestinationsErrors = {
|
|
||||||
/**
|
|
||||||
* Invalid request
|
|
||||||
*/
|
|
||||||
400: {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Password required for sensitive export options
|
|
||||||
*/
|
|
||||||
401: {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Notification destination not found
|
|
||||||
*/
|
|
||||||
404: {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Export failed
|
|
||||||
*/
|
|
||||||
500: {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExportNotificationDestinationsError = ExportNotificationDestinationsErrors[keyof ExportNotificationDestinationsErrors];
|
|
||||||
|
|
||||||
export type ExportNotificationDestinationsResponses = {
|
|
||||||
/**
|
|
||||||
* Notification destinations configuration export
|
|
||||||
*/
|
|
||||||
200: {
|
|
||||||
notificationDestinations: Array<unknown>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExportNotificationDestinationsResponse = ExportNotificationDestinationsResponses[keyof ExportNotificationDestinationsResponses];
|
|
||||||
|
|
||||||
export type ExportBackupSchedulesData = {
|
|
||||||
body?: {
|
|
||||||
password: string;
|
|
||||||
id?: number;
|
|
||||||
includeIds?: boolean;
|
|
||||||
includeRuntimeState?: boolean;
|
|
||||||
includeTimestamps?: boolean;
|
|
||||||
secretsMode?: 'cleartext' | 'encrypted' | 'exclude';
|
|
||||||
};
|
|
||||||
path?: never;
|
|
||||||
query?: never;
|
|
||||||
url: '/api/v1/config/export/backup-schedules';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExportBackupSchedulesErrors = {
|
|
||||||
/**
|
|
||||||
* Invalid request
|
|
||||||
*/
|
|
||||||
400: {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Password required for export
|
|
||||||
*/
|
|
||||||
401: {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Backup schedule not found
|
|
||||||
*/
|
|
||||||
404: {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Export failed
|
|
||||||
*/
|
|
||||||
500: {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExportBackupSchedulesError = ExportBackupSchedulesErrors[keyof ExportBackupSchedulesErrors];
|
|
||||||
|
|
||||||
export type ExportBackupSchedulesResponses = {
|
|
||||||
/**
|
|
||||||
* Backup schedules configuration export
|
|
||||||
*/
|
|
||||||
200: {
|
|
||||||
backupSchedules: Array<unknown>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExportBackupSchedulesResponse = ExportBackupSchedulesResponses[keyof ExportBackupSchedulesResponses];
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,8 @@
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Download } from "lucide-react";
|
import { Download } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { type ReactNode, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import { exportFullConfigMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
exportBackupSchedulesMutation,
|
|
||||||
exportFullConfigMutation,
|
|
||||||
exportNotificationDestinationsMutation,
|
|
||||||
exportRepositoriesMutation,
|
|
||||||
exportVolumesMutation,
|
|
||||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
import { Checkbox } from "~/client/components/ui/checkbox";
|
import { Checkbox } from "~/client/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
|
|
@ -30,7 +24,9 @@ import {
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "~/client/components/ui/select";
|
} from "~/client/components/ui/select";
|
||||||
|
|
||||||
export type SecretsMode = "exclude" | "encrypted" | "cleartext";
|
type SecretsMode = "exclude" | "encrypted" | "cleartext";
|
||||||
|
|
||||||
|
const DEFAULT_EXPORT_FILENAME = "zerobyte-full-config";
|
||||||
|
|
||||||
function downloadAsJson(data: unknown, filename: string): void {
|
function downloadAsJson(data: unknown, filename: string): void {
|
||||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
||||||
|
|
@ -44,60 +40,14 @@ function downloadAsJson(data: unknown, filename: string): void {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExportEntityType = "volumes" | "repositories" | "notification-destinations" | "backup-schedules" | "full";
|
|
||||||
|
|
||||||
type ExportConfig = {
|
|
||||||
label: string;
|
|
||||||
labelPlural: string;
|
|
||||||
getFilename: (id?: string | number, name?: string) => string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportConfigs: Record<ExportEntityType, ExportConfig> = {
|
|
||||||
volumes: {
|
|
||||||
label: "Volume",
|
|
||||||
labelPlural: "Volumes",
|
|
||||||
getFilename: (id, name) => {
|
|
||||||
const identifier = id ?? name;
|
|
||||||
return identifier ? `volume-${identifier}-config` : "volumes-config";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
repositories: {
|
|
||||||
label: "Repository",
|
|
||||||
labelPlural: "Repositories",
|
|
||||||
getFilename: (id, name) => {
|
|
||||||
const identifier = id ?? name;
|
|
||||||
return identifier ? `repository-${identifier}-config` : "repositories-config";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"notification-destinations": {
|
|
||||||
label: "Notification Destination",
|
|
||||||
labelPlural: "Notification Destinations",
|
|
||||||
getFilename: (id, name) => {
|
|
||||||
const identifier = id ?? name;
|
|
||||||
return identifier ? `notification-destination-${identifier}-config` : "notification-destinations-config";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"backup-schedules": {
|
|
||||||
label: "Backup Schedule",
|
|
||||||
labelPlural: "Backup Schedules",
|
|
||||||
getFilename: (id) => (id ? `backup-schedule-${id}-config` : "backup-schedules-config"),
|
|
||||||
},
|
|
||||||
full: {
|
|
||||||
label: "Full Config",
|
|
||||||
labelPlural: "Full Config",
|
|
||||||
getFilename: () => "zerobyte-full-config",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
type BaseExportDialogProps = {
|
type BaseExportDialogProps = {
|
||||||
entityType: ExportEntityType;
|
/** Optional custom filename (without extension). Defaults to `zerobyte-full-config`. */
|
||||||
name?: string;
|
filename?: string;
|
||||||
id?: string | number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ExportDialogWithTrigger = BaseExportDialogProps & {
|
type ExportDialogWithTrigger = BaseExportDialogProps & {
|
||||||
/** Custom trigger element. When provided, variant/size/triggerLabel/showIcon are ignored. */
|
/** Custom trigger element. When provided, variant/size/triggerLabel/showIcon are ignored. */
|
||||||
trigger: React.ReactNode;
|
trigger: ReactNode;
|
||||||
variant?: never;
|
variant?: never;
|
||||||
size?: never;
|
size?: never;
|
||||||
triggerLabel?: never;
|
triggerLabel?: never;
|
||||||
|
|
@ -115,9 +65,7 @@ type ExportDialogWithDefaultTrigger = BaseExportDialogProps & {
|
||||||
type ExportDialogProps = ExportDialogWithTrigger | ExportDialogWithDefaultTrigger;
|
type ExportDialogProps = ExportDialogWithTrigger | ExportDialogWithDefaultTrigger;
|
||||||
|
|
||||||
export function ExportDialog({
|
export function ExportDialog({
|
||||||
entityType,
|
filename = DEFAULT_EXPORT_FILENAME,
|
||||||
name,
|
|
||||||
id,
|
|
||||||
trigger,
|
trigger,
|
||||||
variant = "outline",
|
variant = "outline",
|
||||||
size = "default",
|
size = "default",
|
||||||
|
|
@ -125,85 +73,35 @@ export function ExportDialog({
|
||||||
showIcon = true,
|
showIcon = true,
|
||||||
}: ExportDialogProps) {
|
}: ExportDialogProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [includeIds, setIncludeIds] = useState(true);
|
const [includeMetadata, setIncludeMetadata] = useState(false);
|
||||||
const [includeTimestamps, setIncludeTimestamps] = useState(true);
|
|
||||||
const [includeRuntimeState, setIncludeRuntimeState] = useState(false);
|
|
||||||
const [includeRecoveryKey, setIncludeRecoveryKey] = useState(false);
|
const [includeRecoveryKey, setIncludeRecoveryKey] = useState(false);
|
||||||
const [includePasswordHash, setIncludePasswordHash] = useState(false);
|
const [includePasswordHash, setIncludePasswordHash] = useState(false);
|
||||||
const [secretsMode, setSecretsMode] = useState<SecretsMode>("exclude");
|
const [secretsMode, setSecretsMode] = useState<SecretsMode>("exclude");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
const config = exportConfigs[entityType];
|
|
||||||
const isSingleItem = !!(name || id);
|
|
||||||
const isFullExport = entityType === "full";
|
|
||||||
// TODO: Volumes will have encrypted secrets (e.g., SMB/NFS credentials) in a future PR
|
|
||||||
const hasSecrets = entityType !== "backup-schedules" && entityType !== "volumes";
|
|
||||||
const entityLabel = isSingleItem ? config.label : config.labelPlural;
|
|
||||||
const filename = config.getFilename(id, name);
|
|
||||||
|
|
||||||
const handleExportSuccess = (data: unknown) => {
|
const handleExportSuccess = (data: unknown) => {
|
||||||
downloadAsJson(data, filename);
|
downloadAsJson(data, filename);
|
||||||
toast.success(`${entityLabel} exported successfully`);
|
toast.success("Configuration exported successfully");
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
setPassword("");
|
setPassword("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExportError = (error: unknown) => {
|
const handleExportError = (error: unknown) => {
|
||||||
const message = error && typeof error === "object" && "error" in error
|
const message =
|
||||||
? (error as { error: string }).error
|
error && typeof error === "object" && "error" in error
|
||||||
: "Unknown error";
|
? (error as { error: string }).error
|
||||||
|
: "Unknown error";
|
||||||
toast.error("Export failed", {
|
toast.error("Export failed", {
|
||||||
description: message,
|
description: message,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const fullExport = useMutation({
|
const exportMutation = useMutation({
|
||||||
...exportFullConfigMutation(),
|
...exportFullConfigMutation(),
|
||||||
onSuccess: handleExportSuccess,
|
onSuccess: handleExportSuccess,
|
||||||
onError: handleExportError,
|
onError: handleExportError,
|
||||||
});
|
});
|
||||||
|
|
||||||
const volumesExport = useMutation({
|
|
||||||
...exportVolumesMutation(),
|
|
||||||
onSuccess: handleExportSuccess,
|
|
||||||
onError: handleExportError,
|
|
||||||
});
|
|
||||||
|
|
||||||
const repositoriesExport = useMutation({
|
|
||||||
...exportRepositoriesMutation(),
|
|
||||||
onSuccess: handleExportSuccess,
|
|
||||||
onError: handleExportError,
|
|
||||||
});
|
|
||||||
|
|
||||||
const notificationsExport = useMutation({
|
|
||||||
...exportNotificationDestinationsMutation(),
|
|
||||||
onSuccess: handleExportSuccess,
|
|
||||||
onError: handleExportError,
|
|
||||||
});
|
|
||||||
|
|
||||||
const backupSchedulesExport = useMutation({
|
|
||||||
...exportBackupSchedulesMutation(),
|
|
||||||
onSuccess: handleExportSuccess,
|
|
||||||
onError: handleExportError,
|
|
||||||
});
|
|
||||||
|
|
||||||
const getMutation = () => {
|
|
||||||
switch (entityType) {
|
|
||||||
case "full":
|
|
||||||
return fullExport;
|
|
||||||
case "volumes":
|
|
||||||
return volumesExport;
|
|
||||||
case "repositories":
|
|
||||||
return repositoriesExport;
|
|
||||||
case "notification-destinations":
|
|
||||||
return notificationsExport;
|
|
||||||
case "backup-schedules":
|
|
||||||
return backupSchedulesExport;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportMutation = getMutation();
|
|
||||||
|
|
||||||
const handleExport = (e: React.FormEvent) => {
|
const handleExport = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!password) {
|
if (!password) {
|
||||||
|
|
@ -211,60 +109,15 @@ export function ExportDialog({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseBody = {
|
exportMutation.mutate({
|
||||||
password,
|
body: {
|
||||||
includeIds,
|
password,
|
||||||
includeTimestamps,
|
includeMetadata,
|
||||||
includeRuntimeState,
|
secretsMode,
|
||||||
secretsMode: hasSecrets ? secretsMode : undefined,
|
includeRecoveryKey,
|
||||||
};
|
includePasswordHash,
|
||||||
|
},
|
||||||
switch (entityType) {
|
});
|
||||||
case "full":
|
|
||||||
fullExport.mutate({
|
|
||||||
body: {
|
|
||||||
...baseBody,
|
|
||||||
includeRecoveryKey,
|
|
||||||
includePasswordHash,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "volumes":
|
|
||||||
volumesExport.mutate({
|
|
||||||
body: {
|
|
||||||
...baseBody,
|
|
||||||
id: id as number | string | undefined,
|
|
||||||
name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "repositories":
|
|
||||||
repositoriesExport.mutate({
|
|
||||||
body: {
|
|
||||||
...baseBody,
|
|
||||||
id: id as number | string | undefined,
|
|
||||||
name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "notification-destinations":
|
|
||||||
notificationsExport.mutate({
|
|
||||||
body: {
|
|
||||||
...baseBody,
|
|
||||||
id: id as number | string | undefined,
|
|
||||||
name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "backup-schedules":
|
|
||||||
backupSchedulesExport.mutate({
|
|
||||||
body: {
|
|
||||||
...baseBody,
|
|
||||||
id: typeof id === "number" ? id : undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDialogChange = (isOpen: boolean) => {
|
const handleDialogChange = (isOpen: boolean) => {
|
||||||
|
|
@ -282,13 +135,13 @@ export function ExportDialog({
|
||||||
>
|
>
|
||||||
<Download className="h-8 w-8 text-muted-foreground" />
|
<Download className="h-8 w-8 text-muted-foreground" />
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
{triggerLabel ?? `Export ${isSingleItem ? "config" : "configs"}`}
|
{triggerLabel ?? "Export Config"}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<Button variant={variant} size={size}>
|
<Button variant={variant} size={size}>
|
||||||
{showIcon && <Download className="h-4 w-4 mr-2" />}
|
{showIcon && <Download className="h-4 w-4 mr-2" />}
|
||||||
{triggerLabel ?? `Export ${isSingleItem ? "config" : "configs"}`}
|
{triggerLabel ?? "Export Config"}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -298,118 +151,83 @@ export function ExportDialog({
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<form onSubmit={handleExport}>
|
<form onSubmit={handleExport}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Export {entityLabel}</DialogTitle>
|
<DialogTitle>Export Full Configuration</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{isFullExport
|
Export the complete Zerobyte configuration.
|
||||||
? "Export the complete Zerobyte configuration including all volumes, repositories, backup schedules, and notifications."
|
|
||||||
: isSingleItem
|
|
||||||
? `Export the configuration for this ${config.label.toLowerCase()}.`
|
|
||||||
: `Export all ${config.labelPlural.toLowerCase()} configurations.`}
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="includeIds"
|
id="includeMetadata"
|
||||||
checked={includeIds}
|
checked={includeMetadata}
|
||||||
onCheckedChange={(checked) => setIncludeIds(checked === true)}
|
onCheckedChange={(checked) => setIncludeMetadata(checked === true)}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="includeIds" className="cursor-pointer">
|
<Label htmlFor="includeMetadata" className="cursor-pointer">
|
||||||
Include database IDs
|
Include metadata
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground ml-7">
|
<p className="text-xs text-muted-foreground ml-7">
|
||||||
Include internal database identifiers in the export. Useful for debugging or when IDs are needed for reference.
|
Include database IDs, timestamps, and runtime state (status, health checks, last backup info).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-2 border-t">
|
||||||
|
<Label className="cursor-pointer">Secrets handling</Label>
|
||||||
|
<Select value={secretsMode} onValueChange={(v) => setSecretsMode(v as SecretsMode)}>
|
||||||
|
<SelectTrigger className="w-40">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="exclude">Exclude</SelectItem>
|
||||||
|
<SelectItem value="encrypted">Keep encrypted</SelectItem>
|
||||||
|
<SelectItem value="cleartext">Decrypt</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{secretsMode === "exclude" &&
|
||||||
|
"Sensitive fields (passwords, API keys, webhooks) will be removed from the export."}
|
||||||
|
{secretsMode === "encrypted" &&
|
||||||
|
"Secrets will be exported in encrypted form. Requires the same recovery key to decrypt on import."}
|
||||||
|
{secretsMode === "cleartext" && (
|
||||||
|
<span className="text-yellow-600">
|
||||||
|
⚠️ Secrets will be decrypted and exported as plaintext. Keep this export secure!
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-3 pt-2 border-t">
|
||||||
|
<Checkbox
|
||||||
|
id="includeRecoveryKey"
|
||||||
|
checked={includeRecoveryKey}
|
||||||
|
onCheckedChange={(checked) => setIncludeRecoveryKey(checked === true)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="includeRecoveryKey" className="cursor-pointer">
|
||||||
|
Include recovery key
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground ml-7">
|
||||||
|
<span className="text-yellow-600 font-medium">⚠️ Security sensitive:</span> The recovery
|
||||||
|
key is the master encryption key for all repositories. Keep this export secure and never
|
||||||
|
share it.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="includeTimestamps"
|
id="includePasswordHash"
|
||||||
checked={includeTimestamps}
|
checked={includePasswordHash}
|
||||||
onCheckedChange={(checked) => setIncludeTimestamps(checked === true)}
|
onCheckedChange={(checked) => setIncludePasswordHash(checked === true)}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="includeTimestamps" className="cursor-pointer">
|
<Label htmlFor="includePasswordHash" className="cursor-pointer">
|
||||||
Include timestamps
|
Include password hash
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground ml-7">
|
<p className="text-xs text-muted-foreground ml-7">
|
||||||
Include createdAt and updatedAt timestamps. Disable for cleaner exports when timestamps aren't needed.
|
Include the hashed admin password for seamless migration. The password is already securely
|
||||||
|
hashed (argon2).
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<Checkbox
|
|
||||||
id="includeRuntimeState"
|
|
||||||
checked={includeRuntimeState}
|
|
||||||
onCheckedChange={(checked) => setIncludeRuntimeState(checked === true)}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="includeRuntimeState" className="cursor-pointer">
|
|
||||||
Include runtime state
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground ml-7">
|
|
||||||
Include current status, health checks, and last backup information. Usually not needed for migration.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{hasSecrets && (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center justify-between pt-2 border-t">
|
|
||||||
<Label className="cursor-pointer">Secrets handling</Label>
|
|
||||||
<Select value={secretsMode} onValueChange={(v) => setSecretsMode(v as SecretsMode)}>
|
|
||||||
<SelectTrigger className="w-40">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="exclude">Exclude</SelectItem>
|
|
||||||
<SelectItem value="encrypted">Keep encrypted</SelectItem>
|
|
||||||
<SelectItem value="cleartext">Decrypt</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{secretsMode === "exclude" && "Sensitive fields (passwords, API keys, webhooks) will be removed from the export."}
|
|
||||||
{secretsMode === "encrypted" && "Secrets will be exported in encrypted form. Requires the same recovery key to decrypt on import."}
|
|
||||||
{secretsMode === "cleartext" && (
|
|
||||||
<span className="text-yellow-600">
|
|
||||||
⚠️ Secrets will be decrypted and exported as plaintext. Keep this export secure!
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isFullExport && (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center space-x-3 pt-2 border-t">
|
|
||||||
<Checkbox
|
|
||||||
id="includeRecoveryKey"
|
|
||||||
checked={includeRecoveryKey}
|
|
||||||
onCheckedChange={(checked) => setIncludeRecoveryKey(checked === true)}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="includeRecoveryKey" className="cursor-pointer">
|
|
||||||
Include recovery key
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground ml-7">
|
|
||||||
<span className="text-yellow-600 font-medium">⚠️ Security sensitive:</span> The recovery key is the master encryption key for all repositories. Keep this export secure and never share it.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<Checkbox
|
|
||||||
id="includePasswordHash"
|
|
||||||
checked={includePasswordHash}
|
|
||||||
onCheckedChange={(checked) => setIncludePasswordHash(checked === true)}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="includePasswordHash" className="cursor-pointer">
|
|
||||||
Include password hash
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground ml-7">
|
|
||||||
Include the hashed admin password for seamless migration. The password is already securely hashed (argon2).
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-2 pt-2 border-t">
|
<div className="space-y-2 pt-2 border-t">
|
||||||
<Label htmlFor="export-password">Your Password</Label>
|
<Label htmlFor="export-password">Your Password</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { Eraser, Pencil, Play, Square, Trash2 } from "lucide-react";
|
import { Eraser, Pencil, Play, Square, Trash2 } from "lucide-react";
|
||||||
import { ExportDialog } from "~/client/components/export-dialog";
|
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { OnOff } from "~/client/components/onoff";
|
import { OnOff } from "~/client/components/onoff";
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
|
|
@ -126,7 +125,6 @@ export const ScheduleSummary = (props: Props) => {
|
||||||
<Pencil className="h-4 w-4 mr-2" />
|
<Pencil className="h-4 w-4 mr-2" />
|
||||||
<span className="sm:inline">Edit schedule</span>
|
<span className="sm:inline">Edit schedule</span>
|
||||||
</Button>
|
</Button>
|
||||||
<ExportDialog entityType="backup-schedules" id={schedule.id} size="sm" triggerLabel="Export config" />
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { CalendarClock, Database, HardDrive, Plus } from "lucide-react";
|
import { CalendarClock, Database, HardDrive, Plus } from "lucide-react";
|
||||||
import { ExportDialog } from "~/client/components/export-dialog";
|
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { BackupStatusDot } from "../components/backup-status-dot";
|
import { BackupStatusDot } from "../components/backup-status-dot";
|
||||||
import { EmptyState } from "~/client/components/empty-state";
|
import { EmptyState } from "~/client/components/empty-state";
|
||||||
|
|
@ -120,11 +119,6 @@ export default function Backups({ loaderData }: Route.ComponentProps) {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
<Card className="flex flex-col items-center justify-center h-full hover:bg-muted/50 transition-colors cursor-pointer">
|
|
||||||
<CardContent className="flex flex-col items-center justify-center gap-2 h-full w-full">
|
|
||||||
<ExportDialog entityType="backup-schedules" variant="card" />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,7 @@ import { getNotificationDestination } from "~/client/api-client/sdk.gen";
|
||||||
import type { Route } from "./+types/notification-details";
|
import type { Route } from "./+types/notification-details";
|
||||||
import { cn } from "~/client/lib/utils";
|
import { cn } from "~/client/lib/utils";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||||
import { Bell, TestTube2, Trash2 } from "lucide-react";
|
import { Bell, TestTube2 } from "lucide-react";
|
||||||
import { ExportDialog } from "~/client/components/export-dialog";
|
|
||||||
import { Alert, AlertDescription } from "~/client/components/ui/alert";
|
import { Alert, AlertDescription } from "~/client/components/ui/alert";
|
||||||
import { CreateNotificationForm, type NotificationFormValues } from "../components/create-notification-form";
|
import { CreateNotificationForm, type NotificationFormValues } from "../components/create-notification-form";
|
||||||
|
|
||||||
|
|
@ -143,7 +142,6 @@ export default function NotificationDetailsPage({ loaderData }: Route.ComponentP
|
||||||
<TestTube2 className="h-4 w-4 mr-2" />
|
<TestTube2 className="h-4 w-4 mr-2" />
|
||||||
Test
|
Test
|
||||||
</Button>
|
</Button>
|
||||||
<ExportDialog entityType="notification-destinations" id={data.id} name={data.name} triggerLabel="Export config" />
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowDeleteConfirm(true)}
|
onClick={() => setShowDeleteConfirm(true)}
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Bell, Plus, RotateCcw } from "lucide-react";
|
import { Bell, Plus, RotateCcw } 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 { EmptyState } from "~/client/components/empty-state";
|
import { EmptyState } from "~/client/components/empty-state";
|
||||||
|
|
@ -123,13 +122,10 @@ export default function Notifications({ loaderData }: Route.ComponentProps) {
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-2">
|
<Button onClick={() => navigate("/notifications/create")}>
|
||||||
<ExportDialog entityType="notification-destinations" />
|
<Plus size={16} className="mr-2" />
|
||||||
<Button onClick={() => navigate("/notifications/create")}>
|
Create Destination
|
||||||
<Plus size={16} className="mr-2" />
|
</Button>
|
||||||
Create Destination
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<Table className="border-t">
|
<Table className="border-t">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Database, Plus, RotateCcw } from "lucide-react";
|
import { Database, Plus, RotateCcw } 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 { listRepositories } from "~/client/api-client/sdk.gen";
|
import { listRepositories } from "~/client/api-client/sdk.gen";
|
||||||
|
|
@ -120,13 +119,10 @@ export default function Repositories({ loaderData }: Route.ComponentProps) {
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-2">
|
<Button onClick={() => navigate("/repositories/create")}>
|
||||||
<ExportDialog entityType="repositories" />
|
<Plus size={16} className="mr-2" />
|
||||||
<Button onClick={() => navigate("/repositories/create")}>
|
Create Repository
|
||||||
<Plus size={16} className="mr-2" />
|
</Button>
|
||||||
Create Repository
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<Table className="border-t">
|
<Table className="border-t">
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,7 @@ import { cn } from "~/client/lib/utils";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
|
||||||
import { RepositoryInfoTabContent } from "../tabs/info";
|
import { RepositoryInfoTabContent } from "../tabs/info";
|
||||||
import { RepositorySnapshotsTabContent } from "../tabs/snapshots";
|
import { RepositorySnapshotsTabContent } from "../tabs/snapshots";
|
||||||
import { Loader2, Trash2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { ExportDialog } from "~/client/components/export-dialog";
|
|
||||||
|
|
||||||
export const handle = {
|
export const handle = {
|
||||||
breadcrumb: (match: Route.MetaArgs) => [
|
breadcrumb: (match: Route.MetaArgs) => [
|
||||||
|
|
@ -158,7 +157,6 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
|
||||||
"Run Doctor"
|
"Run Doctor"
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<ExportDialog entityType="repositories" id={data.id} name={data.name} triggerLabel="Export config" />
|
|
||||||
<Button variant="destructive" onClick={() => setShowDeleteConfirm(true)} disabled={deleteRepo.isPending}>
|
<Button variant="destructive" onClick={() => setShowDeleteConfirm(true)} disabled={deleteRepo.isPending}>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -144,9 +144,6 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
|
||||||
<CardDescription className="mt-1.5">Your account details</CardDescription>
|
<CardDescription className="mt-1.5">Your account details</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<CardContent className="p-6 space-y-4">
|
<CardContent className="p-6 space-y-4">
|
||||||
<div className="flex justify-end">
|
|
||||||
<ExportDialog entityType="full" triggerLabel="Export All Config (JSON)" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Username</Label>
|
<Label>Username</Label>
|
||||||
<Input value={loaderData.user?.username || ""} disabled className="max-w-md" />
|
<Input value={loaderData.user?.username || ""} disabled className="max-w-md" />
|
||||||
|
|
@ -266,6 +263,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 triggerLabel="Export Configuration" />
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,6 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/
|
||||||
import { useSystemInfo } from "~/client/hooks/use-system-info";
|
import { useSystemInfo } from "~/client/hooks/use-system-info";
|
||||||
import { getVolume } from "~/client/api-client";
|
import { getVolume } from "~/client/api-client";
|
||||||
import type { VolumeStatus } from "~/client/lib/types";
|
import type { VolumeStatus } from "~/client/lib/types";
|
||||||
import { Trash2 } from "lucide-react";
|
|
||||||
import { ExportDialog } from "~/client/components/export-dialog";
|
|
||||||
import {
|
import {
|
||||||
deleteVolumeMutation,
|
deleteVolumeMutation,
|
||||||
getVolumeOptions,
|
getVolumeOptions,
|
||||||
|
|
@ -162,7 +160,6 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
|
||||||
>
|
>
|
||||||
Unmount
|
Unmount
|
||||||
</Button>
|
</Button>
|
||||||
<ExportDialog entityType="volumes" id={volume.id} name={volume.name} triggerLabel="Export config" />
|
|
||||||
<Button variant="destructive" onClick={() => setShowDeleteConfirm(true)} disabled={deleteVol.isPending}>
|
<Button variant="destructive" onClick={() => setShowDeleteConfirm(true)} disabled={deleteVol.isPending}>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { HardDrive, Plus, RotateCcw } from "lucide-react";
|
import { HardDrive, Plus, RotateCcw } 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 { EmptyState } from "~/client/components/empty-state";
|
import { EmptyState } from "~/client/components/empty-state";
|
||||||
|
|
@ -130,13 +129,10 @@ export default function Volumes({ loaderData }: Route.ComponentProps) {
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-2">
|
<Button onClick={() => navigate("/volumes/create")}>
|
||||||
<ExportDialog entityType="volumes" />
|
<Plus size={16} className="mr-2" />
|
||||||
<Button onClick={() => navigate("/volumes/create")}>
|
Create Volume
|
||||||
<Plus size={16} className="mr-2" />
|
</Button>
|
||||||
Create Volume
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<Table className="border-t">
|
<Table className="border-t">
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
import { validator } from "hono-openapi";
|
import { validator } from "hono-openapi";
|
||||||
|
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import type { Context } from "hono";
|
import type { Context } from "hono";
|
||||||
import { deleteCookie, getCookie } from "hono/cookie";
|
import { deleteCookie, getCookie } from "hono/cookie";
|
||||||
import {
|
import { backupSchedulesTable, backupScheduleNotificationsTable, usersTable } from "../../db/schema";
|
||||||
backupSchedulesTable,
|
|
||||||
backupScheduleNotificationsTable,
|
|
||||||
usersTable,
|
|
||||||
} from "../../db/schema";
|
|
||||||
import { db } from "../../db/db";
|
import { db } from "../../db/db";
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from "../../utils/logger";
|
||||||
import { RESTIC_PASS_FILE } from "../../core/constants";
|
import { RESTIC_PASS_FILE } from "../../core/constants";
|
||||||
|
|
@ -19,17 +14,9 @@ import { notificationsService } from "../notifications/notifications.service";
|
||||||
import { backupsService } from "../backups/backups.service";
|
import { backupsService } from "../backups/backups.service";
|
||||||
import {
|
import {
|
||||||
fullExportBodySchema,
|
fullExportBodySchema,
|
||||||
entityExportBodySchema,
|
|
||||||
backupScheduleExportBodySchema,
|
|
||||||
fullExportDto,
|
fullExportDto,
|
||||||
volumesExportDto,
|
|
||||||
repositoriesExportDto,
|
|
||||||
notificationsExportDto,
|
|
||||||
backupSchedulesExportDto,
|
|
||||||
type SecretsMode,
|
type SecretsMode,
|
||||||
type FullExportBody,
|
type FullExportBody,
|
||||||
type EntityExportBody,
|
|
||||||
type BackupScheduleExportBody,
|
|
||||||
} from "./config-export.dto";
|
} from "./config-export.dto";
|
||||||
|
|
||||||
const COOKIE_NAME = "session_id";
|
const COOKIE_NAME = "session_id";
|
||||||
|
|
@ -41,56 +28,39 @@ const COOKIE_OPTIONS = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type ExportParams = {
|
type ExportParams = {
|
||||||
includeIds: boolean;
|
includeMetadata: boolean;
|
||||||
includeTimestamps: boolean;
|
|
||||||
secretsMode: SecretsMode;
|
secretsMode: SecretsMode;
|
||||||
excludeKeys: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function omitKeys<T extends Record<string, unknown>>(obj: T, keys: string[]): Partial<T> {
|
// Keys to exclude when metadata is not included
|
||||||
|
const METADATA_KEYS = {
|
||||||
|
ids: ["id", "volumeId", "repositoryId", "scheduleId", "destinationId"],
|
||||||
|
timestamps: ["createdAt", "updatedAt", "lastBackupAt", "nextBackupAt", "lastHealthCheck", "lastChecked"],
|
||||||
|
runtimeState: ["status", "lastError", "lastBackupStatus", "lastBackupError", "hasDownloadedResticPassword"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const ALL_METADATA_KEYS = [...METADATA_KEYS.ids, ...METADATA_KEYS.timestamps, ...METADATA_KEYS.runtimeState];
|
||||||
|
|
||||||
|
/** Filter out metadata keys from an object when includeMetadata is false */
|
||||||
|
function filterMetadataOut<T extends Record<string, unknown>>(obj: T, includeMetadata: boolean): Partial<T> {
|
||||||
|
if (includeMetadata) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
const result = { ...obj };
|
const result = { ...obj };
|
||||||
for (const key of keys) {
|
for (const key of ALL_METADATA_KEYS) {
|
||||||
delete result[key as keyof T];
|
delete result[key as keyof T];
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExcludeKeys(includeIds: boolean, includeTimestamps: boolean, includeRuntimeState: boolean): string[] {
|
|
||||||
const idKeys = ["id", "volumeId", "repositoryId", "scheduleId", "destinationId"];
|
|
||||||
const timestampKeys = ["createdAt", "updatedAt"];
|
|
||||||
// Runtime state fields (status, health checks, last backup info, etc.)
|
|
||||||
const runtimeStateKeys = [
|
|
||||||
// Volume state
|
|
||||||
"status", "lastError", "lastHealthCheck",
|
|
||||||
// Repository state
|
|
||||||
"lastChecked",
|
|
||||||
// Backup schedule state
|
|
||||||
"lastBackupAt", "lastBackupStatus", "lastBackupError", "nextBackupAt",
|
|
||||||
];
|
|
||||||
// Redundant fields that are already present inside the config object
|
|
||||||
// (e.g., type is duplicated as config.backend or config.type)
|
|
||||||
const redundantKeys = ["type"];
|
|
||||||
return [
|
|
||||||
...redundantKeys,
|
|
||||||
...(includeRuntimeState ? [] : runtimeStateKeys),
|
|
||||||
...(includeIds ? [] : idKeys),
|
|
||||||
...(includeTimestamps ? [] : timestampKeys),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Parse export params from request body */
|
/** Parse export params from request body */
|
||||||
function parseExportParamsFromBody(body: {
|
function parseExportParamsFromBody(body: {
|
||||||
includeIds?: boolean;
|
includeMetadata?: boolean;
|
||||||
includeTimestamps?: boolean;
|
|
||||||
includeRuntimeState?: boolean;
|
|
||||||
secretsMode?: SecretsMode;
|
secretsMode?: SecretsMode;
|
||||||
}): ExportParams {
|
}): ExportParams {
|
||||||
const includeIds = body.includeIds !== false;
|
const includeMetadata = body.includeMetadata === true;
|
||||||
const includeTimestamps = body.includeTimestamps !== false;
|
|
||||||
const includeRuntimeState = body.includeRuntimeState === true;
|
|
||||||
const secretsMode: SecretsMode = body.secretsMode ?? "exclude";
|
const secretsMode: SecretsMode = body.secretsMode ?? "exclude";
|
||||||
const excludeKeys = getExcludeKeys(includeIds, includeTimestamps, includeRuntimeState);
|
return { includeMetadata, secretsMode };
|
||||||
return { includeIds, includeTimestamps, secretsMode, excludeKeys };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -167,7 +137,7 @@ async function exportEntity(
|
||||||
entity: Record<string, unknown>,
|
entity: Record<string, unknown>,
|
||||||
params: ExportParams
|
params: ExportParams
|
||||||
): Promise<Record<string, unknown>> {
|
): Promise<Record<string, unknown>> {
|
||||||
const cleaned = omitKeys(entity, params.excludeKeys);
|
const cleaned = filterMetadataOut(entity, params.includeMetadata);
|
||||||
return processSecrets(cleaned, params.secretsMode);
|
return processSecrets(cleaned, params.secretsMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,8 +151,8 @@ async function exportEntities<T extends Record<string, unknown>>(
|
||||||
|
|
||||||
/** Transform backup schedules with resolved names and notifications */
|
/** Transform backup schedules with resolved names and notifications */
|
||||||
function transformBackupSchedules(
|
function transformBackupSchedules(
|
||||||
schedules: typeof backupSchedulesTable.$inferSelect[],
|
schedules: (typeof backupSchedulesTable.$inferSelect)[],
|
||||||
scheduleNotifications: typeof backupScheduleNotificationsTable.$inferSelect[],
|
scheduleNotifications: (typeof backupScheduleNotificationsTable.$inferSelect)[],
|
||||||
volumeMap: Map<number, string>,
|
volumeMap: Map<number, string>,
|
||||||
repoMap: Map<string, string>,
|
repoMap: Map<string, string>,
|
||||||
notificationMap: Map<number, string>,
|
notificationMap: Map<number, string>,
|
||||||
|
|
@ -192,15 +162,12 @@ function transformBackupSchedules(
|
||||||
const assignments = scheduleNotifications
|
const assignments = scheduleNotifications
|
||||||
.filter((sn) => sn.scheduleId === schedule.id)
|
.filter((sn) => sn.scheduleId === schedule.id)
|
||||||
.map((sn) => ({
|
.map((sn) => ({
|
||||||
...(params.includeIds ? { destinationId: sn.destinationId } : {}),
|
...filterMetadataOut(sn as unknown as Record<string, unknown>, params.includeMetadata),
|
||||||
name: notificationMap.get(sn.destinationId) ?? null,
|
name: notificationMap.get(sn.destinationId) ?? null,
|
||||||
notifyOnStart: sn.notifyOnStart,
|
|
||||||
notifyOnSuccess: sn.notifyOnSuccess,
|
|
||||||
notifyOnFailure: sn.notifyOnFailure,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...omitKeys(schedule as Record<string, unknown>, params.excludeKeys),
|
...filterMetadataOut(schedule as Record<string, unknown>, params.includeMetadata),
|
||||||
volume: volumeMap.get(schedule.volumeId) ?? null,
|
volume: volumeMap.get(schedule.volumeId) ?? null,
|
||||||
repository: repoMap.get(schedule.repositoryId) ?? null,
|
repository: repoMap.get(schedule.repositoryId) ?? null,
|
||||||
notifications: assignments,
|
notifications: assignments,
|
||||||
|
|
@ -208,8 +175,11 @@ function transformBackupSchedules(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const configExportController = new Hono()
|
export const configExportController = new Hono().post(
|
||||||
.post("/export", fullExportDto, validator("json", fullExportBodySchema), async (c) => {
|
"/export",
|
||||||
|
fullExportDto,
|
||||||
|
validator("json", fullExportBodySchema),
|
||||||
|
async (c) => {
|
||||||
try {
|
try {
|
||||||
const body = c.req.valid("json") as FullExportBody;
|
const body = c.req.valid("json") as FullExportBody;
|
||||||
|
|
||||||
|
|
@ -224,21 +194,27 @@ export const configExportController = new Hono()
|
||||||
const includePasswordHash = body.includePasswordHash === true;
|
const includePasswordHash = body.includePasswordHash === true;
|
||||||
|
|
||||||
// Use services to fetch data
|
// Use services to fetch data
|
||||||
const [volumes, repositories, backupSchedulesRaw, notifications, scheduleNotifications, [admin]] = await Promise.all([
|
const [volumes, repositories, backupSchedulesRaw, notifications, scheduleNotifications, users] =
|
||||||
volumeService.listVolumes(),
|
await Promise.all([
|
||||||
repositoriesService.listRepositories(),
|
volumeService.listVolumes(),
|
||||||
backupsService.listSchedules(),
|
repositoriesService.listRepositories(),
|
||||||
notificationsService.listDestinations(),
|
backupsService.listSchedules(),
|
||||||
db.select().from(backupScheduleNotificationsTable),
|
notificationsService.listDestinations(),
|
||||||
db.select().from(usersTable).limit(1),
|
db.select().from(backupScheduleNotificationsTable),
|
||||||
]);
|
db.select().from(usersTable),
|
||||||
|
]);
|
||||||
|
|
||||||
const volumeMap = new Map<number, string>(volumes.map((v) => [v.id, v.name]));
|
const volumeMap = new Map<number, string>(volumes.map((v) => [v.id, v.name]));
|
||||||
const repoMap = new Map<string, string>(repositories.map((r) => [r.id, r.name]));
|
const repoMap = new Map<string, string>(repositories.map((r) => [r.id, r.name]));
|
||||||
const notificationMap = new Map<number, string>(notifications.map((n) => [n.id, n.name]));
|
const notificationMap = new Map<number, string>(notifications.map((n) => [n.id, n.name]));
|
||||||
|
|
||||||
const backupSchedules = transformBackupSchedules(
|
const backupSchedules = transformBackupSchedules(
|
||||||
backupSchedulesRaw, scheduleNotifications, volumeMap, repoMap, notificationMap, params
|
backupSchedulesRaw,
|
||||||
|
scheduleNotifications,
|
||||||
|
volumeMap,
|
||||||
|
repoMap,
|
||||||
|
notificationMap,
|
||||||
|
params
|
||||||
);
|
);
|
||||||
|
|
||||||
const [exportVolumes, exportRepositories, exportNotifications] = await Promise.all([
|
const [exportVolumes, exportRepositories, exportNotifications] = await Promise.all([
|
||||||
|
|
@ -257,194 +233,27 @@ export const configExportController = new Hono()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Users need special handling for passwordHash (controlled by separate flag)
|
||||||
|
const exportUsers = (await exportEntities(users, params)).map((user) => {
|
||||||
|
if (!includePasswordHash) {
|
||||||
|
delete user.passwordHash;
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
});
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
version: 1,
|
version: 1,
|
||||||
exportedAt: new Date().toISOString(),
|
...(params.includeMetadata ? { exportedAt: new Date().toISOString() } : {}),
|
||||||
|
...(recoveryKey ? { recoveryKey } : {}),
|
||||||
volumes: exportVolumes,
|
volumes: exportVolumes,
|
||||||
repositories: exportRepositories,
|
repositories: exportRepositories,
|
||||||
backupSchedules,
|
backupSchedules,
|
||||||
notificationDestinations: exportNotifications,
|
notificationDestinations: exportNotifications,
|
||||||
admin: admin ? {
|
users: exportUsers,
|
||||||
username: admin.username,
|
|
||||||
...(includePasswordHash ? { passwordHash: admin.passwordHash } : {}),
|
|
||||||
...(recoveryKey ? { recoveryKey } : {}),
|
|
||||||
} : null,
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Config export failed: ${err instanceof Error ? err.message : String(err)}`);
|
logger.error(`Config export failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
return c.json({ error: err instanceof Error ? err.message : "Failed to export config" }, 500);
|
return c.json({ error: err instanceof Error ? err.message : "Failed to export config" }, 500);
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.post("/export/volumes", volumesExportDto, validator("json", entityExportBodySchema), async (c) => {
|
);
|
||||||
try {
|
|
||||||
const body = c.req.valid("json") as EntityExportBody;
|
|
||||||
|
|
||||||
// Verify password - required for all exports
|
|
||||||
const verification = await verifyExportPassword(c, body.password);
|
|
||||||
if (!verification.valid) {
|
|
||||||
return c.json({ error: verification.error }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = parseExportParamsFromBody(body);
|
|
||||||
|
|
||||||
let volumes;
|
|
||||||
// Prefer name over id since volumeService.getVolume expects name (slug)
|
|
||||||
if (body.name) {
|
|
||||||
try {
|
|
||||||
const result = await volumeService.getVolume(body.name);
|
|
||||||
volumes = [result.volume];
|
|
||||||
} catch {
|
|
||||||
return c.json({ error: `Volume '${body.name}' not found` }, 404);
|
|
||||||
}
|
|
||||||
} else if (body.id !== undefined) {
|
|
||||||
// If only ID provided, find volume by numeric ID from list
|
|
||||||
const id = typeof body.id === "string" ? Number.parseInt(body.id, 10) : body.id;
|
|
||||||
if (Number.isNaN(id)) {
|
|
||||||
return c.json({ error: "Invalid volume ID" }, 400);
|
|
||||||
}
|
|
||||||
const allVolumes = await volumeService.listVolumes();
|
|
||||||
const volume = allVolumes.find((v) => v.id === id);
|
|
||||||
if (!volume) {
|
|
||||||
return c.json({ error: `Volume with ID '${body.id}' not found` }, 404);
|
|
||||||
}
|
|
||||||
volumes = [volume];
|
|
||||||
} else {
|
|
||||||
volumes = await volumeService.listVolumes();
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({ volumes: await exportEntities(volumes, params) });
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`Volumes export failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
||||||
return c.json({ error: `Failed to export volumes: ${err instanceof Error ? err.message : String(err)}` }, 500);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.post("/export/repositories", repositoriesExportDto, validator("json", entityExportBodySchema), async (c) => {
|
|
||||||
try {
|
|
||||||
const body = c.req.valid("json") as EntityExportBody;
|
|
||||||
|
|
||||||
// Verify password - required for all exports
|
|
||||||
const verification = await verifyExportPassword(c, body.password);
|
|
||||||
if (!verification.valid) {
|
|
||||||
return c.json({ error: verification.error }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = parseExportParamsFromBody(body);
|
|
||||||
|
|
||||||
let repositories;
|
|
||||||
// Prefer name over id since repositoriesService.getRepository expects name (slug)
|
|
||||||
if (body.name) {
|
|
||||||
try {
|
|
||||||
const result = await repositoriesService.getRepository(body.name);
|
|
||||||
repositories = [result.repository];
|
|
||||||
} catch {
|
|
||||||
return c.json({ error: `Repository '${body.name}' not found` }, 404);
|
|
||||||
}
|
|
||||||
} else if (body.id !== undefined) {
|
|
||||||
// If only ID provided, find repository by ID from list
|
|
||||||
// Repository IDs are strings (UUIDs), not numeric
|
|
||||||
const allRepositories = await repositoriesService.listRepositories();
|
|
||||||
const repository = allRepositories.find((r) => r.id === String(body.id));
|
|
||||||
if (!repository) {
|
|
||||||
return c.json({ error: `Repository with ID '${body.id}' not found` }, 404);
|
|
||||||
}
|
|
||||||
repositories = [repository];
|
|
||||||
} else {
|
|
||||||
repositories = await repositoriesService.listRepositories();
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({ repositories: await exportEntities(repositories, params) });
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`Repositories export failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
||||||
return c.json({ error: `Failed to export repositories: ${err instanceof Error ? err.message : String(err)}` }, 500);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.post("/export/notification-destinations", notificationsExportDto, validator("json", entityExportBodySchema), async (c) => {
|
|
||||||
try {
|
|
||||||
const body = c.req.valid("json") as EntityExportBody;
|
|
||||||
|
|
||||||
// Verify password - required for all exports
|
|
||||||
const verification = await verifyExportPassword(c, body.password);
|
|
||||||
if (!verification.valid) {
|
|
||||||
return c.json({ error: verification.error }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = parseExportParamsFromBody(body);
|
|
||||||
|
|
||||||
let notifications;
|
|
||||||
if (body.id !== undefined) {
|
|
||||||
const id = typeof body.id === "string" ? Number.parseInt(body.id, 10) : body.id;
|
|
||||||
if (Number.isNaN(id)) {
|
|
||||||
return c.json({ error: "Invalid notification destination ID" }, 400);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const destination = await notificationsService.getDestination(id);
|
|
||||||
notifications = [destination];
|
|
||||||
} catch {
|
|
||||||
return c.json({ error: `Notification destination with ID '${body.id}' not found` }, 404);
|
|
||||||
}
|
|
||||||
} else if (body.name) {
|
|
||||||
// notificationsService doesn't have getByName, so we list and filter
|
|
||||||
const allDestinations = await notificationsService.listDestinations();
|
|
||||||
const destination = allDestinations.find((d) => d.name === body.name);
|
|
||||||
if (!destination) {
|
|
||||||
return c.json({ error: `Notification destination '${body.name}' not found` }, 404);
|
|
||||||
}
|
|
||||||
notifications = [destination];
|
|
||||||
} else {
|
|
||||||
notifications = await notificationsService.listDestinations();
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({ notificationDestinations: await exportEntities(notifications, params) });
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`Notification destinations export failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
||||||
return c.json({ error: `Failed to export notification destinations: ${err instanceof Error ? err.message : String(err)}` }, 500);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.post("/export/backup-schedules", backupSchedulesExportDto, validator("json", backupScheduleExportBodySchema), async (c) => {
|
|
||||||
try {
|
|
||||||
const body = c.req.valid("json") as BackupScheduleExportBody;
|
|
||||||
|
|
||||||
// Verify password - required for all exports
|
|
||||||
const verification = await verifyExportPassword(c, body.password);
|
|
||||||
if (!verification.valid) {
|
|
||||||
return c.json({ error: verification.error }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = parseExportParamsFromBody(body);
|
|
||||||
|
|
||||||
// Get all related data for name resolution
|
|
||||||
const [volumes, repositories, notifications, scheduleNotifications] = await Promise.all([
|
|
||||||
volumeService.listVolumes(),
|
|
||||||
repositoriesService.listRepositories(),
|
|
||||||
notificationsService.listDestinations(),
|
|
||||||
db.select().from(backupScheduleNotificationsTable),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let schedules;
|
|
||||||
if (body.id !== undefined) {
|
|
||||||
try {
|
|
||||||
const schedule = await backupsService.getSchedule(body.id);
|
|
||||||
schedules = [schedule];
|
|
||||||
} catch {
|
|
||||||
return c.json({ error: `Backup schedule with ID '${body.id}' not found` }, 404);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
schedules = await backupsService.listSchedules();
|
|
||||||
}
|
|
||||||
|
|
||||||
const volumeMap = new Map<number, string>(volumes.map((v) => [v.id, v.name]));
|
|
||||||
const repoMap = new Map<string, string>(repositories.map((r) => [r.id, r.name]));
|
|
||||||
const notificationMap = new Map<number, string>(notifications.map((n) => [n.id, n.name]));
|
|
||||||
|
|
||||||
const backupSchedules = transformBackupSchedules(
|
|
||||||
schedules, scheduleNotifications, volumeMap, repoMap, notificationMap, params
|
|
||||||
);
|
|
||||||
|
|
||||||
return c.json({ backupSchedules });
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`Backup schedules export failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
||||||
return c.json({ error: `Failed to export backup schedules: ${err instanceof Error ? err.message : String(err)}` }, 500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,47 +3,20 @@ import { describeRoute, resolver } from "hono-openapi";
|
||||||
|
|
||||||
const secretsModeSchema = type("'exclude' | 'encrypted' | 'cleartext'");
|
const secretsModeSchema = type("'exclude' | 'encrypted' | 'cleartext'");
|
||||||
|
|
||||||
const baseExportBodySchema = type({
|
export const fullExportBodySchema = type({
|
||||||
/** Include database IDs in export (default: true) */
|
/** Include metadata (IDs, timestamps, runtime state) in export (default: false) */
|
||||||
"includeIds?": "boolean",
|
"includeMetadata?": "boolean",
|
||||||
/** Include createdAt/updatedAt timestamps (default: true) */
|
|
||||||
"includeTimestamps?": "boolean",
|
|
||||||
/** Include runtime state like status, health checks (default: false) */
|
|
||||||
"includeRuntimeState?": "boolean",
|
|
||||||
/** How to handle secrets: exclude, encrypted, or cleartext (default: exclude) */
|
/** How to handle secrets: exclude, encrypted, or cleartext (default: exclude) */
|
||||||
"secretsMode?": secretsModeSchema,
|
"secretsMode?": secretsModeSchema,
|
||||||
/** Password required for authentication */
|
/** Password required for authentication */
|
||||||
password: "string",
|
password: "string",
|
||||||
|
/** Include the recovery key (requires password) */
|
||||||
|
"includeRecoveryKey?": "boolean",
|
||||||
|
/** Include the admin password hash */
|
||||||
|
"includePasswordHash?": "boolean",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fullExportBodySchema = baseExportBodySchema.and(
|
|
||||||
type({
|
|
||||||
/** Include the recovery key (requires password) */
|
|
||||||
"includeRecoveryKey?": "boolean",
|
|
||||||
/** Include the admin password hash */
|
|
||||||
"includePasswordHash?": "boolean",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const entityExportBodySchema = baseExportBodySchema.and(
|
|
||||||
type({
|
|
||||||
/** Filter by ID */
|
|
||||||
"id?": "string | number",
|
|
||||||
/** Filter by name */
|
|
||||||
"name?": "string",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const backupScheduleExportBodySchema = baseExportBodySchema.and(
|
|
||||||
type({
|
|
||||||
/** Filter by ID */
|
|
||||||
"id?": "number",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export type FullExportBody = typeof fullExportBodySchema.infer;
|
export type FullExportBody = typeof fullExportBodySchema.infer;
|
||||||
export type EntityExportBody = typeof entityExportBodySchema.infer;
|
|
||||||
export type BackupScheduleExportBody = typeof backupScheduleExportBodySchema.infer;
|
|
||||||
export type SecretsMode = typeof secretsModeSchema.infer;
|
export type SecretsMode = typeof secretsModeSchema.infer;
|
||||||
|
|
||||||
const exportResponseSchema = type({
|
const exportResponseSchema = type({
|
||||||
|
|
@ -60,22 +33,6 @@ const exportResponseSchema = type({
|
||||||
}).or("null"),
|
}).or("null"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const volumesExportResponseSchema = type({
|
|
||||||
volumes: "unknown[]",
|
|
||||||
});
|
|
||||||
|
|
||||||
const repositoriesExportResponseSchema = type({
|
|
||||||
repositories: "unknown[]",
|
|
||||||
});
|
|
||||||
|
|
||||||
const notificationsExportResponseSchema = type({
|
|
||||||
notificationDestinations: "unknown[]",
|
|
||||||
});
|
|
||||||
|
|
||||||
const backupSchedulesExportResponseSchema = type({
|
|
||||||
backupSchedules: "unknown[]",
|
|
||||||
});
|
|
||||||
|
|
||||||
const errorResponseSchema = type({
|
const errorResponseSchema = type({
|
||||||
error: "string",
|
error: "string",
|
||||||
});
|
});
|
||||||
|
|
@ -111,195 +68,3 @@ export const fullExportDto = describeRoute({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const volumesExportDto = describeRoute({
|
|
||||||
description: "Export volumes configuration",
|
|
||||||
operationId: "exportVolumes",
|
|
||||||
tags: ["Config Export"],
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
description: "Volumes configuration export",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(volumesExportResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
400: {
|
|
||||||
description: "Invalid request",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(errorResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
401: {
|
|
||||||
description: "Password required for export",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(errorResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
404: {
|
|
||||||
description: "Volume not found",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(errorResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
500: {
|
|
||||||
description: "Export failed",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(errorResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const repositoriesExportDto = describeRoute({
|
|
||||||
description: "Export repositories configuration",
|
|
||||||
operationId: "exportRepositories",
|
|
||||||
tags: ["Config Export"],
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
description: "Repositories configuration export",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(repositoriesExportResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
400: {
|
|
||||||
description: "Invalid request",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(errorResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
401: {
|
|
||||||
description: "Password required for sensitive export options",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(errorResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
404: {
|
|
||||||
description: "Repository not found",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(errorResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
500: {
|
|
||||||
description: "Export failed",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(errorResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const notificationsExportDto = describeRoute({
|
|
||||||
description: "Export notification destinations configuration",
|
|
||||||
operationId: "exportNotificationDestinations",
|
|
||||||
tags: ["Config Export"],
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
description: "Notification destinations configuration export",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(notificationsExportResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
400: {
|
|
||||||
description: "Invalid request",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(errorResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
401: {
|
|
||||||
description: "Password required for sensitive export options",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(errorResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
404: {
|
|
||||||
description: "Notification destination not found",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(errorResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
500: {
|
|
||||||
description: "Export failed",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(errorResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const backupSchedulesExportDto = describeRoute({
|
|
||||||
description: "Export backup schedules configuration",
|
|
||||||
operationId: "exportBackupSchedules",
|
|
||||||
tags: ["Config Export"],
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
description: "Backup schedules configuration export",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(backupSchedulesExportResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
400: {
|
|
||||||
description: "Invalid request",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(errorResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
401: {
|
|
||||||
description: "Password required for export",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(errorResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
404: {
|
|
||||||
description: "Backup schedule not found",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(errorResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
500: {
|
|
||||||
description: "Export failed",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(errorResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue