mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat: remove individual snapshot (#26)
This commit is contained in:
@@ -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, createRepository, createVolume, deleteBackupSchedule, deleteRepository, deleteVolume, doctorRepository, downloadResticPassword, getBackupSchedule, getBackupScheduleForVolume, getContainersUsingVolume, getMe, getRepository, getSnapshotDetails, getStatus, getSystemInfo, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, login, logout, mountVolume, type Options, register, restoreSnapshot, runBackupNow, runForget, stopBackup, testConnection, unmountVolume, updateBackupSchedule, updateVolume } from '../sdk.gen';
|
import { browseFilesystem, changePassword, createBackupSchedule, createRepository, createVolume, deleteBackupSchedule, deleteRepository, deleteSnapshot, deleteVolume, doctorRepository, downloadResticPassword, getBackupSchedule, getBackupScheduleForVolume, getContainersUsingVolume, getMe, getRepository, getSnapshotDetails, getStatus, getSystemInfo, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, login, logout, mountVolume, type Options, register, restoreSnapshot, runBackupNow, runForget, stopBackup, testConnection, unmountVolume, updateBackupSchedule, updateVolume } from '../sdk.gen';
|
||||||
import type { BrowseFilesystemData, BrowseFilesystemResponse, ChangePasswordData, ChangePasswordResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteVolumeData, DeleteVolumeResponse, DoctorRepositoryData, DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetContainersUsingVolumeData, GetContainersUsingVolumeResponse, GetMeData, GetMeResponse, GetRepositoryData, GetRepositoryResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, 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, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
|
import type { BrowseFilesystemData, BrowseFilesystemResponse, ChangePasswordData, ChangePasswordResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteVolumeData, DeleteVolumeResponse, DoctorRepositoryData, DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetContainersUsingVolumeData, GetContainersUsingVolumeResponse, GetMeData, GetMeResponse, GetRepositoryData, GetRepositoryResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, 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, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new user
|
* Register a new user
|
||||||
@@ -460,6 +460,23 @@ export const listSnapshotsOptions = (options: Options<ListSnapshotsData>) => que
|
|||||||
queryKey: listSnapshotsQueryKey(options)
|
queryKey: listSnapshotsQueryKey(options)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a specific snapshot from a repository
|
||||||
|
*/
|
||||||
|
export const deleteSnapshotMutation = (options?: Partial<Options<DeleteSnapshotData>>): UseMutationOptions<DeleteSnapshotResponse, DefaultError, Options<DeleteSnapshotData>> => {
|
||||||
|
const mutationOptions: UseMutationOptions<DeleteSnapshotResponse, DefaultError, Options<DeleteSnapshotData>> = {
|
||||||
|
mutationFn: async (fnOptions) => {
|
||||||
|
const { data } = await deleteSnapshot({
|
||||||
|
...options,
|
||||||
|
...fnOptions,
|
||||||
|
throwOnError: true
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return mutationOptions;
|
||||||
|
};
|
||||||
|
|
||||||
export const getSnapshotDetailsQueryKey = (options: Options<GetSnapshotDetailsData>) => createQueryKey("getSnapshotDetails", options);
|
export const getSnapshotDetailsQueryKey = (options: Options<GetSnapshotDetailsData>) => createQueryKey("getSnapshotDetails", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetContainersUsingVolumeData, GetContainersUsingVolumeErrors, GetContainersUsingVolumeResponses, GetMeData, GetMeResponses, GetRepositoryData, GetRepositoryResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, LoginData, LoginResponses, LogoutData, LogoutResponses, MountVolumeData, MountVolumeResponses, RegisterData, RegisterResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetErrors, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TestConnectionData, TestConnectionResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
|
import type { BrowseFilesystemData, BrowseFilesystemResponses, ChangePasswordData, ChangePasswordResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetContainersUsingVolumeData, GetContainersUsingVolumeErrors, GetContainersUsingVolumeResponses, GetMeData, GetMeResponses, GetRepositoryData, GetRepositoryResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, 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, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, 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> & {
|
||||||
/**
|
/**
|
||||||
@@ -286,6 +286,16 @@ export const listSnapshots = <ThrowOnError extends boolean = false>(options: Opt
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a specific snapshot from a repository
|
||||||
|
*/
|
||||||
|
export const deleteSnapshot = <ThrowOnError extends boolean = false>(options: Options<DeleteSnapshotData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).delete<DeleteSnapshotResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/repositories/{name}/snapshots/{snapshotId}',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get details of a specific snapshot
|
* Get details of a specific snapshot
|
||||||
*/
|
*/
|
||||||
@@ -422,7 +432,7 @@ export const stopBackup = <ThrowOnError extends boolean = false>(options: Option
|
|||||||
* Manually apply retention policy to clean up old snapshots
|
* Manually apply retention policy to clean up old snapshots
|
||||||
*/
|
*/
|
||||||
export const runForget = <ThrowOnError extends boolean = false>(options: Options<RunForgetData, ThrowOnError>) => {
|
export const runForget = <ThrowOnError extends boolean = false>(options: Options<RunForgetData, ThrowOnError>) => {
|
||||||
return (options.client ?? client).post<RunForgetResponses, RunForgetErrors, ThrowOnError>({
|
return (options.client ?? client).post<RunForgetResponses, unknown, ThrowOnError>({
|
||||||
url: '/api/v1/backups/{scheduleId}/forget',
|
url: '/api/v1/backups/{scheduleId}/forget',
|
||||||
...options
|
...options
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -711,30 +711,42 @@ export type ListRepositoriesResponses = {
|
|||||||
bucket: string;
|
bucket: string;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
secretAccessKey: string;
|
secretAccessKey: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
accessKeyId: string;
|
accessKeyId: string;
|
||||||
backend: 's3';
|
backend: 's3';
|
||||||
bucket: string;
|
bucket: string;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
secretAccessKey: string;
|
secretAccessKey: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
accountKey: string;
|
accountKey: string;
|
||||||
accountName: string;
|
accountName: string;
|
||||||
backend: 'azure';
|
backend: 'azure';
|
||||||
container: string;
|
container: string;
|
||||||
|
customPassword?: string;
|
||||||
endpointSuffix?: string;
|
endpointSuffix?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
backend: 'gcs';
|
backend: 'gcs';
|
||||||
bucket: string;
|
bucket: string;
|
||||||
credentialsJson: string;
|
credentialsJson: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
backend: 'local';
|
backend: 'local';
|
||||||
name: string;
|
name: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
backend: 'rclone';
|
backend: 'rclone';
|
||||||
path: string;
|
path: string;
|
||||||
remote: string;
|
remote: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -757,30 +769,42 @@ export type CreateRepositoryData = {
|
|||||||
bucket: string;
|
bucket: string;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
secretAccessKey: string;
|
secretAccessKey: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
accessKeyId: string;
|
accessKeyId: string;
|
||||||
backend: 's3';
|
backend: 's3';
|
||||||
bucket: string;
|
bucket: string;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
secretAccessKey: string;
|
secretAccessKey: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
accountKey: string;
|
accountKey: string;
|
||||||
accountName: string;
|
accountName: string;
|
||||||
backend: 'azure';
|
backend: 'azure';
|
||||||
container: string;
|
container: string;
|
||||||
|
customPassword?: string;
|
||||||
endpointSuffix?: string;
|
endpointSuffix?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
backend: 'gcs';
|
backend: 'gcs';
|
||||||
bucket: string;
|
bucket: string;
|
||||||
credentialsJson: string;
|
credentialsJson: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
backend: 'local';
|
backend: 'local';
|
||||||
name: string;
|
name: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
backend: 'rclone';
|
backend: 'rclone';
|
||||||
path: string;
|
path: string;
|
||||||
remote: string;
|
remote: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
};
|
};
|
||||||
name: string;
|
name: string;
|
||||||
compressionMode?: 'auto' | 'better' | 'fastest' | 'max' | 'off';
|
compressionMode?: 'auto' | 'better' | 'fastest' | 'max' | 'off';
|
||||||
@@ -865,30 +889,42 @@ export type GetRepositoryResponses = {
|
|||||||
bucket: string;
|
bucket: string;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
secretAccessKey: string;
|
secretAccessKey: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
accessKeyId: string;
|
accessKeyId: string;
|
||||||
backend: 's3';
|
backend: 's3';
|
||||||
bucket: string;
|
bucket: string;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
secretAccessKey: string;
|
secretAccessKey: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
accountKey: string;
|
accountKey: string;
|
||||||
accountName: string;
|
accountName: string;
|
||||||
backend: 'azure';
|
backend: 'azure';
|
||||||
container: string;
|
container: string;
|
||||||
|
customPassword?: string;
|
||||||
endpointSuffix?: string;
|
endpointSuffix?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
backend: 'gcs';
|
backend: 'gcs';
|
||||||
bucket: string;
|
bucket: string;
|
||||||
credentialsJson: string;
|
credentialsJson: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
backend: 'local';
|
backend: 'local';
|
||||||
name: string;
|
name: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
backend: 'rclone';
|
backend: 'rclone';
|
||||||
path: string;
|
path: string;
|
||||||
remote: string;
|
remote: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -929,6 +965,27 @@ export type ListSnapshotsResponses = {
|
|||||||
|
|
||||||
export type ListSnapshotsResponse = ListSnapshotsResponses[keyof ListSnapshotsResponses];
|
export type ListSnapshotsResponse = ListSnapshotsResponses[keyof ListSnapshotsResponses];
|
||||||
|
|
||||||
|
export type DeleteSnapshotData = {
|
||||||
|
body?: never;
|
||||||
|
path: {
|
||||||
|
name: string;
|
||||||
|
snapshotId: string;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: '/api/v1/repositories/{name}/snapshots/{snapshotId}';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteSnapshotResponses = {
|
||||||
|
/**
|
||||||
|
* Snapshot deleted successfully
|
||||||
|
*/
|
||||||
|
200: {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteSnapshotResponse = DeleteSnapshotResponses[keyof DeleteSnapshotResponses];
|
||||||
|
|
||||||
export type GetSnapshotDetailsData = {
|
export type GetSnapshotDetailsData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
path: {
|
path: {
|
||||||
@@ -1079,30 +1136,42 @@ export type ListBackupSchedulesResponses = {
|
|||||||
bucket: string;
|
bucket: string;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
secretAccessKey: string;
|
secretAccessKey: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
accessKeyId: string;
|
accessKeyId: string;
|
||||||
backend: 's3';
|
backend: 's3';
|
||||||
bucket: string;
|
bucket: string;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
secretAccessKey: string;
|
secretAccessKey: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
accountKey: string;
|
accountKey: string;
|
||||||
accountName: string;
|
accountName: string;
|
||||||
backend: 'azure';
|
backend: 'azure';
|
||||||
container: string;
|
container: string;
|
||||||
|
customPassword?: string;
|
||||||
endpointSuffix?: string;
|
endpointSuffix?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
backend: 'gcs';
|
backend: 'gcs';
|
||||||
bucket: string;
|
bucket: string;
|
||||||
credentialsJson: string;
|
credentialsJson: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
backend: 'local';
|
backend: 'local';
|
||||||
name: string;
|
name: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
backend: 'rclone';
|
backend: 'rclone';
|
||||||
path: string;
|
path: string;
|
||||||
remote: string;
|
remote: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -1280,30 +1349,42 @@ export type GetBackupScheduleResponses = {
|
|||||||
bucket: string;
|
bucket: string;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
secretAccessKey: string;
|
secretAccessKey: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
accessKeyId: string;
|
accessKeyId: string;
|
||||||
backend: 's3';
|
backend: 's3';
|
||||||
bucket: string;
|
bucket: string;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
secretAccessKey: string;
|
secretAccessKey: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
accountKey: string;
|
accountKey: string;
|
||||||
accountName: string;
|
accountName: string;
|
||||||
backend: 'azure';
|
backend: 'azure';
|
||||||
container: string;
|
container: string;
|
||||||
|
customPassword?: string;
|
||||||
endpointSuffix?: string;
|
endpointSuffix?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
backend: 'gcs';
|
backend: 'gcs';
|
||||||
bucket: string;
|
bucket: string;
|
||||||
credentialsJson: string;
|
credentialsJson: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
backend: 'local';
|
backend: 'local';
|
||||||
name: string;
|
name: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
backend: 'rclone';
|
backend: 'rclone';
|
||||||
path: string;
|
path: string;
|
||||||
remote: string;
|
remote: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -1462,30 +1543,42 @@ export type GetBackupScheduleForVolumeResponses = {
|
|||||||
bucket: string;
|
bucket: string;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
secretAccessKey: string;
|
secretAccessKey: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
accessKeyId: string;
|
accessKeyId: string;
|
||||||
backend: 's3';
|
backend: 's3';
|
||||||
bucket: string;
|
bucket: string;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
secretAccessKey: string;
|
secretAccessKey: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
accountKey: string;
|
accountKey: string;
|
||||||
accountName: string;
|
accountName: string;
|
||||||
backend: 'azure';
|
backend: 'azure';
|
||||||
container: string;
|
container: string;
|
||||||
|
customPassword?: string;
|
||||||
endpointSuffix?: string;
|
endpointSuffix?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
backend: 'gcs';
|
backend: 'gcs';
|
||||||
bucket: string;
|
bucket: string;
|
||||||
credentialsJson: string;
|
credentialsJson: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
backend: 'local';
|
backend: 'local';
|
||||||
name: string;
|
name: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
} | {
|
} | {
|
||||||
backend: 'rclone';
|
backend: 'rclone';
|
||||||
path: string;
|
path: string;
|
||||||
remote: string;
|
remote: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -1611,13 +1704,6 @@ export type RunForgetData = {
|
|||||||
url: '/api/v1/backups/{scheduleId}/forget';
|
url: '/api/v1/backups/{scheduleId}/forget';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RunForgetErrors = {
|
|
||||||
/**
|
|
||||||
* No retention policy configured for this schedule
|
|
||||||
*/
|
|
||||||
400: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RunForgetResponses = {
|
export type RunForgetResponses = {
|
||||||
/**
|
/**
|
||||||
* Retention policy applied successfully
|
* Retention policy applied successfully
|
||||||
|
|||||||
@@ -1,10 +1,26 @@
|
|||||||
import { Calendar, Clock, Database, FolderTree, HardDrive } from "lucide-react";
|
import { useState } from "react";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { Calendar, Clock, Database, FolderTree, HardDrive, Trash2 } from "lucide-react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { ByteSize } from "~/client/components/bytes-size";
|
import { ByteSize } from "~/client/components/bytes-size";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
|
||||||
|
import { Button } from "~/client/components/ui/button";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "~/client/components/ui/alert-dialog";
|
||||||
import { formatDuration } from "~/utils/utils";
|
import { formatDuration } from "~/utils/utils";
|
||||||
import type { ListSnapshotsResponse } from "../api-client";
|
import type { ListSnapshotsResponse } from "../api-client";
|
||||||
|
import { deleteSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
import { parseError } from "~/client/lib/errors";
|
||||||
|
|
||||||
type Snapshot = ListSnapshotsResponse[number];
|
type Snapshot = ListSnapshotsResponse[number];
|
||||||
|
|
||||||
@@ -15,81 +31,149 @@ type Props = {
|
|||||||
|
|
||||||
export const SnapshotsTable = ({ snapshots, repositoryName }: Props) => {
|
export const SnapshotsTable = ({ snapshots, repositoryName }: Props) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
const [snapshotToDelete, setSnapshotToDelete] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const deleteSnapshot = useMutation({
|
||||||
|
...deleteSnapshotMutation(),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["listSnapshots"] });
|
||||||
|
setShowDeleteConfirm(false);
|
||||||
|
setSnapshotToDelete(null);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDeleteClick = (e: React.MouseEvent, snapshotId: string) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSnapshotToDelete(snapshotId);
|
||||||
|
setShowDeleteConfirm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmDelete = () => {
|
||||||
|
if (snapshotToDelete) {
|
||||||
|
toast.promise(
|
||||||
|
deleteSnapshot.mutateAsync({
|
||||||
|
path: { name: repositoryName, snapshotId: snapshotToDelete },
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
loading: "Deleting snapshot...",
|
||||||
|
success: "Snapshot deleted successfully",
|
||||||
|
error: (error) => parseError(error)?.message || "Failed to delete snapshot",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleRowClick = (snapshotId: string) => {
|
const handleRowClick = (snapshotId: string) => {
|
||||||
navigate(`/repositories/${repositoryName}/${snapshotId}`);
|
navigate(`/repositories/${repositoryName}/${snapshotId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<>
|
||||||
<Table className="border-t">
|
<div className="overflow-x-auto">
|
||||||
<TableHeader className="bg-card-header">
|
<Table className="border-t">
|
||||||
<TableRow>
|
<TableHeader className="bg-card-header">
|
||||||
<TableHead className="uppercase">Snapshot ID</TableHead>
|
<TableRow>
|
||||||
<TableHead className="uppercase">Date & Time</TableHead>
|
<TableHead className="uppercase">Snapshot ID</TableHead>
|
||||||
<TableHead className="uppercase">Size</TableHead>
|
<TableHead className="uppercase">Date & Time</TableHead>
|
||||||
<TableHead className="uppercase hidden md:table-cell text-right">Duration</TableHead>
|
<TableHead className="uppercase">Size</TableHead>
|
||||||
<TableHead className="uppercase hidden text-right lg:table-cell">Paths</TableHead>
|
<TableHead className="uppercase hidden md:table-cell text-right">Duration</TableHead>
|
||||||
</TableRow>
|
<TableHead className="uppercase hidden text-right lg:table-cell">Paths</TableHead>
|
||||||
</TableHeader>
|
<TableHead className="uppercase text-right">Actions</TableHead>
|
||||||
<TableBody>
|
|
||||||
{snapshots.map((snapshot) => (
|
|
||||||
<TableRow
|
|
||||||
key={snapshot.short_id}
|
|
||||||
className="hover:bg-accent/50 cursor-pointer"
|
|
||||||
onClick={() => handleRowClick(snapshot.short_id)}
|
|
||||||
>
|
|
||||||
<TableCell className="font-mono text-sm">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<span className="text-strong-accent">{snapshot.short_id}</span>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<span className="text-sm">{new Date(snapshot.time).toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Database className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<span className="font-medium">
|
|
||||||
<ByteSize bytes={snapshot.size} base={1024} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="hidden md:table-cell">
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
|
||||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<span className="text-sm text-muted-foreground">{formatDuration(snapshot.duration / 1000)}</span>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="hidden lg:table-cell">
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
|
||||||
<FolderTree className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<span className="text-xs bg-primary/10 text-primary rounded-md px-2 py-1 cursor-help">
|
|
||||||
{snapshot.paths.length} {snapshot.paths.length === 1 ? "path" : "paths"}
|
|
||||||
</span>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="top" className="max-w-md">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
{snapshot.paths.map((path) => (
|
|
||||||
<div key={`${snapshot.short_id}-${path}`} className="text-xs font-mono">
|
|
||||||
{path}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
</TableHeader>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{snapshots.map((snapshot) => (
|
||||||
</div>
|
<TableRow
|
||||||
|
key={snapshot.short_id}
|
||||||
|
className="hover:bg-accent/50 cursor-pointer"
|
||||||
|
onClick={() => handleRowClick(snapshot.short_id)}
|
||||||
|
>
|
||||||
|
<TableCell className="font-mono text-sm">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-strong-accent">{snapshot.short_id}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm">{new Date(snapshot.time).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Database className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="font-medium">
|
||||||
|
<ByteSize bytes={snapshot.size} base={1024} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="hidden md:table-cell">
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm text-muted-foreground">{formatDuration(snapshot.duration / 1000)}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="hidden lg:table-cell">
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
<FolderTree className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span className="text-xs bg-primary/10 text-primary rounded-md px-2 py-1 cursor-help">
|
||||||
|
{snapshot.paths.length} {snapshot.paths.length === 1 ? "path" : "paths"}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="max-w-md">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{snapshot.paths.map((path) => (
|
||||||
|
<div key={`${snapshot.short_id}-${path}`} className="text-xs font-mono">
|
||||||
|
{path}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => handleDeleteClick(e, snapshot.short_id)}
|
||||||
|
disabled={deleteSnapshot.isPending}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Delete snapshot?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete the snapshot and all its data from the
|
||||||
|
repository.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={handleConfirmDelete}
|
||||||
|
disabled={deleteSnapshot.isPending}
|
||||||
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
>
|
||||||
|
Delete snapshot
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -254,8 +254,8 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Backup paths</CardTitle>
|
<CardTitle>Backup paths</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Select which folders or files to include in the backup. If no paths are selected, the entire volume will be
|
Select which folders or files to include in the backup. If no paths are selected, the entire volume will
|
||||||
backed up.
|
be backed up.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@@ -26,10 +26,12 @@ interface Props {
|
|||||||
snapshot: Snapshot;
|
snapshot: Snapshot;
|
||||||
repositoryName: string;
|
repositoryName: string;
|
||||||
volume?: Volume;
|
volume?: Volume;
|
||||||
|
onDeleteSnapshot?: (snapshotId: string) => void;
|
||||||
|
isDeletingSnapshot?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SnapshotFileBrowser = (props: Props) => {
|
export const SnapshotFileBrowser = (props: Props) => {
|
||||||
const { snapshot, repositoryName, volume } = props;
|
const { snapshot, repositoryName, volume, onDeleteSnapshot, isDeletingSnapshot } = props;
|
||||||
|
|
||||||
const isReadOnly = volume?.config && "readOnly" in volume.config && volume.config.readOnly === true;
|
const isReadOnly = volume?.config && "readOnly" in volume.config && volume.config.readOnly === true;
|
||||||
|
|
||||||
@@ -136,30 +138,43 @@ export const SnapshotFileBrowser = (props: Props) => {
|
|||||||
<CardTitle>File Browser</CardTitle>
|
<CardTitle>File Browser</CardTitle>
|
||||||
<CardDescription>{`Viewing snapshot from ${new Date(snapshot?.time ?? 0).toLocaleString()}`}</CardDescription>
|
<CardDescription>{`Viewing snapshot from ${new Date(snapshot?.time ?? 0).toLocaleString()}`}</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{selectedPaths.size > 0 && (
|
<div className="flex gap-2">
|
||||||
<Tooltip>
|
{selectedPaths.size > 0 && (
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<span tabIndex={isReadOnly ? 0 : undefined}>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<span tabIndex={isReadOnly ? 0 : undefined}>
|
||||||
onClick={handleRestoreClick}
|
<Button
|
||||||
variant="primary"
|
onClick={handleRestoreClick}
|
||||||
size="sm"
|
variant="primary"
|
||||||
disabled={isRestoring || isReadOnly}
|
size="sm"
|
||||||
>
|
disabled={isRestoring || isReadOnly}
|
||||||
{isRestoring
|
>
|
||||||
? "Restoring..."
|
{isRestoring
|
||||||
: `Restore ${selectedPaths.size} selected ${selectedPaths.size === 1 ? "item" : "items"}`}
|
? "Restoring..."
|
||||||
</Button>
|
: `Restore ${selectedPaths.size} selected ${selectedPaths.size === 1 ? "item" : "items"}`}
|
||||||
</span>
|
</Button>
|
||||||
</TooltipTrigger>
|
</span>
|
||||||
{isReadOnly && (
|
</TooltipTrigger>
|
||||||
<TooltipContent className="text-center">
|
{isReadOnly && (
|
||||||
<p>Volume is mounted as read-only.</p>
|
<TooltipContent className="text-center">
|
||||||
<p>Please remount with read-only disabled to restore files.</p>
|
<p>Volume is mounted as read-only.</p>
|
||||||
</TooltipContent>
|
<p>Please remount with read-only disabled to restore files.</p>
|
||||||
)}
|
</TooltipContent>
|
||||||
</Tooltip>
|
)}
|
||||||
)}
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{onDeleteSnapshot && (
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onDeleteSnapshot(snapshot.short_id)}
|
||||||
|
disabled={isDeletingSnapshot}
|
||||||
|
loading={isDeletingSnapshot}
|
||||||
|
>
|
||||||
|
{isDeletingSnapshot ? "Deleting..." : "Delete Snapshot"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex-1 overflow-hidden flex flex-col p-0">
|
<CardContent className="flex-1 overflow-hidden flex flex-col p-0">
|
||||||
|
|||||||
@@ -3,6 +3,16 @@ import { useQuery, useMutation } from "@tanstack/react-query";
|
|||||||
import { redirect, useNavigate } from "react-router";
|
import { redirect, useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "~/client/components/ui/alert-dialog";
|
||||||
import {
|
import {
|
||||||
getBackupScheduleOptions,
|
getBackupScheduleOptions,
|
||||||
runBackupNowMutation,
|
runBackupNowMutation,
|
||||||
@@ -10,6 +20,7 @@ import {
|
|||||||
listSnapshotsOptions,
|
listSnapshotsOptions,
|
||||||
updateBackupScheduleMutation,
|
updateBackupScheduleMutation,
|
||||||
stopBackupMutation,
|
stopBackupMutation,
|
||||||
|
deleteSnapshotMutation,
|
||||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import { parseError } from "~/client/lib/errors";
|
import { parseError } from "~/client/lib/errors";
|
||||||
import { getCronExpression } from "~/utils/utils";
|
import { getCronExpression } from "~/utils/utils";
|
||||||
@@ -50,6 +61,8 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
|||||||
const [isEditMode, setIsEditMode] = useState(false);
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
const formId = useId();
|
const formId = useId();
|
||||||
const [selectedSnapshotId, setSelectedSnapshotId] = useState<string>();
|
const [selectedSnapshotId, setSelectedSnapshotId] = useState<string>();
|
||||||
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
const [snapshotToDelete, setSnapshotToDelete] = useState<string | null>(null);
|
||||||
|
|
||||||
const { data: schedule } = useQuery({
|
const { data: schedule } = useQuery({
|
||||||
...getBackupScheduleOptions({ path: { scheduleId: params.id } }),
|
...getBackupScheduleOptions({ path: { scheduleId: params.id } }),
|
||||||
@@ -110,6 +123,17 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const deleteSnapshot = useMutation({
|
||||||
|
...deleteSnapshotMutation(),
|
||||||
|
onSuccess: () => {
|
||||||
|
setShowDeleteConfirm(false);
|
||||||
|
setSnapshotToDelete(null);
|
||||||
|
if (selectedSnapshotId === snapshotToDelete) {
|
||||||
|
setSelectedSnapshotId(undefined);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleSubmit = (formValues: BackupScheduleFormValues) => {
|
const handleSubmit = (formValues: BackupScheduleFormValues) => {
|
||||||
if (!schedule) return;
|
if (!schedule) return;
|
||||||
|
|
||||||
@@ -150,6 +174,26 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteSnapshot = (snapshotId: string) => {
|
||||||
|
setSnapshotToDelete(snapshotId);
|
||||||
|
setShowDeleteConfirm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmDelete = () => {
|
||||||
|
if (snapshotToDelete) {
|
||||||
|
toast.promise(
|
||||||
|
deleteSnapshot.mutateAsync({
|
||||||
|
path: { name: schedule.repository.name, snapshotId: snapshotToDelete },
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
loading: "Deleting snapshot...",
|
||||||
|
success: "Snapshot deleted successfully",
|
||||||
|
error: (error) => parseError(error)?.message || "Failed to delete snapshot",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -191,8 +235,32 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
|||||||
snapshot={selectedSnapshot}
|
snapshot={selectedSnapshot}
|
||||||
repositoryName={schedule.repository.name}
|
repositoryName={schedule.repository.name}
|
||||||
volume={schedule.volume}
|
volume={schedule.volume}
|
||||||
|
onDeleteSnapshot={handleDeleteSnapshot}
|
||||||
|
isDeletingSnapshot={deleteSnapshot.isPending}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Delete snapshot?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete the snapshot and all its data from the
|
||||||
|
repository.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={handleConfirmDelete}
|
||||||
|
disabled={deleteSnapshot.isPending}
|
||||||
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
>
|
||||||
|
Delete snapshot
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
createRepositoryBody,
|
createRepositoryBody,
|
||||||
createRepositoryDto,
|
createRepositoryDto,
|
||||||
deleteRepositoryDto,
|
deleteRepositoryDto,
|
||||||
|
deleteSnapshotDto,
|
||||||
doctorRepositoryDto,
|
doctorRepositoryDto,
|
||||||
getRepositoryDto,
|
getRepositoryDto,
|
||||||
getSnapshotDetailsDto,
|
getSnapshotDetailsDto,
|
||||||
@@ -16,6 +17,7 @@ import {
|
|||||||
restoreSnapshotBody,
|
restoreSnapshotBody,
|
||||||
restoreSnapshotDto,
|
restoreSnapshotDto,
|
||||||
type DeleteRepositoryDto,
|
type DeleteRepositoryDto,
|
||||||
|
type DeleteSnapshotDto,
|
||||||
type DoctorRepositoryDto,
|
type DoctorRepositoryDto,
|
||||||
type GetRepositoryDto,
|
type GetRepositoryDto,
|
||||||
type GetSnapshotDetailsDto,
|
type GetSnapshotDetailsDto,
|
||||||
@@ -142,4 +144,11 @@ export const repositoriesController = new Hono()
|
|||||||
const result = await repositoriesService.doctorRepository(name);
|
const result = await repositoriesService.doctorRepository(name);
|
||||||
|
|
||||||
return c.json<DoctorRepositoryDto>(result, 200);
|
return c.json<DoctorRepositoryDto>(result, 200);
|
||||||
|
})
|
||||||
|
.delete("/:name/snapshots/:snapshotId", deleteSnapshotDto, async (c) => {
|
||||||
|
const { name, snapshotId } = c.req.param();
|
||||||
|
|
||||||
|
await repositoriesService.deleteSnapshot(name, snapshotId);
|
||||||
|
|
||||||
|
return c.json<DeleteSnapshotDto>({ message: "Snapshot deleted" }, 200);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -326,3 +326,28 @@ export const listRcloneRemotesDto = describeRoute({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a snapshot
|
||||||
|
*/
|
||||||
|
export const deleteSnapshotResponse = type({
|
||||||
|
message: "string",
|
||||||
|
});
|
||||||
|
|
||||||
|
export type DeleteSnapshotDto = typeof deleteSnapshotResponse.infer;
|
||||||
|
|
||||||
|
export const deleteSnapshotDto = describeRoute({
|
||||||
|
description: "Delete a specific snapshot from a repository",
|
||||||
|
tags: ["Repositories"],
|
||||||
|
operationId: "deleteSnapshot",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Snapshot deleted successfully",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(deleteSnapshotResponse),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -327,6 +327,18 @@ const doctorRepository = async (name: string) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteSnapshot = async (name: string, snapshotId: string) => {
|
||||||
|
const repository = await db.query.repositoriesTable.findFirst({
|
||||||
|
where: eq(repositoriesTable.name, name),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!repository) {
|
||||||
|
throw new NotFoundError("Repository not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await restic.deleteSnapshot(repository.config, snapshotId);
|
||||||
|
};
|
||||||
|
|
||||||
export const repositoriesService = {
|
export const repositoriesService = {
|
||||||
listRepositories,
|
listRepositories,
|
||||||
createRepository,
|
createRepository,
|
||||||
@@ -338,4 +350,5 @@ export const repositoriesService = {
|
|||||||
getSnapshotDetails,
|
getSnapshotDetails,
|
||||||
checkHealth,
|
checkHealth,
|
||||||
doctorRepository,
|
doctorRepository,
|
||||||
|
deleteSnapshot,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -441,6 +441,22 @@ const forget = async (config: RepositoryConfig, options: RetentionPolicy, extra:
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteSnapshot = async (config: RepositoryConfig, snapshotId: string) => {
|
||||||
|
const repoUrl = buildRepoUrl(config);
|
||||||
|
const env = await buildEnv(config);
|
||||||
|
|
||||||
|
const args: string[] = ["--repo", repoUrl, "forget", snapshotId, "--prune"];
|
||||||
|
|
||||||
|
const res = await $`restic ${args}`.env(env).nothrow();
|
||||||
|
|
||||||
|
if (res.exitCode !== 0) {
|
||||||
|
logger.error(`Restic snapshot deletion failed: ${res.stderr}`);
|
||||||
|
throw new Error(`Failed to delete snapshot: ${res.stderr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
};
|
||||||
|
|
||||||
const lsNodeSchema = type({
|
const lsNodeSchema = type({
|
||||||
name: "string",
|
name: "string",
|
||||||
type: "string",
|
type: "string",
|
||||||
@@ -601,6 +617,7 @@ export const restic = {
|
|||||||
restore,
|
restore,
|
||||||
snapshots,
|
snapshots,
|
||||||
forget,
|
forget,
|
||||||
|
deleteSnapshot,
|
||||||
unlock,
|
unlock,
|
||||||
ls,
|
ls,
|
||||||
check,
|
check,
|
||||||
|
|||||||
Reference in New Issue
Block a user