feat: snapshots bulk actions (#256)
* feat: delete snapshots in bulk * feat: re-tag snapshots in bulk * chore: pr feedbacks
This commit is contained in:
parent
013fe5a521
commit
2c786dfb35
9 changed files with 464 additions and 70 deletions
|
|
@ -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, getBackupSchedule, getBackupScheduleForVolume, getMe, getMirrorCompatibility, getNotificationDestination, getRepository, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getStatus, getSystemInfo, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, login, logout, mountVolume, type Options, register, reorderBackupSchedules, restoreSnapshot, runBackupNow, runForget, stopBackup, testConnection, testNotificationDestination, unmountVolume, updateBackupSchedule, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateVolume } from '../sdk.gen';
|
import { browseFilesystem, changePassword, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteSnapshots, deleteVolume, doctorRepository, downloadResticPassword, getBackupSchedule, getBackupScheduleForVolume, getMe, getMirrorCompatibility, getNotificationDestination, getRepository, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getStatus, getSystemInfo, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, login, logout, mountVolume, type Options, register, reorderBackupSchedules, restoreSnapshot, runBackupNow, runForget, stopBackup, tagSnapshots, testConnection, testNotificationDestination, unmountVolume, updateBackupSchedule, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateVolume } from '../sdk.gen';
|
||||||
import type { BrowseFilesystemData, BrowseFilesystemResponse, ChangePasswordData, ChangePasswordResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteVolumeData, DeleteVolumeResponse, DoctorRepositoryData, DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetMeData, GetMeResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetRepositoryData, GetRepositoryResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, LoginData, LoginResponse, LogoutData, LogoutResponse, MountVolumeData, MountVolumeResponse, RegisterData, RegisterResponse, ReorderBackupSchedulesData, ReorderBackupSchedulesResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, StopBackupData, StopBackupResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
|
import type { BrowseFilesystemData, BrowseFilesystemResponse, ChangePasswordData, ChangePasswordResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteSnapshotsData, DeleteSnapshotsResponse, DeleteVolumeData, DeleteVolumeResponse, DoctorRepositoryData, DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetMeData, GetMeResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetRepositoryData, GetRepositoryResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, 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, ReorderBackupSchedulesData, ReorderBackupSchedulesResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, StopBackupData, StopBackupResponse, TagSnapshotsData, TagSnapshotsResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new user
|
* Register a new user
|
||||||
|
|
@ -439,6 +439,23 @@ export const updateRepositoryMutation = (options?: Partial<Options<UpdateReposit
|
||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete multiple snapshots from a repository
|
||||||
|
*/
|
||||||
|
export const deleteSnapshotsMutation = (options?: Partial<Options<DeleteSnapshotsData>>): UseMutationOptions<DeleteSnapshotsResponse, DefaultError, Options<DeleteSnapshotsData>> => {
|
||||||
|
const mutationOptions: UseMutationOptions<DeleteSnapshotsResponse, DefaultError, Options<DeleteSnapshotsData>> = {
|
||||||
|
mutationFn: async (fnOptions) => {
|
||||||
|
const { data } = await deleteSnapshots({
|
||||||
|
...options,
|
||||||
|
...fnOptions,
|
||||||
|
throwOnError: true
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return mutationOptions;
|
||||||
|
};
|
||||||
|
|
||||||
export const listSnapshotsQueryKey = (options: Options<ListSnapshotsData>) => createQueryKey('listSnapshots', options);
|
export const listSnapshotsQueryKey = (options: Options<ListSnapshotsData>) => createQueryKey('listSnapshots', options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -544,6 +561,23 @@ export const doctorRepositoryMutation = (options?: Partial<Options<DoctorReposit
|
||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag multiple snapshots in a repository
|
||||||
|
*/
|
||||||
|
export const tagSnapshotsMutation = (options?: Partial<Options<TagSnapshotsData>>): UseMutationOptions<TagSnapshotsResponse, DefaultError, Options<TagSnapshotsData>> => {
|
||||||
|
const mutationOptions: UseMutationOptions<TagSnapshotsResponse, DefaultError, Options<TagSnapshotsData>> = {
|
||||||
|
mutationFn: async (fnOptions) => {
|
||||||
|
const { data } = await tagSnapshots({
|
||||||
|
...options,
|
||||||
|
...fnOptions,
|
||||||
|
throwOnError: true
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return mutationOptions;
|
||||||
|
};
|
||||||
|
|
||||||
export const listBackupSchedulesQueryKey = (options?: Options<ListBackupSchedulesData>) => createQueryKey('listBackupSchedules', options);
|
export const listBackupSchedulesQueryKey = (options?: Options<ListBackupSchedulesData>) => createQueryKey('listBackupSchedules', 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, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetMeData, GetMeResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetRepositoryData, GetRepositoryResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, LoginData, LoginResponses, LogoutData, LogoutResponses, MountVolumeData, MountVolumeResponses, RegisterData, RegisterResponses, ReorderBackupSchedulesData, ReorderBackupSchedulesResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
|
import type { BrowseFilesystemData, BrowseFilesystemResponses, ChangePasswordData, ChangePasswordResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteSnapshotsData, DeleteSnapshotsResponses, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetMeData, GetMeResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetRepositoryData, GetRepositoryResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, 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, ReorderBackupSchedulesData, ReorderBackupSchedulesResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TagSnapshotsData, TagSnapshotsResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
|
||||||
|
|
||||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
||||||
/**
|
/**
|
||||||
|
|
@ -189,6 +189,18 @@ export const updateRepository = <ThrowOnError extends boolean = false>(options:
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete multiple snapshots from a repository
|
||||||
|
*/
|
||||||
|
export const deleteSnapshots = <ThrowOnError extends boolean = false>(options: Options<DeleteSnapshotsData, ThrowOnError>) => (options.client ?? client).delete<DeleteSnapshotsResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/repositories/{id}/snapshots',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all snapshots in a repository
|
* List all snapshots in a repository
|
||||||
*/
|
*/
|
||||||
|
|
@ -226,6 +238,18 @@ export const restoreSnapshot = <ThrowOnError extends boolean = false>(options: O
|
||||||
*/
|
*/
|
||||||
export const doctorRepository = <ThrowOnError extends boolean = false>(options: Options<DoctorRepositoryData, ThrowOnError>) => (options.client ?? client).post<DoctorRepositoryResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{id}/doctor', ...options });
|
export const doctorRepository = <ThrowOnError extends boolean = false>(options: Options<DoctorRepositoryData, ThrowOnError>) => (options.client ?? client).post<DoctorRepositoryResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{id}/doctor', ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag multiple snapshots in a repository
|
||||||
|
*/
|
||||||
|
export const tagSnapshots = <ThrowOnError extends boolean = false>(options: Options<TagSnapshotsData, ThrowOnError>) => (options.client ?? client).post<TagSnapshotsResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/repositories/{id}/snapshots/tag',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all backup schedules
|
* List all backup schedules
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1195,6 +1195,28 @@ export type UpdateRepositoryResponses = {
|
||||||
|
|
||||||
export type UpdateRepositoryResponse = UpdateRepositoryResponses[keyof UpdateRepositoryResponses];
|
export type UpdateRepositoryResponse = UpdateRepositoryResponses[keyof UpdateRepositoryResponses];
|
||||||
|
|
||||||
|
export type DeleteSnapshotsData = {
|
||||||
|
body?: {
|
||||||
|
snapshotIds: Array<string>;
|
||||||
|
};
|
||||||
|
path: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: '/api/v1/repositories/{id}/snapshots';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteSnapshotsResponses = {
|
||||||
|
/**
|
||||||
|
* Snapshots deleted successfully
|
||||||
|
*/
|
||||||
|
200: {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteSnapshotsResponse = DeleteSnapshotsResponses[keyof DeleteSnapshotsResponses];
|
||||||
|
|
||||||
export type ListSnapshotsData = {
|
export type ListSnapshotsData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
path: {
|
path: {
|
||||||
|
|
@ -1367,6 +1389,31 @@ export type DoctorRepositoryResponses = {
|
||||||
|
|
||||||
export type DoctorRepositoryResponse = DoctorRepositoryResponses[keyof DoctorRepositoryResponses];
|
export type DoctorRepositoryResponse = DoctorRepositoryResponses[keyof DoctorRepositoryResponses];
|
||||||
|
|
||||||
|
export type TagSnapshotsData = {
|
||||||
|
body?: {
|
||||||
|
snapshotIds: Array<string>;
|
||||||
|
add?: Array<string>;
|
||||||
|
remove?: Array<string>;
|
||||||
|
set?: Array<string>;
|
||||||
|
};
|
||||||
|
path: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: '/api/v1/repositories/{id}/snapshots/tag';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TagSnapshotsResponses = {
|
||||||
|
/**
|
||||||
|
* Snapshots tagged successfully
|
||||||
|
*/
|
||||||
|
200: {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TagSnapshotsResponse = TagSnapshotsResponses[keyof TagSnapshotsResponses];
|
||||||
|
|
||||||
export type ListBackupSchedulesData = {
|
export type ListBackupSchedulesData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
path?: never;
|
path?: never;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Calendar, Clock, Database, HardDrive, Server, Trash2 } from "lucide-react";
|
import { Calendar, Clock, Database, HardDrive, Tag, Trash2, X } from "lucide-react";
|
||||||
import { Link, useNavigate } from "react-router";
|
import { Link, useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
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 { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
|
import { Checkbox } from "~/client/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
|
|
@ -16,8 +17,17 @@ import {
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "~/client/components/ui/alert-dialog";
|
} from "~/client/components/ui/alert-dialog";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "~/client/components/ui/dialog";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||||
import { formatDuration } from "~/utils/utils";
|
import { formatDuration } from "~/utils/utils";
|
||||||
import { deleteSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
import { deleteSnapshotsMutation, tagSnapshotsMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import { parseError } from "~/client/lib/errors";
|
import { parseError } from "~/client/lib/errors";
|
||||||
import type { BackupSchedule, Snapshot } from "../lib/types";
|
import type { BackupSchedule, Snapshot } from "../lib/types";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
|
|
@ -31,68 +41,126 @@ type Props = {
|
||||||
export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
|
export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
||||||
const [snapshotToDelete, setSnapshotToDelete] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const deleteSnapshot = useMutation({
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
...deleteSnapshotMutation(),
|
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false);
|
||||||
|
const [showReTagDialog, setShowReTagDialog] = useState(false);
|
||||||
|
const [targetScheduleId, setTargetScheduleId] = useState<string>("");
|
||||||
|
|
||||||
|
const deleteSnapshots = useMutation({
|
||||||
|
...deleteSnapshotsMutation(),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["listSnapshots"] });
|
queryClient.invalidateQueries({ queryKey: ["listSnapshots"] });
|
||||||
setShowDeleteConfirm(false);
|
setShowBulkDeleteConfirm(false);
|
||||||
setSnapshotToDelete(null);
|
setSelectedIds(new Set());
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDeleteClick = (e: React.MouseEvent, snapshotId: string) => {
|
const tagSnapshots = useMutation({
|
||||||
e.stopPropagation();
|
...tagSnapshotsMutation(),
|
||||||
setSnapshotToDelete(snapshotId);
|
onMutate: () => {
|
||||||
setShowDeleteConfirm(true);
|
setShowReTagDialog(false);
|
||||||
};
|
},
|
||||||
|
onSuccess: () => {
|
||||||
const handleConfirmDelete = () => {
|
queryClient.invalidateQueries({ queryKey: ["listSnapshots"] });
|
||||||
if (snapshotToDelete) {
|
setShowReTagDialog(false);
|
||||||
toast.promise(
|
setSelectedIds(new Set());
|
||||||
deleteSnapshot.mutateAsync({
|
setTargetScheduleId("");
|
||||||
path: { id: repositoryId, 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/${repositoryId}/${snapshotId}`);
|
navigate(`/repositories/${repositoryId}/${snapshotId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleSelectAll = () => {
|
||||||
|
if (selectedIds.size === snapshots.length) {
|
||||||
|
setSelectedIds(new Set());
|
||||||
|
} else {
|
||||||
|
setSelectedIds(new Set(snapshots.map((s) => s.short_id)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkDelete = () => {
|
||||||
|
toast.promise(
|
||||||
|
deleteSnapshots.mutateAsync({
|
||||||
|
path: { id: repositoryId },
|
||||||
|
body: { snapshotIds: Array.from(selectedIds) },
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
loading: `Deleting ${selectedIds.size} snapshots...`,
|
||||||
|
success: "Snapshots deleted successfully",
|
||||||
|
error: (error) => parseError(error)?.message || "Failed to delete snapshots",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkReTag = () => {
|
||||||
|
const schedule = backups.find((b) => String(b.id) === targetScheduleId);
|
||||||
|
if (!schedule) return;
|
||||||
|
|
||||||
|
toast.promise(
|
||||||
|
tagSnapshots.mutateAsync({
|
||||||
|
path: { id: repositoryId },
|
||||||
|
body: {
|
||||||
|
snapshotIds: Array.from(selectedIds),
|
||||||
|
set: [schedule.shortId],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
loading: `Re-tagging ${selectedIds.size} snapshots...`,
|
||||||
|
success: `Snapshots re-tagged to ${schedule.name}`,
|
||||||
|
error: (error) => parseError(error)?.message || "Failed to re-tag snapshots",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto relative">
|
||||||
<Table className="border-t">
|
<Table className="border-t">
|
||||||
<TableHeader className="bg-card-header">
|
<TableHeader className="bg-card-header">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
<TableHead className="w-10">
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedIds.size === snapshots.length && snapshots.length > 0}
|
||||||
|
onCheckedChange={toggleSelectAll}
|
||||||
|
aria-label="Select all"
|
||||||
|
/>
|
||||||
|
</TableHead>
|
||||||
<TableHead className="uppercase">Snapshot ID</TableHead>
|
<TableHead className="uppercase">Snapshot ID</TableHead>
|
||||||
<TableHead className="uppercase">Schedule</TableHead>
|
<TableHead className="uppercase">Schedule</TableHead>
|
||||||
<TableHead className="uppercase">Date & Time</TableHead>
|
<TableHead className="uppercase">Date & Time</TableHead>
|
||||||
<TableHead className="uppercase">Size</TableHead>
|
<TableHead className="uppercase">Size</TableHead>
|
||||||
<TableHead className="uppercase hidden md:table-cell text-right">Duration</TableHead>
|
<TableHead className="uppercase hidden md:table-cell text-right">Duration</TableHead>
|
||||||
<TableHead className="uppercase hidden text-right lg:table-cell">Volume</TableHead>
|
|
||||||
<TableHead className="uppercase text-right">Actions</TableHead>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{snapshots.map((snapshot) => {
|
{snapshots.map((snapshot) => {
|
||||||
const backup = backups.find((b) => snapshot.tags.includes(b.shortId));
|
const backup = backups.find((b) => snapshot.tags.includes(b.shortId));
|
||||||
|
const isSelected = selectedIds.has(snapshot.short_id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={snapshot.short_id}
|
key={snapshot.short_id}
|
||||||
className="hover:bg-accent/50 cursor-pointer"
|
className={cn("hover:bg-accent/50 cursor-pointer", isSelected && "bg-accent/30")}
|
||||||
onClick={() => handleRowClick(snapshot.short_id)}
|
onClick={() => handleRowClick(snapshot.short_id)}
|
||||||
>
|
>
|
||||||
|
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Checkbox
|
||||||
|
checked={isSelected}
|
||||||
|
onCheckedChange={() => {
|
||||||
|
const newSelected = new Set(selectedIds);
|
||||||
|
if (newSelected.has(snapshot.short_id)) {
|
||||||
|
newSelected.delete(snapshot.short_id);
|
||||||
|
} else {
|
||||||
|
newSelected.add(snapshot.short_id);
|
||||||
|
}
|
||||||
|
setSelectedIds(newSelected);
|
||||||
|
}}
|
||||||
|
aria-label={`Select snapshot ${snapshot.short_id}`}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
<TableCell className="font-mono text-sm">
|
<TableCell className="font-mono text-sm">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||||
|
|
@ -134,32 +202,6 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
|
||||||
<span className="text-sm text-muted-foreground">{formatDuration(snapshot.duration / 1000)}</span>
|
<span className="text-sm text-muted-foreground">{formatDuration(snapshot.duration / 1000)}</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="hidden lg:table-cell">
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
|
||||||
<Server className={cn("h-4 w-4 text-muted-foreground", { hidden: !backup })} />
|
|
||||||
<Link
|
|
||||||
hidden={!backup}
|
|
||||||
to={backup ? `/volumes/${backup.volume.name}` : "#"}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
className="hover:underline"
|
|
||||||
>
|
|
||||||
<span className="text-sm">{backup ? backup.volume.name : "-"}</span>
|
|
||||||
</Link>
|
|
||||||
<span hidden={!!backup} className="text-sm text-muted-foreground">
|
|
||||||
-
|
|
||||||
</span>
|
|
||||||
</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>
|
</TableRow>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -167,27 +209,99 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
{selectedIds.size > 0 && (
|
||||||
|
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 animate-in fade-in slide-in-from-bottom-4 duration-300">
|
||||||
|
<div className="bg-card border shadow-2xl rounded-full px-4 py-2 flex items-center gap-4 min-w-75 justify-between">
|
||||||
|
<div className="flex items-center gap-3 border-r pr-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 rounded-full"
|
||||||
|
onClick={() => setSelectedIds(new Set())}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm font-medium">{selectedIds.size} selected</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="rounded-full gap-2"
|
||||||
|
onClick={() => setShowReTagDialog(true)}
|
||||||
|
>
|
||||||
|
<Tag className="h-4 w-4 mr-2" />
|
||||||
|
Re-tag
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
className="rounded-full gap-2"
|
||||||
|
onClick={() => setShowBulkDeleteConfirm(true)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AlertDialog open={showBulkDeleteConfirm} onOpenChange={setShowBulkDeleteConfirm}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Delete snapshot?</AlertDialogTitle>
|
<AlertDialogTitle>Delete {selectedIds.size} snapshots?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This action cannot be undone. This will permanently delete the snapshot and all its data from the
|
This action cannot be undone. This will permanently delete the selected snapshots and all their data from
|
||||||
repository.
|
the repository.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={handleConfirmDelete}
|
onClick={handleBulkDelete}
|
||||||
disabled={deleteSnapshot.isPending}
|
disabled={deleteSnapshots.isPending}
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
>
|
>
|
||||||
Delete snapshot
|
Delete {selectedIds.size} snapshots
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
|
<Dialog open={showReTagDialog} onOpenChange={setShowReTagDialog}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Re-tag snapshots</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Select a backup schedule to re-tag the {selectedIds.size} selected snapshots. All {selectedIds.size}{" "}
|
||||||
|
selected snapshots will be associated with the chosen schedule.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="py-4">
|
||||||
|
<Select value={targetScheduleId} onValueChange={setTargetScheduleId}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a schedule" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{backups.map((backup) => (
|
||||||
|
<SelectItem key={backup.id} value={String(backup.id)}>
|
||||||
|
{backup.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setShowReTagDialog(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleBulkReTag} disabled={!targetScheduleId}>
|
||||||
|
Apply tags
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ describe("repositories security", () => {
|
||||||
{ method: "POST", path: "/api/v1/repositories/test-repo/restore" },
|
{ method: "POST", path: "/api/v1/repositories/test-repo/restore" },
|
||||||
{ method: "POST", path: "/api/v1/repositories/test-repo/doctor" },
|
{ method: "POST", path: "/api/v1/repositories/test-repo/doctor" },
|
||||||
{ method: "DELETE", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot" },
|
{ method: "DELETE", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot" },
|
||||||
|
{ method: "DELETE", path: "/api/v1/repositories/test-repo/snapshots" },
|
||||||
{ method: "PATCH", path: "/api/v1/repositories/test-repo" },
|
{ method: "PATCH", path: "/api/v1/repositories/test-repo" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import {
|
||||||
createRepositoryDto,
|
createRepositoryDto,
|
||||||
deleteRepositoryDto,
|
deleteRepositoryDto,
|
||||||
deleteSnapshotDto,
|
deleteSnapshotDto,
|
||||||
|
deleteSnapshotsBody,
|
||||||
|
deleteSnapshotsDto,
|
||||||
doctorRepositoryDto,
|
doctorRepositoryDto,
|
||||||
getRepositoryDto,
|
getRepositoryDto,
|
||||||
getSnapshotDetailsDto,
|
getSnapshotDetailsDto,
|
||||||
|
|
@ -16,10 +18,13 @@ import {
|
||||||
listSnapshotsFilters,
|
listSnapshotsFilters,
|
||||||
restoreSnapshotBody,
|
restoreSnapshotBody,
|
||||||
restoreSnapshotDto,
|
restoreSnapshotDto,
|
||||||
|
tagSnapshotsBody,
|
||||||
|
tagSnapshotsDto,
|
||||||
updateRepositoryBody,
|
updateRepositoryBody,
|
||||||
updateRepositoryDto,
|
updateRepositoryDto,
|
||||||
type DeleteRepositoryDto,
|
type DeleteRepositoryDto,
|
||||||
type DeleteSnapshotDto,
|
type DeleteSnapshotDto,
|
||||||
|
type DeleteSnapshotsResponseDto,
|
||||||
type DoctorRepositoryDto,
|
type DoctorRepositoryDto,
|
||||||
type GetRepositoryDto,
|
type GetRepositoryDto,
|
||||||
type GetSnapshotDetailsDto,
|
type GetSnapshotDetailsDto,
|
||||||
|
|
@ -27,6 +32,7 @@ import {
|
||||||
type ListSnapshotFilesDto,
|
type ListSnapshotFilesDto,
|
||||||
type ListSnapshotsDto,
|
type ListSnapshotsDto,
|
||||||
type RestoreSnapshotDto,
|
type RestoreSnapshotDto,
|
||||||
|
type TagSnapshotsResponseDto,
|
||||||
type UpdateRepositoryDto,
|
type UpdateRepositoryDto,
|
||||||
} from "./repositories.dto";
|
} from "./repositories.dto";
|
||||||
import { repositoriesService } from "./repositories.service";
|
import { repositoriesService } from "./repositories.service";
|
||||||
|
|
@ -160,6 +166,22 @@ export const repositoriesController = new Hono()
|
||||||
|
|
||||||
return c.json<DeleteSnapshotDto>({ message: "Snapshot deleted" }, 200);
|
return c.json<DeleteSnapshotDto>({ message: "Snapshot deleted" }, 200);
|
||||||
})
|
})
|
||||||
|
.delete("/:id/snapshots", deleteSnapshotsDto, validator("json", deleteSnapshotsBody), async (c) => {
|
||||||
|
const { id } = c.req.param();
|
||||||
|
const { snapshotIds } = c.req.valid("json");
|
||||||
|
|
||||||
|
await repositoriesService.deleteSnapshots(id, snapshotIds);
|
||||||
|
|
||||||
|
return c.json<DeleteSnapshotsResponseDto>({ message: "Snapshots deleted" }, 200);
|
||||||
|
})
|
||||||
|
.post("/:id/snapshots/tag", tagSnapshotsDto, validator("json", tagSnapshotsBody), async (c) => {
|
||||||
|
const { id } = c.req.param();
|
||||||
|
const { snapshotIds, ...tags } = c.req.valid("json");
|
||||||
|
|
||||||
|
await repositoriesService.tagSnapshots(id, snapshotIds, tags);
|
||||||
|
|
||||||
|
return c.json<TagSnapshotsResponseDto>({ message: "Snapshots tagged" }, 200);
|
||||||
|
})
|
||||||
.patch("/:id", updateRepositoryDto, validator("json", updateRepositoryBody), async (c) => {
|
.patch("/:id", updateRepositoryDto, validator("json", updateRepositoryBody), async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
|
|
|
||||||
|
|
@ -400,3 +400,64 @@ export const deleteSnapshotDto = describeRoute({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete multiple snapshots
|
||||||
|
*/
|
||||||
|
export const deleteSnapshotsBody = type({
|
||||||
|
snapshotIds: "string[]>=1",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteSnapshotsResponse = type({
|
||||||
|
message: "string",
|
||||||
|
});
|
||||||
|
|
||||||
|
export type DeleteSnapshotsResponseDto = typeof deleteSnapshotsResponse.infer;
|
||||||
|
|
||||||
|
export const deleteSnapshotsDto = describeRoute({
|
||||||
|
description: "Delete multiple snapshots from a repository",
|
||||||
|
tags: ["Repositories"],
|
||||||
|
operationId: "deleteSnapshots",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Snapshots deleted successfully",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(deleteSnapshotsResponse),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag multiple snapshots
|
||||||
|
*/
|
||||||
|
export const tagSnapshotsBody = type({
|
||||||
|
snapshotIds: "string[]>=1",
|
||||||
|
add: "string[]?",
|
||||||
|
remove: "string[]?",
|
||||||
|
set: "string[]?",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tagSnapshotsResponse = type({
|
||||||
|
message: "string",
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TagSnapshotsResponseDto = typeof tagSnapshotsResponse.infer;
|
||||||
|
|
||||||
|
export const tagSnapshotsDto = describeRoute({
|
||||||
|
description: "Tag multiple snapshots in a repository",
|
||||||
|
tags: ["Repositories"],
|
||||||
|
operationId: "tagSnapshots",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Snapshots tagged successfully",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(tagSnapshotsResponse),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -389,6 +389,40 @@ const deleteSnapshot = async (id: string, snapshotId: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteSnapshots = async (id: string, snapshotIds: string[]) => {
|
||||||
|
const repository = await findRepository(id);
|
||||||
|
|
||||||
|
if (!repository) {
|
||||||
|
throw new NotFoundError("Repository not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseLock = await repoMutex.acquireExclusive(repository.id, `delete:bulk`);
|
||||||
|
try {
|
||||||
|
await restic.deleteSnapshots(repository.config, snapshotIds);
|
||||||
|
} finally {
|
||||||
|
releaseLock();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tagSnapshots = async (
|
||||||
|
id: string,
|
||||||
|
snapshotIds: string[],
|
||||||
|
tags: { add?: string[]; remove?: string[]; set?: string[] },
|
||||||
|
) => {
|
||||||
|
const repository = await findRepository(id);
|
||||||
|
|
||||||
|
if (!repository) {
|
||||||
|
throw new NotFoundError("Repository not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseLock = await repoMutex.acquireExclusive(repository.id, `tag:bulk`);
|
||||||
|
try {
|
||||||
|
await restic.tagSnapshots(repository.config, snapshotIds, tags);
|
||||||
|
} finally {
|
||||||
|
releaseLock();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateRepository = async (id: string, updates: { name?: string; compressionMode?: CompressionMode }) => {
|
const updateRepository = async (id: string, updates: { name?: string; compressionMode?: CompressionMode }) => {
|
||||||
const existing = await findRepository(id);
|
const existing = await findRepository(id);
|
||||||
|
|
||||||
|
|
@ -439,4 +473,6 @@ export const repositoriesService = {
|
||||||
checkHealth,
|
checkHealth,
|
||||||
doctorRepository,
|
doctorRepository,
|
||||||
deleteSnapshot,
|
deleteSnapshot,
|
||||||
|
deleteSnapshots,
|
||||||
|
tagSnapshots,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -567,11 +567,15 @@ const forget = async (config: RepositoryConfig, options: RetentionPolicy, extra:
|
||||||
return { success: true };
|
return { success: true };
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteSnapshot = async (config: RepositoryConfig, snapshotId: string) => {
|
const deleteSnapshots = async (config: RepositoryConfig, snapshotIds: string[]) => {
|
||||||
const repoUrl = buildRepoUrl(config);
|
const repoUrl = buildRepoUrl(config);
|
||||||
const env = await buildEnv(config);
|
const env = await buildEnv(config);
|
||||||
|
|
||||||
const args: string[] = ["--repo", repoUrl, "forget", snapshotId, "--prune"];
|
if (snapshotIds.length === 0) {
|
||||||
|
throw new Error("No snapshot IDs provided for deletion.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const args: string[] = ["--repo", repoUrl, "forget", ...snapshotIds, "--prune"];
|
||||||
addCommonArgs(args, env);
|
addCommonArgs(args, env);
|
||||||
|
|
||||||
const res = await safeSpawn({ command: "restic", args, env });
|
const res = await safeSpawn({ command: "restic", args, env });
|
||||||
|
|
@ -585,6 +589,55 @@ const deleteSnapshot = async (config: RepositoryConfig, snapshotId: string) => {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteSnapshot = async (config: RepositoryConfig, snapshotId: string) => {
|
||||||
|
return deleteSnapshots(config, [snapshotId]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tagSnapshots = async (
|
||||||
|
config: RepositoryConfig,
|
||||||
|
snapshotIds: string[],
|
||||||
|
tags: { add?: string[]; remove?: string[]; set?: string[] },
|
||||||
|
) => {
|
||||||
|
const repoUrl = buildRepoUrl(config);
|
||||||
|
const env = await buildEnv(config);
|
||||||
|
|
||||||
|
if (snapshotIds.length === 0) {
|
||||||
|
throw new Error("No snapshot IDs provided for tagging.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const args: string[] = ["--repo", repoUrl, "tag", ...snapshotIds];
|
||||||
|
|
||||||
|
if (tags.add) {
|
||||||
|
for (const tag of tags.add) {
|
||||||
|
args.push("--add", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tags.remove) {
|
||||||
|
for (const tag of tags.remove) {
|
||||||
|
args.push("--remove", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tags.set) {
|
||||||
|
for (const tag of tags.set) {
|
||||||
|
args.push("--set", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addCommonArgs(args, env);
|
||||||
|
|
||||||
|
const res = await safeSpawn({ command: "restic", args, env });
|
||||||
|
await cleanupTemporaryKeys(config, env);
|
||||||
|
|
||||||
|
if (res.exitCode !== 0) {
|
||||||
|
logger.error(`Restic snapshot tagging failed: ${res.stderr}`);
|
||||||
|
throw new ResticError(res.exitCode, res.stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
};
|
||||||
|
|
||||||
const lsNodeSchema = type({
|
const lsNodeSchema = type({
|
||||||
name: "string",
|
name: "string",
|
||||||
type: "string",
|
type: "string",
|
||||||
|
|
@ -841,6 +894,8 @@ export const restic = {
|
||||||
snapshots,
|
snapshots,
|
||||||
forget,
|
forget,
|
||||||
deleteSnapshot,
|
deleteSnapshot,
|
||||||
|
deleteSnapshots,
|
||||||
|
tagSnapshots,
|
||||||
unlock,
|
unlock,
|
||||||
ls,
|
ls,
|
||||||
check,
|
check,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue