Compare commits
1 commit
main
...
graphite-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ae5f73783 |
17 changed files with 2588 additions and 102 deletions
|
|
@ -4,8 +4,8 @@
|
|||
import { type DefaultError, type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query';
|
||||
|
||||
import { client } from '../client.gen';
|
||||
import { browseFilesystem, cancelDoctor, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteSnapshots, deleteSsoInvitation, deleteSsoProvider, deleteUserAccount, deleteVolume, downloadResticPassword, dumpSnapshot, getAdminUsers, getBackupProgress, getBackupSchedule, getBackupScheduleForVolume, getDevPanel, getMirrorCompatibility, getNotificationDestination, getOrgMembers, getPublicSsoProviders, getRegistrationStatus, getRepository, getRepositoryStats, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getSsoSettings, getStatus, getSystemInfo, getUpdates, getUserDeletionImpact, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, mountVolume, type Options, refreshSnapshots, removeOrgMember, reorderBackupSchedules, restoreSnapshot, runBackupNow, runForget, setRegistrationStatus, startDoctor, stopBackup, tagSnapshots, testConnection, testNotificationDestination, unlockRepository, unmountVolume, updateBackupSchedule, updateMemberRole, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateSsoProviderAutoLinking, updateVolume } from '../sdk.gen';
|
||||
import type { BrowseFilesystemData, BrowseFilesystemResponse, CancelDoctorData, CancelDoctorResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteSnapshotsData, DeleteSnapshotsResponse, DeleteSsoInvitationData, DeleteSsoProviderData, DeleteUserAccountData, DeleteVolumeData, DeleteVolumeResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, DumpSnapshotData, DumpSnapshotResponse, GetAdminUsersData, GetAdminUsersResponse, GetBackupProgressData, GetBackupProgressResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetDevPanelData, GetDevPanelResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetOrgMembersData, GetOrgMembersResponse, GetPublicSsoProvidersData, GetPublicSsoProvidersResponse, GetRegistrationStatusData, GetRegistrationStatusResponse, GetRepositoryData, GetRepositoryResponse, GetRepositoryStatsData, GetRepositoryStatsResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetSsoSettingsData, GetSsoSettingsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetUpdatesData, GetUpdatesResponse, GetUserDeletionImpactData, GetUserDeletionImpactResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, MountVolumeData, MountVolumeResponse, RefreshSnapshotsData, RefreshSnapshotsResponse, RemoveOrgMemberData, ReorderBackupSchedulesData, ReorderBackupSchedulesResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, SetRegistrationStatusData, SetRegistrationStatusResponse, StartDoctorData, StartDoctorResponse, StopBackupData, StopBackupResponse, TagSnapshotsData, TagSnapshotsResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnlockRepositoryData, UnlockRepositoryResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateMemberRoleData, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateSsoProviderAutoLinkingData, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
|
||||
import { browseFilesystem, cancelDoctor, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteSnapshots, deleteSsoInvitation, deleteSsoProvider, deleteUserAccount, deleteVolume, downloadResticPassword, dumpSnapshot, getAdminUsers, getBackupProgress, getBackupSchedule, getBackupScheduleForVolume, getDevPanel, getMirrorCompatibility, getNotificationDestination, getOrgMembers, getPublicSsoProviders, getRegistrationStatus, getRepository, getRepositoryStats, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getSsoSettings, getStatus, getSystemInfo, getUpdates, getUserDeletionImpact, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, mountVolume, type Options, refreshRepositoryStats, refreshSnapshots, removeOrgMember, reorderBackupSchedules, restoreSnapshot, runBackupNow, runForget, setRegistrationStatus, startDoctor, stopBackup, tagSnapshots, testConnection, testNotificationDestination, unlockRepository, unmountVolume, updateBackupSchedule, updateMemberRole, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateSsoProviderAutoLinking, updateVolume } from '../sdk.gen';
|
||||
import type { BrowseFilesystemData, BrowseFilesystemResponse, CancelDoctorData, CancelDoctorResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteSnapshotsData, DeleteSnapshotsResponse, DeleteSsoInvitationData, DeleteSsoProviderData, DeleteUserAccountData, DeleteVolumeData, DeleteVolumeResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, DumpSnapshotData, DumpSnapshotResponse, GetAdminUsersData, GetAdminUsersResponse, GetBackupProgressData, GetBackupProgressResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetDevPanelData, GetDevPanelResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetOrgMembersData, GetOrgMembersResponse, GetPublicSsoProvidersData, GetPublicSsoProvidersResponse, GetRegistrationStatusData, GetRegistrationStatusResponse, GetRepositoryData, GetRepositoryResponse, GetRepositoryStatsData, GetRepositoryStatsResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetSsoSettingsData, GetSsoSettingsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetUpdatesData, GetUpdatesResponse, GetUserDeletionImpactData, GetUserDeletionImpactResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, MountVolumeData, MountVolumeResponse, RefreshRepositoryStatsData, RefreshRepositoryStatsResponse, RefreshSnapshotsData, RefreshSnapshotsResponse, RemoveOrgMemberData, ReorderBackupSchedulesData, ReorderBackupSchedulesResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, SetRegistrationStatusData, SetRegistrationStatusResponse, StartDoctorData, StartDoctorResponse, StopBackupData, StopBackupResponse, TagSnapshotsData, TagSnapshotsResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnlockRepositoryData, UnlockRepositoryResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateMemberRoleData, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateSsoProviderAutoLinkingData, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
|
||||
|
||||
export type QueryKey<TOptions extends Options> = [
|
||||
Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & {
|
||||
|
|
@ -620,6 +620,23 @@ export const getRepositoryStatsOptions = (options: Options<GetRepositoryStatsDat
|
|||
queryKey: getRepositoryStatsQueryKey(options)
|
||||
});
|
||||
|
||||
/**
|
||||
* Refresh repository storage and compression statistics
|
||||
*/
|
||||
export const refreshRepositoryStatsMutation = (options?: Partial<Options<RefreshRepositoryStatsData>>): UseMutationOptions<RefreshRepositoryStatsResponse, DefaultError, Options<RefreshRepositoryStatsData>> => {
|
||||
const mutationOptions: UseMutationOptions<RefreshRepositoryStatsResponse, DefaultError, Options<RefreshRepositoryStatsData>> = {
|
||||
mutationFn: async (fnOptions) => {
|
||||
const { data } = await refreshRepositoryStats({
|
||||
...options,
|
||||
...fnOptions,
|
||||
throwOnError: true
|
||||
});
|
||||
return data;
|
||||
}
|
||||
};
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete multiple snapshots from a repository
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export {
|
|||
listVolumes,
|
||||
mountVolume,
|
||||
type Options,
|
||||
refreshRepositoryStats,
|
||||
refreshSnapshots,
|
||||
removeOrgMember,
|
||||
reorderBackupSchedules,
|
||||
|
|
@ -230,6 +231,9 @@ export type {
|
|||
MountVolumeData,
|
||||
MountVolumeResponse,
|
||||
MountVolumeResponses,
|
||||
RefreshRepositoryStatsData,
|
||||
RefreshRepositoryStatsResponse,
|
||||
RefreshRepositoryStatsResponses,
|
||||
RefreshSnapshotsData,
|
||||
RefreshSnapshotsResponse,
|
||||
RefreshSnapshotsResponses,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import type { Client, Options as Options2, TDataShape } from './client';
|
||||
import { client } from './client.gen';
|
||||
import type { BrowseFilesystemData, BrowseFilesystemResponses, CancelDoctorData, CancelDoctorErrors, CancelDoctorResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteSnapshotsData, DeleteSnapshotsResponses, DeleteSsoInvitationData, DeleteSsoInvitationErrors, DeleteSsoInvitationResponses, DeleteSsoProviderData, DeleteSsoProviderErrors, DeleteSsoProviderResponses, DeleteUserAccountData, DeleteUserAccountErrors, DeleteUserAccountResponses, DeleteVolumeData, DeleteVolumeResponses, DevPanelExecData, DevPanelExecErrors, DevPanelExecResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, DumpSnapshotData, DumpSnapshotResponses, GetAdminUsersData, GetAdminUsersResponses, GetBackupProgressData, GetBackupProgressResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetDevPanelData, GetDevPanelResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetOrgMembersData, GetOrgMembersResponses, GetPublicSsoProvidersData, GetPublicSsoProvidersResponses, GetRegistrationStatusData, GetRegistrationStatusResponses, GetRepositoryData, GetRepositoryResponses, GetRepositoryStatsData, GetRepositoryStatsResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetSsoSettingsData, GetSsoSettingsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetUpdatesData, GetUpdatesResponses, GetUserDeletionImpactData, GetUserDeletionImpactResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, MountVolumeData, MountVolumeResponses, RefreshSnapshotsData, RefreshSnapshotsResponses, RemoveOrgMemberData, RemoveOrgMemberErrors, RemoveOrgMemberResponses, ReorderBackupSchedulesData, ReorderBackupSchedulesResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, SetRegistrationStatusData, SetRegistrationStatusResponses, StartDoctorData, StartDoctorErrors, StartDoctorResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TagSnapshotsData, TagSnapshotsResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnlockRepositoryData, UnlockRepositoryResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateMemberRoleData, UpdateMemberRoleErrors, UpdateMemberRoleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateSsoProviderAutoLinkingData, UpdateSsoProviderAutoLinkingErrors, UpdateSsoProviderAutoLinkingResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
|
||||
import type { BrowseFilesystemData, BrowseFilesystemResponses, CancelDoctorData, CancelDoctorErrors, CancelDoctorResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteSnapshotsData, DeleteSnapshotsResponses, DeleteSsoInvitationData, DeleteSsoInvitationErrors, DeleteSsoInvitationResponses, DeleteSsoProviderData, DeleteSsoProviderErrors, DeleteSsoProviderResponses, DeleteUserAccountData, DeleteUserAccountErrors, DeleteUserAccountResponses, DeleteVolumeData, DeleteVolumeResponses, DevPanelExecData, DevPanelExecErrors, DevPanelExecResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, DumpSnapshotData, DumpSnapshotResponses, GetAdminUsersData, GetAdminUsersResponses, GetBackupProgressData, GetBackupProgressResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetDevPanelData, GetDevPanelResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetOrgMembersData, GetOrgMembersResponses, GetPublicSsoProvidersData, GetPublicSsoProvidersResponses, GetRegistrationStatusData, GetRegistrationStatusResponses, GetRepositoryData, GetRepositoryResponses, GetRepositoryStatsData, GetRepositoryStatsResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetSsoSettingsData, GetSsoSettingsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetUpdatesData, GetUpdatesResponses, GetUserDeletionImpactData, GetUserDeletionImpactResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, MountVolumeData, MountVolumeResponses, RefreshRepositoryStatsData, RefreshRepositoryStatsResponses, RefreshSnapshotsData, RefreshSnapshotsResponses, RemoveOrgMemberData, RemoveOrgMemberErrors, RemoveOrgMemberResponses, ReorderBackupSchedulesData, ReorderBackupSchedulesResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, SetRegistrationStatusData, SetRegistrationStatusResponses, StartDoctorData, StartDoctorErrors, StartDoctorResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TagSnapshotsData, TagSnapshotsResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnlockRepositoryData, UnlockRepositoryResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateMemberRoleData, UpdateMemberRoleErrors, UpdateMemberRoleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateSsoProviderAutoLinkingData, UpdateSsoProviderAutoLinkingErrors, UpdateSsoProviderAutoLinkingResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
|
||||
|
||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
||||
/**
|
||||
|
|
@ -218,6 +218,11 @@ export const updateRepository = <ThrowOnError extends boolean = false>(options:
|
|||
*/
|
||||
export const getRepositoryStats = <ThrowOnError extends boolean = false>(options: Options<GetRepositoryStatsData, ThrowOnError>) => (options.client ?? client).get<GetRepositoryStatsResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/stats', ...options });
|
||||
|
||||
/**
|
||||
* Refresh repository storage and compression statistics
|
||||
*/
|
||||
export const refreshRepositoryStats = <ThrowOnError extends boolean = false>(options: Options<RefreshRepositoryStatsData, ThrowOnError>) => (options.client ?? client).post<RefreshRepositoryStatsResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/stats/refresh', ...options });
|
||||
|
||||
/**
|
||||
* Delete multiple snapshots from a repository
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1985,6 +1985,31 @@ export type GetRepositoryStatsResponses = {
|
|||
|
||||
export type GetRepositoryStatsResponse = GetRepositoryStatsResponses[keyof GetRepositoryStatsResponses];
|
||||
|
||||
export type RefreshRepositoryStatsData = {
|
||||
body?: never;
|
||||
path: {
|
||||
shortId: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/repositories/{shortId}/stats/refresh';
|
||||
};
|
||||
|
||||
export type RefreshRepositoryStatsResponses = {
|
||||
/**
|
||||
* Refreshed repository statistics
|
||||
*/
|
||||
200: {
|
||||
compression_progress?: number;
|
||||
compression_ratio?: number;
|
||||
compression_space_saving?: number;
|
||||
snapshots_count?: number;
|
||||
total_size?: number;
|
||||
total_uncompressed_size?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type RefreshRepositoryStatsResponse = RefreshRepositoryStatsResponses[keyof RefreshRepositoryStatsResponses];
|
||||
|
||||
export type DeleteSnapshotsData = {
|
||||
body: {
|
||||
snapshotIds: Array<string>;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Archive } from "lucide-react";
|
||||
import { getRepositoryStatsOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { ByteSize } from "~/client/components/bytes-size";
|
||||
import { Card, CardContent, CardTitle } from "~/client/components/ui/card";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Archive, RefreshCw } from "lucide-react";
|
||||
import {
|
||||
getRepositoryStatsOptions,
|
||||
refreshRepositoryStatsMutation,
|
||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import type { GetRepositoryStatsResponse } from "~/client/api-client/types.gen";
|
||||
import { ByteSize } from "~/client/components/bytes-size";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
import { Card, CardContent, CardTitle } from "~/client/components/ui/card";
|
||||
import { parseError } from "~/client/lib/errors";
|
||||
import { cn } from "~/client/lib/utils";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type Props = {
|
||||
repositoryShortId: string;
|
||||
|
|
@ -19,6 +26,18 @@ const toSafeNumber = (value: number | undefined) => {
|
|||
};
|
||||
|
||||
export function CompressionStatsChart({ repositoryShortId, initialStats }: Props) {
|
||||
const refreshStats = useMutation({
|
||||
...refreshRepositoryStatsMutation(),
|
||||
onSuccess: () => {
|
||||
toast.success("Repository stats refreshed");
|
||||
},
|
||||
onError: (mutationError) => {
|
||||
toast.error("Failed to refresh repository stats", {
|
||||
description: parseError(mutationError)?.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
data: stats,
|
||||
isPending,
|
||||
|
|
@ -43,43 +62,39 @@ export function CompressionStatsChart({ repositoryShortId, initialStats }: Props
|
|||
|
||||
const hasStats = !!stats && (storedSize > 0 || uncompressedSize > 0 || snapshotsCount > 0);
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<p className="text-sm text-muted-foreground">Loading compression statistics...</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card className="p-6 border-red-500/20 bg-red-500/5">
|
||||
<p className="text-sm font-medium text-red-500">Failed to load compression statistics</p>
|
||||
<p className="mt-2 text-sm text-red-500/80 wrap-break-word">{error.message}</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasStats) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No compression statistics available yet. Run a backup to populate repository stats.
|
||||
</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col px-6 py-6">
|
||||
<div className="pb-4">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<div className="flex items-start justify-between gap-3 pb-4">
|
||||
<CardTitle className="flex items-center gap-2 w-full">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<Archive className="h-5 w-5 text-muted-foreground" />
|
||||
Compression Statistics
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => refreshStats.mutate({ path: { shortId: repositoryShortId } })}
|
||||
disabled={refreshStats.isPending}
|
||||
title="Refresh statistics"
|
||||
>
|
||||
<RefreshCw className={refreshStats.isPending ? "h-4 w-4 animate-spin" : "h-4 w-4"} />
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</div>
|
||||
<div>
|
||||
<CardContent className="grid grid-cols-2 lg:grid-cols-3 gap-y-6 gap-x-4 px-0">
|
||||
<p className={cn("text-sm text-muted-foreground", { hidden: !isPending })}>Loading compression statistics...</p>
|
||||
<div className={cn("space-y-2", { hidden: !error || isPending })}>
|
||||
<p className="text-sm font-medium text-destructive">Failed to load compression statistics</p>
|
||||
<p className="text-sm text-muted-foreground wrap-break-word">{error?.message}</p>
|
||||
</div>
|
||||
<p className={cn("text-sm text-muted-foreground", { hidden: isPending || !!error || hasStats })}>
|
||||
Stats will be populated after your first backup. You can also refresh them manually.
|
||||
</p>
|
||||
<CardContent
|
||||
className={cn("grid grid-cols-2 lg:grid-cols-3 gap-y-6 gap-x-4 px-0", {
|
||||
hidden: isPending || !!error || !hasStats,
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="text-sm font-medium text-muted-foreground">Stored Size</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
|
|
@ -110,7 +125,7 @@ export function CompressionStatsChart({ repositoryShortId, initialStats }: Props
|
|||
<div className="text-sm font-medium text-muted-foreground">Ratio</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-xl font-semibold text-foreground font-mono">
|
||||
{compressionRatio > 0 ? `${compressionRatio.toFixed(2)}x` : "—"}
|
||||
{compressionRatio > 0 ? `${compressionRatio.toFixed(2)}x` : "-"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -125,7 +140,6 @@ export function CompressionStatsChart({ repositoryShortId, initialStats }: Props
|
|||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
2
app/drizzle/20260305160806_watery_iron_lad/migration.sql
Normal file
2
app/drizzle/20260305160806_watery_iron_lad/migration.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE `repositories_table` ADD `stats` text;--> statement-breakpoint
|
||||
ALTER TABLE `repositories_table` ADD `stats_updated_at` integer;
|
||||
2215
app/drizzle/20260305160806_watery_iron_lad/snapshot.json
Normal file
2215
app/drizzle/20260305160806_watery_iron_lad/snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -8,6 +8,7 @@ import type {
|
|||
BandwidthUnit,
|
||||
DoctorResult,
|
||||
} from "~/schemas/restic";
|
||||
import type { ResticStatsDto } from "~/schemas/restic-dto";
|
||||
import type { BackendStatus, BackendType, volumeConfigSchema } from "~/schemas/volumes";
|
||||
import type { NotificationType, notificationConfigSchema } from "~/schemas/notifications";
|
||||
import type { ShortId } from "~/server/utils/branded";
|
||||
|
|
@ -241,6 +242,8 @@ export const repositoriesTable = sqliteTable("repositories_table", {
|
|||
lastChecked: int("last_checked", { mode: "number" }),
|
||||
lastError: text("last_error"),
|
||||
doctorResult: text("doctor_result", { mode: "json" }).$type<DoctorResult>(),
|
||||
stats: text("stats", { mode: "json" }).$type<ResticStatsDto | null>(),
|
||||
statsUpdatedAt: int("stats_updated_at", { mode: "number" }),
|
||||
uploadLimitEnabled: int("upload_limit_enabled", { mode: "boolean" }).notNull().default(false),
|
||||
uploadLimitValue: real("upload_limit_value").notNull().default(1),
|
||||
uploadLimitUnit: text("upload_limit_unit").$type<BandwidthUnit>().notNull().default("Mbps"),
|
||||
|
|
|
|||
|
|
@ -14,21 +14,34 @@ import { restic } from "~/server/utils/restic";
|
|||
import { NotFoundError, BadRequestError } from "http-errors-enhanced";
|
||||
import { scheduleQueries } from "../backups.queries";
|
||||
import { fromAny } from "@total-typescript/shoehorn";
|
||||
import { repositoriesService } from "~/server/modules/repositories/repositories.service";
|
||||
|
||||
const setup = () => {
|
||||
const resticBackupMock = mock(() => Promise.resolve({ exitCode: 0, summary: generateBackupOutput(), error: "" }));
|
||||
const resticForgetMock = mock(() => Promise.resolve({ success: true, data: null }));
|
||||
const resticCopyMock = mock(() => Promise.resolve({ success: true, output: "" }));
|
||||
const refreshStatsMock = mock(() =>
|
||||
Promise.resolve({
|
||||
total_size: 0,
|
||||
total_uncompressed_size: 0,
|
||||
compression_ratio: 0,
|
||||
compression_progress: 0,
|
||||
compression_space_saving: 0,
|
||||
snapshots_count: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
spyOn(spawnModule, "safeSpawn").mockImplementation(resticBackupMock);
|
||||
spyOn(restic, "forget").mockImplementation(resticForgetMock);
|
||||
spyOn(restic, "copy").mockImplementation(resticCopyMock);
|
||||
spyOn(repositoriesService, "refreshRepositoryStats").mockImplementation(refreshStatsMock);
|
||||
spyOn(context, "getOrganizationId").mockReturnValue(TEST_ORG_ID);
|
||||
|
||||
return {
|
||||
resticBackupMock,
|
||||
resticForgetMock,
|
||||
resticCopyMock,
|
||||
refreshStatsMock,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -13,14 +13,27 @@ import { backupScheduleMirrorsTable, repositoriesTable, volumesTable } from "~/s
|
|||
import { TEST_ORG_ID } from "~/test/helpers/organization";
|
||||
import * as context from "~/server/core/request-context";
|
||||
import { backupsExecutionService } from "../backups.execution";
|
||||
import { repositoriesService } from "~/server/modules/repositories/repositories.service";
|
||||
|
||||
const setup = () => {
|
||||
const resticBackupMock = mock(() => Promise.resolve({ exitCode: 0, summary: "", error: "" }));
|
||||
const refreshStatsMock = mock(() =>
|
||||
Promise.resolve({
|
||||
total_size: 0,
|
||||
total_uncompressed_size: 0,
|
||||
compression_ratio: 0,
|
||||
compression_progress: 0,
|
||||
compression_space_saving: 0,
|
||||
snapshots_count: 0,
|
||||
}),
|
||||
);
|
||||
spyOn(spawnModule, "safeSpawn").mockImplementation(resticBackupMock);
|
||||
spyOn(repositoriesService, "refreshRepositoryStats").mockImplementation(refreshStatsMock);
|
||||
spyOn(context, "getOrganizationId").mockReturnValue(TEST_ORG_ID);
|
||||
|
||||
return {
|
||||
resticBackupMock,
|
||||
refreshStatsMock,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { toMessage } from "../../utils/errors";
|
|||
import { serverEvents } from "../../core/events";
|
||||
import { notificationsService } from "../notifications/notifications.service";
|
||||
import { repoMutex } from "../../core/repository-mutex";
|
||||
import { repositoriesService } from "../repositories/repositories.service";
|
||||
import { getOrganizationId } from "~/server/core/request-context";
|
||||
import { scheduleQueries, mirrorQueries, repositoryQueries } from "./backups.queries";
|
||||
import { calculateNextRun, createBackupOptions } from "./backup.helpers";
|
||||
|
|
@ -163,6 +164,12 @@ const finalizeSuccessfulBackup = async (
|
|||
|
||||
cache.delByPrefix(cacheKeys.repository.all(ctx.repository.id));
|
||||
|
||||
void repositoriesService.refreshRepositoryStats(ctx.repository.shortId).catch((error) => {
|
||||
logger.error(
|
||||
`Background repository stats refresh failed for schedule ${scheduleId} (${ctx.repository.shortId}): ${toMessage(error)}`,
|
||||
);
|
||||
});
|
||||
|
||||
const nextBackupAt = calculateNextRun(ctx.schedule.cronExpression);
|
||||
await scheduleQueries.updateStatus(scheduleId, ctx.organizationId, {
|
||||
lastBackupAt: Date.now(),
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ describe("repositories security", () => {
|
|||
{ method: "GET", path: "/api/v1/repositories/rclone-remotes" },
|
||||
{ method: "GET", path: "/api/v1/repositories/test-repo" },
|
||||
{ method: "GET", path: "/api/v1/repositories/test-repo/stats" },
|
||||
{ method: "POST", path: "/api/v1/repositories/test-repo/stats/refresh" },
|
||||
{ method: "DELETE", path: "/api/v1/repositories/test-repo" },
|
||||
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots" },
|
||||
{ method: "POST", path: "/api/v1/repositories/test-repo/snapshots/refresh" },
|
||||
|
|
@ -146,6 +147,18 @@ describe("repositories security", () => {
|
|||
expect(body.message).toBe("Repository not found");
|
||||
});
|
||||
|
||||
test("should return 404 for stats refresh of non-existent repository", async () => {
|
||||
const { headers } = await createTestSession();
|
||||
const res = await app.request("/api/v1/repositories/non-existent-repo/stats/refresh", {
|
||||
method: "POST",
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Repository not found");
|
||||
});
|
||||
|
||||
test("should return 400 for invalid payload on create", async () => {
|
||||
const { headers } = await createTestSession();
|
||||
const res = await app.request("/api/v1/repositories", {
|
||||
|
|
|
|||
|
|
@ -135,6 +135,63 @@ describe("repositoriesService.createRepository", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("repositoriesService repository stats", () => {
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
test("returns empty stats when repository has not been populated yet", async () => {
|
||||
const { organizationId, user } = await createTestSession();
|
||||
const repository = await createTestRepository(organizationId);
|
||||
|
||||
const stats = await withContext({ organizationId, userId: user.id }, () =>
|
||||
repositoriesService.getRepositoryStats(repository.shortId),
|
||||
);
|
||||
|
||||
expect(stats).toEqual({
|
||||
total_size: 0,
|
||||
total_uncompressed_size: 0,
|
||||
compression_ratio: 0,
|
||||
compression_progress: 0,
|
||||
compression_space_saving: 0,
|
||||
snapshots_count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test("refreshes and persists repository stats", async () => {
|
||||
const { organizationId, user } = await createTestSession();
|
||||
const repository = await createTestRepository(organizationId);
|
||||
const expectedStats = {
|
||||
total_size: 1024,
|
||||
total_uncompressed_size: 2048,
|
||||
compression_ratio: 2,
|
||||
compression_progress: 50,
|
||||
compression_space_saving: 50,
|
||||
snapshots_count: 3,
|
||||
};
|
||||
|
||||
const statsSpy = spyOn(restic, "stats").mockResolvedValue(expectedStats);
|
||||
|
||||
const refreshed = await withContext({ organizationId, userId: user.id }, () =>
|
||||
repositoriesService.refreshRepositoryStats(repository.shortId),
|
||||
);
|
||||
|
||||
expect(refreshed).toEqual(expectedStats);
|
||||
expect(statsSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
const persistedRepository = await db.query.repositoriesTable.findFirst({ where: { id: repository.id } });
|
||||
expect(persistedRepository?.stats).toEqual(expectedStats);
|
||||
expect(typeof persistedRepository?.statsUpdatedAt).toBe("number");
|
||||
|
||||
const loaded = await withContext({ organizationId, userId: user.id }, () =>
|
||||
repositoriesService.getRepositoryStats(repository.shortId),
|
||||
);
|
||||
|
||||
expect(loaded).toEqual(expectedStats);
|
||||
expect(statsSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("repositoriesService.dumpSnapshot", () => {
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
|
|
@ -337,6 +394,34 @@ describe("repositoriesService.deleteSnapshot", () => {
|
|||
mock.restore();
|
||||
});
|
||||
|
||||
test("refreshes repository stats in background after successful deletion", async () => {
|
||||
const { organizationId, user } = await createTestSession();
|
||||
const repository = await createTestRepository(organizationId);
|
||||
const expectedStats = {
|
||||
total_size: 128,
|
||||
total_uncompressed_size: 256,
|
||||
compression_ratio: 2,
|
||||
compression_progress: 50,
|
||||
compression_space_saving: 50,
|
||||
snapshots_count: 1,
|
||||
};
|
||||
|
||||
spyOn(restic, "deleteSnapshot").mockResolvedValue({ success: true });
|
||||
const statsSpy = spyOn(restic, "stats").mockResolvedValue(expectedStats);
|
||||
|
||||
await withContext({ organizationId, userId: user.id }, () =>
|
||||
repositoriesService.deleteSnapshot(repository.shortId, "snap-1"),
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
|
||||
expect(statsSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
const updatedRepository = await db.query.repositoriesTable.findFirst({ where: { id: repository.id } });
|
||||
expect(updatedRepository?.stats).toEqual(expectedStats);
|
||||
expect(typeof updatedRepository?.statsUpdatedAt).toBe("number");
|
||||
});
|
||||
|
||||
test("should throw original error when restic deleteSnapshot fails", async () => {
|
||||
const { organizationId, user } = await createTestSession();
|
||||
const repository = await createTestRepository(organizationId);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
cancelDoctorDto,
|
||||
getRepositoryDto,
|
||||
getRepositoryStatsDto,
|
||||
refreshRepositoryStatsDto,
|
||||
getSnapshotDetailsDto,
|
||||
refreshSnapshotsDto,
|
||||
listRcloneRemotesDto,
|
||||
|
|
@ -40,6 +41,7 @@ import {
|
|||
type CancelDoctorDto,
|
||||
type GetRepositoryDto,
|
||||
type GetRepositoryStatsDto,
|
||||
type RefreshRepositoryStatsDto,
|
||||
type GetSnapshotDetailsDto,
|
||||
type RefreshSnapshotsDto,
|
||||
type ListRepositoriesDto,
|
||||
|
|
@ -98,6 +100,12 @@ export const repositoriesController = new Hono()
|
|||
|
||||
return c.json<GetRepositoryStatsDto>(stats, 200);
|
||||
})
|
||||
.post("/:shortId/stats/refresh", refreshRepositoryStatsDto, async (c) => {
|
||||
const shortId = asShortId(c.req.param("shortId"));
|
||||
const stats = await repositoriesService.refreshRepositoryStats(shortId);
|
||||
|
||||
return c.json<RefreshRepositoryStatsDto>(stats, 200);
|
||||
})
|
||||
.delete("/:shortId", deleteRepositoryDto, async (c) => {
|
||||
const shortId = asShortId(c.req.param("shortId"));
|
||||
await repositoriesService.deleteRepository(shortId);
|
||||
|
|
|
|||
|
|
@ -132,6 +132,25 @@ export const getRepositoryStatsDto = describeRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const refreshRepositoryStatsResponse = repositoryStatsSchema;
|
||||
export type RefreshRepositoryStatsDto = typeof refreshRepositoryStatsResponse.infer;
|
||||
|
||||
export const refreshRepositoryStatsDto = describeRoute({
|
||||
description: "Refresh repository storage and compression statistics",
|
||||
tags: ["Repositories"],
|
||||
operationId: "refreshRepositoryStats",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Refreshed repository statistics",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(refreshRepositoryStatsResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete a repository
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
type RepositoryConfig,
|
||||
repositoryConfigSchema,
|
||||
} from "~/schemas/restic";
|
||||
import type { ResticStatsDto } from "~/schemas/restic-dto";
|
||||
import { config as appConfig } from "~/server/core/config";
|
||||
import { serverEvents } from "~/server/core/events";
|
||||
import { getOrganizationId } from "~/server/core/request-context";
|
||||
|
|
@ -16,7 +17,7 @@ import { logger } from "~/server/utils/logger";
|
|||
import { parseRetentionCategories, type RetentionCategory } from "~/server/utils/retention-categories";
|
||||
import { repoMutex } from "../../core/repository-mutex";
|
||||
import { db } from "../../db/db";
|
||||
import { repositoriesTable } from "../../db/schema";
|
||||
import { repositoriesTable, type Repository } from "../../db/schema";
|
||||
import { cache, cacheKeys } from "../../utils/cache";
|
||||
import { cryptoUtils } from "../../utils/crypto";
|
||||
import { toMessage } from "../../utils/errors";
|
||||
|
|
@ -32,6 +33,15 @@ import type { ShortId } from "~/server/utils/branded";
|
|||
|
||||
const runningDoctors = new Map<string, AbortController>();
|
||||
|
||||
const emptyRepositoryStats: ResticStatsDto = {
|
||||
total_size: 0,
|
||||
total_uncompressed_size: 0,
|
||||
compression_ratio: 0,
|
||||
compression_progress: 0,
|
||||
compression_space_saving: 0,
|
||||
snapshots_count: 0,
|
||||
};
|
||||
|
||||
const findRepository = async (shortId: ShortId) => {
|
||||
const organizationId = getOrganizationId();
|
||||
return await db.query.repositoriesTable.findFirst({
|
||||
|
|
@ -197,28 +207,42 @@ const getRepository = async (shortId: ShortId) => {
|
|||
return { repository };
|
||||
};
|
||||
|
||||
const getRepositoryStats = async (shortId: ShortId) => {
|
||||
const organizationId = getOrganizationId();
|
||||
const runAndStoreRepositoryStats = async (repository: Repository): Promise<ResticStatsDto> => {
|
||||
const releaseLock = await repoMutex.acquireShared(repository.id, "stats");
|
||||
try {
|
||||
const stats = await restic.stats(repository.config, { organizationId: repository.organizationId });
|
||||
|
||||
await db
|
||||
.update(repositoriesTable)
|
||||
.set({ stats, statsUpdatedAt: Date.now() })
|
||||
.where(
|
||||
and(eq(repositoriesTable.id, repository.id), eq(repositoriesTable.organizationId, repository.organizationId)),
|
||||
);
|
||||
|
||||
return stats;
|
||||
} finally {
|
||||
releaseLock();
|
||||
}
|
||||
};
|
||||
|
||||
const refreshRepositoryStats = async (shortId: ShortId) => {
|
||||
const repository = await findRepository(shortId);
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
}
|
||||
|
||||
const cacheKey = cacheKeys.repository.stats(repository.id);
|
||||
const cached = cache.get<Awaited<ReturnType<typeof restic.stats>>>(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
return runAndStoreRepositoryStats(repository);
|
||||
};
|
||||
|
||||
const getRepositoryStats = async (shortId: ShortId) => {
|
||||
const repository = await findRepository(shortId);
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
}
|
||||
|
||||
const releaseLock = await repoMutex.acquireShared(repository.id, "stats");
|
||||
try {
|
||||
const stats = await restic.stats(repository.config, { organizationId });
|
||||
cache.set(cacheKey, stats);
|
||||
return stats;
|
||||
} finally {
|
||||
releaseLock();
|
||||
}
|
||||
return repository.stats ?? { ...emptyRepositoryStats };
|
||||
};
|
||||
|
||||
const deleteRepository = async (shortId: ShortId) => {
|
||||
|
|
@ -616,6 +640,11 @@ const deleteSnapshot = async (shortId: ShortId, snapshotId: string) => {
|
|||
try {
|
||||
await restic.deleteSnapshot(repository.config, snapshotId, organizationId);
|
||||
cache.delByPrefix(cacheKeys.repository.all(repository.id));
|
||||
void runAndStoreRepositoryStats(repository).catch((error) => {
|
||||
logger.error(
|
||||
`Failed to refresh repository stats after snapshot deletion for ${repository.shortId}: ${toMessage(error)}`,
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
releaseLock();
|
||||
}
|
||||
|
|
@ -629,13 +658,25 @@ const deleteSnapshots = async (shortId: ShortId, snapshotIds: string[]) => {
|
|||
throw new NotFoundError("Repository not found");
|
||||
}
|
||||
|
||||
let shouldRefreshStats = false;
|
||||
const releaseLock = await repoMutex.acquireExclusive(repository.id, `delete:bulk`);
|
||||
try {
|
||||
await restic.deleteSnapshots(repository.config, snapshotIds, organizationId);
|
||||
cache.delByPrefix(cacheKeys.repository.all(repository.id));
|
||||
shouldRefreshStats = true;
|
||||
} finally {
|
||||
releaseLock();
|
||||
}
|
||||
|
||||
if (!shouldRefreshStats) {
|
||||
return;
|
||||
}
|
||||
|
||||
void runAndStoreRepositoryStats(repository).catch((error) => {
|
||||
logger.error(
|
||||
`Failed to refresh repository stats after snapshot deletion for ${repository.shortId}: ${toMessage(error)}`,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const tagSnapshots = async (
|
||||
|
|
@ -734,6 +775,8 @@ const updateRepository = async (shortId: ShortId, updates: UpdateRepositoryBody)
|
|||
lastChecked: null,
|
||||
lastError: null,
|
||||
doctorResult: null,
|
||||
stats: null,
|
||||
statsUpdatedAt: null,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
|
@ -852,6 +895,7 @@ export const repositoriesService = {
|
|||
createRepository,
|
||||
getRepository,
|
||||
getRepositoryStats,
|
||||
refreshRepositoryStats,
|
||||
deleteRepository,
|
||||
updateRepository,
|
||||
listSnapshots,
|
||||
|
|
|
|||
|
|
@ -108,7 +108,6 @@ export const createCache = (options: CacheOptions = {}) => {
|
|||
export const cacheKeys = {
|
||||
repository: {
|
||||
all: (repositoryId: string) => `repo:${repositoryId}:`,
|
||||
stats: (repositoryId: string) => `repo:${repositoryId}:stats`,
|
||||
snapshots: (repositoryId: string, backupId = "all") => `repo:${repositoryId}:snapshots:${backupId}`,
|
||||
ls: (repositoryId: string, snapshotId: string, path = "root", offset: number, limit: number) =>
|
||||
`repo:${repositoryId}:ls:${snapshotId}:${path}:${offset}:${limit}`,
|
||||
|
|
|
|||
Loading…
Reference in a new issue