mirror of
https://github.com/nicotsx/zerobyte.git
synced 2026-07-30 17:46:14 -04:00
feat: handle native windows snapshot restore paths
Add support for snapshot restore on windows
This commit is contained in:
@@ -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, createApiKey, createBackupSchedule, createDesktopSession, createNotificationDestination, createRepository, createVolume, deleteApiKey, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteSnapshots, deleteSsoInvitation, deleteSsoProvider, deleteUserAccount, deleteVolume, downloadResticPassword, dumpSnapshot, getAdminUsers, getApiKeys, getBackupProgress, getBackupSchedule, getBackupScheduleForVolume, getDevPanel, getMirrorCompatibility, getMirrorSyncStatus, getNotificationDestination, getOrgMembers, getPasswordLoginStatus, getPublicSsoProviders, getRegistrationStatus, getRepository, getRepositoryStats, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getSsoSettings, getStatus, getSystemInfo, getUpdates, getUserDeletionImpact, getUserSsoInvitations, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, mountVolume, type Options, refreshRepositoryStats, refreshSnapshots, removeOrgMember, reorderBackupSchedules, restoreSnapshot, runBackupNow, runForget, setPasswordLoginStatus, setRegistrationStatus, startDoctor, startInvitationSsoVerification, stopBackup, syncMirror, tagSnapshots, testConnection, testNotificationDestination, unlockRepository, unmountVolume, updateBackupSchedule, updateMemberRole, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateSsoProviderAutoLinking, updateVolume } from '../sdk.gen';
|
||||
import type { BrowseFilesystemData, BrowseFilesystemResponse, CancelDoctorData, CancelDoctorResponse, CreateApiKeyData, CreateApiKeyResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateDesktopSessionData, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteApiKeyData, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteSnapshotsData, DeleteSnapshotsResponse, DeleteSsoInvitationData, DeleteSsoProviderData, DeleteUserAccountData, DeleteVolumeData, DeleteVolumeResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, DumpSnapshotData, DumpSnapshotResponse, GetAdminUsersData, GetAdminUsersResponse, GetApiKeysData, GetApiKeysResponse, GetBackupProgressData, GetBackupProgressResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetDevPanelData, GetDevPanelResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetMirrorSyncStatusData, GetMirrorSyncStatusResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetOrgMembersData, GetOrgMembersResponse, GetPasswordLoginStatusData, GetPasswordLoginStatusResponse, GetPublicSsoProvidersData, GetPublicSsoProvidersResponse, GetRegistrationStatusData, GetRegistrationStatusResponse, GetRepositoryData, GetRepositoryResponse, GetRepositoryStatsData, GetRepositoryStatsResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetSsoSettingsData, GetSsoSettingsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetUpdatesData, GetUpdatesResponse, GetUserDeletionImpactData, GetUserDeletionImpactResponse, GetUserSsoInvitationsData, GetUserSsoInvitationsResponse, 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, SetPasswordLoginStatusData, SetPasswordLoginStatusResponse, SetRegistrationStatusData, SetRegistrationStatusResponse, StartDoctorData, StartDoctorResponse, StartInvitationSsoVerificationData, StopBackupData, StopBackupResponse, SyncMirrorData, SyncMirrorResponse, 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, createApiKey, createBackupSchedule, createDesktopSession, createNotificationDestination, createRepository, createVolume, deleteApiKey, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteSnapshots, deleteSsoInvitation, deleteSsoProvider, deleteUserAccount, deleteVolume, downloadResticPassword, dumpSnapshot, getAdminUsers, getApiKeys, getBackupProgress, getBackupSchedule, getBackupScheduleForVolume, getDevPanel, getMirrorCompatibility, getMirrorSyncStatus, getNotificationDestination, getOrgMembers, getPasswordLoginStatus, getPublicSsoProviders, getRegistrationStatus, getRepository, getRepositoryStats, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getSnapshotRestorePlan, getSsoSettings, getStatus, getSystemInfo, getUpdates, getUserDeletionImpact, getUserSsoInvitations, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, mountVolume, type Options, refreshRepositoryStats, refreshSnapshots, removeOrgMember, reorderBackupSchedules, restoreSnapshot, runBackupNow, runForget, setPasswordLoginStatus, setRegistrationStatus, startDoctor, startInvitationSsoVerification, stopBackup, syncMirror, tagSnapshots, testConnection, testNotificationDestination, unlockRepository, unmountVolume, updateBackupSchedule, updateMemberRole, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateSsoProviderAutoLinking, updateVolume } from '../sdk.gen';
|
||||
import type { BrowseFilesystemData, BrowseFilesystemResponse, CancelDoctorData, CancelDoctorResponse, CreateApiKeyData, CreateApiKeyResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateDesktopSessionData, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteApiKeyData, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteSnapshotsData, DeleteSnapshotsResponse, DeleteSsoInvitationData, DeleteSsoProviderData, DeleteUserAccountData, DeleteVolumeData, DeleteVolumeResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, DumpSnapshotData, DumpSnapshotResponse, GetAdminUsersData, GetAdminUsersResponse, GetApiKeysData, GetApiKeysResponse, GetBackupProgressData, GetBackupProgressResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetDevPanelData, GetDevPanelResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetMirrorSyncStatusData, GetMirrorSyncStatusResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetOrgMembersData, GetOrgMembersResponse, GetPasswordLoginStatusData, GetPasswordLoginStatusResponse, GetPublicSsoProvidersData, GetPublicSsoProvidersResponse, GetRegistrationStatusData, GetRegistrationStatusResponse, GetRepositoryData, GetRepositoryResponse, GetRepositoryStatsData, GetRepositoryStatsResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetSnapshotRestorePlanData, GetSnapshotRestorePlanResponse, GetSsoSettingsData, GetSsoSettingsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetUpdatesData, GetUpdatesResponse, GetUserDeletionImpactData, GetUserDeletionImpactResponse, GetUserSsoInvitationsData, GetUserSsoInvitationsResponse, 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, SetPasswordLoginStatusData, SetPasswordLoginStatusResponse, SetRegistrationStatusData, SetRegistrationStatusResponse, StartDoctorData, StartDoctorResponse, StartInvitationSsoVerificationData, StopBackupData, StopBackupResponse, SyncMirrorData, SyncMirrorResponse, 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'> & {
|
||||
@@ -811,6 +811,24 @@ export const getSnapshotDetailsOptions = (options: Options<GetSnapshotDetailsDat
|
||||
queryKey: getSnapshotDetailsQueryKey(options)
|
||||
});
|
||||
|
||||
export const getSnapshotRestorePlanQueryKey = (options: Options<GetSnapshotRestorePlanData>) => createQueryKey('getSnapshotRestorePlan', options);
|
||||
|
||||
/**
|
||||
* Get a target-scoped restore plan for a snapshot
|
||||
*/
|
||||
export const getSnapshotRestorePlanOptions = (options: Options<GetSnapshotRestorePlanData>) => queryOptions<GetSnapshotRestorePlanResponse, DefaultError, GetSnapshotRestorePlanResponse, ReturnType<typeof getSnapshotRestorePlanQueryKey>>({
|
||||
queryFn: async ({ queryKey, signal }) => {
|
||||
const { data } = await getSnapshotRestorePlan({
|
||||
...options,
|
||||
...queryKey[0],
|
||||
signal,
|
||||
throwOnError: true
|
||||
});
|
||||
return data;
|
||||
},
|
||||
queryKey: getSnapshotRestorePlanQueryKey(options)
|
||||
});
|
||||
|
||||
export const listSnapshotFilesQueryKey = (options: Options<ListSnapshotFilesData>) => createQueryKey('listSnapshotFiles', options);
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,8 @@ export type { QuerySerializerOptions } from "../core/bodySerializer.gen";
|
||||
export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer } from "../core/bodySerializer.gen";
|
||||
export { buildClientParams } from "../core/params.gen";
|
||||
export { serializeQueryKeyValue } from "../core/queryKeySerializer.gen";
|
||||
export type { ServerSentEventsResult } from "../core/serverSentEvents.gen";
|
||||
export type { ClientMeta } from "../core/types.gen";
|
||||
export { createClient } from "./client.gen";
|
||||
export type {
|
||||
Client,
|
||||
|
||||
@@ -15,8 +15,8 @@ import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'
|
||||
export const createQuerySerializer = <T = unknown>({
|
||||
parameters = {},
|
||||
...args
|
||||
}: QuerySerializerOptions = {}) => {
|
||||
const querySerializer = (queryParams: T) => {
|
||||
}: QuerySerializerOptions = {}): ((queryParams: T) => string) => {
|
||||
const querySerializer = (queryParams: T): string => {
|
||||
const search: string[] = [];
|
||||
if (queryParams && typeof queryParams === 'object') {
|
||||
for (const name in queryParams) {
|
||||
|
||||
@@ -10,6 +10,13 @@ export interface Auth {
|
||||
* @default 'header'
|
||||
*/
|
||||
in?: 'header' | 'query' | 'cookie';
|
||||
/**
|
||||
* A unique identifier for the security scheme.
|
||||
*
|
||||
* Defined only when there are multiple security schemes whose `Auth`
|
||||
* shape would otherwise be identical.
|
||||
*/
|
||||
key?: string;
|
||||
/**
|
||||
* Header or query parameter name.
|
||||
*
|
||||
|
||||
@@ -63,7 +63,7 @@ type KeyMap = Map<
|
||||
}
|
||||
>;
|
||||
|
||||
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
|
||||
function buildKeyMap(fields: FieldsConfig, map?: KeyMap): KeyMap {
|
||||
if (!map) {
|
||||
map = new Map();
|
||||
}
|
||||
@@ -86,7 +86,7 @@ const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
|
||||
}
|
||||
|
||||
return map;
|
||||
};
|
||||
}
|
||||
|
||||
interface Params {
|
||||
body: unknown;
|
||||
@@ -95,16 +95,18 @@ interface Params {
|
||||
query: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const stripEmptySlots = (params: Params) => {
|
||||
type ParamsSlotMap = Record<Slot, unknown>;
|
||||
|
||||
function stripEmptySlots(params: ParamsSlotMap): void {
|
||||
for (const [slot, value] of Object.entries(params)) {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) {
|
||||
delete params[slot as Slot];
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => {
|
||||
const params: Params = {
|
||||
export function buildClientParams(args: ReadonlyArray<unknown>, fields: FieldsConfig): Params {
|
||||
const params: ParamsSlotMap = {
|
||||
body: Object.create(null),
|
||||
headers: Object.create(null),
|
||||
path: Object.create(null),
|
||||
@@ -166,5 +168,5 @@ export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsCo
|
||||
|
||||
stripEmptySlots(params);
|
||||
|
||||
return params;
|
||||
};
|
||||
return params as Params;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ interface SerializePrimitiveParam extends SerializePrimitiveOptions {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
|
||||
export const separatorArrayExplode = (style: ArraySeparatorStyle): '.' | ';' | ',' | '&' => {
|
||||
switch (style) {
|
||||
case 'label':
|
||||
return '.';
|
||||
@@ -39,7 +39,7 @@ export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
|
||||
export const separatorArrayNoExplode = (style: ArraySeparatorStyle): ',' | '|' | '%20' => {
|
||||
switch (style) {
|
||||
case 'form':
|
||||
return ',';
|
||||
@@ -52,7 +52,7 @@ export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
|
||||
export const separatorObjectExplode = (style: ObjectSeparatorStyle): '.' | ';' | ',' | '&' => {
|
||||
switch (style) {
|
||||
case 'label':
|
||||
return '.';
|
||||
@@ -73,7 +73,7 @@ export const serializeArrayParam = ({
|
||||
value,
|
||||
}: SerializeOptions<ArraySeparatorStyle> & {
|
||||
value: unknown[];
|
||||
}) => {
|
||||
}): string => {
|
||||
if (!explode) {
|
||||
const joinedValues = (
|
||||
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
|
||||
@@ -111,7 +111,7 @@ export const serializePrimitiveParam = ({
|
||||
allowReserved,
|
||||
name,
|
||||
value,
|
||||
}: SerializePrimitiveParam) => {
|
||||
}: SerializePrimitiveParam): string => {
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
@@ -135,7 +135,7 @@ export const serializeObjectParam = ({
|
||||
}: SerializeOptions<ObjectSeparatorStyle> & {
|
||||
value: Record<string, unknown> | Date;
|
||||
valueOnly?: boolean;
|
||||
}) => {
|
||||
}): string => {
|
||||
if (value instanceof Date) {
|
||||
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export type JsonValue =
|
||||
/**
|
||||
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
|
||||
*/
|
||||
export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
|
||||
export const queryKeyJsonReplacer = (_key: string, value: unknown): unknown | undefined => {
|
||||
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -92,6 +92,12 @@ export interface Config {
|
||||
responseValidator?: (data: unknown) => Promise<unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Arbitrary metadata passed through the `meta` request option.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface ClientMeta {}
|
||||
|
||||
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
|
||||
? true
|
||||
: [T] extends [never | undefined]
|
||||
|
||||
@@ -14,9 +14,9 @@ export interface PathSerializer {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const PATH_PARAM_RE = /\{[^{}]+\}/g;
|
||||
export const PATH_PARAM_RE: RegExp = /\{[^{}]+\}/g;
|
||||
|
||||
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
||||
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer): string => {
|
||||
let url = _url;
|
||||
const matches = _url.match(PATH_PARAM_RE);
|
||||
if (matches) {
|
||||
@@ -95,7 +95,7 @@ export const getUrl = ({
|
||||
query?: Record<string, unknown>;
|
||||
querySerializer: QuerySerializer;
|
||||
url: string;
|
||||
}) => {
|
||||
}): string => {
|
||||
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
|
||||
let url = (baseUrl ?? '') + pathUrl;
|
||||
if (path) {
|
||||
@@ -115,7 +115,7 @@ export function getValidRequestBody(options: {
|
||||
body?: unknown;
|
||||
bodySerializer?: BodySerializer | null;
|
||||
serializedBody?: unknown;
|
||||
}) {
|
||||
}): unknown {
|
||||
const hasBody = options.body !== undefined;
|
||||
const isSerializedBody = hasBody && options.bodySerializer;
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ export {
|
||||
getScheduleMirrors,
|
||||
getScheduleNotifications,
|
||||
getSnapshotDetails,
|
||||
getSnapshotRestorePlan,
|
||||
getSsoSettings,
|
||||
getStatus,
|
||||
getSystemInfo,
|
||||
@@ -209,6 +210,9 @@ export type {
|
||||
GetSnapshotDetailsData,
|
||||
GetSnapshotDetailsResponse,
|
||||
GetSnapshotDetailsResponses,
|
||||
GetSnapshotRestorePlanData,
|
||||
GetSnapshotRestorePlanResponse,
|
||||
GetSnapshotRestorePlanResponses,
|
||||
GetSsoSettingsData,
|
||||
GetSsoSettingsResponse,
|
||||
GetSsoSettingsResponses,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @ts-nocheck
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { Client, Options as Options2, TDataShape } from './client';
|
||||
import type { Client, ClientMeta, Options as Options2, RequestResult, ServerSentEventsResult, TDataShape } from './client';
|
||||
import { client } from './client.gen';
|
||||
import type { BrowseFilesystemData, BrowseFilesystemResponses, CancelDoctorData, CancelDoctorErrors, CancelDoctorResponses, CreateApiKeyData, CreateApiKeyErrors, CreateApiKeyResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateDesktopSessionData, CreateDesktopSessionResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteApiKeyData, DeleteApiKeyErrors, DeleteApiKeyResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteSnapshotsData, DeleteSnapshotsResponses, DeleteSsoInvitationData, DeleteSsoInvitationErrors, DeleteSsoInvitationResponses, DeleteSsoProviderData, DeleteSsoProviderErrors, DeleteSsoProviderResponses, DeleteUserAccountData, DeleteUserAccountErrors, DeleteUserAccountResponses, DeleteVolumeData, DeleteVolumeResponses, DevPanelExecData, DevPanelExecErrors, DevPanelExecResponse, DevPanelExecResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, DumpSnapshotData, DumpSnapshotResponses, GetAdminUsersData, GetAdminUsersResponses, GetApiKeysData, GetApiKeysResponses, GetBackupProgressData, GetBackupProgressResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetDevPanelData, GetDevPanelResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetMirrorSyncStatusData, GetMirrorSyncStatusResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetOrgMembersData, GetOrgMembersResponses, GetPasswordLoginStatusData, GetPasswordLoginStatusResponses, GetPublicSsoProvidersData, GetPublicSsoProvidersResponses, GetRegistrationStatusData, GetRegistrationStatusResponses, GetRepositoryData, GetRepositoryResponses, GetRepositoryStatsData, GetRepositoryStatsResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetSsoSettingsData, GetSsoSettingsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetUpdatesData, GetUpdatesResponses, GetUserDeletionImpactData, GetUserDeletionImpactResponses, GetUserSsoInvitationsData, GetUserSsoInvitationsResponses, 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, SetPasswordLoginStatusData, SetPasswordLoginStatusResponses, SetRegistrationStatusData, SetRegistrationStatusResponses, StartDoctorData, StartDoctorErrors, StartDoctorResponses, StartInvitationSsoVerificationData, StartInvitationSsoVerificationErrors, StartInvitationSsoVerificationResponses, StopBackupData, StopBackupErrors, StopBackupResponses, SyncMirrorData, SyncMirrorErrors, SyncMirrorResponses, 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, CreateApiKeyData, CreateApiKeyErrors, CreateApiKeyResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateDesktopSessionData, CreateDesktopSessionResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteApiKeyData, DeleteApiKeyErrors, DeleteApiKeyResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteSnapshotsData, DeleteSnapshotsResponses, DeleteSsoInvitationData, DeleteSsoInvitationErrors, DeleteSsoInvitationResponses, DeleteSsoProviderData, DeleteSsoProviderErrors, DeleteSsoProviderResponses, DeleteUserAccountData, DeleteUserAccountErrors, DeleteUserAccountResponses, DeleteVolumeData, DeleteVolumeResponses, DevPanelExecData, DevPanelExecErrors, DevPanelExecResponse, DevPanelExecResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, DumpSnapshotData, DumpSnapshotResponses, GetAdminUsersData, GetAdminUsersResponses, GetApiKeysData, GetApiKeysResponses, GetBackupProgressData, GetBackupProgressResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetDevPanelData, GetDevPanelResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetMirrorSyncStatusData, GetMirrorSyncStatusResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetOrgMembersData, GetOrgMembersResponses, GetPasswordLoginStatusData, GetPasswordLoginStatusResponses, GetPublicSsoProvidersData, GetPublicSsoProvidersResponses, GetRegistrationStatusData, GetRegistrationStatusResponses, GetRepositoryData, GetRepositoryResponses, GetRepositoryStatsData, GetRepositoryStatsResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetSnapshotRestorePlanData, GetSnapshotRestorePlanResponses, GetSsoSettingsData, GetSsoSettingsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetUpdatesData, GetUpdatesResponses, GetUserDeletionImpactData, GetUserDeletionImpactResponses, GetUserSsoInvitationsData, GetUserSsoInvitationsResponses, 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, SetPasswordLoginStatusData, SetPasswordLoginStatusResponses, SetRegistrationStatusData, SetRegistrationStatusResponses, StartDoctorData, StartDoctorErrors, StartDoctorResponses, StartInvitationSsoVerificationData, StartInvitationSsoVerificationErrors, StartInvitationSsoVerificationResponses, StopBackupData, StopBackupErrors, StopBackupResponses, SyncMirrorData, SyncMirrorErrors, SyncMirrorResponses, 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, TResponse = unknown> = Options2<TData, ThrowOnError, TResponse> & {
|
||||
/**
|
||||
@@ -16,38 +16,38 @@ export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends
|
||||
* You can pass arbitrary values through the `meta` object. This can be
|
||||
* used to access values that aren't defined as part of the SDK function.
|
||||
*/
|
||||
meta?: Record<string, unknown>;
|
||||
meta?: keyof ClientMeta extends never ? Record<string, unknown> : ClientMeta;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get authentication system status
|
||||
*/
|
||||
export const getStatus = <ThrowOnError extends boolean = false>(options?: Options<GetStatusData, ThrowOnError>) => (options?.client ?? client).get<GetStatusResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/status', ...options });
|
||||
export const getStatus = <ThrowOnError extends boolean = false>(options?: Options<GetStatusData, ThrowOnError>): RequestResult<GetStatusResponses, unknown, ThrowOnError> => (options?.client ?? client).get<GetStatusResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/status', ...options });
|
||||
|
||||
/**
|
||||
* List admin users for settings management
|
||||
*/
|
||||
export const getAdminUsers = <ThrowOnError extends boolean = false>(options?: Options<GetAdminUsersData, ThrowOnError>) => (options?.client ?? client).get<GetAdminUsersResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/admin-users', ...options });
|
||||
export const getAdminUsers = <ThrowOnError extends boolean = false>(options?: Options<GetAdminUsersData, ThrowOnError>): RequestResult<GetAdminUsersResponses, unknown, ThrowOnError> => (options?.client ?? client).get<GetAdminUsersResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/admin-users', ...options });
|
||||
|
||||
/**
|
||||
* Delete an account linked to a user
|
||||
*/
|
||||
export const deleteUserAccount = <ThrowOnError extends boolean = false>(options: Options<DeleteUserAccountData, ThrowOnError>) => (options.client ?? client).delete<DeleteUserAccountResponses, DeleteUserAccountErrors, ThrowOnError>({ url: '/api/v1/auth/admin-users/{userId}/accounts/{accountId}', ...options });
|
||||
export const deleteUserAccount = <ThrowOnError extends boolean = false>(options: Options<DeleteUserAccountData, ThrowOnError>): RequestResult<DeleteUserAccountResponses, DeleteUserAccountErrors, ThrowOnError> => (options.client ?? client).delete<DeleteUserAccountResponses, DeleteUserAccountErrors, ThrowOnError>({ url: '/api/v1/auth/admin-users/{userId}/accounts/{accountId}', ...options });
|
||||
|
||||
/**
|
||||
* Get impact of deleting a user
|
||||
*/
|
||||
export const getUserDeletionImpact = <ThrowOnError extends boolean = false>(options: Options<GetUserDeletionImpactData, ThrowOnError>) => (options.client ?? client).get<GetUserDeletionImpactResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/deletion-impact/{userId}', ...options });
|
||||
export const getUserDeletionImpact = <ThrowOnError extends boolean = false>(options: Options<GetUserDeletionImpactData, ThrowOnError>): RequestResult<GetUserDeletionImpactResponses, unknown, ThrowOnError> => (options.client ?? client).get<GetUserDeletionImpactResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/deletion-impact/{userId}', ...options });
|
||||
|
||||
/**
|
||||
* Get members of the active organization
|
||||
*/
|
||||
export const getOrgMembers = <ThrowOnError extends boolean = false>(options?: Options<GetOrgMembersData, ThrowOnError>) => (options?.client ?? client).get<GetOrgMembersResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/org-members', ...options });
|
||||
export const getOrgMembers = <ThrowOnError extends boolean = false>(options?: Options<GetOrgMembersData, ThrowOnError>): RequestResult<GetOrgMembersResponses, unknown, ThrowOnError> => (options?.client ?? client).get<GetOrgMembersResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/org-members', ...options });
|
||||
|
||||
/**
|
||||
* Update a member's role in the active organization
|
||||
*/
|
||||
export const updateMemberRole = <ThrowOnError extends boolean = false>(options: Options<UpdateMemberRoleData, ThrowOnError>) => (options.client ?? client).patch<UpdateMemberRoleResponses, UpdateMemberRoleErrors, ThrowOnError>({
|
||||
export const updateMemberRole = <ThrowOnError extends boolean = false>(options: Options<UpdateMemberRoleData, ThrowOnError>): RequestResult<UpdateMemberRoleResponses, UpdateMemberRoleErrors, ThrowOnError> => (options.client ?? client).patch<UpdateMemberRoleResponses, UpdateMemberRoleErrors, ThrowOnError>({
|
||||
url: '/api/v1/auth/org-members/{memberId}/role',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -59,17 +59,17 @@ export const updateMemberRole = <ThrowOnError extends boolean = false>(options:
|
||||
/**
|
||||
* Remove a member from the active organization
|
||||
*/
|
||||
export const removeOrgMember = <ThrowOnError extends boolean = false>(options: Options<RemoveOrgMemberData, ThrowOnError>) => (options.client ?? client).delete<RemoveOrgMemberResponses, RemoveOrgMemberErrors, ThrowOnError>({ url: '/api/v1/auth/org-members/{memberId}', ...options });
|
||||
export const removeOrgMember = <ThrowOnError extends boolean = false>(options: Options<RemoveOrgMemberData, ThrowOnError>): RequestResult<RemoveOrgMemberResponses, RemoveOrgMemberErrors, ThrowOnError> => (options.client ?? client).delete<RemoveOrgMemberResponses, RemoveOrgMemberErrors, ThrowOnError>({ url: '/api/v1/auth/org-members/{memberId}', ...options });
|
||||
|
||||
/**
|
||||
* List API keys for the current user in the active organization
|
||||
*/
|
||||
export const getApiKeys = <ThrowOnError extends boolean = false>(options?: Options<GetApiKeysData, ThrowOnError>) => (options?.client ?? client).get<GetApiKeysResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/api-keys', ...options });
|
||||
export const getApiKeys = <ThrowOnError extends boolean = false>(options?: Options<GetApiKeysData, ThrowOnError>): RequestResult<GetApiKeysResponses, unknown, ThrowOnError> => (options?.client ?? client).get<GetApiKeysResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/api-keys', ...options });
|
||||
|
||||
/**
|
||||
* Create an API key for the current user in the active organization
|
||||
*/
|
||||
export const createApiKey = <ThrowOnError extends boolean = false>(options: Options<CreateApiKeyData, ThrowOnError>) => (options.client ?? client).post<CreateApiKeyResponses, CreateApiKeyErrors, ThrowOnError>({
|
||||
export const createApiKey = <ThrowOnError extends boolean = false>(options: Options<CreateApiKeyData, ThrowOnError>): RequestResult<CreateApiKeyResponses, CreateApiKeyErrors, ThrowOnError> => (options.client ?? client).post<CreateApiKeyResponses, CreateApiKeyErrors, ThrowOnError>({
|
||||
url: '/api/v1/auth/api-keys',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -81,27 +81,27 @@ export const createApiKey = <ThrowOnError extends boolean = false>(options: Opti
|
||||
/**
|
||||
* Revoke an API key for the current user in the active organization
|
||||
*/
|
||||
export const deleteApiKey = <ThrowOnError extends boolean = false>(options: Options<DeleteApiKeyData, ThrowOnError>) => (options.client ?? client).delete<DeleteApiKeyResponses, DeleteApiKeyErrors, ThrowOnError>({ url: '/api/v1/auth/api-keys/{keyId}', ...options });
|
||||
export const deleteApiKey = <ThrowOnError extends boolean = false>(options: Options<DeleteApiKeyData, ThrowOnError>): RequestResult<DeleteApiKeyResponses, DeleteApiKeyErrors, ThrowOnError> => (options.client ?? client).delete<DeleteApiKeyResponses, DeleteApiKeyErrors, ThrowOnError>({ url: '/api/v1/auth/api-keys/{keyId}', ...options });
|
||||
|
||||
/**
|
||||
* Get public SSO providers for the instance
|
||||
*/
|
||||
export const getPublicSsoProviders = <ThrowOnError extends boolean = false>(options?: Options<GetPublicSsoProvidersData, ThrowOnError>) => (options?.client ?? client).get<GetPublicSsoProvidersResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/sso-providers', ...options });
|
||||
export const getPublicSsoProviders = <ThrowOnError extends boolean = false>(options?: Options<GetPublicSsoProvidersData, ThrowOnError>): RequestResult<GetPublicSsoProvidersResponses, unknown, ThrowOnError> => (options?.client ?? client).get<GetPublicSsoProvidersResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/sso-providers', ...options });
|
||||
|
||||
/**
|
||||
* Get SSO providers and invitations for the active organization
|
||||
*/
|
||||
export const getSsoSettings = <ThrowOnError extends boolean = false>(options?: Options<GetSsoSettingsData, ThrowOnError>) => (options?.client ?? client).get<GetSsoSettingsResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/sso-settings', ...options });
|
||||
export const getSsoSettings = <ThrowOnError extends boolean = false>(options?: Options<GetSsoSettingsData, ThrowOnError>): RequestResult<GetSsoSettingsResponses, unknown, ThrowOnError> => (options?.client ?? client).get<GetSsoSettingsResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/sso-settings', ...options });
|
||||
|
||||
/**
|
||||
* Get pending SSO invitations for the authenticated user
|
||||
*/
|
||||
export const getUserSsoInvitations = <ThrowOnError extends boolean = false>(options?: Options<GetUserSsoInvitationsData, ThrowOnError>) => (options?.client ?? client).get<GetUserSsoInvitationsResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/sso-invitations', ...options });
|
||||
export const getUserSsoInvitations = <ThrowOnError extends boolean = false>(options?: Options<GetUserSsoInvitationsData, ThrowOnError>): RequestResult<GetUserSsoInvitationsResponses, unknown, ThrowOnError> => (options?.client ?? client).get<GetUserSsoInvitationsResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/sso-invitations', ...options });
|
||||
|
||||
/**
|
||||
* Start SSO verification for accepting an organization invitation
|
||||
*/
|
||||
export const startInvitationSsoVerification = <ThrowOnError extends boolean = false>(options: Options<StartInvitationSsoVerificationData, ThrowOnError>) => (options.client ?? client).post<StartInvitationSsoVerificationResponses, StartInvitationSsoVerificationErrors, ThrowOnError>({
|
||||
export const startInvitationSsoVerification = <ThrowOnError extends boolean = false>(options: Options<StartInvitationSsoVerificationData, ThrowOnError>): RequestResult<StartInvitationSsoVerificationResponses, StartInvitationSsoVerificationErrors, ThrowOnError> => (options.client ?? client).post<StartInvitationSsoVerificationResponses, StartInvitationSsoVerificationErrors, ThrowOnError>({
|
||||
url: '/api/v1/auth/sso-invitations/{invitationId}/verify',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -113,12 +113,12 @@ export const startInvitationSsoVerification = <ThrowOnError extends boolean = fa
|
||||
/**
|
||||
* Delete an SSO provider
|
||||
*/
|
||||
export const deleteSsoProvider = <ThrowOnError extends boolean = false>(options: Options<DeleteSsoProviderData, ThrowOnError>) => (options.client ?? client).delete<DeleteSsoProviderResponses, DeleteSsoProviderErrors, ThrowOnError>({ url: '/api/v1/auth/sso-providers/{providerId}', ...options });
|
||||
export const deleteSsoProvider = <ThrowOnError extends boolean = false>(options: Options<DeleteSsoProviderData, ThrowOnError>): RequestResult<DeleteSsoProviderResponses, DeleteSsoProviderErrors, ThrowOnError> => (options.client ?? client).delete<DeleteSsoProviderResponses, DeleteSsoProviderErrors, ThrowOnError>({ url: '/api/v1/auth/sso-providers/{providerId}', ...options });
|
||||
|
||||
/**
|
||||
* Update whether SSO sign-in can auto-link existing accounts by email
|
||||
*/
|
||||
export const updateSsoProviderAutoLinking = <ThrowOnError extends boolean = false>(options: Options<UpdateSsoProviderAutoLinkingData, ThrowOnError>) => (options.client ?? client).patch<UpdateSsoProviderAutoLinkingResponses, UpdateSsoProviderAutoLinkingErrors, ThrowOnError>({
|
||||
export const updateSsoProviderAutoLinking = <ThrowOnError extends boolean = false>(options: Options<UpdateSsoProviderAutoLinkingData, ThrowOnError>): RequestResult<UpdateSsoProviderAutoLinkingResponses, UpdateSsoProviderAutoLinkingErrors, ThrowOnError> => (options.client ?? client).patch<UpdateSsoProviderAutoLinkingResponses, UpdateSsoProviderAutoLinkingErrors, ThrowOnError>({
|
||||
url: '/api/v1/auth/sso-providers/{providerId}/auto-linking',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -130,17 +130,17 @@ export const updateSsoProviderAutoLinking = <ThrowOnError extends boolean = fals
|
||||
/**
|
||||
* Delete an SSO invitation
|
||||
*/
|
||||
export const deleteSsoInvitation = <ThrowOnError extends boolean = false>(options: Options<DeleteSsoInvitationData, ThrowOnError>) => (options.client ?? client).delete<DeleteSsoInvitationResponses, DeleteSsoInvitationErrors, ThrowOnError>({ url: '/api/v1/auth/sso-invitations/{invitationId}', ...options });
|
||||
export const deleteSsoInvitation = <ThrowOnError extends boolean = false>(options: Options<DeleteSsoInvitationData, ThrowOnError>): RequestResult<DeleteSsoInvitationResponses, DeleteSsoInvitationErrors, ThrowOnError> => (options.client ?? client).delete<DeleteSsoInvitationResponses, DeleteSsoInvitationErrors, ThrowOnError>({ url: '/api/v1/auth/sso-invitations/{invitationId}', ...options });
|
||||
|
||||
/**
|
||||
* List all volumes
|
||||
*/
|
||||
export const listVolumes = <ThrowOnError extends boolean = false>(options?: Options<ListVolumesData, ThrowOnError>) => (options?.client ?? client).get<ListVolumesResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes', ...options });
|
||||
export const listVolumes = <ThrowOnError extends boolean = false>(options?: Options<ListVolumesData, ThrowOnError>): RequestResult<ListVolumesResponses, unknown, ThrowOnError> => (options?.client ?? client).get<ListVolumesResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes', ...options });
|
||||
|
||||
/**
|
||||
* Create a new volume
|
||||
*/
|
||||
export const createVolume = <ThrowOnError extends boolean = false>(options: Options<CreateVolumeData, ThrowOnError>) => (options.client ?? client).post<CreateVolumeResponses, unknown, ThrowOnError>({
|
||||
export const createVolume = <ThrowOnError extends boolean = false>(options: Options<CreateVolumeData, ThrowOnError>): RequestResult<CreateVolumeResponses, unknown, ThrowOnError> => (options.client ?? client).post<CreateVolumeResponses, unknown, ThrowOnError>({
|
||||
url: '/api/v1/volumes',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -152,7 +152,7 @@ export const createVolume = <ThrowOnError extends boolean = false>(options: Opti
|
||||
/**
|
||||
* Test connection to backend
|
||||
*/
|
||||
export const testConnection = <ThrowOnError extends boolean = false>(options: Options<TestConnectionData, ThrowOnError>) => (options.client ?? client).post<TestConnectionResponses, unknown, ThrowOnError>({
|
||||
export const testConnection = <ThrowOnError extends boolean = false>(options: Options<TestConnectionData, ThrowOnError>): RequestResult<TestConnectionResponses, unknown, ThrowOnError> => (options.client ?? client).post<TestConnectionResponses, unknown, ThrowOnError>({
|
||||
url: '/api/v1/volumes/test-connection',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -164,17 +164,17 @@ export const testConnection = <ThrowOnError extends boolean = false>(options: Op
|
||||
/**
|
||||
* Delete a volume
|
||||
*/
|
||||
export const deleteVolume = <ThrowOnError extends boolean = false>(options: Options<DeleteVolumeData, ThrowOnError>) => (options.client ?? client).delete<DeleteVolumeResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/{shortId}', ...options });
|
||||
export const deleteVolume = <ThrowOnError extends boolean = false>(options: Options<DeleteVolumeData, ThrowOnError>): RequestResult<DeleteVolumeResponses, unknown, ThrowOnError> => (options.client ?? client).delete<DeleteVolumeResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/{shortId}', ...options });
|
||||
|
||||
/**
|
||||
* Get a volume by name
|
||||
*/
|
||||
export const getVolume = <ThrowOnError extends boolean = false>(options: Options<GetVolumeData, ThrowOnError>) => (options.client ?? client).get<GetVolumeResponses, GetVolumeErrors, ThrowOnError>({ url: '/api/v1/volumes/{shortId}', ...options });
|
||||
export const getVolume = <ThrowOnError extends boolean = false>(options: Options<GetVolumeData, ThrowOnError>): RequestResult<GetVolumeResponses, GetVolumeErrors, ThrowOnError> => (options.client ?? client).get<GetVolumeResponses, GetVolumeErrors, ThrowOnError>({ url: '/api/v1/volumes/{shortId}', ...options });
|
||||
|
||||
/**
|
||||
* Update a volume's configuration
|
||||
*/
|
||||
export const updateVolume = <ThrowOnError extends boolean = false>(options: Options<UpdateVolumeData, ThrowOnError>) => (options.client ?? client).put<UpdateVolumeResponses, UpdateVolumeErrors, ThrowOnError>({
|
||||
export const updateVolume = <ThrowOnError extends boolean = false>(options: Options<UpdateVolumeData, ThrowOnError>): RequestResult<UpdateVolumeResponses, UpdateVolumeErrors, ThrowOnError> => (options.client ?? client).put<UpdateVolumeResponses, UpdateVolumeErrors, ThrowOnError>({
|
||||
url: '/api/v1/volumes/{shortId}',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -186,37 +186,37 @@ export const updateVolume = <ThrowOnError extends boolean = false>(options: Opti
|
||||
/**
|
||||
* Mount a volume
|
||||
*/
|
||||
export const mountVolume = <ThrowOnError extends boolean = false>(options: Options<MountVolumeData, ThrowOnError>) => (options.client ?? client).post<MountVolumeResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/{shortId}/mount', ...options });
|
||||
export const mountVolume = <ThrowOnError extends boolean = false>(options: Options<MountVolumeData, ThrowOnError>): RequestResult<MountVolumeResponses, unknown, ThrowOnError> => (options.client ?? client).post<MountVolumeResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/{shortId}/mount', ...options });
|
||||
|
||||
/**
|
||||
* Unmount a volume
|
||||
*/
|
||||
export const unmountVolume = <ThrowOnError extends boolean = false>(options: Options<UnmountVolumeData, ThrowOnError>) => (options.client ?? client).post<UnmountVolumeResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/{shortId}/unmount', ...options });
|
||||
export const unmountVolume = <ThrowOnError extends boolean = false>(options: Options<UnmountVolumeData, ThrowOnError>): RequestResult<UnmountVolumeResponses, unknown, ThrowOnError> => (options.client ?? client).post<UnmountVolumeResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/{shortId}/unmount', ...options });
|
||||
|
||||
/**
|
||||
* Perform a health check on a volume
|
||||
*/
|
||||
export const healthCheckVolume = <ThrowOnError extends boolean = false>(options: Options<HealthCheckVolumeData, ThrowOnError>) => (options.client ?? client).post<HealthCheckVolumeResponses, HealthCheckVolumeErrors, ThrowOnError>({ url: '/api/v1/volumes/{shortId}/health-check', ...options });
|
||||
export const healthCheckVolume = <ThrowOnError extends boolean = false>(options: Options<HealthCheckVolumeData, ThrowOnError>): RequestResult<HealthCheckVolumeResponses, HealthCheckVolumeErrors, ThrowOnError> => (options.client ?? client).post<HealthCheckVolumeResponses, HealthCheckVolumeErrors, ThrowOnError>({ url: '/api/v1/volumes/{shortId}/health-check', ...options });
|
||||
|
||||
/**
|
||||
* List files in a volume directory
|
||||
*/
|
||||
export const listFiles = <ThrowOnError extends boolean = false>(options: Options<ListFilesData, ThrowOnError>) => (options.client ?? client).get<ListFilesResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/{shortId}/files', ...options });
|
||||
export const listFiles = <ThrowOnError extends boolean = false>(options: Options<ListFilesData, ThrowOnError>): RequestResult<ListFilesResponses, unknown, ThrowOnError> => (options.client ?? client).get<ListFilesResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/{shortId}/files', ...options });
|
||||
|
||||
/**
|
||||
* Browse directories on the host filesystem
|
||||
*/
|
||||
export const browseFilesystem = <ThrowOnError extends boolean = false>(options?: Options<BrowseFilesystemData, ThrowOnError>) => (options?.client ?? client).get<BrowseFilesystemResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/filesystem/browse', ...options });
|
||||
export const browseFilesystem = <ThrowOnError extends boolean = false>(options?: Options<BrowseFilesystemData, ThrowOnError>): RequestResult<BrowseFilesystemResponses, unknown, ThrowOnError> => (options?.client ?? client).get<BrowseFilesystemResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/filesystem/browse', ...options });
|
||||
|
||||
/**
|
||||
* List all repositories
|
||||
*/
|
||||
export const listRepositories = <ThrowOnError extends boolean = false>(options?: Options<ListRepositoriesData, ThrowOnError>) => (options?.client ?? client).get<ListRepositoriesResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories', ...options });
|
||||
export const listRepositories = <ThrowOnError extends boolean = false>(options?: Options<ListRepositoriesData, ThrowOnError>): RequestResult<ListRepositoriesResponses, unknown, ThrowOnError> => (options?.client ?? client).get<ListRepositoriesResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories', ...options });
|
||||
|
||||
/**
|
||||
* Create a new restic repository
|
||||
*/
|
||||
export const createRepository = <ThrowOnError extends boolean = false>(options: Options<CreateRepositoryData, ThrowOnError>) => (options.client ?? client).post<CreateRepositoryResponses, unknown, ThrowOnError>({
|
||||
export const createRepository = <ThrowOnError extends boolean = false>(options: Options<CreateRepositoryData, ThrowOnError>): RequestResult<CreateRepositoryResponses, unknown, ThrowOnError> => (options.client ?? client).post<CreateRepositoryResponses, unknown, ThrowOnError>({
|
||||
url: '/api/v1/repositories',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -228,22 +228,22 @@ export const createRepository = <ThrowOnError extends boolean = false>(options:
|
||||
/**
|
||||
* List all configured rclone remotes on the host system
|
||||
*/
|
||||
export const listRcloneRemotes = <ThrowOnError extends boolean = false>(options?: Options<ListRcloneRemotesData, ThrowOnError>) => (options?.client ?? client).get<ListRcloneRemotesResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/rclone-remotes', ...options });
|
||||
export const listRcloneRemotes = <ThrowOnError extends boolean = false>(options?: Options<ListRcloneRemotesData, ThrowOnError>): RequestResult<ListRcloneRemotesResponses, unknown, ThrowOnError> => (options?.client ?? client).get<ListRcloneRemotesResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/rclone-remotes', ...options });
|
||||
|
||||
/**
|
||||
* Delete a repository
|
||||
*/
|
||||
export const deleteRepository = <ThrowOnError extends boolean = false>(options: Options<DeleteRepositoryData, ThrowOnError>) => (options.client ?? client).delete<DeleteRepositoryResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}', ...options });
|
||||
export const deleteRepository = <ThrowOnError extends boolean = false>(options: Options<DeleteRepositoryData, ThrowOnError>): RequestResult<DeleteRepositoryResponses, unknown, ThrowOnError> => (options.client ?? client).delete<DeleteRepositoryResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}', ...options });
|
||||
|
||||
/**
|
||||
* Get a single repository by ID
|
||||
*/
|
||||
export const getRepository = <ThrowOnError extends boolean = false>(options: Options<GetRepositoryData, ThrowOnError>) => (options.client ?? client).get<GetRepositoryResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}', ...options });
|
||||
export const getRepository = <ThrowOnError extends boolean = false>(options: Options<GetRepositoryData, ThrowOnError>): RequestResult<GetRepositoryResponses, unknown, ThrowOnError> => (options.client ?? client).get<GetRepositoryResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}', ...options });
|
||||
|
||||
/**
|
||||
* Update a repository's name or settings
|
||||
*/
|
||||
export const updateRepository = <ThrowOnError extends boolean = false>(options: Options<UpdateRepositoryData, ThrowOnError>) => (options.client ?? client).patch<UpdateRepositoryResponses, UpdateRepositoryErrors, ThrowOnError>({
|
||||
export const updateRepository = <ThrowOnError extends boolean = false>(options: Options<UpdateRepositoryData, ThrowOnError>): RequestResult<UpdateRepositoryResponses, UpdateRepositoryErrors, ThrowOnError> => (options.client ?? client).patch<UpdateRepositoryResponses, UpdateRepositoryErrors, ThrowOnError>({
|
||||
url: '/api/v1/repositories/{shortId}',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -255,17 +255,17 @@ export const updateRepository = <ThrowOnError extends boolean = false>(options:
|
||||
/**
|
||||
* Get repository storage and compression statistics
|
||||
*/
|
||||
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 });
|
||||
export const getRepositoryStats = <ThrowOnError extends boolean = false>(options: Options<GetRepositoryStatsData, ThrowOnError>): RequestResult<GetRepositoryStatsResponses, unknown, 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 });
|
||||
export const refreshRepositoryStats = <ThrowOnError extends boolean = false>(options: Options<RefreshRepositoryStatsData, ThrowOnError>): RequestResult<RefreshRepositoryStatsResponses, unknown, ThrowOnError> => (options.client ?? client).post<RefreshRepositoryStatsResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/stats/refresh', ...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>({
|
||||
export const deleteSnapshots = <ThrowOnError extends boolean = false>(options: Options<DeleteSnapshotsData, ThrowOnError>): RequestResult<DeleteSnapshotsResponses, unknown, ThrowOnError> => (options.client ?? client).delete<DeleteSnapshotsResponses, unknown, ThrowOnError>({
|
||||
url: '/api/v1/repositories/{shortId}/snapshots',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -277,37 +277,42 @@ export const deleteSnapshots = <ThrowOnError extends boolean = false>(options: O
|
||||
/**
|
||||
* List all snapshots in a repository
|
||||
*/
|
||||
export const listSnapshots = <ThrowOnError extends boolean = false>(options: Options<ListSnapshotsData, ThrowOnError>) => (options.client ?? client).get<ListSnapshotsResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots', ...options });
|
||||
export const listSnapshots = <ThrowOnError extends boolean = false>(options: Options<ListSnapshotsData, ThrowOnError>): RequestResult<ListSnapshotsResponses, unknown, ThrowOnError> => (options.client ?? client).get<ListSnapshotsResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots', ...options });
|
||||
|
||||
/**
|
||||
* Clear snapshot cache and force refresh from repository
|
||||
*/
|
||||
export const refreshSnapshots = <ThrowOnError extends boolean = false>(options: Options<RefreshSnapshotsData, ThrowOnError>) => (options.client ?? client).post<RefreshSnapshotsResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots/refresh', ...options });
|
||||
export const refreshSnapshots = <ThrowOnError extends boolean = false>(options: Options<RefreshSnapshotsData, ThrowOnError>): RequestResult<RefreshSnapshotsResponses, unknown, ThrowOnError> => (options.client ?? client).post<RefreshSnapshotsResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots/refresh', ...options });
|
||||
|
||||
/**
|
||||
* Delete a specific snapshot from a repository
|
||||
*/
|
||||
export const deleteSnapshot = <ThrowOnError extends boolean = false>(options: Options<DeleteSnapshotData, ThrowOnError>) => (options.client ?? client).delete<DeleteSnapshotResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}', ...options });
|
||||
export const deleteSnapshot = <ThrowOnError extends boolean = false>(options: Options<DeleteSnapshotData, ThrowOnError>): RequestResult<DeleteSnapshotResponses, unknown, ThrowOnError> => (options.client ?? client).delete<DeleteSnapshotResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}', ...options });
|
||||
|
||||
/**
|
||||
* Get details of a specific snapshot
|
||||
*/
|
||||
export const getSnapshotDetails = <ThrowOnError extends boolean = false>(options: Options<GetSnapshotDetailsData, ThrowOnError>) => (options.client ?? client).get<GetSnapshotDetailsResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}', ...options });
|
||||
export const getSnapshotDetails = <ThrowOnError extends boolean = false>(options: Options<GetSnapshotDetailsData, ThrowOnError>): RequestResult<GetSnapshotDetailsResponses, unknown, ThrowOnError> => (options.client ?? client).get<GetSnapshotDetailsResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}', ...options });
|
||||
|
||||
/**
|
||||
* Get a target-scoped restore plan for a snapshot
|
||||
*/
|
||||
export const getSnapshotRestorePlan = <ThrowOnError extends boolean = false>(options: Options<GetSnapshotRestorePlanData, ThrowOnError>): RequestResult<GetSnapshotRestorePlanResponses, unknown, ThrowOnError> => (options.client ?? client).get<GetSnapshotRestorePlanResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}/restore-plan', ...options });
|
||||
|
||||
/**
|
||||
* List files and directories in a snapshot
|
||||
*/
|
||||
export const listSnapshotFiles = <ThrowOnError extends boolean = false>(options: Options<ListSnapshotFilesData, ThrowOnError>) => (options.client ?? client).get<ListSnapshotFilesResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}/files', ...options });
|
||||
export const listSnapshotFiles = <ThrowOnError extends boolean = false>(options: Options<ListSnapshotFilesData, ThrowOnError>): RequestResult<ListSnapshotFilesResponses, unknown, ThrowOnError> => (options.client ?? client).get<ListSnapshotFilesResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}/files', ...options });
|
||||
|
||||
/**
|
||||
* Download a snapshot path as a tar archive (folders) or raw file stream (single files)
|
||||
*/
|
||||
export const dumpSnapshot = <ThrowOnError extends boolean = false>(options: Options<DumpSnapshotData, ThrowOnError>) => (options.client ?? client).get<DumpSnapshotResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}/dump', ...options });
|
||||
export const dumpSnapshot = <ThrowOnError extends boolean = false>(options: Options<DumpSnapshotData, ThrowOnError>): RequestResult<DumpSnapshotResponses, unknown, ThrowOnError> => (options.client ?? client).get<DumpSnapshotResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}/dump', ...options });
|
||||
|
||||
/**
|
||||
* Restore a snapshot to a target path on the filesystem
|
||||
*/
|
||||
export const restoreSnapshot = <ThrowOnError extends boolean = false>(options: Options<RestoreSnapshotData, ThrowOnError>) => (options.client ?? client).post<RestoreSnapshotResponses, unknown, ThrowOnError>({
|
||||
export const restoreSnapshot = <ThrowOnError extends boolean = false>(options: Options<RestoreSnapshotData, ThrowOnError>): RequestResult<RestoreSnapshotResponses, unknown, ThrowOnError> => (options.client ?? client).post<RestoreSnapshotResponses, unknown, ThrowOnError>({
|
||||
url: '/api/v1/repositories/{shortId}/restore',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -319,22 +324,22 @@ export const restoreSnapshot = <ThrowOnError extends boolean = false>(options: O
|
||||
/**
|
||||
* Cancel a running doctor operation on a repository
|
||||
*/
|
||||
export const cancelDoctor = <ThrowOnError extends boolean = false>(options: Options<CancelDoctorData, ThrowOnError>) => (options.client ?? client).delete<CancelDoctorResponses, CancelDoctorErrors, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/doctor', ...options });
|
||||
export const cancelDoctor = <ThrowOnError extends boolean = false>(options: Options<CancelDoctorData, ThrowOnError>): RequestResult<CancelDoctorResponses, CancelDoctorErrors, ThrowOnError> => (options.client ?? client).delete<CancelDoctorResponses, CancelDoctorErrors, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/doctor', ...options });
|
||||
|
||||
/**
|
||||
* Start an asynchronous doctor operation on a repository to fix common issues (unlock, check, repair index). The operation runs in the background and sends results via SSE events.
|
||||
*/
|
||||
export const startDoctor = <ThrowOnError extends boolean = false>(options: Options<StartDoctorData, ThrowOnError>) => (options.client ?? client).post<StartDoctorResponses, StartDoctorErrors, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/doctor', ...options });
|
||||
export const startDoctor = <ThrowOnError extends boolean = false>(options: Options<StartDoctorData, ThrowOnError>): RequestResult<StartDoctorResponses, StartDoctorErrors, ThrowOnError> => (options.client ?? client).post<StartDoctorResponses, StartDoctorErrors, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/doctor', ...options });
|
||||
|
||||
/**
|
||||
* Unlock a repository by removing all stale locks
|
||||
*/
|
||||
export const unlockRepository = <ThrowOnError extends boolean = false>(options: Options<UnlockRepositoryData, ThrowOnError>) => (options.client ?? client).post<UnlockRepositoryResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/unlock', ...options });
|
||||
export const unlockRepository = <ThrowOnError extends boolean = false>(options: Options<UnlockRepositoryData, ThrowOnError>): RequestResult<UnlockRepositoryResponses, unknown, ThrowOnError> => (options.client ?? client).post<UnlockRepositoryResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{shortId}/unlock', ...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>({
|
||||
export const tagSnapshots = <ThrowOnError extends boolean = false>(options: Options<TagSnapshotsData, ThrowOnError>): RequestResult<TagSnapshotsResponses, unknown, ThrowOnError> => (options.client ?? client).post<TagSnapshotsResponses, unknown, ThrowOnError>({
|
||||
url: '/api/v1/repositories/{shortId}/snapshots/tag',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -346,7 +351,7 @@ export const tagSnapshots = <ThrowOnError extends boolean = false>(options: Opti
|
||||
/**
|
||||
* Execute a restic command against a repository (dev panel only)
|
||||
*/
|
||||
export const devPanelExec = <ThrowOnError extends boolean = false>(options: Options<DevPanelExecData, ThrowOnError, DevPanelExecResponse>) => (options.client ?? client).sse.post<DevPanelExecResponses, DevPanelExecErrors, ThrowOnError>({
|
||||
export const devPanelExec = <ThrowOnError extends boolean = false>(options: Options<DevPanelExecData, ThrowOnError, DevPanelExecResponse>): Promise<ServerSentEventsResult<DevPanelExecResponses>> => (options.client ?? client).sse.post<DevPanelExecResponses, DevPanelExecErrors, ThrowOnError>({
|
||||
url: '/api/v1/repositories/{shortId}/exec',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -358,12 +363,12 @@ export const devPanelExec = <ThrowOnError extends boolean = false>(options: Opti
|
||||
/**
|
||||
* List all backup schedules
|
||||
*/
|
||||
export const listBackupSchedules = <ThrowOnError extends boolean = false>(options?: Options<ListBackupSchedulesData, ThrowOnError>) => (options?.client ?? client).get<ListBackupSchedulesResponses, unknown, ThrowOnError>({ url: '/api/v1/backups', ...options });
|
||||
export const listBackupSchedules = <ThrowOnError extends boolean = false>(options?: Options<ListBackupSchedulesData, ThrowOnError>): RequestResult<ListBackupSchedulesResponses, unknown, ThrowOnError> => (options?.client ?? client).get<ListBackupSchedulesResponses, unknown, ThrowOnError>({ url: '/api/v1/backups', ...options });
|
||||
|
||||
/**
|
||||
* Create a new backup schedule for a volume
|
||||
*/
|
||||
export const createBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<CreateBackupScheduleData, ThrowOnError>) => (options.client ?? client).post<CreateBackupScheduleResponses, unknown, ThrowOnError>({
|
||||
export const createBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<CreateBackupScheduleData, ThrowOnError>): RequestResult<CreateBackupScheduleResponses, unknown, ThrowOnError> => (options.client ?? client).post<CreateBackupScheduleResponses, unknown, ThrowOnError>({
|
||||
url: '/api/v1/backups',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -375,17 +380,17 @@ export const createBackupSchedule = <ThrowOnError extends boolean = false>(optio
|
||||
/**
|
||||
* Delete a backup schedule
|
||||
*/
|
||||
export const deleteBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<DeleteBackupScheduleData, ThrowOnError>) => (options.client ?? client).delete<DeleteBackupScheduleResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}', ...options });
|
||||
export const deleteBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<DeleteBackupScheduleData, ThrowOnError>): RequestResult<DeleteBackupScheduleResponses, unknown, ThrowOnError> => (options.client ?? client).delete<DeleteBackupScheduleResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}', ...options });
|
||||
|
||||
/**
|
||||
* Get a backup schedule by ID
|
||||
*/
|
||||
export const getBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<GetBackupScheduleData, ThrowOnError>) => (options.client ?? client).get<GetBackupScheduleResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}', ...options });
|
||||
export const getBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<GetBackupScheduleData, ThrowOnError>): RequestResult<GetBackupScheduleResponses, unknown, ThrowOnError> => (options.client ?? client).get<GetBackupScheduleResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}', ...options });
|
||||
|
||||
/**
|
||||
* Update a backup schedule
|
||||
*/
|
||||
export const updateBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<UpdateBackupScheduleData, ThrowOnError>) => (options.client ?? client).patch<UpdateBackupScheduleResponses, unknown, ThrowOnError>({
|
||||
export const updateBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<UpdateBackupScheduleData, ThrowOnError>): RequestResult<UpdateBackupScheduleResponses, unknown, ThrowOnError> => (options.client ?? client).patch<UpdateBackupScheduleResponses, unknown, ThrowOnError>({
|
||||
url: '/api/v1/backups/{shortId}',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -397,32 +402,32 @@ export const updateBackupSchedule = <ThrowOnError extends boolean = false>(optio
|
||||
/**
|
||||
* Get a backup schedule for a specific volume
|
||||
*/
|
||||
export const getBackupScheduleForVolume = <ThrowOnError extends boolean = false>(options: Options<GetBackupScheduleForVolumeData, ThrowOnError>) => (options.client ?? client).get<GetBackupScheduleForVolumeResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/volume/{volumeShortId}', ...options });
|
||||
export const getBackupScheduleForVolume = <ThrowOnError extends boolean = false>(options: Options<GetBackupScheduleForVolumeData, ThrowOnError>): RequestResult<GetBackupScheduleForVolumeResponses, unknown, ThrowOnError> => (options.client ?? client).get<GetBackupScheduleForVolumeResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/volume/{volumeShortId}', ...options });
|
||||
|
||||
/**
|
||||
* Trigger a backup immediately for a schedule
|
||||
*/
|
||||
export const runBackupNow = <ThrowOnError extends boolean = false>(options: Options<RunBackupNowData, ThrowOnError>) => (options.client ?? client).post<RunBackupNowResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/run', ...options });
|
||||
export const runBackupNow = <ThrowOnError extends boolean = false>(options: Options<RunBackupNowData, ThrowOnError>): RequestResult<RunBackupNowResponses, unknown, ThrowOnError> => (options.client ?? client).post<RunBackupNowResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/run', ...options });
|
||||
|
||||
/**
|
||||
* Stop a backup that is currently in progress
|
||||
*/
|
||||
export const stopBackup = <ThrowOnError extends boolean = false>(options: Options<StopBackupData, ThrowOnError>) => (options.client ?? client).post<StopBackupResponses, StopBackupErrors, ThrowOnError>({ url: '/api/v1/backups/{shortId}/stop', ...options });
|
||||
export const stopBackup = <ThrowOnError extends boolean = false>(options: Options<StopBackupData, ThrowOnError>): RequestResult<StopBackupResponses, StopBackupErrors, ThrowOnError> => (options.client ?? client).post<StopBackupResponses, StopBackupErrors, ThrowOnError>({ url: '/api/v1/backups/{shortId}/stop', ...options });
|
||||
|
||||
/**
|
||||
* Manually apply retention policy to clean up old snapshots
|
||||
*/
|
||||
export const runForget = <ThrowOnError extends boolean = false>(options: Options<RunForgetData, ThrowOnError>) => (options.client ?? client).post<RunForgetResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/forget', ...options });
|
||||
export const runForget = <ThrowOnError extends boolean = false>(options: Options<RunForgetData, ThrowOnError>): RequestResult<RunForgetResponses, unknown, ThrowOnError> => (options.client ?? client).post<RunForgetResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/forget', ...options });
|
||||
|
||||
/**
|
||||
* Get notification assignments for a backup schedule
|
||||
*/
|
||||
export const getScheduleNotifications = <ThrowOnError extends boolean = false>(options: Options<GetScheduleNotificationsData, ThrowOnError>) => (options.client ?? client).get<GetScheduleNotificationsResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/notifications', ...options });
|
||||
export const getScheduleNotifications = <ThrowOnError extends boolean = false>(options: Options<GetScheduleNotificationsData, ThrowOnError>): RequestResult<GetScheduleNotificationsResponses, unknown, ThrowOnError> => (options.client ?? client).get<GetScheduleNotificationsResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/notifications', ...options });
|
||||
|
||||
/**
|
||||
* Update notification assignments for a backup schedule
|
||||
*/
|
||||
export const updateScheduleNotifications = <ThrowOnError extends boolean = false>(options: Options<UpdateScheduleNotificationsData, ThrowOnError>) => (options.client ?? client).put<UpdateScheduleNotificationsResponses, unknown, ThrowOnError>({
|
||||
export const updateScheduleNotifications = <ThrowOnError extends boolean = false>(options: Options<UpdateScheduleNotificationsData, ThrowOnError>): RequestResult<UpdateScheduleNotificationsResponses, unknown, ThrowOnError> => (options.client ?? client).put<UpdateScheduleNotificationsResponses, unknown, ThrowOnError>({
|
||||
url: '/api/v1/backups/{shortId}/notifications',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -434,12 +439,12 @@ export const updateScheduleNotifications = <ThrowOnError extends boolean = false
|
||||
/**
|
||||
* Get mirror repository assignments for a backup schedule
|
||||
*/
|
||||
export const getScheduleMirrors = <ThrowOnError extends boolean = false>(options: Options<GetScheduleMirrorsData, ThrowOnError>) => (options.client ?? client).get<GetScheduleMirrorsResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/mirrors', ...options });
|
||||
export const getScheduleMirrors = <ThrowOnError extends boolean = false>(options: Options<GetScheduleMirrorsData, ThrowOnError>): RequestResult<GetScheduleMirrorsResponses, unknown, ThrowOnError> => (options.client ?? client).get<GetScheduleMirrorsResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/mirrors', ...options });
|
||||
|
||||
/**
|
||||
* Update mirror repository assignments for a backup schedule
|
||||
*/
|
||||
export const updateScheduleMirrors = <ThrowOnError extends boolean = false>(options: Options<UpdateScheduleMirrorsData, ThrowOnError>) => (options.client ?? client).put<UpdateScheduleMirrorsResponses, unknown, ThrowOnError>({
|
||||
export const updateScheduleMirrors = <ThrowOnError extends boolean = false>(options: Options<UpdateScheduleMirrorsData, ThrowOnError>): RequestResult<UpdateScheduleMirrorsResponses, unknown, ThrowOnError> => (options.client ?? client).put<UpdateScheduleMirrorsResponses, unknown, ThrowOnError>({
|
||||
url: '/api/v1/backups/{shortId}/mirrors',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -451,12 +456,12 @@ export const updateScheduleMirrors = <ThrowOnError extends boolean = false>(opti
|
||||
/**
|
||||
* Get sync status for a specific mirror, including missing snapshots
|
||||
*/
|
||||
export const getMirrorSyncStatus = <ThrowOnError extends boolean = false>(options: Options<GetMirrorSyncStatusData, ThrowOnError>) => (options.client ?? client).get<GetMirrorSyncStatusResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/mirrors/{mirrorShortId}/status', ...options });
|
||||
export const getMirrorSyncStatus = <ThrowOnError extends boolean = false>(options: Options<GetMirrorSyncStatusData, ThrowOnError>): RequestResult<GetMirrorSyncStatusResponses, unknown, ThrowOnError> => (options.client ?? client).get<GetMirrorSyncStatusResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/mirrors/{mirrorShortId}/status', ...options });
|
||||
|
||||
/**
|
||||
* Sync selected snapshots to a specific mirror repository
|
||||
*/
|
||||
export const syncMirror = <ThrowOnError extends boolean = false>(options: Options<SyncMirrorData, ThrowOnError>) => (options.client ?? client).post<SyncMirrorResponses, SyncMirrorErrors, ThrowOnError>({
|
||||
export const syncMirror = <ThrowOnError extends boolean = false>(options: Options<SyncMirrorData, ThrowOnError>): RequestResult<SyncMirrorResponses, SyncMirrorErrors, ThrowOnError> => (options.client ?? client).post<SyncMirrorResponses, SyncMirrorErrors, ThrowOnError>({
|
||||
url: '/api/v1/backups/{shortId}/mirrors/{mirrorShortId}/sync',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -468,12 +473,12 @@ export const syncMirror = <ThrowOnError extends boolean = false>(options: Option
|
||||
/**
|
||||
* Get mirror compatibility info for all repositories relative to a backup schedule's primary repository
|
||||
*/
|
||||
export const getMirrorCompatibility = <ThrowOnError extends boolean = false>(options: Options<GetMirrorCompatibilityData, ThrowOnError>) => (options.client ?? client).get<GetMirrorCompatibilityResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/mirrors/compatibility', ...options });
|
||||
export const getMirrorCompatibility = <ThrowOnError extends boolean = false>(options: Options<GetMirrorCompatibilityData, ThrowOnError>): RequestResult<GetMirrorCompatibilityResponses, unknown, ThrowOnError> => (options.client ?? client).get<GetMirrorCompatibilityResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/mirrors/compatibility', ...options });
|
||||
|
||||
/**
|
||||
* Reorder backup schedules by providing an array of schedule short IDs in the desired order
|
||||
*/
|
||||
export const reorderBackupSchedules = <ThrowOnError extends boolean = false>(options: Options<ReorderBackupSchedulesData, ThrowOnError>) => (options.client ?? client).post<ReorderBackupSchedulesResponses, unknown, ThrowOnError>({
|
||||
export const reorderBackupSchedules = <ThrowOnError extends boolean = false>(options: Options<ReorderBackupSchedulesData, ThrowOnError>): RequestResult<ReorderBackupSchedulesResponses, unknown, ThrowOnError> => (options.client ?? client).post<ReorderBackupSchedulesResponses, unknown, ThrowOnError>({
|
||||
url: '/api/v1/backups/reorder',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -485,17 +490,17 @@ export const reorderBackupSchedules = <ThrowOnError extends boolean = false>(opt
|
||||
/**
|
||||
* Get the last known progress for a currently running backup. Returns null if no progress has been reported yet.
|
||||
*/
|
||||
export const getBackupProgress = <ThrowOnError extends boolean = false>(options: Options<GetBackupProgressData, ThrowOnError>) => (options.client ?? client).get<GetBackupProgressResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/progress', ...options });
|
||||
export const getBackupProgress = <ThrowOnError extends boolean = false>(options: Options<GetBackupProgressData, ThrowOnError>): RequestResult<GetBackupProgressResponses, unknown, ThrowOnError> => (options.client ?? client).get<GetBackupProgressResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{shortId}/progress', ...options });
|
||||
|
||||
/**
|
||||
* List all notification destinations
|
||||
*/
|
||||
export const listNotificationDestinations = <ThrowOnError extends boolean = false>(options?: Options<ListNotificationDestinationsData, ThrowOnError>) => (options?.client ?? client).get<ListNotificationDestinationsResponses, unknown, ThrowOnError>({ url: '/api/v1/notifications/destinations', ...options });
|
||||
export const listNotificationDestinations = <ThrowOnError extends boolean = false>(options?: Options<ListNotificationDestinationsData, ThrowOnError>): RequestResult<ListNotificationDestinationsResponses, unknown, ThrowOnError> => (options?.client ?? client).get<ListNotificationDestinationsResponses, unknown, ThrowOnError>({ url: '/api/v1/notifications/destinations', ...options });
|
||||
|
||||
/**
|
||||
* Create a new notification destination
|
||||
*/
|
||||
export const createNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<CreateNotificationDestinationData, ThrowOnError>) => (options.client ?? client).post<CreateNotificationDestinationResponses, unknown, ThrowOnError>({
|
||||
export const createNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<CreateNotificationDestinationData, ThrowOnError>): RequestResult<CreateNotificationDestinationResponses, unknown, ThrowOnError> => (options.client ?? client).post<CreateNotificationDestinationResponses, unknown, ThrowOnError>({
|
||||
url: '/api/v1/notifications/destinations',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -507,17 +512,17 @@ export const createNotificationDestination = <ThrowOnError extends boolean = fal
|
||||
/**
|
||||
* Delete a notification destination
|
||||
*/
|
||||
export const deleteNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<DeleteNotificationDestinationData, ThrowOnError>) => (options.client ?? client).delete<DeleteNotificationDestinationResponses, DeleteNotificationDestinationErrors, ThrowOnError>({ url: '/api/v1/notifications/destinations/{id}', ...options });
|
||||
export const deleteNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<DeleteNotificationDestinationData, ThrowOnError>): RequestResult<DeleteNotificationDestinationResponses, DeleteNotificationDestinationErrors, ThrowOnError> => (options.client ?? client).delete<DeleteNotificationDestinationResponses, DeleteNotificationDestinationErrors, ThrowOnError>({ url: '/api/v1/notifications/destinations/{id}', ...options });
|
||||
|
||||
/**
|
||||
* Get a notification destination by ID
|
||||
*/
|
||||
export const getNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<GetNotificationDestinationData, ThrowOnError>) => (options.client ?? client).get<GetNotificationDestinationResponses, GetNotificationDestinationErrors, ThrowOnError>({ url: '/api/v1/notifications/destinations/{id}', ...options });
|
||||
export const getNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<GetNotificationDestinationData, ThrowOnError>): RequestResult<GetNotificationDestinationResponses, GetNotificationDestinationErrors, ThrowOnError> => (options.client ?? client).get<GetNotificationDestinationResponses, GetNotificationDestinationErrors, ThrowOnError>({ url: '/api/v1/notifications/destinations/{id}', ...options });
|
||||
|
||||
/**
|
||||
* Update a notification destination
|
||||
*/
|
||||
export const updateNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<UpdateNotificationDestinationData, ThrowOnError>) => (options.client ?? client).patch<UpdateNotificationDestinationResponses, UpdateNotificationDestinationErrors, ThrowOnError>({
|
||||
export const updateNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<UpdateNotificationDestinationData, ThrowOnError>): RequestResult<UpdateNotificationDestinationResponses, UpdateNotificationDestinationErrors, ThrowOnError> => (options.client ?? client).patch<UpdateNotificationDestinationResponses, UpdateNotificationDestinationErrors, ThrowOnError>({
|
||||
url: '/api/v1/notifications/destinations/{id}',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -529,27 +534,27 @@ export const updateNotificationDestination = <ThrowOnError extends boolean = fal
|
||||
/**
|
||||
* Test a notification destination by sending a test message
|
||||
*/
|
||||
export const testNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<TestNotificationDestinationData, ThrowOnError>) => (options.client ?? client).post<TestNotificationDestinationResponses, TestNotificationDestinationErrors, ThrowOnError>({ url: '/api/v1/notifications/destinations/{id}/test', ...options });
|
||||
export const testNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<TestNotificationDestinationData, ThrowOnError>): RequestResult<TestNotificationDestinationResponses, TestNotificationDestinationErrors, ThrowOnError> => (options.client ?? client).post<TestNotificationDestinationResponses, TestNotificationDestinationErrors, ThrowOnError>({ url: '/api/v1/notifications/destinations/{id}/test', ...options });
|
||||
|
||||
/**
|
||||
* Get system information including available capabilities
|
||||
*/
|
||||
export const getSystemInfo = <ThrowOnError extends boolean = false>(options?: Options<GetSystemInfoData, ThrowOnError>) => (options?.client ?? client).get<GetSystemInfoResponses, unknown, ThrowOnError>({ url: '/api/v1/system/info', ...options });
|
||||
export const getSystemInfo = <ThrowOnError extends boolean = false>(options?: Options<GetSystemInfoData, ThrowOnError>): RequestResult<GetSystemInfoResponses, unknown, ThrowOnError> => (options?.client ?? client).get<GetSystemInfoResponses, unknown, ThrowOnError>({ url: '/api/v1/system/info', ...options });
|
||||
|
||||
/**
|
||||
* Check for application updates from GitHub
|
||||
*/
|
||||
export const getUpdates = <ThrowOnError extends boolean = false>(options?: Options<GetUpdatesData, ThrowOnError>) => (options?.client ?? client).get<GetUpdatesResponses, unknown, ThrowOnError>({ url: '/api/v1/system/updates', ...options });
|
||||
export const getUpdates = <ThrowOnError extends boolean = false>(options?: Options<GetUpdatesData, ThrowOnError>): RequestResult<GetUpdatesResponses, unknown, ThrowOnError> => (options?.client ?? client).get<GetUpdatesResponses, unknown, ThrowOnError>({ url: '/api/v1/system/updates', ...options });
|
||||
|
||||
/**
|
||||
* Get the current registration status for new users
|
||||
*/
|
||||
export const getRegistrationStatus = <ThrowOnError extends boolean = false>(options?: Options<GetRegistrationStatusData, ThrowOnError>) => (options?.client ?? client).get<GetRegistrationStatusResponses, unknown, ThrowOnError>({ url: '/api/v1/system/registration-status', ...options });
|
||||
export const getRegistrationStatus = <ThrowOnError extends boolean = false>(options?: Options<GetRegistrationStatusData, ThrowOnError>): RequestResult<GetRegistrationStatusResponses, unknown, ThrowOnError> => (options?.client ?? client).get<GetRegistrationStatusResponses, unknown, ThrowOnError>({ url: '/api/v1/system/registration-status', ...options });
|
||||
|
||||
/**
|
||||
* Update the registration status for new users. Requires global admin role.
|
||||
*/
|
||||
export const setRegistrationStatus = <ThrowOnError extends boolean = false>(options: Options<SetRegistrationStatusData, ThrowOnError>) => (options.client ?? client).put<SetRegistrationStatusResponses, unknown, ThrowOnError>({
|
||||
export const setRegistrationStatus = <ThrowOnError extends boolean = false>(options: Options<SetRegistrationStatusData, ThrowOnError>): RequestResult<SetRegistrationStatusResponses, unknown, ThrowOnError> => (options.client ?? client).put<SetRegistrationStatusResponses, unknown, ThrowOnError>({
|
||||
url: '/api/v1/system/registration-status',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -561,7 +566,7 @@ export const setRegistrationStatus = <ThrowOnError extends boolean = false>(opti
|
||||
/**
|
||||
* Download the organization's Restic password for backup recovery. Requires organization owner or admin role and may require password re-authentication.
|
||||
*/
|
||||
export const downloadResticPassword = <ThrowOnError extends boolean = false>(options: Options<DownloadResticPasswordData, ThrowOnError>) => (options.client ?? client).post<DownloadResticPasswordResponses, unknown, ThrowOnError>({
|
||||
export const downloadResticPassword = <ThrowOnError extends boolean = false>(options: Options<DownloadResticPasswordData, ThrowOnError>): RequestResult<DownloadResticPasswordResponses, unknown, ThrowOnError> => (options.client ?? client).post<DownloadResticPasswordResponses, unknown, ThrowOnError>({
|
||||
url: '/api/v1/system/restic-password',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -573,12 +578,12 @@ export const downloadResticPassword = <ThrowOnError extends boolean = false>(opt
|
||||
/**
|
||||
* Get whether password-based login is disabled
|
||||
*/
|
||||
export const getPasswordLoginStatus = <ThrowOnError extends boolean = false>(options?: Options<GetPasswordLoginStatusData, ThrowOnError>) => (options?.client ?? client).get<GetPasswordLoginStatusResponses, unknown, ThrowOnError>({ url: '/api/v1/system/password-login-status', ...options });
|
||||
export const getPasswordLoginStatus = <ThrowOnError extends boolean = false>(options?: Options<GetPasswordLoginStatusData, ThrowOnError>): RequestResult<GetPasswordLoginStatusResponses, unknown, ThrowOnError> => (options?.client ?? client).get<GetPasswordLoginStatusResponses, unknown, ThrowOnError>({ url: '/api/v1/system/password-login-status', ...options });
|
||||
|
||||
/**
|
||||
* Disable or re-enable password-based login. Requires global admin role.
|
||||
*/
|
||||
export const setPasswordLoginStatus = <ThrowOnError extends boolean = false>(options: Options<SetPasswordLoginStatusData, ThrowOnError>) => (options.client ?? client).put<SetPasswordLoginStatusResponses, unknown, ThrowOnError>({
|
||||
export const setPasswordLoginStatus = <ThrowOnError extends boolean = false>(options: Options<SetPasswordLoginStatusData, ThrowOnError>): RequestResult<SetPasswordLoginStatusResponses, unknown, ThrowOnError> => (options.client ?? client).put<SetPasswordLoginStatusResponses, unknown, ThrowOnError>({
|
||||
url: '/api/v1/system/password-login-status',
|
||||
...options,
|
||||
headers: {
|
||||
@@ -590,12 +595,12 @@ export const setPasswordLoginStatus = <ThrowOnError extends boolean = false>(opt
|
||||
/**
|
||||
* Get the dev panel status
|
||||
*/
|
||||
export const getDevPanel = <ThrowOnError extends boolean = false>(options?: Options<GetDevPanelData, ThrowOnError>) => (options?.client ?? client).get<GetDevPanelResponses, unknown, ThrowOnError>({ url: '/api/v1/system/dev-panel', ...options });
|
||||
export const getDevPanel = <ThrowOnError extends boolean = false>(options?: Options<GetDevPanelData, ThrowOnError>): RequestResult<GetDevPanelResponses, unknown, ThrowOnError> => (options?.client ?? client).get<GetDevPanelResponses, unknown, ThrowOnError>({ url: '/api/v1/system/dev-panel', ...options });
|
||||
|
||||
/**
|
||||
* Create an authenticated desktop session
|
||||
*/
|
||||
export const createDesktopSession = <ThrowOnError extends boolean = false>(options: Options<CreateDesktopSessionData, ThrowOnError>) => (options.client ?? client).post<CreateDesktopSessionResponses, unknown, ThrowOnError>({
|
||||
export const createDesktopSession = <ThrowOnError extends boolean = false>(options: Options<CreateDesktopSessionData, ThrowOnError>): RequestResult<CreateDesktopSessionResponses, unknown, ThrowOnError> => (options.client ?? client).post<CreateDesktopSessionResponses, unknown, ThrowOnError>({
|
||||
url: '/api/v1/desktop/session',
|
||||
...options,
|
||||
headers: {
|
||||
|
||||
@@ -2336,6 +2336,30 @@ export type GetSnapshotDetailsResponses = {
|
||||
|
||||
export type GetSnapshotDetailsResponse = GetSnapshotDetailsResponses[keyof GetSnapshotDetailsResponses];
|
||||
|
||||
export type GetSnapshotRestorePlanData = {
|
||||
body?: never;
|
||||
path: {
|
||||
shortId: string;
|
||||
snapshotId: string;
|
||||
};
|
||||
query?: {
|
||||
targetAgentId?: string;
|
||||
};
|
||||
url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}/restore-plan';
|
||||
};
|
||||
|
||||
export type GetSnapshotRestorePlanResponses = {
|
||||
/**
|
||||
* Target-scoped snapshot restore plan
|
||||
*/
|
||||
200: {
|
||||
queryBasePath: string;
|
||||
requiresCustomTarget: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type GetSnapshotRestorePlanResponse = GetSnapshotRestorePlanResponses[keyof GetSnapshotRestorePlanResponses];
|
||||
|
||||
export type ListSnapshotFilesData = {
|
||||
body?: never;
|
||||
path: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { HttpResponse, http, server } from "~/test/msw/server";
|
||||
import { cleanup, render, screen, userEvent, waitFor, within } from "~/test/test-utils";
|
||||
import { fromAny } from "@total-typescript/shoehorn";
|
||||
import { getSnapshotSourcePathPlan } from "@zerobyte/core/utils";
|
||||
|
||||
vi.mock("@tanstack/react-router", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@tanstack/react-router")>();
|
||||
@@ -14,6 +15,9 @@ vi.mock("@tanstack/react-router", async (importOriginal) => {
|
||||
|
||||
import { RestoreForm } from "../restore-form";
|
||||
|
||||
const sourcePathPlan = (snapshotPaths: string[], hostPathKind: "posix" | "windows" = "posix") =>
|
||||
getSnapshotSourcePathPlan({ snapshotPaths, hostPathKind });
|
||||
|
||||
class MockEventSource {
|
||||
addEventListener() {}
|
||||
close() {}
|
||||
@@ -26,6 +30,19 @@ const originalEventSource = globalThis.EventSource;
|
||||
|
||||
beforeEach(() => {
|
||||
globalThis.EventSource = MockEventSource as unknown as typeof EventSource;
|
||||
server.use(
|
||||
http.get("/api/v1/system/info", () =>
|
||||
HttpResponse.json({
|
||||
runtime: "server",
|
||||
capabilities: {
|
||||
rclone: false,
|
||||
sysAdmin: false,
|
||||
volumeBackends: ["directory"],
|
||||
repositoryBackends: ["local", "s3", "r2", "gcs", "azure", "sftp", "rest"],
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -62,7 +79,7 @@ describe("RestoreForm", () => {
|
||||
repository={fromAny({ shortId: "repo-1", name: "Repo 1" })}
|
||||
snapshotId="snap-1"
|
||||
returnPath="/repositories/repo-1/snap-1"
|
||||
queryBasePath="/mnt/project/subdir"
|
||||
snapshotSourcePathPlan={sourcePathPlan(["/mnt/project/subdir"])}
|
||||
displayBasePath="/mnt"
|
||||
/>,
|
||||
);
|
||||
@@ -81,7 +98,7 @@ describe("RestoreForm", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("restores the selected full path when the display root is unrelated", async () => {
|
||||
test("requires a custom target when the display root is unrelated", async () => {
|
||||
let restoreRequestBody: unknown;
|
||||
|
||||
server.use(
|
||||
@@ -115,7 +132,7 @@ describe("RestoreForm", () => {
|
||||
repository={fromAny({ shortId: "repo-1", name: "Repo 1" })}
|
||||
snapshotId="snap-1"
|
||||
returnPath="/repositories/repo-1/snap-1"
|
||||
queryBasePath="/mnt/project"
|
||||
snapshotSourcePathPlan={sourcePathPlan(["/mnt/project"])}
|
||||
displayBasePath="/other/root"
|
||||
/>,
|
||||
);
|
||||
@@ -152,4 +169,25 @@ describe("RestoreForm", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("allows original restore when Windows snapshot paths match a Windows host", () => {
|
||||
server.use(
|
||||
http.get("/api/v1/repositories/:shortId/snapshots/:snapshotId/files", () =>
|
||||
HttpResponse.json({ files: [] }),
|
||||
),
|
||||
);
|
||||
|
||||
render(
|
||||
<RestoreForm
|
||||
repository={fromAny({ shortId: "repo-1", name: "Repo 1" })}
|
||||
snapshotId="snap-1"
|
||||
returnPath="/repositories/repo-1/snap-1"
|
||||
snapshotSourcePathPlan={sourcePathPlan(["C:\\Users\\nicolas\\Photos"], "windows")}
|
||||
displayBasePath="C:\\Users\\nicolas"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Source paths do not match")).toBeNull();
|
||||
expect(screen.getByRole("button", { name: "Original location" }).hasAttribute("disabled")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { HttpResponse, http, server } from "~/test/msw/server";
|
||||
import { cleanup, render, waitFor } from "~/test/test-utils";
|
||||
import { LocalFileBrowser } from "../local-file-browser";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("LocalFileBrowser", () => {
|
||||
test("keeps Windows drive roots as host paths", async () => {
|
||||
const requests: string[] = [];
|
||||
|
||||
server.use(
|
||||
http.get("/api/v1/volumes/filesystem/browse", ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
requests.push(url.searchParams.get("path") ?? "");
|
||||
|
||||
return HttpResponse.json({
|
||||
directories: [],
|
||||
path: "C:\\",
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
render(<LocalFileBrowser initialPath={"C:\\"} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(requests).toEqual(["C:\\"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -208,6 +208,33 @@ describe("SnapshotTreeBrowser", () => {
|
||||
expect(selectedKind === "dir").toBe(true);
|
||||
});
|
||||
|
||||
test("maps Windows display roots to restic snapshot tree paths", async () => {
|
||||
mockListSnapshotFiles({
|
||||
files: [
|
||||
{ name: "Downloads", path: "/C/Users/nicolas/Downloads", type: "dir" },
|
||||
{ name: "a.txt", path: "/C/Users/nicolas/Downloads/a.txt", type: "file" },
|
||||
],
|
||||
});
|
||||
|
||||
let selectedPaths: Set<string> | undefined;
|
||||
|
||||
renderSnapshotTreeBrowser({
|
||||
queryBasePath: "/C/Users/nicolas",
|
||||
displayBasePath: "C:\\Users\\nicolas",
|
||||
withCheckboxes: true,
|
||||
onSelectionChange: (paths) => {
|
||||
selectedPaths = paths;
|
||||
},
|
||||
});
|
||||
|
||||
const row = await screen.findByRole("button", { name: "Downloads" });
|
||||
const checkbox = within(row).getByRole("checkbox");
|
||||
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
expect(selectedPaths ? Array.from(selectedPaths) : []).toEqual(["/C/Users/nicolas/Downloads"]);
|
||||
});
|
||||
|
||||
test("uses the query base path for the initial request when display base path is broader", async () => {
|
||||
const requests = mockListSnapshotFiles();
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { browseFilesystemOptions } from "~/client/api-client/@tanstack/react-que
|
||||
import { FileBrowser, type FileBrowserUiProps } from "~/client/components/file-browsers/file-browser";
|
||||
import { useFileBrowser } from "~/client/hooks/use-file-browser";
|
||||
import { parseError } from "~/client/lib/errors";
|
||||
import { normalizeAbsolutePath } from "@zerobyte/core/utils";
|
||||
import { logger } from "~/client/lib/logger";
|
||||
|
||||
type LocalFileBrowserProps = FileBrowserUiProps & {
|
||||
@@ -13,10 +12,10 @@ type LocalFileBrowserProps = FileBrowserUiProps & {
|
||||
|
||||
export const LocalFileBrowser = ({ initialPath = "/", enabled = true, ...uiProps }: LocalFileBrowserProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const normalizedInitialPath = normalizeAbsolutePath(initialPath);
|
||||
const browsePath = initialPath.trim() ? initialPath : "/";
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
...browseFilesystemOptions({ query: { path: normalizedInitialPath } }),
|
||||
...browseFilesystemOptions({ query: { path: browsePath } }),
|
||||
enabled,
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@ import { listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-qu
|
||||
import { FileBrowser, type FileBrowserUiProps } from "~/client/components/file-browsers/file-browser";
|
||||
import { useFileBrowser } from "~/client/hooks/use-file-browser";
|
||||
import { parseError } from "~/client/lib/errors";
|
||||
import { isPathWithin, normalizeAbsolutePath } from "@zerobyte/core/utils";
|
||||
import { isPathWithin, normalizeAbsolutePath, windowsHostPathToResticSnapshotPath } from "@zerobyte/core/utils";
|
||||
|
||||
function normalizeSnapshotTreeBasePath(basePath?: string) {
|
||||
return windowsHostPathToResticSnapshotPath(basePath ?? "") ?? normalizeAbsolutePath(basePath ?? "/");
|
||||
}
|
||||
|
||||
function createPathPrefixFns(basePath: string) {
|
||||
return {
|
||||
@@ -46,7 +50,7 @@ export const SnapshotTreeBrowser = (props: SnapshotTreeBrowserProps) => {
|
||||
const { selectedPaths, onSelectionChange, onSingleSelectionKindChange, ...fileBrowserUiProps } = uiProps;
|
||||
const queryClient = useQueryClient();
|
||||
const normalizedQueryBasePath = normalizeAbsolutePath(queryBasePath);
|
||||
const normalizedDisplayBasePath = normalizeAbsolutePath(displayBasePath ?? "/");
|
||||
const normalizedDisplayBasePath = normalizeSnapshotTreeBasePath(displayBasePath);
|
||||
const effectiveDisplayBasePath = isPathWithin(normalizedDisplayBasePath, normalizedQueryBasePath)
|
||||
? normalizedDisplayBasePath
|
||||
: "/";
|
||||
|
||||
@@ -23,37 +23,40 @@ import { RestoreProgress } from "~/client/components/restore-progress";
|
||||
import { restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { type RestoreCompletedEvent, useServerEvents } from "~/client/hooks/use-server-events";
|
||||
import { OVERWRITE_MODES, type OverwriteMode } from "@zerobyte/core/restic";
|
||||
import { isPathWithin } from "@zerobyte/core/utils";
|
||||
import { isPathWithin, normalizeAbsolutePath, windowsHostPathToResticSnapshotPath } from "@zerobyte/core/utils";
|
||||
import type { Repository } from "~/client/lib/types";
|
||||
import { handleRepositoryError } from "~/client/lib/errors";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { cn } from "~/client/lib/utils";
|
||||
|
||||
type RestoreLocation = "original" | "custom";
|
||||
type SnapshotRestorePlan = { queryBasePath: string; requiresCustomTarget: boolean };
|
||||
|
||||
const normalizeSnapshotBasePath = (value?: string) =>
|
||||
windowsHostPathToResticSnapshotPath(value ?? "") ?? normalizeAbsolutePath(value ?? "/");
|
||||
|
||||
interface RestoreFormProps {
|
||||
repository: Repository;
|
||||
snapshotId: string;
|
||||
returnPath: string;
|
||||
queryBasePath?: string;
|
||||
snapshotSourcePathPlan: SnapshotRestorePlan;
|
||||
displayBasePath?: string;
|
||||
hasNonPosixSnapshotPaths?: boolean;
|
||||
}
|
||||
|
||||
export function RestoreForm({
|
||||
repository,
|
||||
snapshotId,
|
||||
returnPath,
|
||||
queryBasePath,
|
||||
snapshotSourcePathPlan,
|
||||
displayBasePath,
|
||||
hasNonPosixSnapshotPaths = false,
|
||||
}: RestoreFormProps) {
|
||||
const navigate = useNavigate();
|
||||
const { addEventListener } = useServerEvents();
|
||||
|
||||
const snapshotBasePath = queryBasePath ?? "/";
|
||||
const hasMismatchedDisplayBasePath = displayBasePath && !isPathWithin(displayBasePath, snapshotBasePath);
|
||||
const restoreRequiresCustomTarget = hasNonPosixSnapshotPaths || hasMismatchedDisplayBasePath;
|
||||
const snapshotBasePath = snapshotSourcePathPlan.queryBasePath;
|
||||
const hasMismatchedDisplayBasePath =
|
||||
displayBasePath && !isPathWithin(normalizeSnapshotBasePath(displayBasePath), snapshotBasePath);
|
||||
const restoreRequiresCustomTarget = snapshotSourcePathPlan.requiresCustomTarget || !!hasMismatchedDisplayBasePath;
|
||||
|
||||
const [restoreLocation, setRestoreLocation] = useState<RestoreLocation>(
|
||||
restoreRequiresCustomTarget ? "custom" : "original",
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTimeFormat } from "~/client/lib/datetime";
|
||||
import { cn } from "~/client/lib/utils";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { SnapshotTreeBrowser } from "~/client/components/file-browsers/snapshot-tree-browser";
|
||||
import { findCommonAncestor } from "@zerobyte/core/utils";
|
||||
import { getSnapshotSourcePathPlan, hostPathKindFromPath } from "@zerobyte/core/utils";
|
||||
|
||||
interface Props {
|
||||
snapshot: Snapshot;
|
||||
@@ -30,8 +30,10 @@ export const SnapshotFileBrowser = (props: Props) => {
|
||||
const { snapshot, repositoryId, backupId, displayBasePath, onDeleteSnapshot, isDeletingSnapshot } = props;
|
||||
const { formatDateTime } = useTimeFormat();
|
||||
|
||||
const hasNonPosixSnapshotPaths = snapshot.paths.some((path) => !path.startsWith("/"));
|
||||
const queryBasePath = hasNonPosixSnapshotPaths ? "/" : findCommonAncestor(snapshot.paths);
|
||||
const snapshotSourcePathPlan = getSnapshotSourcePathPlan({
|
||||
snapshotPaths: snapshot.paths,
|
||||
hostPathKind: hostPathKindFromPath(displayBasePath),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -80,7 +82,7 @@ export const SnapshotFileBrowser = (props: Props) => {
|
||||
<SnapshotTreeBrowser
|
||||
repositoryId={repositoryId}
|
||||
snapshotId={snapshot.short_id}
|
||||
queryBasePath={queryBasePath}
|
||||
queryBasePath={snapshotSourcePathPlan.queryBasePath}
|
||||
displayBasePath={displayBasePath}
|
||||
{...treeProps}
|
||||
/>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { RestoreForm } from "~/client/components/restore-form";
|
||||
import type { Repository } from "~/client/lib/types";
|
||||
|
||||
type SnapshotRestorePlan = { queryBasePath: string; requiresCustomTarget: boolean };
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
snapshotId: string;
|
||||
returnPath: string;
|
||||
queryBasePath?: string;
|
||||
snapshotSourcePathPlan: SnapshotRestorePlan;
|
||||
displayBasePath?: string;
|
||||
hasNonPosixSnapshotPaths?: boolean;
|
||||
};
|
||||
|
||||
export function RestoreSnapshotPage(props: Props) {
|
||||
const { returnPath, snapshotId, repository, queryBasePath, displayBasePath, hasNonPosixSnapshotPaths } = props;
|
||||
const { returnPath, snapshotId, repository, snapshotSourcePathPlan, displayBasePath } = props;
|
||||
|
||||
return (
|
||||
<RestoreForm
|
||||
repository={repository}
|
||||
snapshotId={snapshotId}
|
||||
returnPath={returnPath}
|
||||
queryBasePath={queryBasePath}
|
||||
snapshotSourcePathPlan={snapshotSourcePathPlan}
|
||||
displayBasePath={displayBasePath}
|
||||
hasNonPosixSnapshotPaths={hasNonPosixSnapshotPaths}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { getBackupSchedule } from "~/client/api-client";
|
||||
import { getRepositoryOptions, getSnapshotDetailsOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import {
|
||||
getRepositoryOptions,
|
||||
getSnapshotDetailsOptions,
|
||||
getSnapshotRestorePlanOptions,
|
||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { RestoreSnapshotPage } from "~/client/modules/repositories/routes/restore-snapshot";
|
||||
import { getVolumeMountPath } from "~/client/lib/volume-path";
|
||||
import { findCommonAncestor } from "@zerobyte/core/utils";
|
||||
|
||||
export const Route = createFileRoute("/(dashboard)/backups/$backupId/$snapshotId/restore")({
|
||||
component: RouteComponent,
|
||||
@@ -15,7 +18,7 @@ export const Route = createFileRoute("/(dashboard)/backups/$backupId/$snapshotId
|
||||
throw new Response("Not Found", { status: 404 });
|
||||
}
|
||||
|
||||
const [snapshot, repository] = await Promise.all([
|
||||
const [snapshot, repository, restorePlan] = await Promise.all([
|
||||
context.queryClient.ensureQueryData({
|
||||
...getSnapshotDetailsOptions({
|
||||
path: { shortId: schedule.data.repository.shortId, snapshotId: params.snapshotId },
|
||||
@@ -24,17 +27,21 @@ export const Route = createFileRoute("/(dashboard)/backups/$backupId/$snapshotId
|
||||
context.queryClient.ensureQueryData({
|
||||
...getRepositoryOptions({ path: { shortId: schedule.data.repository.shortId } }),
|
||||
}),
|
||||
context.queryClient.ensureQueryData({
|
||||
...getSnapshotRestorePlanOptions({
|
||||
path: { shortId: schedule.data.repository.shortId, snapshotId: params.snapshotId },
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
const hasNonPosixSnapshotPaths = snapshot.paths.some((path) => !path.startsWith("/"));
|
||||
const displayBasePath = getVolumeMountPath(schedule.data.volume);
|
||||
|
||||
return {
|
||||
snapshot,
|
||||
repository,
|
||||
schedule: schedule.data,
|
||||
queryBasePath: hasNonPosixSnapshotPaths ? "/" : findCommonAncestor(snapshot.paths),
|
||||
displayBasePath: getVolumeMountPath(schedule.data.volume),
|
||||
hasNonPosixSnapshotPaths,
|
||||
displayBasePath,
|
||||
snapshotSourcePathPlan: restorePlan,
|
||||
};
|
||||
},
|
||||
head: ({ params }) => ({
|
||||
@@ -58,16 +65,15 @@ export const Route = createFileRoute("/(dashboard)/backups/$backupId/$snapshotId
|
||||
|
||||
function RouteComponent() {
|
||||
const { backupId, snapshotId } = Route.useParams();
|
||||
const { repository, queryBasePath, displayBasePath, hasNonPosixSnapshotPaths } = Route.useLoaderData();
|
||||
const { repository, displayBasePath, snapshotSourcePathPlan } = Route.useLoaderData();
|
||||
|
||||
return (
|
||||
<RestoreSnapshotPage
|
||||
returnPath={`/backups/${backupId}`}
|
||||
snapshotId={snapshotId}
|
||||
repository={repository}
|
||||
queryBasePath={queryBasePath}
|
||||
snapshotSourcePathPlan={snapshotSourcePathPlan}
|
||||
displayBasePath={displayBasePath}
|
||||
hasNonPosixSnapshotPaths={hasNonPosixSnapshotPaths}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { getBackupSchedule } from "~/client/api-client";
|
||||
import { getRepositoryOptions, getSnapshotDetailsOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import {
|
||||
getRepositoryOptions,
|
||||
getSnapshotDetailsOptions,
|
||||
getSnapshotRestorePlanOptions,
|
||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { RestoreSnapshotPage } from "~/client/modules/repositories/routes/restore-snapshot";
|
||||
import { getVolumeMountPath } from "~/client/lib/volume-path";
|
||||
import { findCommonAncestor } from "@zerobyte/core/utils";
|
||||
|
||||
export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/$snapshotId/restore")({
|
||||
component: RouteComponent,
|
||||
errorComponent: (e) => <div>{e.error.message}</div>,
|
||||
loader: async ({ params, context }) => {
|
||||
const [snapshot, repository] = await Promise.all([
|
||||
const [snapshot, repository, restorePlan] = await Promise.all([
|
||||
context.queryClient.ensureQueryData({
|
||||
...getSnapshotDetailsOptions({ path: { shortId: params.repositoryId, snapshotId: params.snapshotId } }),
|
||||
}),
|
||||
context.queryClient.ensureQueryData({ ...getRepositoryOptions({ path: { shortId: params.repositoryId } }) }),
|
||||
context.queryClient.ensureQueryData({
|
||||
...getRepositoryOptions({ path: { shortId: params.repositoryId } }),
|
||||
}),
|
||||
context.queryClient.ensureQueryData({
|
||||
...getSnapshotRestorePlanOptions({
|
||||
path: { shortId: params.repositoryId, snapshotId: params.snapshotId },
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
let displayBasePath: string | undefined;
|
||||
@@ -25,21 +35,24 @@ export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/$s
|
||||
}
|
||||
}
|
||||
|
||||
const hasNonPosixSnapshotPaths = snapshot.paths.some((path) => !path.startsWith("/"));
|
||||
|
||||
return {
|
||||
snapshot,
|
||||
repository,
|
||||
queryBasePath: hasNonPosixSnapshotPaths ? "/" : findCommonAncestor(snapshot.paths),
|
||||
displayBasePath,
|
||||
hasNonPosixSnapshotPaths,
|
||||
snapshotSourcePathPlan: restorePlan,
|
||||
};
|
||||
},
|
||||
staticData: {
|
||||
breadcrumb: (match) => [
|
||||
{ label: "Repositories", href: "/repositories" },
|
||||
{ label: match.loaderData?.repository?.name || "Repository", href: `/repositories/${match.params.repositoryId}` },
|
||||
{ label: match.params.snapshotId, href: `/repositories/${match.params.repositoryId}/${match.params.snapshotId}` },
|
||||
{
|
||||
label: match.loaderData?.repository?.name || "Repository",
|
||||
href: `/repositories/${match.params.repositoryId}`,
|
||||
},
|
||||
{
|
||||
label: match.params.snapshotId,
|
||||
href: `/repositories/${match.params.repositoryId}/${match.params.snapshotId}`,
|
||||
},
|
||||
{ label: "Restore" },
|
||||
],
|
||||
},
|
||||
@@ -56,16 +69,15 @@ export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/$s
|
||||
|
||||
function RouteComponent() {
|
||||
const { repositoryId, snapshotId } = Route.useParams();
|
||||
const { repository, queryBasePath, displayBasePath, hasNonPosixSnapshotPaths } = Route.useLoaderData();
|
||||
const { repository, displayBasePath, snapshotSourcePathPlan } = Route.useLoaderData();
|
||||
|
||||
return (
|
||||
<RestoreSnapshotPage
|
||||
returnPath={`/repositories/${repositoryId}/${snapshotId}`}
|
||||
repository={repository}
|
||||
snapshotId={snapshotId}
|
||||
queryBasePath={queryBasePath}
|
||||
snapshotSourcePathPlan={snapshotSourcePathPlan}
|
||||
displayBasePath={displayBasePath}
|
||||
hasNonPosixSnapshotPaths={hasNonPosixSnapshotPaths}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ import { Effect } from "effect";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import waitForExpect from "wait-for-expect";
|
||||
import { fromPartial } from "@total-typescript/shoehorn";
|
||||
import { createAgentMessage } from "@zerobyte/contracts/agent-protocol";
|
||||
import {
|
||||
createAgentMessage,
|
||||
AGENT_PROTOCOL_VERSION,
|
||||
SUPPORTED_AGENT_PROTOCOL_MAX_VERSION,
|
||||
} from "@zerobyte/contracts/agent-protocol";
|
||||
import type { Volume } from "@zerobyte/contracts/volumes";
|
||||
import { LOCAL_AGENT_ID, LOCAL_AGENT_KIND, LOCAL_AGENT_NAME } from "../constants";
|
||||
|
||||
@@ -55,7 +59,7 @@ const backupVolume = {
|
||||
|
||||
const readyPayload = {
|
||||
agentId: LOCAL_AGENT_ID,
|
||||
protocolVersion: 1,
|
||||
protocolVersion: AGENT_PROTOCOL_VERSION,
|
||||
hostname: "host",
|
||||
platform: "linux",
|
||||
capabilities: { backup: true },
|
||||
@@ -205,11 +209,11 @@ test("websocket lifecycle updates agent connection status", async () => {
|
||||
agentKind: LOCAL_AGENT_KIND,
|
||||
});
|
||||
expect(agentsServiceMocks.markAgentOnline).toHaveBeenCalledWith(LOCAL_AGENT_ID, expect.any(Number), {
|
||||
platform: "linux",
|
||||
backup: true,
|
||||
protocolVersion: 1,
|
||||
protocolVersion: AGENT_PROTOCOL_VERSION,
|
||||
protocolCompatible: true,
|
||||
hostname: "host",
|
||||
platform: "linux",
|
||||
});
|
||||
expect(agentsServiceMocks.markAgentSeen).toHaveBeenCalledWith(LOCAL_AGENT_ID, expect.any(Number));
|
||||
expect(agentsServiceMocks.markAgentOffline).toHaveBeenCalledWith(LOCAL_AGENT_ID);
|
||||
@@ -230,7 +234,7 @@ test("websocket protocol rejection forwards the event and closes the connection"
|
||||
JSON.stringify({
|
||||
type: "agent.ready",
|
||||
payload: {
|
||||
protocolVersion: 2,
|
||||
protocolVersion: SUPPORTED_AGENT_PROTOCOL_MAX_VERSION + 1,
|
||||
hostname: "host",
|
||||
platform: "linux",
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import waitForExpect from "wait-for-expect";
|
||||
import { fromPartial } from "@total-typescript/shoehorn";
|
||||
import {
|
||||
createAgentMessage,
|
||||
AGENT_PROTOCOL_VERSION,
|
||||
SUPPORTED_AGENT_PROTOCOL_MAX_VERSION,
|
||||
type AgentMessage,
|
||||
} from "@zerobyte/contracts/agent-protocol";
|
||||
@@ -141,7 +142,7 @@ test("invalid inbound messages are ignored", () => {
|
||||
session.handleMessage(
|
||||
createAgentMessage("agent.ready", {
|
||||
agentId: LOCAL_AGENT_ID,
|
||||
protocolVersion: 1,
|
||||
protocolVersion: AGENT_PROTOCOL_VERSION,
|
||||
hostname: "host",
|
||||
platform: "linux",
|
||||
capabilities: { backup: true },
|
||||
@@ -166,7 +167,7 @@ test("agent.ready marks the session ready and forwards the event", () => {
|
||||
session.handleMessage(
|
||||
createAgentMessage("agent.ready", {
|
||||
agentId: LOCAL_AGENT_ID,
|
||||
protocolVersion: 1,
|
||||
protocolVersion: AGENT_PROTOCOL_VERSION,
|
||||
hostname: "host",
|
||||
platform: "linux",
|
||||
capabilities: { backup: true },
|
||||
@@ -179,7 +180,7 @@ test("agent.ready marks the session ready and forwards the event", () => {
|
||||
type: "agent.ready",
|
||||
payload: {
|
||||
agentId: LOCAL_AGENT_ID,
|
||||
protocolVersion: 1,
|
||||
protocolVersion: AGENT_PROTOCOL_VERSION,
|
||||
hostname: "host",
|
||||
platform: "linux",
|
||||
capabilities: { backup: true },
|
||||
@@ -214,7 +215,7 @@ test("backup agent messages are forwarded unchanged", () => {
|
||||
session.handleMessage(
|
||||
createAgentMessage("agent.ready", {
|
||||
agentId: LOCAL_AGENT_ID,
|
||||
protocolVersion: 1,
|
||||
protocolVersion: AGENT_PROTOCOL_VERSION,
|
||||
hostname: "host",
|
||||
platform: "linux",
|
||||
capabilities: { backup: true },
|
||||
|
||||
@@ -56,6 +56,7 @@ const ensureLocalAgent = async () => {
|
||||
|
||||
const markAgentConnecting = async (params: AgentConnectionRegistration) => {
|
||||
const { agentId, organizationId, agentName, agentKind, capabilities, connectedAt = Date.now() } = params;
|
||||
const nextCapabilities = capabilities ?? LOCAL_AGENT_CAPABILITIES;
|
||||
|
||||
await db
|
||||
.insert(agentsTable)
|
||||
@@ -65,7 +66,7 @@ const markAgentConnecting = async (params: AgentConnectionRegistration) => {
|
||||
name: agentName,
|
||||
kind: agentKind,
|
||||
status: "connecting",
|
||||
capabilities: capabilities ?? {},
|
||||
capabilities: nextCapabilities,
|
||||
lastSeenAt: connectedAt,
|
||||
updatedAt: connectedAt,
|
||||
})
|
||||
@@ -78,7 +79,7 @@ const markAgentConnecting = async (params: AgentConnectionRegistration) => {
|
||||
status: "connecting",
|
||||
lastSeenAt: connectedAt,
|
||||
updatedAt: connectedAt,
|
||||
capabilities: capabilities ?? {},
|
||||
capabilities: nextCapabilities,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -95,7 +96,7 @@ const updateAgentRuntime = async (agentId: string, values: Partial<Agent>) => {
|
||||
return updatedAgent;
|
||||
};
|
||||
|
||||
const markAgentOnline = async (agentId: string, readyAt = Date.now(), metadata?: AgentCapabilities) => {
|
||||
const markAgentOnline = async (agentId: string, readyAt = Date.now(), metadata = LOCAL_AGENT_CAPABILITIES) => {
|
||||
return updateAgentRuntime(agentId, {
|
||||
status: "online",
|
||||
capabilities: metadata,
|
||||
|
||||
@@ -121,6 +121,7 @@ describe("repositories security", () => {
|
||||
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots" },
|
||||
{ method: "POST", path: "/api/v1/repositories/test-repo/snapshots/refresh" },
|
||||
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot" },
|
||||
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot/restore-plan" },
|
||||
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot/files" },
|
||||
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot/dump" },
|
||||
{ method: "POST", path: "/api/v1/repositories/test-repo/restore" },
|
||||
|
||||
@@ -526,7 +526,7 @@ describe("repositoriesService.dumpSnapshot", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("downloads the full snapshot from root when source paths are non-posix", async () => {
|
||||
test("downloads the full snapshot from root when source paths are native Windows paths", async () => {
|
||||
const { organizationId, userId, shortId } = await setupDumpSnapshotScenario({
|
||||
snapshotId: "snapshot-windows",
|
||||
basePath: "/tmp/repro/source",
|
||||
@@ -574,6 +574,17 @@ describe("repositoriesService.restoreSnapshot", () => {
|
||||
});
|
||||
};
|
||||
|
||||
const withPlatform = async <T>(platform: NodeJS.Platform, run: () => Promise<T>) => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, "platform", { value: platform });
|
||||
|
||||
try {
|
||||
return await run();
|
||||
} finally {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform });
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnableLocalAgent = config.flags.enableLocalAgent;
|
||||
config.flags.enableLocalAgent = true;
|
||||
@@ -647,11 +658,10 @@ describe("repositoriesService.restoreSnapshot", () => {
|
||||
expect.objectContaining({
|
||||
payload: expect.objectContaining({
|
||||
snapshotId: "snapshot-restore",
|
||||
target: targetPath,
|
||||
snapshotPaths: ["/var/lib/zerobyte/volumes/vol123/_data"],
|
||||
repositoryConfig: expect.objectContaining({ backend: "local" }),
|
||||
options: expect.objectContaining({
|
||||
organizationId,
|
||||
basePath: "/var/lib/zerobyte/volumes/vol123/_data",
|
||||
request: expect.objectContaining({
|
||||
location: { kind: "custom", targetPath },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
@@ -768,7 +778,7 @@ describe("repositoriesService.restoreSnapshot", () => {
|
||||
name: "Remote Agent",
|
||||
kind: "remote",
|
||||
status: "online",
|
||||
capabilities: {},
|
||||
capabilities: { platform: "linux" },
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
vi.spyOn(restic, "snapshots").mockReturnValue(
|
||||
@@ -802,7 +812,9 @@ describe("repositoriesService.restoreSnapshot", () => {
|
||||
agentId,
|
||||
expect.objectContaining({
|
||||
payload: expect.objectContaining({
|
||||
target: targetPath,
|
||||
request: expect.objectContaining({
|
||||
location: { kind: "custom", targetPath },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -830,7 +842,7 @@ describe("repositoriesService.restoreSnapshot", () => {
|
||||
name: "Other Org Agent",
|
||||
kind: "remote",
|
||||
status: "online",
|
||||
capabilities: {},
|
||||
capabilities: { platform: "linux" },
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
vi.spyOn(restic, "snapshots").mockReturnValue(
|
||||
@@ -895,7 +907,7 @@ describe("repositoriesService.restoreSnapshot", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects original-location restore for snapshots with non-posix source paths", async () => {
|
||||
test("rejects original-location restore for Windows snapshot paths on POSIX hosts", async () => {
|
||||
const { organizationId, userId, repositoryShortId, restoreMock } = await setupRestoreSnapshotScenario([
|
||||
"d:\\some\\path",
|
||||
]);
|
||||
@@ -912,7 +924,112 @@ describe("repositoriesService.restoreSnapshot", () => {
|
||||
expect(restoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("allows restore-all to a custom target for snapshots with non-posix source paths", async () => {
|
||||
test("allows original-location restore for Windows snapshot paths on Windows hosts", async () => {
|
||||
const { organizationId, userId, repositoryShortId, restoreMock } = await setupRestoreSnapshotScenario([
|
||||
"C:\\Users\\nicolas\\Photos",
|
||||
"C:\\Users\\nicolas\\Documents",
|
||||
]);
|
||||
|
||||
await withPlatform("win32", () =>
|
||||
withContext({ organizationId, userId }, () =>
|
||||
repositoriesService.restoreSnapshot(repositoryShortId, "snapshot-restore"),
|
||||
),
|
||||
);
|
||||
|
||||
await waitForExpect(() => {
|
||||
expect(restoreMock).toHaveBeenCalledWith(
|
||||
"local",
|
||||
expect.objectContaining({
|
||||
payload: expect.objectContaining({
|
||||
snapshotId: "snapshot-restore",
|
||||
snapshotPaths: ["C:\\Users\\nicolas\\Photos", "C:\\Users\\nicolas\\Documents"],
|
||||
request: expect.objectContaining({
|
||||
location: { kind: "original" },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("keeps the selected Windows file in its original folder", async () => {
|
||||
const { organizationId, userId, repositoryShortId, restoreMock } = await setupRestoreSnapshotScenario([
|
||||
"C:\\Users\\Nicolas\\Downloads",
|
||||
]);
|
||||
|
||||
await withPlatform("win32", () =>
|
||||
withContext({ organizationId, userId }, () =>
|
||||
repositoriesService.restoreSnapshot(repositoryShortId, "snapshot-restore", {
|
||||
include: ["/C/Users/Nicolas/Downloads/DumpStack.log"],
|
||||
selectedItemKind: "file",
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await waitForExpect(() => {
|
||||
expect(restoreMock).toHaveBeenCalledWith(
|
||||
"local",
|
||||
expect.objectContaining({
|
||||
payload: expect.objectContaining({
|
||||
request: expect.objectContaining({
|
||||
location: { kind: "original" },
|
||||
include: ["/C/Users/Nicolas/Downloads/DumpStack.log"],
|
||||
selectedItemKind: "file",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("uses the target agent platform for target-scoped restore plans", async () => {
|
||||
const organizationId = session.organizationId;
|
||||
const agentId = `agent-${randomUUID()}`;
|
||||
const repository = await createTestRepository(organizationId, {
|
||||
type: "s3",
|
||||
config: {
|
||||
backend: "s3",
|
||||
endpoint: "https://s3.example.com",
|
||||
bucket: "bucket",
|
||||
accessKeyId: "access-key",
|
||||
secretAccessKey: "secret-key",
|
||||
},
|
||||
});
|
||||
await db.insert(agentsTable).values({
|
||||
id: agentId,
|
||||
organizationId,
|
||||
name: "Windows Agent",
|
||||
kind: "remote",
|
||||
status: "online",
|
||||
capabilities: { platform: "win32" },
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
vi.spyOn(restic, "snapshots").mockReturnValue(
|
||||
Effect.succeed([
|
||||
{
|
||||
id: "snapshot-restore",
|
||||
short_id: "snapshot-restore",
|
||||
time: new Date().toISOString(),
|
||||
paths: ["C:\\Users\\nicolas\\Photos", "C:\\Users\\nicolas\\Documents"],
|
||||
hostname: "host",
|
||||
},
|
||||
]),
|
||||
);
|
||||
const plan = await withPlatform("linux", () =>
|
||||
withContext({ organizationId, userId: session.user.id }, () =>
|
||||
repositoriesService.getSnapshotRestorePlan(repository.shortId, "snapshot-restore", {
|
||||
targetAgentId: agentId,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(plan).toEqual({
|
||||
queryBasePath: "/C/Users/nicolas",
|
||||
requiresCustomTarget: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("allows restore-all to a custom target for snapshots with native Windows source paths", async () => {
|
||||
const { organizationId, userId, repositoryShortId, restoreMock } = await setupRestoreSnapshotScenario([
|
||||
"d:\\some\\path",
|
||||
]);
|
||||
@@ -934,11 +1051,10 @@ describe("repositoriesService.restoreSnapshot", () => {
|
||||
expect.objectContaining({
|
||||
payload: expect.objectContaining({
|
||||
snapshotId: "snapshot-restore",
|
||||
target: targetPath,
|
||||
snapshotPaths: ["d:\\some\\path"],
|
||||
repositoryConfig: expect.objectContaining({ backend: "local" }),
|
||||
options: expect.objectContaining({
|
||||
organizationId,
|
||||
basePath: "/",
|
||||
request: expect.objectContaining({
|
||||
location: { kind: "custom", targetPath },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BadRequestError } from "http-errors-enhanced";
|
||||
import path from "node:path";
|
||||
import { findCommonAncestor, normalizeAbsolutePath } from "@zerobyte/core/utils";
|
||||
import { getSnapshotSourcePathPlan, normalizeAbsolutePath } from "@zerobyte/core/utils";
|
||||
|
||||
const sanitizeFilenamePart = (value: string): string => {
|
||||
const sanitized = value.replace(/[^a-zA-Z0-9._-]/g, "_").replace(/^_+|_+$/g, "");
|
||||
@@ -16,8 +16,7 @@ export const prepareSnapshotDump = (params: {
|
||||
|
||||
const archiveFilename = `snapshot-${sanitizeFilenamePart(snapshotId)}.tar`;
|
||||
const normalizedRequestedPath = normalizeAbsolutePath(requestedPath);
|
||||
const hasNonPosixSnapshotPaths = snapshotPaths.some((snapshotPath) => !snapshotPath.startsWith("/"));
|
||||
const basePath = hasNonPosixSnapshotPaths ? "/" : findCommonAncestor(snapshotPaths);
|
||||
const { queryBasePath: basePath } = getSnapshotSourcePathPlan({ snapshotPaths });
|
||||
|
||||
if (basePath === "/") {
|
||||
return {
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
getRepositoryStatsDto,
|
||||
refreshRepositoryStatsDto,
|
||||
getSnapshotDetailsDto,
|
||||
getSnapshotRestorePlanDto,
|
||||
getSnapshotRestorePlanQuery,
|
||||
refreshSnapshotsDto,
|
||||
listRcloneRemotesDto,
|
||||
listRepositoriesDto,
|
||||
@@ -43,6 +45,7 @@ import {
|
||||
type GetRepositoryStatsDto,
|
||||
type RefreshRepositoryStatsDto,
|
||||
type GetSnapshotDetailsDto,
|
||||
type GetSnapshotRestorePlanDto,
|
||||
type RefreshSnapshotsDto,
|
||||
type ListRepositoriesDto,
|
||||
type ListSnapshotFilesDto,
|
||||
@@ -168,6 +171,19 @@ export const repositoriesController = new Hono()
|
||||
|
||||
return c.json<GetSnapshotDetailsDto>(response, 200);
|
||||
})
|
||||
.get(
|
||||
"/:shortId/snapshots/:snapshotId/restore-plan",
|
||||
getSnapshotRestorePlanDto,
|
||||
validator("query", getSnapshotRestorePlanQuery),
|
||||
async (c) => {
|
||||
const shortId = asShortId(c.req.param("shortId"));
|
||||
const snapshotId = c.req.param("snapshotId");
|
||||
const query = c.req.valid("query");
|
||||
const result = await repositoriesService.getSnapshotRestorePlan(shortId, snapshotId, query);
|
||||
|
||||
return c.json<GetSnapshotRestorePlanDto>(result, 200);
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/:shortId/snapshots/:snapshotId/files",
|
||||
listSnapshotFilesDto,
|
||||
|
||||
@@ -205,6 +205,15 @@ const snapshotSchema = z.object({
|
||||
summary: resticSnapshotSummarySchema.optional(),
|
||||
});
|
||||
|
||||
const snapshotSourcePathPlanSchema = z.object({
|
||||
queryBasePath: z.string(),
|
||||
requiresCustomTarget: z.boolean(),
|
||||
});
|
||||
|
||||
export const getSnapshotRestorePlanQuery = z.object({
|
||||
targetAgentId: z.string().optional(),
|
||||
});
|
||||
|
||||
const listSnapshotsResponse = snapshotSchema.array();
|
||||
|
||||
export type ListSnapshotsDto = z.infer<typeof listSnapshotsResponse>;
|
||||
@@ -249,6 +258,35 @@ export const getSnapshotDetailsDto = describeRoute({
|
||||
},
|
||||
});
|
||||
|
||||
const getSnapshotRestorePlanResponse = snapshotSourcePathPlanSchema;
|
||||
|
||||
export type GetSnapshotRestorePlanDto = z.infer<typeof getSnapshotRestorePlanResponse>;
|
||||
|
||||
export const getSnapshotRestorePlanDto = describeRoute({
|
||||
description: "Get a target-scoped restore plan for a snapshot",
|
||||
tags: ["Repositories"],
|
||||
operationId: "getSnapshotRestorePlan",
|
||||
parameters: [
|
||||
{
|
||||
in: "query",
|
||||
name: "targetAgentId",
|
||||
required: false,
|
||||
schema: { type: "string" },
|
||||
description: "Agent that will execute the restore. Defaults to the local agent.",
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Target-scoped snapshot restore plan",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(getSnapshotRestorePlanResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const snapshotFileNodeSchema = z.object({
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
|
||||
@@ -8,7 +8,11 @@ import {
|
||||
type RepositoryConfig,
|
||||
type ResticDumpStream,
|
||||
type ResticStatsDto,
|
||||
createSnapshotRestoreExecutionPlan,
|
||||
getSnapshotRestoreTargetPlan,
|
||||
isSnapshotRestorePlanningError,
|
||||
repositoryConfigSchema,
|
||||
type SnapshotRestoreRequest,
|
||||
} from "@zerobyte/core/restic";
|
||||
import { isPathWithin } from "@zerobyte/core/utils";
|
||||
import { config as appConfig } from "~/server/core/config";
|
||||
@@ -27,7 +31,6 @@ import { addCommonArgs, buildEnv, buildRepoUrl, cleanupTemporaryKeys } from "@ze
|
||||
import { restic, resticDeps } from "../../core/restic";
|
||||
import { safeSpawn } from "@zerobyte/core/node";
|
||||
import type { DumpPathKind, UpdateRepositoryBody } from "./repositories.dto";
|
||||
import { findCommonAncestor } from "@zerobyte/core/utils";
|
||||
import { prepareSnapshotDump } from "./helpers/dump";
|
||||
import { executeDoctor } from "./helpers/doctor";
|
||||
import { restoreExecutor } from "./restore-executor";
|
||||
@@ -236,13 +239,70 @@ const finishRestoreExecution = async (restoreId: string, resultPromise: Promise<
|
||||
|
||||
const assertAllowedRestoreAgent = async (agentId: string, organizationId: string) => {
|
||||
if (agentId === LOCAL_AGENT_ID) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const agent = await agentsService.getAgent(agentId);
|
||||
if (!agent || agent.organizationId !== organizationId) {
|
||||
throw new NotFoundError("Restore target agent not found");
|
||||
}
|
||||
|
||||
return agent;
|
||||
};
|
||||
|
||||
const getRestoreAgentPlatform = (agentId: string, agent: Awaited<ReturnType<typeof assertAllowedRestoreAgent>>) => {
|
||||
if (agentId === LOCAL_AGENT_ID) {
|
||||
return process.platform;
|
||||
}
|
||||
|
||||
if (!agent) {
|
||||
throw new Error("Remote restore agent should be available after restore-agent authorization");
|
||||
}
|
||||
|
||||
const platform = agent.capabilities.platform;
|
||||
if (typeof platform !== "string") {
|
||||
throw new BadRequestError("Restore target agent platform is unavailable");
|
||||
}
|
||||
|
||||
return platform;
|
||||
};
|
||||
|
||||
const createSnapshotRestoreRequest = ({
|
||||
targetPath,
|
||||
...options
|
||||
}: {
|
||||
include?: string[];
|
||||
selectedItemKind?: "file" | "dir";
|
||||
exclude?: string[];
|
||||
excludeXattr?: string[];
|
||||
delete?: boolean;
|
||||
targetPath?: string;
|
||||
overwrite?: OverwriteMode;
|
||||
}): SnapshotRestoreRequest => ({
|
||||
location: targetPath ? { kind: "custom", targetPath } : { kind: "original" },
|
||||
...(options.include ? { include: options.include } : {}),
|
||||
...(options.selectedItemKind ? { selectedItemKind: options.selectedItemKind } : {}),
|
||||
...(options.exclude ? { exclude: options.exclude } : {}),
|
||||
...(options.excludeXattr ? { excludeXattr: options.excludeXattr } : {}),
|
||||
...(options.delete !== undefined ? { delete: options.delete } : {}),
|
||||
...(options.overwrite ? { overwrite: options.overwrite } : {}),
|
||||
});
|
||||
|
||||
const assertAllowedControllerLocalRestoreRequest = (snapshotPaths: string[], request: SnapshotRestoreRequest) => {
|
||||
try {
|
||||
const plan = createSnapshotRestoreExecutionPlan({
|
||||
snapshotPaths,
|
||||
platform: process.platform,
|
||||
request,
|
||||
});
|
||||
assertAllowedControllerLocalRestoreTarget(plan.target);
|
||||
} catch (error) {
|
||||
if (isSnapshotRestorePlanningError(error)) {
|
||||
throw new BadRequestError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const findRepository = async (shortId: ShortId) => {
|
||||
@@ -541,21 +601,16 @@ const restoreSnapshot = async (
|
||||
}
|
||||
|
||||
const { targetAgentId, targetPath, ...restoreExecutionOptions } = options ?? {};
|
||||
const target = targetPath || "/";
|
||||
|
||||
const snapshot = await getSnapshotDetails(repository.shortId, snapshotId);
|
||||
const hasNonPosixSnapshotPaths = snapshot.paths.some((path) => !path.startsWith("/"));
|
||||
|
||||
if (hasNonPosixSnapshotPaths && !targetPath) {
|
||||
throw new BadRequestError(
|
||||
"Original location restore is unavailable for this snapshot. Restore it to a custom location instead.",
|
||||
);
|
||||
}
|
||||
|
||||
const basePath = hasNonPosixSnapshotPaths ? "/" : findCommonAncestor(snapshot.paths);
|
||||
const executionAgentId = targetAgentId ?? LOCAL_AGENT_ID;
|
||||
const useControllerLocalRestoreFallback = executionAgentId === LOCAL_AGENT_ID && !appConfig.flags.enableLocalAgent;
|
||||
await assertAllowedRestoreAgent(executionAgentId, organizationId);
|
||||
const snapshot = await getSnapshotDetails(repository.shortId, snapshotId);
|
||||
const restoreRequest = createSnapshotRestoreRequest({ targetPath, ...restoreExecutionOptions });
|
||||
|
||||
const useControllerLocalRestoreFallback = executionAgentId === LOCAL_AGENT_ID && !appConfig.flags.enableLocalAgent;
|
||||
let taskTargetAgentId: string | null = executionAgentId;
|
||||
if (useControllerLocalRestoreFallback) {
|
||||
taskTargetAgentId = null;
|
||||
}
|
||||
|
||||
if (!useControllerLocalRestoreFallback && repository.type === "local" && executionAgentId !== LOCAL_AGENT_ID) {
|
||||
throw new BadRequestError(
|
||||
@@ -564,7 +619,7 @@ const restoreSnapshot = async (
|
||||
}
|
||||
|
||||
if (executionAgentId === LOCAL_AGENT_ID) {
|
||||
assertAllowedControllerLocalRestoreTarget(target);
|
||||
assertAllowedControllerLocalRestoreRequest(snapshot.paths, restoreRequest);
|
||||
}
|
||||
|
||||
const activeRestore = findActiveRestoreTask(organizationId, repository.shortId, snapshotId);
|
||||
@@ -576,8 +631,14 @@ const restoreSnapshot = async (
|
||||
organizationId,
|
||||
resourceType: RESTORE_TASK_RESOURCE_TYPE,
|
||||
resourceId: repository.shortId,
|
||||
targetAgentId: useControllerLocalRestoreFallback ? null : executionAgentId,
|
||||
input: { kind: "restore", repositoryId: repository.shortId, snapshotId, target },
|
||||
targetAgentId: taskTargetAgentId,
|
||||
input: {
|
||||
kind: "restore",
|
||||
repositoryId: repository.shortId,
|
||||
snapshotId,
|
||||
restoreLocation: restoreRequest.location.kind,
|
||||
...(restoreRequest.location.kind === "custom" ? { targetPath: restoreRequest.location.targetPath } : {}),
|
||||
},
|
||||
});
|
||||
const restoreId = task.id;
|
||||
try {
|
||||
@@ -589,12 +650,9 @@ const restoreSnapshot = async (
|
||||
repositoryShortId: repository.shortId,
|
||||
repositoryConfig,
|
||||
snapshotId,
|
||||
target,
|
||||
snapshotPaths: snapshot.paths,
|
||||
restoreRequest,
|
||||
executionAgentId,
|
||||
options: {
|
||||
basePath,
|
||||
...restoreExecutionOptions,
|
||||
},
|
||||
onStarted: () => markRestoreStarted(restoreId),
|
||||
onProgress: (progress) => updateRestoreProgress(restoreId, progress),
|
||||
});
|
||||
@@ -704,6 +762,17 @@ const getSnapshotDetails = async (shortId: ShortId, snapshotId: string) => {
|
||||
return snapshot;
|
||||
};
|
||||
|
||||
const getSnapshotRestorePlan = async (shortId: ShortId, snapshotId: string, options?: { targetAgentId?: string }) => {
|
||||
const organizationId = getOrganizationId();
|
||||
const executionAgentId = options?.targetAgentId ?? LOCAL_AGENT_ID;
|
||||
const targetAgent = await assertAllowedRestoreAgent(executionAgentId, organizationId);
|
||||
const platform = getRestoreAgentPlatform(executionAgentId, targetAgent);
|
||||
|
||||
const snapshot = await getSnapshotDetails(shortId, snapshotId);
|
||||
|
||||
return getSnapshotRestoreTargetPlan({ snapshotPaths: snapshot.paths, platform });
|
||||
};
|
||||
|
||||
const checkHealth = async (shortId: ShortId) => {
|
||||
const organizationId = getOrganizationId();
|
||||
const repository = await findRepository(shortId);
|
||||
@@ -1089,6 +1158,7 @@ export const repositoriesService = {
|
||||
restoreSnapshot,
|
||||
dumpSnapshot,
|
||||
getSnapshotDetails,
|
||||
getSnapshotRestorePlan,
|
||||
checkHealth,
|
||||
startDoctor,
|
||||
cancelDoctor,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { RepositoryConfig } from "@zerobyte/core/restic";
|
||||
import {
|
||||
createSnapshotRestoreExecutionPlan,
|
||||
type RepositoryConfig,
|
||||
type SnapshotRestoreRequest,
|
||||
} from "@zerobyte/core/restic";
|
||||
import type { RestoreRunPayload } from "@zerobyte/contracts/agent-protocol";
|
||||
import { config as appConfig } from "~/server/core/config";
|
||||
import { repoMutex } from "../../core/repository-mutex";
|
||||
@@ -7,8 +11,6 @@ import { runEffectPromise, toMessage } from "../../utils/errors";
|
||||
import { agentManager, type RestoreExecutionProgress, type RestoreExecutionResult } from "../agents/agents-manager";
|
||||
import { LOCAL_AGENT_ID } from "../agents/constants";
|
||||
|
||||
type RestoreExecutionOptions = Omit<Parameters<typeof restic.restore>[3], "organizationId" | "signal" | "onProgress">;
|
||||
|
||||
type RestoreExecutionRequest = {
|
||||
restoreId: string;
|
||||
organizationId: string;
|
||||
@@ -16,9 +18,9 @@ type RestoreExecutionRequest = {
|
||||
repositoryShortId: string;
|
||||
repositoryConfig: RepositoryConfig;
|
||||
snapshotId: string;
|
||||
target: string;
|
||||
snapshotPaths: string[];
|
||||
restoreRequest: SnapshotRestoreRequest;
|
||||
executionAgentId: string;
|
||||
options: RestoreExecutionOptions;
|
||||
onStarted: () => void;
|
||||
onProgress: (progress: RestoreExecutionProgress) => void;
|
||||
};
|
||||
@@ -38,13 +40,10 @@ const createRestoreRunPayload = async (request: RestoreExecutionRequest): Promis
|
||||
organizationId: request.organizationId,
|
||||
repositoryId: request.repositoryShortId,
|
||||
snapshotId: request.snapshotId,
|
||||
target: request.target,
|
||||
snapshotPaths: request.snapshotPaths,
|
||||
repositoryConfig: request.repositoryConfig,
|
||||
runtime: { password: resticPassword },
|
||||
options: {
|
||||
...request.options,
|
||||
organizationId: request.organizationId,
|
||||
},
|
||||
request: request.restoreRequest,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -56,12 +55,18 @@ const executeControllerRestore = async (
|
||||
return { status: "cancelled", message: "Restore was cancelled" };
|
||||
}
|
||||
|
||||
request.onStarted();
|
||||
|
||||
try {
|
||||
const plan = createSnapshotRestoreExecutionPlan({
|
||||
snapshotPaths: request.snapshotPaths,
|
||||
platform: process.platform,
|
||||
request: request.restoreRequest,
|
||||
});
|
||||
|
||||
request.onStarted();
|
||||
|
||||
const result = await runEffectPromise(
|
||||
restic.restore(request.repositoryConfig, request.snapshotId, request.target, {
|
||||
...request.options,
|
||||
restic.restore(request.repositoryConfig, request.snapshotId, plan.target, {
|
||||
...plan.options,
|
||||
organizationId: request.organizationId,
|
||||
signal,
|
||||
onProgress: request.onProgress,
|
||||
|
||||
@@ -55,7 +55,13 @@ const createRestoreTask = (overrides: Partial<Parameters<typeof taskStore.create
|
||||
resourceType: "repository",
|
||||
resourceId: "repo-short",
|
||||
targetAgentId: "local",
|
||||
input: { kind: "restore", repositoryId: "repo-short", snapshotId: "snapshot-1", target: "/tmp/restore" },
|
||||
input: {
|
||||
kind: "restore",
|
||||
repositoryId: "repo-short",
|
||||
snapshotId: "snapshot-1",
|
||||
restoreLocation: "custom",
|
||||
targetPath: "/tmp/restore",
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ export const taskInputSchema = z.discriminatedUnion("kind", [
|
||||
kind: z.literal("restore"),
|
||||
repositoryId: z.string(),
|
||||
snapshotId: z.string(),
|
||||
target: z.string(),
|
||||
restoreLocation: z.enum(["original", "custom"]).optional(),
|
||||
targetPath: z.string().optional(),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ const createRunPayload = (overrides: Partial<RestoreRunPayload> = {}) =>
|
||||
organizationId: "org-1",
|
||||
repositoryId: "repo-1",
|
||||
snapshotId: "snapshot-1",
|
||||
target: `${process.cwd()}/restore-target`,
|
||||
snapshotPaths: [`${process.cwd()}/source`],
|
||||
repositoryConfig: { backend: "local", path: "/tmp/repository" },
|
||||
runtime: { password: "password" },
|
||||
options: { organizationId: "org-1", basePath: "/" },
|
||||
request: { location: { kind: "custom", targetPath: `${process.cwd()}/restore-target` } },
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
||||
@@ -58,7 +58,11 @@ test("runs backend-backed volume commands on the agent host", async () => {
|
||||
commandId: "command-1",
|
||||
command: {
|
||||
name: "volume.mount",
|
||||
volume: { id: 1, config: { backend: "directory", path: "/tmp/source" }, provisioningId: undefined },
|
||||
volume: {
|
||||
id: 1,
|
||||
config: { backend: "directory", path: "/tmp/source" },
|
||||
provisioningId: undefined,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { Effect, Runtime } from "effect";
|
||||
import { createAgentMessage, type RestoreRunPayload } from "@zerobyte/contracts/agent-protocol";
|
||||
import { createSnapshotRestoreExecutionPlan } from "@zerobyte/core/restic";
|
||||
import { createRestic } from "@zerobyte/core/restic/server";
|
||||
import { isPathWithin, toMessage } from "@zerobyte/core/utils";
|
||||
import { logger } from "@zerobyte/core/node";
|
||||
@@ -52,15 +53,21 @@ export const handleRestoreRunCommand = (context: ControllerCommandContext, paylo
|
||||
|
||||
yield* Effect.fork(
|
||||
Effect.gen(function* () {
|
||||
assertAllowedRestoreTarget(payload.target);
|
||||
const plan = createSnapshotRestoreExecutionPlan({
|
||||
snapshotPaths: payload.snapshotPaths,
|
||||
platform: process.platform,
|
||||
request: payload.request,
|
||||
});
|
||||
assertAllowedRestoreTarget(plan.target);
|
||||
|
||||
const runtime = yield* Effect.runtime<never>();
|
||||
const restic = createRestic(resticDeps(payload.runtime.password));
|
||||
|
||||
yield* context.offerOutbound(createAgentMessage("restore.started", restoreContext));
|
||||
|
||||
const result = yield* restic.restore(payload.repositoryConfig, payload.snapshotId, payload.target, {
|
||||
...payload.options,
|
||||
const result = yield* restic.restore(payload.repositoryConfig, payload.snapshotId, plan.target, {
|
||||
...plan.options,
|
||||
organizationId: payload.organizationId,
|
||||
signal: abortController.signal,
|
||||
onProgress: (progress) => {
|
||||
void Runtime.runPromise(
|
||||
|
||||
@@ -145,7 +145,12 @@ export const createControllerSession = (ws: WebSocket): ControllerSession => {
|
||||
protocolVersion: AGENT_PROTOCOL_VERSION,
|
||||
hostname: resolveResticHostname(),
|
||||
platform: process.platform,
|
||||
capabilities: { backup: true, restore: true, volume: true, restic: true },
|
||||
capabilities: {
|
||||
backup: true,
|
||||
restore: true,
|
||||
volume: true,
|
||||
restic: true,
|
||||
},
|
||||
}),
|
||||
),
|
||||
).catch((error) => {
|
||||
|
||||
@@ -62,5 +62,5 @@ test("listVolumeFiles rejects traversal outside the volume", async () => {
|
||||
test("listVolumeFiles reports missing directories consistently", async () => {
|
||||
const volume = await createDirectoryVolume();
|
||||
|
||||
await expect(listVolumeFiles(volume, "missing", 0, 10)).rejects.toThrow("Directory not found");
|
||||
await expect(listVolumeFiles(volume, "/missing", 0, 10)).rejects.toThrow("Directory not found");
|
||||
});
|
||||
|
||||
@@ -2,13 +2,28 @@ import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { BackendConfig, Volume as AgentVolume } from "@zerobyte/contracts/volumes";
|
||||
import { toMessage } from "@zerobyte/core/utils";
|
||||
import { normalizeAbsolutePath, toMessage } from "@zerobyte/core/utils";
|
||||
import { Data, Effect } from "effect";
|
||||
import { createVolumeBackend, getVolumePath, isNodeJSErrnoException } from ".";
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 500;
|
||||
const MAX_PAGE_SIZE = 500;
|
||||
|
||||
const normalizeVolumePath = (value: string) => {
|
||||
if (!value.startsWith("/")) {
|
||||
throw new Error("Invalid path");
|
||||
}
|
||||
|
||||
return normalizeAbsolutePath(value);
|
||||
};
|
||||
|
||||
const resolveVolumePath = (volumePath: string, volumeRelativePath: string) => {
|
||||
const segments = volumeRelativePath.slice(1).split("/").filter(Boolean);
|
||||
return path.join(volumePath, ...segments);
|
||||
};
|
||||
|
||||
const toVolumePath = (nativeRelativePath: string) => normalizeVolumePath(`/${nativeRelativePath.replace(/\\/g, "/")}`);
|
||||
|
||||
export const listVolumeFiles = async (
|
||||
volume: AgentVolume,
|
||||
subPath?: string,
|
||||
@@ -16,7 +31,13 @@ export const listVolumeFiles = async (
|
||||
limit: number = DEFAULT_PAGE_SIZE,
|
||||
) => {
|
||||
const volumePath = getVolumePath(volume);
|
||||
const requestedPath = subPath ? path.join(volumePath, subPath) : volumePath;
|
||||
let requestPath: string;
|
||||
try {
|
||||
requestPath = normalizeVolumePath(subPath ?? "/");
|
||||
} catch {
|
||||
throw new Error("Invalid path");
|
||||
}
|
||||
const requestedPath = resolveVolumePath(volumePath, requestPath);
|
||||
const normalizedPath = path.normalize(requestedPath);
|
||||
const requestedRelativePath = path.relative(volumePath, normalizedPath);
|
||||
|
||||
@@ -66,7 +87,7 @@ export const listVolumeFiles = async (
|
||||
|
||||
return {
|
||||
name: dirent.name,
|
||||
path: `/${relativePath}`,
|
||||
path: toVolumePath(relativePath),
|
||||
type: dirent.isDirectory() ? ("directory" as const) : ("file" as const),
|
||||
size: dirent.isFile() ? stats.size : undefined,
|
||||
modifiedAt: stats.mtimeMs,
|
||||
@@ -80,7 +101,7 @@ export const listVolumeFiles = async (
|
||||
|
||||
return {
|
||||
files: entries,
|
||||
path: subPath || "/",
|
||||
path: requestPath,
|
||||
offset: startOffset,
|
||||
limit: pageSize,
|
||||
total,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
resticRestoreOutputSchema,
|
||||
restoreProgressSchema,
|
||||
type CompressionMode,
|
||||
type SnapshotRestoreRequest,
|
||||
} from "@zerobyte/core/restic";
|
||||
import {
|
||||
browseFilesystemResponseSchema,
|
||||
@@ -126,22 +127,28 @@ const restoreIdentitySchema = z.object({
|
||||
snapshotId: z.string(),
|
||||
});
|
||||
|
||||
const overwriteModeSchema = z.enum(["always", "if-changed", "if-newer", "never"]);
|
||||
|
||||
const snapshotRestoreRequestSchema = z.object({
|
||||
location: z.discriminatedUnion("kind", [
|
||||
z.object({ kind: z.literal("original") }),
|
||||
z.object({ kind: z.literal("custom"), targetPath: z.string() }),
|
||||
]),
|
||||
include: z.array(z.string()).optional(),
|
||||
selectedItemKind: z.enum(["file", "dir"]).optional(),
|
||||
exclude: z.array(z.string()).optional(),
|
||||
excludeXattr: z.array(z.string()).optional(),
|
||||
delete: z.boolean().optional(),
|
||||
overwrite: overwriteModeSchema.optional(),
|
||||
}) satisfies z.ZodType<SnapshotRestoreRequest>;
|
||||
|
||||
const restoreRunSchema = z.object({
|
||||
type: z.literal("restore.run"),
|
||||
payload: restoreIdentitySchema.extend({
|
||||
target: z.string(),
|
||||
snapshotPaths: z.array(z.string()),
|
||||
repositoryConfig: repositoryConfigSchema,
|
||||
runtime: commandRuntimeSchema,
|
||||
options: z.object({
|
||||
basePath: z.string().optional(),
|
||||
organizationId: z.string(),
|
||||
include: z.array(z.string()).optional(),
|
||||
selectedItemKind: z.enum(["file", "dir"]).optional(),
|
||||
exclude: z.array(z.string()).optional(),
|
||||
excludeXattr: z.array(z.string()).optional(),
|
||||
delete: z.boolean().optional(),
|
||||
overwrite: z.enum(["always", "if-changed", "if-newer", "never"]).optional(),
|
||||
}),
|
||||
request: snapshotRestoreRequestSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -186,6 +186,80 @@ describe("restore command", () => {
|
||||
expect(separatorIndex).toBeGreaterThan(-1);
|
||||
expect(getRestoreArg()).toBe("--help:/var/lib/zerobyte/volumes/vol123/_data");
|
||||
});
|
||||
|
||||
test("uses restic snapshot tree syntax for Windows base paths restored to drive roots", async () => {
|
||||
const { getRestoreArg, getOptionValues } = setup();
|
||||
|
||||
await runRestore(
|
||||
config,
|
||||
"snapshot-windows",
|
||||
"C:\\",
|
||||
{
|
||||
organizationId: "org-1",
|
||||
basePath: "C:\\Users\\nicolas",
|
||||
},
|
||||
mockDeps,
|
||||
);
|
||||
|
||||
expect(getOptionValues("--target")).toEqual(["C:\\"]);
|
||||
expect(getRestoreArg()).toBe("snapshot-windows:/C/Users/nicolas");
|
||||
});
|
||||
|
||||
test("keeps the precomputed Windows original-location target for restore-all", async () => {
|
||||
const { getRestoreArg, getOptionValues } = setup();
|
||||
|
||||
await runRestore(
|
||||
config,
|
||||
"snapshot-windows",
|
||||
"C:\\Users",
|
||||
{
|
||||
organizationId: "org-1",
|
||||
basePath: "/C/Users/nicolas",
|
||||
},
|
||||
mockDeps,
|
||||
);
|
||||
|
||||
expect(getOptionValues("--target")).toEqual(["C:\\Users"]);
|
||||
expect(getRestoreArg()).toBe("snapshot-windows:/C/Users/nicolas");
|
||||
});
|
||||
|
||||
test("keeps the precomputed Windows original-location target for selections", async () => {
|
||||
const { getRestoreArg, getOptionValues } = setup();
|
||||
|
||||
await runRestore(
|
||||
config,
|
||||
"snapshot-windows",
|
||||
"C:\\Users\\nicolas",
|
||||
{
|
||||
organizationId: "org-1",
|
||||
include: ["/C/Users/nicolas/Downloads"],
|
||||
selectedItemKind: "dir",
|
||||
},
|
||||
mockDeps,
|
||||
);
|
||||
|
||||
expect(getOptionValues("--target")).toEqual(["C:\\Users\\nicolas"]);
|
||||
expect(getRestoreArg()).toBe("snapshot-windows:/C/Users/nicolas/Downloads");
|
||||
});
|
||||
|
||||
test("strips selected Windows directories for custom drive-root targets", async () => {
|
||||
const { getRestoreArg, getOptionValues } = setup();
|
||||
|
||||
await runRestore(
|
||||
config,
|
||||
"snapshot-windows",
|
||||
"C:\\",
|
||||
{
|
||||
organizationId: "org-1",
|
||||
include: ["C:\\Users\\nicolas\\Documents"],
|
||||
selectedItemKind: "dir",
|
||||
},
|
||||
mockDeps,
|
||||
);
|
||||
|
||||
expect(getRestoreArg()).toBe("snapshot-windows:/C/Users/nicolas/Documents");
|
||||
expect(getOptionValues("--include")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("output handling", () => {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import path from "node:path";
|
||||
import { findCommonAncestor } from "../../utils/common-ancestor";
|
||||
import { addCommonArgs } from "../helpers/add-common-args";
|
||||
import { buildEnv } from "../helpers/build-env";
|
||||
import { buildRepoUrl } from "../helpers/build-repo-url";
|
||||
@@ -16,14 +14,13 @@ import {
|
||||
import type { ResticDeps } from "../types";
|
||||
import { Data, Effect } from "effect";
|
||||
import { toMessage } from "../../utils";
|
||||
import { createRestorePathArgs } from "../helpers/restore-paths";
|
||||
|
||||
class ResticRestoreCommandError extends Data.TaggedError("ResticRestoreCommandError")<{
|
||||
cause: unknown;
|
||||
message: string;
|
||||
}> {}
|
||||
|
||||
const escapeResticIncludePattern = (pattern: string) => pattern.replace(/[\\*?[\]]/g, "\\$&");
|
||||
|
||||
export const restore = (
|
||||
config: RepositoryConfig,
|
||||
snapshotId: string,
|
||||
@@ -50,12 +47,13 @@ export const restore = (
|
||||
(env) => Effect.promise(() => cleanupTemporaryKeys(env, deps)),
|
||||
);
|
||||
|
||||
const includes = options.include?.length ? options.include : [options.basePath ?? "/"];
|
||||
const commonAncestor =
|
||||
options.selectedItemKind === "file" && includes.length === 1
|
||||
? path.posix.dirname(includes[0] ?? "/")
|
||||
: findCommonAncestor(includes);
|
||||
const restoreArg = target === "/" ? snapshotId : `${snapshotId}:${commonAncestor}`;
|
||||
const pathArgs = createRestorePathArgs({
|
||||
snapshotId,
|
||||
target,
|
||||
basePath: options.basePath,
|
||||
include: options.include,
|
||||
selectedItemKind: options.selectedItemKind,
|
||||
});
|
||||
|
||||
const args = ["--repo", repoUrl, "restore", "--target", target];
|
||||
|
||||
@@ -63,25 +61,8 @@ export const restore = (
|
||||
args.push("--overwrite", options.overwrite);
|
||||
}
|
||||
|
||||
if (options.include?.length) {
|
||||
if (target === "/") {
|
||||
for (const pattern of options.include) {
|
||||
args.push("--include", escapeResticIncludePattern(pattern));
|
||||
}
|
||||
} else {
|
||||
const strippedIncludes = options.include.map((pattern) =>
|
||||
path.posix.relative(commonAncestor, pattern),
|
||||
);
|
||||
const includesCoverRestoreRoot = strippedIncludes.some(
|
||||
(pattern) => pattern === "" || pattern === ".",
|
||||
);
|
||||
|
||||
if (!includesCoverRestoreRoot) {
|
||||
for (const pattern of strippedIncludes) {
|
||||
args.push("--include", escapeResticIncludePattern(pattern));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const pattern of pathArgs.includePatterns) {
|
||||
args.push("--include", pattern);
|
||||
}
|
||||
|
||||
if (options.exclude?.length) {
|
||||
@@ -97,7 +78,7 @@ export const restore = (
|
||||
}
|
||||
|
||||
addCommonArgs(args, env, config);
|
||||
args.push("--", restoreArg);
|
||||
args.push("--", pathArgs.restoreArg);
|
||||
|
||||
const onProgress = options.onProgress;
|
||||
const resticProgressFps = process.env.RESTIC_PROGRESS_FPS ?? "1";
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { createSnapshotRestoreExecutionPlan, SnapshotRestorePlanningError } from "../snapshot-restore-plan";
|
||||
|
||||
describe("snapshot restore planning", () => {
|
||||
test("plans a POSIX custom-target restore from the snapshot source base", () => {
|
||||
const plan = createSnapshotRestoreExecutionPlan({
|
||||
snapshotPaths: ["/var/lib/zerobyte/volumes/vol123/_data"],
|
||||
platform: "linux",
|
||||
request: { location: { kind: "custom", targetPath: "/restore-target" } },
|
||||
});
|
||||
|
||||
expect(plan).toMatchObject({
|
||||
target: "/restore-target",
|
||||
options: { basePath: "/var/lib/zerobyte/volumes/vol123/_data" },
|
||||
});
|
||||
});
|
||||
|
||||
test("plans Windows original-location restores on Windows hosts", () => {
|
||||
const plan = createSnapshotRestoreExecutionPlan({
|
||||
snapshotPaths: ["C:\\Users\\nicolas\\Photos", "C:\\Users\\nicolas\\Documents"],
|
||||
platform: "win32",
|
||||
request: { location: { kind: "original" } },
|
||||
});
|
||||
|
||||
expect(plan).toMatchObject({
|
||||
target: "C:\\Users\\nicolas",
|
||||
options: { basePath: "/C/Users/nicolas" },
|
||||
});
|
||||
});
|
||||
|
||||
test("requires custom targets for Windows original-location restores on POSIX hosts", () => {
|
||||
expect(() =>
|
||||
createSnapshotRestoreExecutionPlan({
|
||||
snapshotPaths: ["C:\\Users\\nicolas\\Photos"],
|
||||
platform: "linux",
|
||||
request: { location: { kind: "original" } },
|
||||
}),
|
||||
).toThrow(SnapshotRestorePlanningError);
|
||||
});
|
||||
});
|
||||
99
packages/core/src/restic/helpers/restore-paths.ts
Normal file
99
packages/core/src/restic/helpers/restore-paths.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import path from "node:path";
|
||||
import { findCommonAncestor } from "../../utils/common-ancestor";
|
||||
import { windowsHostPathToResticSnapshotPath } from "../../utils/path";
|
||||
|
||||
type RestorePathInput = {
|
||||
snapshotId: string;
|
||||
target: string;
|
||||
basePath?: string;
|
||||
include?: string[];
|
||||
selectedItemKind?: "file" | "dir";
|
||||
};
|
||||
|
||||
type RestorePathArgs = {
|
||||
restoreArg: string;
|
||||
includePatterns: string[];
|
||||
};
|
||||
|
||||
const escapeResticIncludePattern = (pattern: string): string => pattern.replace(/[\\*?[\]]/g, "\\$&");
|
||||
|
||||
const toResticSnapshotPath = (snapshotPath: string): string => {
|
||||
return windowsHostPathToResticSnapshotPath(snapshotPath) ?? snapshotPath;
|
||||
};
|
||||
|
||||
export const findResticCommonAncestor = (snapshotPaths: string[]): string => {
|
||||
const resticPaths = snapshotPaths.map(toResticSnapshotPath);
|
||||
if (resticPaths.every((snapshotPath) => snapshotPath.startsWith("/"))) {
|
||||
return findCommonAncestor(resticPaths);
|
||||
}
|
||||
|
||||
if (resticPaths.some((snapshotPath) => snapshotPath.startsWith("/"))) {
|
||||
throw new Error("Snapshot paths must use a single path format.");
|
||||
}
|
||||
|
||||
if (resticPaths.length === 0) return "/";
|
||||
if (resticPaths.length === 1) return resticPaths[0] || "/";
|
||||
|
||||
const splitPaths = resticPaths.map((snapshotPath) => snapshotPath.split("/").filter(Boolean));
|
||||
const minLength = Math.min(...splitPaths.map((parts) => parts.length));
|
||||
|
||||
const commonParts: string[] = [];
|
||||
for (let i = 0; i < minLength; i++) {
|
||||
const firstPart = splitPaths[0]?.[i];
|
||||
if (!firstPart) break;
|
||||
|
||||
if (splitPaths.every((parts) => parts[i]?.toLowerCase() === firstPart.toLowerCase())) {
|
||||
commonParts.push(firstPart);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!commonParts.length) {
|
||||
throw new Error("Snapshot paths must be on the same drive.");
|
||||
}
|
||||
|
||||
return commonParts.join("/");
|
||||
};
|
||||
|
||||
export const getResticRestoreRoot = (includes: string[], selectedItemKind?: "file" | "dir"): string => {
|
||||
if (selectedItemKind === "file" && includes.length === 1) {
|
||||
return path.posix.dirname(toResticSnapshotPath(includes[0] ?? "/"));
|
||||
}
|
||||
|
||||
return findResticCommonAncestor(includes);
|
||||
};
|
||||
|
||||
const getIncludePatterns = (include: string[] | undefined, target: string, restoreRoot: string): string[] => {
|
||||
if (!include?.length) return [];
|
||||
|
||||
if (target === "/") {
|
||||
return include.map((pattern) => escapeResticIncludePattern(toResticSnapshotPath(pattern)));
|
||||
}
|
||||
|
||||
const strippedIncludes = include.map((pattern) => path.posix.relative(restoreRoot, toResticSnapshotPath(pattern)));
|
||||
const includesCoverRestoreRoot = strippedIncludes.some((pattern) => pattern === "" || pattern === ".");
|
||||
|
||||
if (includesCoverRestoreRoot) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return strippedIncludes.map(escapeResticIncludePattern);
|
||||
};
|
||||
|
||||
export const createRestorePathArgs = ({
|
||||
snapshotId,
|
||||
target,
|
||||
basePath,
|
||||
include,
|
||||
selectedItemKind,
|
||||
}: RestorePathInput): RestorePathArgs => {
|
||||
const includes = include?.length ? include : [basePath ?? "/"];
|
||||
const restoreRoot = getResticRestoreRoot(includes, selectedItemKind);
|
||||
const restoreArg = target === "/" ? snapshotId : `${snapshotId}:${restoreRoot}`;
|
||||
|
||||
return {
|
||||
restoreArg,
|
||||
includePatterns: getIncludePatterns(include, target, restoreRoot),
|
||||
};
|
||||
};
|
||||
117
packages/core/src/restic/helpers/snapshot-restore-plan.ts
Normal file
117
packages/core/src/restic/helpers/snapshot-restore-plan.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
getOriginalRestoreTargetForRoot,
|
||||
getSnapshotSourcePathPlan,
|
||||
hostPathKindFromPlatform,
|
||||
type SnapshotSourcePathPlan,
|
||||
} from "../../utils";
|
||||
import type { OverwriteMode } from "../schemas";
|
||||
import { getResticRestoreRoot } from "./restore-paths";
|
||||
|
||||
export type SnapshotRestoreLocation = { kind: "original" } | { kind: "custom"; targetPath: string };
|
||||
|
||||
export type SnapshotRestoreRequest = {
|
||||
location: SnapshotRestoreLocation;
|
||||
include?: string[];
|
||||
selectedItemKind?: "file" | "dir";
|
||||
exclude?: string[];
|
||||
excludeXattr?: string[];
|
||||
delete?: boolean;
|
||||
overwrite?: OverwriteMode;
|
||||
};
|
||||
|
||||
export type SnapshotRestoreExecutionPlan = {
|
||||
target: string;
|
||||
options: {
|
||||
basePath: string;
|
||||
include?: string[];
|
||||
selectedItemKind?: "file" | "dir";
|
||||
exclude?: string[];
|
||||
excludeXattr?: string[];
|
||||
delete?: boolean;
|
||||
overwrite?: OverwriteMode;
|
||||
};
|
||||
sourcePathPlan: SnapshotSourcePathPlan;
|
||||
};
|
||||
|
||||
export type SnapshotRestoreTargetPlan = Pick<SnapshotSourcePathPlan, "queryBasePath" | "requiresCustomTarget">;
|
||||
|
||||
export class SnapshotRestorePlanningError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "SnapshotRestorePlanningError";
|
||||
}
|
||||
}
|
||||
|
||||
export const isSnapshotRestorePlanningError = (error: unknown): error is SnapshotRestorePlanningError => {
|
||||
return error instanceof SnapshotRestorePlanningError;
|
||||
};
|
||||
|
||||
export const getSnapshotRestoreTargetPlan = ({
|
||||
snapshotPaths,
|
||||
platform,
|
||||
}: {
|
||||
snapshotPaths: string[];
|
||||
platform: string;
|
||||
}): SnapshotRestoreTargetPlan => {
|
||||
const sourcePathPlan = getSnapshotSourcePathPlan({
|
||||
snapshotPaths,
|
||||
hostPathKind: hostPathKindFromPlatform(platform),
|
||||
});
|
||||
|
||||
return {
|
||||
queryBasePath: sourcePathPlan.queryBasePath,
|
||||
requiresCustomTarget: sourcePathPlan.requiresCustomTarget,
|
||||
};
|
||||
};
|
||||
|
||||
export const createSnapshotRestoreExecutionPlan = ({
|
||||
snapshotPaths,
|
||||
platform,
|
||||
request,
|
||||
}: {
|
||||
snapshotPaths: string[];
|
||||
platform: string;
|
||||
request: SnapshotRestoreRequest;
|
||||
}): SnapshotRestoreExecutionPlan => {
|
||||
const sourcePathPlan = getSnapshotSourcePathPlan({
|
||||
snapshotPaths,
|
||||
hostPathKind: hostPathKindFromPlatform(platform),
|
||||
});
|
||||
const location = request.location;
|
||||
const isCustomLocation = location.kind === "custom";
|
||||
|
||||
if (location.kind === "custom" && location.targetPath.length === 0) {
|
||||
throw new SnapshotRestorePlanningError("Restore target path is required for custom-location restores.");
|
||||
}
|
||||
|
||||
if (!isCustomLocation && sourcePathPlan.requiresCustomTarget) {
|
||||
throw new SnapshotRestorePlanningError(
|
||||
"Original location restore is unavailable for this snapshot. Restore it to a custom location instead.",
|
||||
);
|
||||
}
|
||||
|
||||
const basePath = isCustomLocation ? sourcePathPlan.customRestoreBasePath : sourcePathPlan.originalRestoreBasePath;
|
||||
const restoreIncludes = request.include?.length ? request.include : [basePath];
|
||||
const restoreRoot = getResticRestoreRoot(restoreIncludes, request.selectedItemKind);
|
||||
const target =
|
||||
location.kind === "custom"
|
||||
? location.targetPath
|
||||
: getOriginalRestoreTargetForRoot({
|
||||
restoreRoot,
|
||||
sourcePathKind: sourcePathPlan.sourcePathKind,
|
||||
});
|
||||
|
||||
return {
|
||||
target,
|
||||
options: {
|
||||
basePath,
|
||||
...(request.include ? { include: request.include } : {}),
|
||||
...(request.selectedItemKind ? { selectedItemKind: request.selectedItemKind } : {}),
|
||||
...(request.exclude ? { exclude: request.exclude } : {}),
|
||||
...(request.excludeXattr ? { excludeXattr: request.excludeXattr } : {}),
|
||||
...(request.delete !== undefined ? { delete: request.delete } : {}),
|
||||
...(request.overwrite ? { overwrite: request.overwrite } : {}),
|
||||
},
|
||||
sourcePathPlan,
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,18 @@
|
||||
export * from "./schemas";
|
||||
export * from "./restic-dto";
|
||||
export { isResticError, ResticError, ResticLockError } from "./error";
|
||||
export {
|
||||
createSnapshotRestoreExecutionPlan,
|
||||
getSnapshotRestoreTargetPlan,
|
||||
isSnapshotRestorePlanningError,
|
||||
SnapshotRestorePlanningError,
|
||||
} from "./helpers/snapshot-restore-plan";
|
||||
export type {
|
||||
SnapshotRestoreExecutionPlan,
|
||||
SnapshotRestoreLocation,
|
||||
SnapshotRestoreRequest,
|
||||
SnapshotRestoreTargetPlan,
|
||||
} from "./helpers/snapshot-restore-plan";
|
||||
export type {
|
||||
ResticDeps,
|
||||
ResticEnv,
|
||||
|
||||
@@ -23,6 +23,7 @@ export { addCommonArgs } from "./helpers/add-common-args";
|
||||
export { buildEnv } from "./helpers/build-env";
|
||||
export { buildRepoUrl } from "./helpers/build-repo-url";
|
||||
export { cleanupTemporaryKeys } from "./helpers/cleanup-temporary-keys";
|
||||
export { getResticRestoreRoot } from "./helpers/restore-paths";
|
||||
export { validateCustomResticParams } from "./helpers/validate-custom-params";
|
||||
export { isResticError, ResticError, ResticLockError } from "./error";
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import path from "node:path";
|
||||
import fc from "fast-check";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { hasPathListSeparator, isPathWithin, normalizeAbsolutePath } from "../path";
|
||||
import {
|
||||
hasPathListSeparator,
|
||||
isPathWithin,
|
||||
normalizeAbsolutePath,
|
||||
windowsHostPathToResticSnapshotPath,
|
||||
windowsResticSnapshotPathToHostPath,
|
||||
} from "../path";
|
||||
|
||||
const safePathSegmentArb = fc
|
||||
.array(fc.constantFrom("a", "b", "c", "x", "y", "z", "0", "1", "2", "-", "_", ".", " "), {
|
||||
@@ -94,6 +100,18 @@ describe("isPathWithin", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Windows restic snapshot path conversion", () => {
|
||||
test("maps native Windows host paths to restic snapshot paths", () => {
|
||||
expect(windowsHostPathToResticSnapshotPath("C:\\Users\\nicolas")).toBe("/C/Users/nicolas");
|
||||
expect(windowsHostPathToResticSnapshotPath("c:/Users/nicolas/")).toBe("/C/Users/nicolas");
|
||||
});
|
||||
|
||||
test("maps restic snapshot paths to native Windows host paths only when called explicitly", () => {
|
||||
expect(windowsResticSnapshotPathToHostPath("/C/Users/nicolas")).toBe("C:\\Users\\nicolas");
|
||||
expect(windowsResticSnapshotPathToHostPath("/d/source")).toBe("D:\\source");
|
||||
});
|
||||
});
|
||||
|
||||
describe("path list character support", () => {
|
||||
test("allows line breaks in raw path lists", () => {
|
||||
expect(hasPathListSeparator("Photos", "raw")).toBe(false);
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { getOriginalRestoreTargetForRoot, getSnapshotSourcePathPlan } from "../snapshot-source-paths";
|
||||
|
||||
describe("getSnapshotSourcePathPlan", () => {
|
||||
test("keeps POSIX snapshots restorable to original by default", () => {
|
||||
expect(
|
||||
getSnapshotSourcePathPlan({
|
||||
snapshotPaths: ["/mnt/source/photos", "/mnt/source/docs"],
|
||||
}),
|
||||
).toMatchObject({
|
||||
sourcePathKind: "posix",
|
||||
queryBasePath: "/mnt/source",
|
||||
originalRestoreBasePath: "/mnt/source",
|
||||
customRestoreBasePath: "/mnt/source",
|
||||
requiresCustomTarget: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("keeps Windows snapshots on custom-target restore for POSIX hosts", () => {
|
||||
expect(
|
||||
getSnapshotSourcePathPlan({
|
||||
snapshotPaths: ["C:\\Users\\nicolas\\Photos"],
|
||||
}),
|
||||
).toMatchObject({
|
||||
sourcePathKind: "windows",
|
||||
queryBasePath: "/",
|
||||
originalRestoreBasePath: "/",
|
||||
customRestoreBasePath: "/",
|
||||
requiresCustomTarget: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("keeps Windows snapshots restorable to original on Windows hosts", () => {
|
||||
expect(
|
||||
getSnapshotSourcePathPlan({
|
||||
snapshotPaths: ["C:\\Users\\nicolas\\Photos", "c:/Users/nicolas/Documents"],
|
||||
hostPathKind: "windows",
|
||||
}),
|
||||
).toMatchObject({
|
||||
sourcePathKind: "windows",
|
||||
queryBasePath: "/C/Users/nicolas",
|
||||
originalRestoreBasePath: "/C/Users/nicolas",
|
||||
customRestoreBasePath: "/",
|
||||
requiresCustomTarget: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("targets the selected Windows restore root itself for original-location restores", () => {
|
||||
expect(
|
||||
getOriginalRestoreTargetForRoot({
|
||||
restoreRoot: "/C/Users/Nicolas/Downloads",
|
||||
sourcePathKind: "windows",
|
||||
}),
|
||||
).toBe("C:\\Users\\Nicolas\\Downloads");
|
||||
});
|
||||
|
||||
test("requires custom target for unsupported native paths", () => {
|
||||
expect(
|
||||
getSnapshotSourcePathPlan({
|
||||
snapshotPaths: ["relative\\path"],
|
||||
}),
|
||||
).toMatchObject({
|
||||
sourcePathKind: "unsupported",
|
||||
queryBasePath: "/",
|
||||
originalRestoreBasePath: "/",
|
||||
customRestoreBasePath: "/",
|
||||
requiresCustomTarget: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("requires custom target for mixed POSIX and Windows paths", () => {
|
||||
expect(
|
||||
getSnapshotSourcePathPlan({
|
||||
snapshotPaths: ["C:\\some\\path", "/mnt/source"],
|
||||
}),
|
||||
).toMatchObject({
|
||||
sourcePathKind: "unsupported",
|
||||
queryBasePath: "/",
|
||||
originalRestoreBasePath: "/",
|
||||
customRestoreBasePath: "/",
|
||||
requiresCustomTarget: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("requires custom target for Windows paths spanning drives", () => {
|
||||
expect(
|
||||
getSnapshotSourcePathPlan({
|
||||
snapshotPaths: ["C:\\some\\path", "D:\\other\\path"],
|
||||
hostPathKind: "windows",
|
||||
}),
|
||||
).toMatchObject({
|
||||
queryBasePath: "/",
|
||||
originalRestoreBasePath: "/",
|
||||
customRestoreBasePath: "/",
|
||||
requiresCustomTarget: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,20 @@
|
||||
export { safeJsonParse } from "./json.js";
|
||||
export { toErrorDetails, toMessage } from "./errors.js";
|
||||
export { hasPathListSeparator, isPathWithin, normalizeAbsolutePath } from "./path.js";
|
||||
export {
|
||||
hasPathListSeparator,
|
||||
isPathWithin,
|
||||
normalizeAbsolutePath,
|
||||
normalizeWindowsHostPath,
|
||||
windowsHostPathToResticSnapshotPath,
|
||||
windowsResticSnapshotPathToHostPath,
|
||||
} from "./path.js";
|
||||
export { findCommonAncestor } from "./common-ancestor.js";
|
||||
export {
|
||||
getOriginalRestoreTargetForRoot,
|
||||
getSnapshotSourcePathPlan,
|
||||
hostPathKindFromPath,
|
||||
hostPathKindFromPlatform,
|
||||
} from "./snapshot-source-paths.js";
|
||||
export type { HostPathKind, SnapshotSourcePathKind, SnapshotSourcePathPlan } from "./snapshot-source-paths.js";
|
||||
export { DATE_FORMATS, DEFAULT_TIME_FORMAT, inferDateTimePreferences, TIME_FORMATS } from "./datetime.js";
|
||||
export type { DateFormatPreference, TimeFormatPreference } from "./datetime.js";
|
||||
|
||||
@@ -1,3 +1,89 @@
|
||||
export const isWindowsHostPath = (value: string): boolean => /^[A-Za-z]:(?:[\\/]|$)/.test(value);
|
||||
|
||||
export const normalizeWindowsHostPath = (value: string): string | undefined => {
|
||||
if (!isWindowsHostPath(value)) return undefined;
|
||||
|
||||
const parts: string[] = [];
|
||||
for (const part of value.slice(2).replace(/\\/g, "/").split("/")) {
|
||||
if (!part || part === ".") continue;
|
||||
if (part === "..") {
|
||||
parts.pop();
|
||||
continue;
|
||||
}
|
||||
parts.push(part);
|
||||
}
|
||||
|
||||
return `${value[0]?.toUpperCase()}:\\${parts.join("\\")}`;
|
||||
};
|
||||
|
||||
export const windowsHostPathToResticSnapshotPath = (value: string): string | undefined => {
|
||||
const normalized = normalizeWindowsHostPath(value);
|
||||
if (!normalized) return undefined;
|
||||
|
||||
const withoutDrive = normalized
|
||||
.slice(2)
|
||||
.replace(/\\/g, "/")
|
||||
.replace(/^\/+|\/+$/g, "");
|
||||
return withoutDrive ? `/${normalized[0]}/${withoutDrive}` : `/${normalized[0]}`;
|
||||
};
|
||||
|
||||
export const windowsResticSnapshotPathToHostPath = (value: string): string | undefined => {
|
||||
const match = /^\/([A-Za-z])(?:\/(.*))?$/.exec(value);
|
||||
if (!match?.[1]) return undefined;
|
||||
|
||||
const segments = match[2]?.split("/").filter(Boolean) ?? [];
|
||||
return `${match[1].toUpperCase()}:\\${segments.join("\\")}`;
|
||||
};
|
||||
|
||||
export const findWindowsHostCommonAncestor = (paths: string[]): string | undefined => {
|
||||
const normalizedPaths: string[] = [];
|
||||
for (const path of paths) {
|
||||
const normalizedPath = normalizeWindowsHostPath(path);
|
||||
if (!normalizedPath) return undefined;
|
||||
normalizedPaths.push(normalizedPath);
|
||||
}
|
||||
|
||||
const drive = normalizedPaths[0]?.slice(0, 2);
|
||||
if (
|
||||
!drive ||
|
||||
normalizedPaths.some((normalizedPath) => normalizedPath.slice(0, 2).toLowerCase() !== drive.toLowerCase())
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const splitPaths = normalizedPaths.map((normalizedPath) => normalizedPath.slice(3).split("\\").filter(Boolean));
|
||||
const minLength = Math.min(...splitPaths.map((parts) => parts.length));
|
||||
|
||||
const commonParts: string[] = [];
|
||||
for (let i = 0; i < minLength; i++) {
|
||||
const firstPart = splitPaths[0]?.[i];
|
||||
if (!firstPart) break;
|
||||
|
||||
if (splitPaths.every((parts) => parts[i]?.toLowerCase() === firstPart.toLowerCase())) {
|
||||
commonParts.push(firstPart);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return `${drive.toUpperCase()}\\${commonParts.join("\\")}`;
|
||||
};
|
||||
|
||||
export const getWindowsHostParentPath = (value: string): string | undefined => {
|
||||
const normalized = normalizeWindowsHostPath(value);
|
||||
if (!normalized) return undefined;
|
||||
|
||||
const driveRoot = `${normalized.slice(0, 2)}\\`;
|
||||
const withoutTrailingSlash = normalized.replace(/\\+$/, "");
|
||||
const lastSlashIndex = withoutTrailingSlash.lastIndexOf("\\");
|
||||
|
||||
if (lastSlashIndex <= 2) {
|
||||
return driveRoot;
|
||||
}
|
||||
|
||||
return withoutTrailingSlash.slice(0, lastSlashIndex);
|
||||
};
|
||||
|
||||
export const normalizeAbsolutePath = (value?: string): string => {
|
||||
if (!value?.trim()) return "/";
|
||||
|
||||
|
||||
99
packages/core/src/utils/snapshot-source-paths.ts
Normal file
99
packages/core/src/utils/snapshot-source-paths.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { findCommonAncestor } from "./common-ancestor.js";
|
||||
import {
|
||||
findWindowsHostCommonAncestor,
|
||||
isWindowsHostPath,
|
||||
windowsHostPathToResticSnapshotPath,
|
||||
windowsResticSnapshotPathToHostPath,
|
||||
} from "./path.js";
|
||||
|
||||
export type HostPathKind = "posix" | "windows";
|
||||
export type SnapshotSourcePathKind = HostPathKind | "unsupported";
|
||||
|
||||
export type SnapshotSourcePathPlan = {
|
||||
sourcePathKind: SnapshotSourcePathKind;
|
||||
queryBasePath: string;
|
||||
originalRestoreBasePath: string;
|
||||
customRestoreBasePath: string;
|
||||
requiresCustomTarget: boolean;
|
||||
};
|
||||
|
||||
const rootPlan = (
|
||||
requiresCustomTarget = false,
|
||||
sourcePathKind: SnapshotSourcePathKind = requiresCustomTarget ? "unsupported" : "posix",
|
||||
): SnapshotSourcePathPlan => ({
|
||||
sourcePathKind,
|
||||
queryBasePath: "/",
|
||||
originalRestoreBasePath: "/",
|
||||
customRestoreBasePath: "/",
|
||||
requiresCustomTarget,
|
||||
});
|
||||
|
||||
export const hostPathKindFromPath = (hostPath?: string): HostPathKind =>
|
||||
hostPath && isWindowsHostPath(hostPath) ? "windows" : "posix";
|
||||
|
||||
export const hostPathKindFromPlatform = (platform: string): HostPathKind =>
|
||||
platform === "win32" ? "windows" : "posix";
|
||||
|
||||
export const getSnapshotSourcePathPlan = ({
|
||||
snapshotPaths,
|
||||
hostPathKind = "posix",
|
||||
}: {
|
||||
snapshotPaths: string[];
|
||||
hostPathKind?: HostPathKind;
|
||||
}): SnapshotSourcePathPlan => {
|
||||
if (snapshotPaths.length === 0) return rootPlan();
|
||||
|
||||
const hasWindowsPaths = snapshotPaths.some(isWindowsHostPath);
|
||||
const hasPosixPaths = snapshotPaths.some((path) => path.startsWith("/"));
|
||||
const hasUnsupportedNativePaths = snapshotPaths.some((path) => !path.startsWith("/") && !isWindowsHostPath(path));
|
||||
|
||||
if (hasUnsupportedNativePaths || (hasWindowsPaths && hasPosixPaths)) {
|
||||
return rootPlan(true);
|
||||
}
|
||||
|
||||
if (hasWindowsPaths) {
|
||||
if (hostPathKind !== "windows") {
|
||||
return rootPlan(true, "windows");
|
||||
}
|
||||
|
||||
const basePath = findWindowsHostCommonAncestor(snapshotPaths);
|
||||
if (!basePath) return rootPlan(true);
|
||||
const resticBasePath = windowsHostPathToResticSnapshotPath(basePath);
|
||||
if (!resticBasePath) return rootPlan(true, "windows");
|
||||
|
||||
return {
|
||||
sourcePathKind: "windows",
|
||||
queryBasePath: resticBasePath,
|
||||
originalRestoreBasePath: resticBasePath,
|
||||
customRestoreBasePath: "/",
|
||||
requiresCustomTarget: false,
|
||||
};
|
||||
}
|
||||
|
||||
const basePath = findCommonAncestor(snapshotPaths);
|
||||
|
||||
return {
|
||||
sourcePathKind: "posix",
|
||||
queryBasePath: basePath,
|
||||
originalRestoreBasePath: basePath,
|
||||
customRestoreBasePath: basePath,
|
||||
requiresCustomTarget: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const getOriginalRestoreTargetForRoot = ({
|
||||
restoreRoot,
|
||||
sourcePathKind,
|
||||
}: {
|
||||
restoreRoot: string;
|
||||
sourcePathKind: SnapshotSourcePathKind;
|
||||
}): string => {
|
||||
if (sourcePathKind !== "windows") return "/";
|
||||
|
||||
const hostRestoreRoot = windowsResticSnapshotPathToHostPath(restoreRoot);
|
||||
if (!hostRestoreRoot) {
|
||||
throw new Error("Windows original restore root must use restic drive syntax.");
|
||||
}
|
||||
|
||||
return hostRestoreRoot;
|
||||
};
|
||||
Reference in New Issue
Block a user