mirror of
https://github.com/nicotsx/zerobyte.git
synced 2025-12-10 12:10:51 +01:00
Compare commits
3 Commits
v0.15.0
...
v0.16.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba3e73d76a | ||
|
|
ce07c588ad | ||
|
|
e7f0a2828d |
@@ -1,4 +1,4 @@
|
|||||||
ARG BUN_VERSION="1.3.1"
|
ARG BUN_VERSION="1.3.3"
|
||||||
|
|
||||||
FROM oven/bun:${BUN_VERSION}-alpine AS base
|
FROM oven/bun:${BUN_VERSION}-alpine AS base
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import { type DefaultError, queryOptions, type UseMutationOptions } from '@tanstack/react-query';
|
import { type DefaultError, queryOptions, type UseMutationOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { client } from '../client.gen';
|
import { client } from '../client.gen';
|
||||||
import { browseFilesystem, changePassword, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteVolume, doctorRepository, downloadResticPassword, getBackupSchedule, getBackupScheduleForVolume, getContainersUsingVolume, getMe, getNotificationDestination, getRepository, getScheduleNotifications, getSnapshotDetails, getStatus, getSystemInfo, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, login, logout, mountVolume, type Options, register, restoreSnapshot, runBackupNow, runForget, stopBackup, testConnection, testNotificationDestination, unmountVolume, updateBackupSchedule, updateNotificationDestination, updateRepository, updateScheduleNotifications, updateVolume } from '../sdk.gen';
|
import { browseFilesystem, changePassword, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteVolume, doctorRepository, downloadResticPassword, getBackupSchedule, getBackupScheduleForVolume, getContainersUsingVolume, getMe, getMirrorCompatibility, getNotificationDestination, getRepository, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getStatus, getSystemInfo, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, login, logout, mountVolume, type Options, register, restoreSnapshot, runBackupNow, runForget, stopBackup, testConnection, testNotificationDestination, unmountVolume, updateBackupSchedule, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateVolume } from '../sdk.gen';
|
||||||
import type { BrowseFilesystemData, BrowseFilesystemResponse, ChangePasswordData, ChangePasswordResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteVolumeData, DeleteVolumeResponse, DoctorRepositoryData, DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetContainersUsingVolumeData, GetContainersUsingVolumeResponse, GetMeData, GetMeResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetRepositoryData, GetRepositoryResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, LoginData, LoginResponse, LogoutData, LogoutResponse, MountVolumeData, MountVolumeResponse, RegisterData, RegisterResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, StopBackupData, StopBackupResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
|
import type { BrowseFilesystemData, BrowseFilesystemResponse, ChangePasswordData, ChangePasswordResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteVolumeData, DeleteVolumeResponse, DoctorRepositoryData, DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetContainersUsingVolumeData, GetContainersUsingVolumeResponse, GetMeData, GetMeResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetRepositoryData, GetRepositoryResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, LoginData, LoginResponse, LogoutData, LogoutResponse, MountVolumeData, MountVolumeResponse, RegisterData, RegisterResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, StopBackupData, StopBackupResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new user
|
* Register a new user
|
||||||
@@ -755,6 +755,59 @@ export const updateScheduleNotificationsMutation = (options?: Partial<Options<Up
|
|||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getScheduleMirrorsQueryKey = (options: Options<GetScheduleMirrorsData>) => createQueryKey("getScheduleMirrors", options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get mirror repository assignments for a backup schedule
|
||||||
|
*/
|
||||||
|
export const getScheduleMirrorsOptions = (options: Options<GetScheduleMirrorsData>) => queryOptions<GetScheduleMirrorsResponse, DefaultError, GetScheduleMirrorsResponse, ReturnType<typeof getScheduleMirrorsQueryKey>>({
|
||||||
|
queryFn: async ({ queryKey, signal }) => {
|
||||||
|
const { data } = await getScheduleMirrors({
|
||||||
|
...options,
|
||||||
|
...queryKey[0],
|
||||||
|
signal,
|
||||||
|
throwOnError: true
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
queryKey: getScheduleMirrorsQueryKey(options)
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update mirror repository assignments for a backup schedule
|
||||||
|
*/
|
||||||
|
export const updateScheduleMirrorsMutation = (options?: Partial<Options<UpdateScheduleMirrorsData>>): UseMutationOptions<UpdateScheduleMirrorsResponse, DefaultError, Options<UpdateScheduleMirrorsData>> => {
|
||||||
|
const mutationOptions: UseMutationOptions<UpdateScheduleMirrorsResponse, DefaultError, Options<UpdateScheduleMirrorsData>> = {
|
||||||
|
mutationFn: async (fnOptions) => {
|
||||||
|
const { data } = await updateScheduleMirrors({
|
||||||
|
...options,
|
||||||
|
...fnOptions,
|
||||||
|
throwOnError: true
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return mutationOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMirrorCompatibilityQueryKey = (options: Options<GetMirrorCompatibilityData>) => createQueryKey("getMirrorCompatibility", options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get mirror compatibility info for all repositories relative to a backup schedule's primary repository
|
||||||
|
*/
|
||||||
|
export const getMirrorCompatibilityOptions = (options: Options<GetMirrorCompatibilityData>) => queryOptions<GetMirrorCompatibilityResponse, DefaultError, GetMirrorCompatibilityResponse, ReturnType<typeof getMirrorCompatibilityQueryKey>>({
|
||||||
|
queryFn: async ({ queryKey, signal }) => {
|
||||||
|
const { data } = await getMirrorCompatibility({
|
||||||
|
...options,
|
||||||
|
...queryKey[0],
|
||||||
|
signal,
|
||||||
|
throwOnError: true
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
queryKey: getMirrorCompatibilityQueryKey(options)
|
||||||
|
});
|
||||||
|
|
||||||
export const listNotificationDestinationsQueryKey = (options?: Options<ListNotificationDestinationsData>) => createQueryKey("listNotificationDestinations", options);
|
export const listNotificationDestinationsQueryKey = (options?: Options<ListNotificationDestinationsData>) => createQueryKey("listNotificationDestinations", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import type { Client, Options as Options2, TDataShape } from './client';
|
import type { Client, Options as Options2, TDataShape } from './client';
|
||||||
import { client } from './client.gen';
|
import { client } from './client.gen';
|
||||||
import type { BrowseFilesystemData, BrowseFilesystemResponses, ChangePasswordData, ChangePasswordResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetContainersUsingVolumeData, GetContainersUsingVolumeErrors, GetContainersUsingVolumeResponses, GetMeData, GetMeResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetRepositoryData, GetRepositoryResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, LoginData, LoginResponses, LogoutData, LogoutResponses, MountVolumeData, MountVolumeResponses, RegisterData, RegisterResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
|
import type { BrowseFilesystemData, BrowseFilesystemResponses, ChangePasswordData, ChangePasswordResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetContainersUsingVolumeData, GetContainersUsingVolumeErrors, GetContainersUsingVolumeResponses, GetMeData, GetMeResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetRepositoryData, GetRepositoryResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, LoginData, LoginResponses, LogoutData, LogoutResponses, MountVolumeData, MountVolumeResponses, RegisterData, RegisterResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
|
||||||
|
|
||||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
||||||
/**
|
/**
|
||||||
@@ -476,6 +476,40 @@ 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>) => {
|
||||||
|
return (options.client ?? client).get<GetScheduleMirrorsResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/backups/{scheduleId}/mirrors',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update mirror repository assignments for a backup schedule
|
||||||
|
*/
|
||||||
|
export const updateScheduleMirrors = <ThrowOnError extends boolean = false>(options: Options<UpdateScheduleMirrorsData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).put<UpdateScheduleMirrorsResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/backups/{scheduleId}/mirrors',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>) => {
|
||||||
|
return (options.client ?? client).get<GetMirrorCompatibilityResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/backups/{scheduleId}/mirrors/compatibility',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all notification destinations
|
* List all notification destinations
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1124,6 +1124,7 @@ export type ListSnapshotsResponses = {
|
|||||||
paths: Array<string>;
|
paths: Array<string>;
|
||||||
short_id: string;
|
short_id: string;
|
||||||
size: number;
|
size: number;
|
||||||
|
tags: Array<string>;
|
||||||
time: number;
|
time: number;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
@@ -1170,6 +1171,7 @@ export type GetSnapshotDetailsResponses = {
|
|||||||
paths: Array<string>;
|
paths: Array<string>;
|
||||||
short_id: string;
|
short_id: string;
|
||||||
size: number;
|
size: number;
|
||||||
|
tags: Array<string>;
|
||||||
time: number;
|
time: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -2112,6 +2114,231 @@ export type UpdateScheduleNotificationsResponses = {
|
|||||||
|
|
||||||
export type UpdateScheduleNotificationsResponse = UpdateScheduleNotificationsResponses[keyof UpdateScheduleNotificationsResponses];
|
export type UpdateScheduleNotificationsResponse = UpdateScheduleNotificationsResponses[keyof UpdateScheduleNotificationsResponses];
|
||||||
|
|
||||||
|
export type GetScheduleMirrorsData = {
|
||||||
|
body?: never;
|
||||||
|
path: {
|
||||||
|
scheduleId: string;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: '/api/v1/backups/{scheduleId}/mirrors';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetScheduleMirrorsResponses = {
|
||||||
|
/**
|
||||||
|
* List of mirror repository assignments for the schedule
|
||||||
|
*/
|
||||||
|
200: Array<{
|
||||||
|
createdAt: number;
|
||||||
|
enabled: boolean;
|
||||||
|
lastCopyAt: number | null;
|
||||||
|
lastCopyError: string | null;
|
||||||
|
lastCopyStatus: 'error' | 'success' | null;
|
||||||
|
repository: {
|
||||||
|
compressionMode: 'auto' | 'max' | 'off' | null;
|
||||||
|
config: {
|
||||||
|
accessKeyId: string;
|
||||||
|
backend: 'r2';
|
||||||
|
bucket: string;
|
||||||
|
endpoint: string;
|
||||||
|
secretAccessKey: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
} | {
|
||||||
|
accessKeyId: string;
|
||||||
|
backend: 's3';
|
||||||
|
bucket: string;
|
||||||
|
endpoint: string;
|
||||||
|
secretAccessKey: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
} | {
|
||||||
|
accountKey: string;
|
||||||
|
accountName: string;
|
||||||
|
backend: 'azure';
|
||||||
|
container: string;
|
||||||
|
customPassword?: string;
|
||||||
|
endpointSuffix?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
} | {
|
||||||
|
backend: 'gcs';
|
||||||
|
bucket: string;
|
||||||
|
credentialsJson: string;
|
||||||
|
projectId: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
} | {
|
||||||
|
backend: 'local';
|
||||||
|
name: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
path?: string;
|
||||||
|
} | {
|
||||||
|
backend: 'rclone';
|
||||||
|
path: string;
|
||||||
|
remote: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
} | {
|
||||||
|
backend: 'rest';
|
||||||
|
url: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
password?: string;
|
||||||
|
path?: string;
|
||||||
|
username?: string;
|
||||||
|
} | {
|
||||||
|
backend: 'sftp';
|
||||||
|
host: string;
|
||||||
|
path: string;
|
||||||
|
privateKey: string;
|
||||||
|
user: string;
|
||||||
|
port?: number;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
};
|
||||||
|
createdAt: number;
|
||||||
|
id: string;
|
||||||
|
lastChecked: number | null;
|
||||||
|
lastError: string | null;
|
||||||
|
name: string;
|
||||||
|
shortId: string;
|
||||||
|
status: 'error' | 'healthy' | 'unknown' | null;
|
||||||
|
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp';
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
|
repositoryId: string;
|
||||||
|
scheduleId: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetScheduleMirrorsResponse = GetScheduleMirrorsResponses[keyof GetScheduleMirrorsResponses];
|
||||||
|
|
||||||
|
export type UpdateScheduleMirrorsData = {
|
||||||
|
body?: {
|
||||||
|
mirrors: Array<{
|
||||||
|
enabled: boolean;
|
||||||
|
repositoryId: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
path: {
|
||||||
|
scheduleId: string;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: '/api/v1/backups/{scheduleId}/mirrors';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateScheduleMirrorsResponses = {
|
||||||
|
/**
|
||||||
|
* Mirror assignments updated successfully
|
||||||
|
*/
|
||||||
|
200: Array<{
|
||||||
|
createdAt: number;
|
||||||
|
enabled: boolean;
|
||||||
|
lastCopyAt: number | null;
|
||||||
|
lastCopyError: string | null;
|
||||||
|
lastCopyStatus: 'error' | 'success' | null;
|
||||||
|
repository: {
|
||||||
|
compressionMode: 'auto' | 'max' | 'off' | null;
|
||||||
|
config: {
|
||||||
|
accessKeyId: string;
|
||||||
|
backend: 'r2';
|
||||||
|
bucket: string;
|
||||||
|
endpoint: string;
|
||||||
|
secretAccessKey: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
} | {
|
||||||
|
accessKeyId: string;
|
||||||
|
backend: 's3';
|
||||||
|
bucket: string;
|
||||||
|
endpoint: string;
|
||||||
|
secretAccessKey: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
} | {
|
||||||
|
accountKey: string;
|
||||||
|
accountName: string;
|
||||||
|
backend: 'azure';
|
||||||
|
container: string;
|
||||||
|
customPassword?: string;
|
||||||
|
endpointSuffix?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
} | {
|
||||||
|
backend: 'gcs';
|
||||||
|
bucket: string;
|
||||||
|
credentialsJson: string;
|
||||||
|
projectId: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
} | {
|
||||||
|
backend: 'local';
|
||||||
|
name: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
path?: string;
|
||||||
|
} | {
|
||||||
|
backend: 'rclone';
|
||||||
|
path: string;
|
||||||
|
remote: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
} | {
|
||||||
|
backend: 'rest';
|
||||||
|
url: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
password?: string;
|
||||||
|
path?: string;
|
||||||
|
username?: string;
|
||||||
|
} | {
|
||||||
|
backend: 'sftp';
|
||||||
|
host: string;
|
||||||
|
path: string;
|
||||||
|
privateKey: string;
|
||||||
|
user: string;
|
||||||
|
port?: number;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
};
|
||||||
|
createdAt: number;
|
||||||
|
id: string;
|
||||||
|
lastChecked: number | null;
|
||||||
|
lastError: string | null;
|
||||||
|
name: string;
|
||||||
|
shortId: string;
|
||||||
|
status: 'error' | 'healthy' | 'unknown' | null;
|
||||||
|
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp';
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
|
repositoryId: string;
|
||||||
|
scheduleId: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateScheduleMirrorsResponse = UpdateScheduleMirrorsResponses[keyof UpdateScheduleMirrorsResponses];
|
||||||
|
|
||||||
|
export type GetMirrorCompatibilityData = {
|
||||||
|
body?: never;
|
||||||
|
path: {
|
||||||
|
scheduleId: string;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: '/api/v1/backups/{scheduleId}/mirrors/compatibility';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetMirrorCompatibilityResponses = {
|
||||||
|
/**
|
||||||
|
* List of repositories with their mirror compatibility status
|
||||||
|
*/
|
||||||
|
200: Array<{
|
||||||
|
compatible: boolean;
|
||||||
|
reason: string | null;
|
||||||
|
repositoryId: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetMirrorCompatibilityResponse = GetMirrorCompatibilityResponses[keyof GetMirrorCompatibilityResponses];
|
||||||
|
|
||||||
export type ListNotificationDestinationsData = {
|
export type ListNotificationDestinationsData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
path?: never;
|
path?: never;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Calendar, Clock, Database, FolderTree, HardDrive, Trash2 } from "lucide-react";
|
import { Calendar, Clock, Database, FolderTree, HardDrive, Trash2 } from "lucide-react";
|
||||||
import { useNavigate } from "react-router";
|
import { Link, useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ByteSize } from "~/client/components/bytes-size";
|
import { ByteSize } from "~/client/components/bytes-size";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
|
||||||
@@ -18,18 +18,17 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "~/client/components/ui/alert-dialog";
|
} from "~/client/components/ui/alert-dialog";
|
||||||
import { formatDuration } from "~/utils/utils";
|
import { formatDuration } from "~/utils/utils";
|
||||||
import type { ListSnapshotsResponse } from "../api-client";
|
|
||||||
import { deleteSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
import { deleteSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import { parseError } from "~/client/lib/errors";
|
import { parseError } from "~/client/lib/errors";
|
||||||
|
import type { BackupSchedule, Snapshot } from "../lib/types";
|
||||||
type Snapshot = ListSnapshotsResponse[number];
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
snapshots: Snapshot[];
|
snapshots: Snapshot[];
|
||||||
|
backups: BackupSchedule[];
|
||||||
repositoryName: string;
|
repositoryName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SnapshotsTable = ({ snapshots, repositoryName }: Props) => {
|
export const SnapshotsTable = ({ snapshots, repositoryName, backups }: Props) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
@@ -76,6 +75,7 @@ export const SnapshotsTable = ({ snapshots, repositoryName }: Props) => {
|
|||||||
<TableHeader className="bg-card-header">
|
<TableHeader className="bg-card-header">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="uppercase">Snapshot ID</TableHead>
|
<TableHead className="uppercase">Snapshot ID</TableHead>
|
||||||
|
<TableHead className="uppercase">Schedule</TableHead>
|
||||||
<TableHead className="uppercase">Date & Time</TableHead>
|
<TableHead className="uppercase">Date & Time</TableHead>
|
||||||
<TableHead className="uppercase">Size</TableHead>
|
<TableHead className="uppercase">Size</TableHead>
|
||||||
<TableHead className="uppercase hidden md:table-cell text-right">Duration</TableHead>
|
<TableHead className="uppercase hidden md:table-cell text-right">Duration</TableHead>
|
||||||
@@ -84,71 +84,91 @@ export const SnapshotsTable = ({ snapshots, repositoryName }: Props) => {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{snapshots.map((snapshot) => (
|
{snapshots.map((snapshot) => {
|
||||||
<TableRow
|
const backupIds = snapshot.tags.map(Number).filter((tag) => !Number.isNaN(tag));
|
||||||
key={snapshot.short_id}
|
const backup = backups.find((b) => backupIds.includes(b.id));
|
||||||
className="hover:bg-accent/50 cursor-pointer"
|
|
||||||
onClick={() => handleRowClick(snapshot.short_id)}
|
return (
|
||||||
>
|
<TableRow
|
||||||
<TableCell className="font-mono text-sm">
|
key={snapshot.short_id}
|
||||||
<div className="flex items-center gap-2">
|
className="hover:bg-accent/50 cursor-pointer"
|
||||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
onClick={() => handleRowClick(snapshot.short_id)}
|
||||||
<span className="text-strong-accent">{snapshot.short_id}</span>
|
>
|
||||||
</div>
|
<TableCell className="font-mono text-sm">
|
||||||
</TableCell>
|
<div className="flex items-center gap-2">
|
||||||
<TableCell>
|
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||||
<div className="flex items-center gap-2">
|
<span className="text-strong-accent">{snapshot.short_id}</span>
|
||||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
</div>
|
||||||
<span className="text-sm">{new Date(snapshot.time).toLocaleString()}</span>
|
</TableCell>
|
||||||
</div>
|
<TableCell>
|
||||||
</TableCell>
|
<div className="flex items-center gap-2">
|
||||||
<TableCell>
|
<Link
|
||||||
<div className="flex items-center gap-2">
|
hidden={!backup}
|
||||||
<Database className="h-4 w-4 text-muted-foreground" />
|
to={backup ? `/backups/${backup.id}` : "#"}
|
||||||
<span className="font-medium">
|
onClick={(e) => e.stopPropagation()}
|
||||||
<ByteSize bytes={snapshot.size} base={1024} />
|
className="hover:underline"
|
||||||
</span>
|
>
|
||||||
</div>
|
<span className="text-sm">{backup ? backup.id : "-"}</span>
|
||||||
</TableCell>
|
</Link>
|
||||||
<TableCell className="hidden md:table-cell">
|
<span hidden={!!backup} className="text-sm text-muted-foreground">
|
||||||
<div className="flex items-center justify-end gap-2">
|
-
|
||||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
</span>
|
||||||
<span className="text-sm text-muted-foreground">{formatDuration(snapshot.duration / 1000)}</span>
|
</div>
|
||||||
</div>
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell>
|
||||||
<TableCell className="hidden lg:table-cell">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center justify-end gap-2">
|
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||||
<FolderTree className="h-4 w-4 text-muted-foreground" />
|
<span className="text-sm">{new Date(snapshot.time).toLocaleString()}</span>
|
||||||
<Tooltip>
|
</div>
|
||||||
<TooltipTrigger asChild>
|
</TableCell>
|
||||||
<span className="text-xs bg-primary/10 text-primary rounded-md px-2 py-1 cursor-help">
|
<TableCell>
|
||||||
{snapshot.paths.length} {snapshot.paths.length === 1 ? "path" : "paths"}
|
<div className="flex items-center gap-2">
|
||||||
</span>
|
<Database className="h-4 w-4 text-muted-foreground" />
|
||||||
</TooltipTrigger>
|
<span className="font-medium">
|
||||||
<TooltipContent side="top" className="max-w-md">
|
<ByteSize bytes={snapshot.size} base={1024} />
|
||||||
<div className="flex flex-col gap-1">
|
</span>
|
||||||
{snapshot.paths.map((path) => (
|
</div>
|
||||||
<div key={`${snapshot.short_id}-${path}`} className="text-xs font-mono">
|
</TableCell>
|
||||||
{path}
|
<TableCell className="hidden md:table-cell">
|
||||||
</div>
|
<div className="flex items-center justify-end gap-2">
|
||||||
))}
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||||
</div>
|
<span className="text-sm text-muted-foreground">{formatDuration(snapshot.duration / 1000)}</span>
|
||||||
</TooltipContent>
|
</div>
|
||||||
</Tooltip>
|
</TableCell>
|
||||||
</div>
|
<TableCell className="hidden lg:table-cell">
|
||||||
</TableCell>
|
<div className="flex items-center justify-end gap-2">
|
||||||
<TableCell className="text-right">
|
<FolderTree className="h-4 w-4 text-muted-foreground" />
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="ghost"
|
<TooltipTrigger asChild>
|
||||||
size="sm"
|
<span className="text-xs bg-primary/10 text-primary rounded-md px-2 py-1 cursor-help">
|
||||||
onClick={(e) => handleDeleteClick(e, snapshot.short_id)}
|
{snapshot.paths.length} {snapshot.paths.length === 1 ? "path" : "paths"}
|
||||||
disabled={deleteSnapshot.isPending}
|
</span>
|
||||||
>
|
</TooltipTrigger>
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<TooltipContent side="top" className="max-w-md">
|
||||||
</Button>
|
<div className="flex flex-col gap-1">
|
||||||
</TableCell>
|
{snapshot.paths.map((path) => (
|
||||||
</TableRow>
|
<div key={`${snapshot.short_id}-${path}`} className="text-xs font-mono">
|
||||||
))}
|
{path}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => handleDeleteClick(e, snapshot.short_id)}
|
||||||
|
disabled={deleteSnapshot.isPending}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ type ServerEventType =
|
|||||||
| "backup:completed"
|
| "backup:completed"
|
||||||
| "volume:mounted"
|
| "volume:mounted"
|
||||||
| "volume:unmounted"
|
| "volume:unmounted"
|
||||||
| "volume:updated";
|
| "volume:updated"
|
||||||
|
| "mirror:started"
|
||||||
|
| "mirror:completed";
|
||||||
|
|
||||||
export interface BackupEvent {
|
export interface BackupEvent {
|
||||||
scheduleId: number;
|
scheduleId: number;
|
||||||
@@ -35,6 +37,14 @@ export interface VolumeEvent {
|
|||||||
volumeName: string;
|
volumeName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MirrorEvent {
|
||||||
|
scheduleId: number;
|
||||||
|
repositoryId: string;
|
||||||
|
repositoryName: string;
|
||||||
|
status?: "success" | "error";
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
type EventHandler = (data: unknown) => void;
|
type EventHandler = (data: unknown) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,6 +135,27 @@ export function useServerEvents() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
eventSource.addEventListener("mirror:started", (e) => {
|
||||||
|
const data = JSON.parse(e.data) as MirrorEvent;
|
||||||
|
console.log("[SSE] Mirror copy started:", data);
|
||||||
|
|
||||||
|
handlersRef.current.get("mirror:started")?.forEach((handler) => {
|
||||||
|
handler(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.addEventListener("mirror:completed", (e) => {
|
||||||
|
const data = JSON.parse(e.data) as MirrorEvent;
|
||||||
|
console.log("[SSE] Mirror copy completed:", data);
|
||||||
|
|
||||||
|
// Invalidate queries to refresh mirror status in the UI
|
||||||
|
queryClient.invalidateQueries();
|
||||||
|
|
||||||
|
handlersRef.current.get("mirror:completed")?.forEach((handler) => {
|
||||||
|
handler(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
eventSource.onerror = (error) => {
|
eventSource.onerror = (error) => {
|
||||||
console.error("[SSE] Connection error:", error);
|
console.error("[SSE] Connection error:", error);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,352 @@
|
|||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import { Copy, Plus, Trash2 } from "lucide-react";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Button } from "~/client/components/ui/button";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||||
|
import { Switch } from "~/client/components/ui/switch";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
|
||||||
|
import { Badge } from "~/client/components/ui/badge";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
|
||||||
|
import {
|
||||||
|
getScheduleMirrorsOptions,
|
||||||
|
getMirrorCompatibilityOptions,
|
||||||
|
updateScheduleMirrorsMutation,
|
||||||
|
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
import { parseError } from "~/client/lib/errors";
|
||||||
|
import type { Repository } from "~/client/lib/types";
|
||||||
|
import { RepositoryIcon } from "~/client/components/repository-icon";
|
||||||
|
import { StatusDot } from "~/client/components/status-dot";
|
||||||
|
import { formatDistanceToNow } from "date-fns";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
import { cn } from "~/client/lib/utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
scheduleId: number;
|
||||||
|
primaryRepositoryId: string;
|
||||||
|
repositories: Repository[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type MirrorAssignment = {
|
||||||
|
repositoryId: string;
|
||||||
|
enabled: boolean;
|
||||||
|
lastCopyAt: number | null;
|
||||||
|
lastCopyStatus: "success" | "error" | null;
|
||||||
|
lastCopyError: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, repositories }: Props) => {
|
||||||
|
const [assignments, setAssignments] = useState<Map<string, MirrorAssignment>>(new Map());
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
const [isAddingNew, setIsAddingNew] = useState(false);
|
||||||
|
|
||||||
|
const { data: currentMirrors } = useQuery({
|
||||||
|
...getScheduleMirrorsOptions({ path: { scheduleId: scheduleId.toString() } }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: compatibility } = useQuery({
|
||||||
|
...getMirrorCompatibilityOptions({ path: { scheduleId: scheduleId.toString() } }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateMirrors = useMutation({
|
||||||
|
...updateScheduleMirrorsMutation(),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Mirror settings saved successfully");
|
||||||
|
setHasChanges(false);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error("Failed to save mirror settings", {
|
||||||
|
description: parseError(error)?.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const compatibilityMap = useMemo(() => {
|
||||||
|
const map = new Map<string, { compatible: boolean; reason: string | null }>();
|
||||||
|
if (compatibility) {
|
||||||
|
for (const item of compatibility) {
|
||||||
|
map.set(item.repositoryId, { compatible: item.compatible, reason: item.reason });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, [compatibility]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentMirrors && !hasChanges) {
|
||||||
|
const map = new Map<string, MirrorAssignment>();
|
||||||
|
for (const mirror of currentMirrors) {
|
||||||
|
map.set(mirror.repositoryId, {
|
||||||
|
repositoryId: mirror.repositoryId,
|
||||||
|
enabled: mirror.enabled,
|
||||||
|
lastCopyAt: mirror.lastCopyAt,
|
||||||
|
lastCopyStatus: mirror.lastCopyStatus,
|
||||||
|
lastCopyError: mirror.lastCopyError,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setAssignments(map);
|
||||||
|
}
|
||||||
|
}, [currentMirrors, hasChanges]);
|
||||||
|
|
||||||
|
const addRepository = (repositoryId: string) => {
|
||||||
|
const newAssignments = new Map(assignments);
|
||||||
|
newAssignments.set(repositoryId, {
|
||||||
|
repositoryId,
|
||||||
|
enabled: true,
|
||||||
|
lastCopyAt: null,
|
||||||
|
lastCopyStatus: null,
|
||||||
|
lastCopyError: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
setAssignments(newAssignments);
|
||||||
|
setHasChanges(true);
|
||||||
|
setIsAddingNew(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeRepository = (repositoryId: string) => {
|
||||||
|
const newAssignments = new Map(assignments);
|
||||||
|
newAssignments.delete(repositoryId);
|
||||||
|
setAssignments(newAssignments);
|
||||||
|
setHasChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleEnabled = (repositoryId: string) => {
|
||||||
|
const assignment = assignments.get(repositoryId);
|
||||||
|
if (!assignment) return;
|
||||||
|
|
||||||
|
const newAssignments = new Map(assignments);
|
||||||
|
newAssignments.set(repositoryId, {
|
||||||
|
...assignment,
|
||||||
|
enabled: !assignment.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
setAssignments(newAssignments);
|
||||||
|
setHasChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const mirrorsList = Array.from(assignments.values()).map((a) => ({
|
||||||
|
repositoryId: a.repositoryId,
|
||||||
|
enabled: a.enabled,
|
||||||
|
}));
|
||||||
|
updateMirrors.mutate({
|
||||||
|
path: { scheduleId: scheduleId.toString() },
|
||||||
|
body: {
|
||||||
|
mirrors: mirrorsList,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
if (currentMirrors) {
|
||||||
|
const map = new Map<string, MirrorAssignment>();
|
||||||
|
for (const mirror of currentMirrors) {
|
||||||
|
map.set(mirror.repositoryId, {
|
||||||
|
repositoryId: mirror.repositoryId,
|
||||||
|
enabled: mirror.enabled,
|
||||||
|
lastCopyAt: mirror.lastCopyAt,
|
||||||
|
lastCopyStatus: mirror.lastCopyStatus,
|
||||||
|
lastCopyError: mirror.lastCopyError,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setAssignments(map);
|
||||||
|
setHasChanges(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectableRepositories =
|
||||||
|
repositories?.filter((r) => {
|
||||||
|
if (r.id === primaryRepositoryId) return false;
|
||||||
|
if (assignments.has(r.id)) return false;
|
||||||
|
return true;
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
const hasAvailableRepositories = selectableRepositories.some((r) => {
|
||||||
|
const compat = compatibilityMap.get(r.id);
|
||||||
|
return compat?.compatible !== false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const assignedRepositories = Array.from(assignments.keys())
|
||||||
|
.map((id) => repositories?.find((r) => r.id === id))
|
||||||
|
.filter((r) => r !== undefined);
|
||||||
|
|
||||||
|
const getStatusVariant = (status: "success" | "error" | null) => {
|
||||||
|
if (status === "success") return "success";
|
||||||
|
if (status === "error") return "error";
|
||||||
|
return "neutral";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusLabel = (assignment: MirrorAssignment) => {
|
||||||
|
if (assignment.lastCopyStatus === "error" && assignment.lastCopyError) {
|
||||||
|
return assignment.lastCopyError;
|
||||||
|
}
|
||||||
|
if (assignment.lastCopyStatus === "success") {
|
||||||
|
return "Last copy successful";
|
||||||
|
}
|
||||||
|
return "Never copied";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Copy className="h-5 w-5" />
|
||||||
|
Mirror Repositories
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Configure secondary repositories where snapshots will be automatically copied after each backup
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
{!isAddingNew && selectableRepositories.length > 0 && (
|
||||||
|
<Button variant="outline" size="sm" onClick={() => setIsAddingNew(true)}>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Add mirror
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{isAddingNew && (
|
||||||
|
<div className="mb-6 flex items-center gap-2 max-w-md">
|
||||||
|
<Select onValueChange={addRepository}>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select a repository to mirror to..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{selectableRepositories.map((repository) => {
|
||||||
|
const compat = compatibilityMap.get(repository.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip key={repository.id}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div>
|
||||||
|
<SelectItem value={repository.id} disabled={!compat?.compatible}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RepositoryIcon backend={repository.type} className="h-4 w-4" />
|
||||||
|
<span>{repository.name}</span>
|
||||||
|
<span className="text-xs uppercase text-muted-foreground">({repository.type})</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" className={cn("max-w-xs", { hidden: compat?.compatible })}>
|
||||||
|
<p>{compat?.reason || "This repository is not compatible for mirroring."}</p>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
Consider creating a new backup scheduler with the desired destination instead.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{!hasAvailableRepositories && selectableRepositories.length > 0 && (
|
||||||
|
<div className="px-2 py-3 text-sm text-muted-foreground text-center">
|
||||||
|
All available repositories have conflicting backends.
|
||||||
|
<br />
|
||||||
|
<span className="text-xs">
|
||||||
|
Consider creating a new backup scheduler with the desired destination instead.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => setIsAddingNew(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{assignedRepositories.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-8 text-center text-muted-foreground">
|
||||||
|
<Copy className="h-8 w-8 mb-2 opacity-20" />
|
||||||
|
<p className="text-sm">No mirror repositories configured for this schedule.</p>
|
||||||
|
<p className="text-xs mt-1">Click "Add mirror" to replicate backups to additional repositories.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Repository</TableHead>
|
||||||
|
<TableHead className="text-center w-[100px]">Enabled</TableHead>
|
||||||
|
<TableHead className="w-[180px]">Last Copy</TableHead>
|
||||||
|
<TableHead className="w-[50px]"></TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{assignedRepositories.map((repository) => {
|
||||||
|
const assignment = assignments.get(repository.id);
|
||||||
|
if (!assignment) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow key={repository.id}>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Link
|
||||||
|
to={`/repositories/${repository.name}`}
|
||||||
|
className="hover:underline flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<RepositoryIcon backend={repository.type} className="h-4 w-4" />
|
||||||
|
<span className="font-medium">{repository.name}</span>
|
||||||
|
</Link>
|
||||||
|
<Badge variant="outline" className="text-[10px] align-middle">
|
||||||
|
{repository.type}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<Switch
|
||||||
|
className="align-middle"
|
||||||
|
checked={assignment.enabled}
|
||||||
|
onCheckedChange={() => toggleEnabled(repository.id)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{assignment.lastCopyAt ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<StatusDot
|
||||||
|
variant={getStatusVariant(assignment.lastCopyStatus)}
|
||||||
|
label={getStatusLabel(assignment)}
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{formatDistanceToNow(new Date(assignment.lastCopyAt), { addSuffix: true })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-muted-foreground">Never</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => removeRepository(repository.id)}
|
||||||
|
className="h-8 w-8 text-muted-foreground hover:text-destructive align-baseline"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasChanges && (
|
||||||
|
<div className="flex gap-2 justify-end mt-4 pt-4">
|
||||||
|
<Button variant="outline" size="sm" onClick={handleReset}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="default" size="sm" onClick={handleSave} loading={updateMirrors.isPending}>
|
||||||
|
Save changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -29,8 +29,9 @@ import { ScheduleSummary } from "../components/schedule-summary";
|
|||||||
import type { Route } from "./+types/backup-details";
|
import type { Route } from "./+types/backup-details";
|
||||||
import { SnapshotFileBrowser } from "../components/snapshot-file-browser";
|
import { SnapshotFileBrowser } from "../components/snapshot-file-browser";
|
||||||
import { SnapshotTimeline } from "../components/snapshot-timeline";
|
import { SnapshotTimeline } from "../components/snapshot-timeline";
|
||||||
import { getBackupSchedule, listNotificationDestinations } from "~/client/api-client";
|
import { getBackupSchedule, listNotificationDestinations, listRepositories } from "~/client/api-client";
|
||||||
import { ScheduleNotificationsConfig } from "../components/schedule-notifications-config";
|
import { ScheduleNotificationsConfig } from "../components/schedule-notifications-config";
|
||||||
|
import { ScheduleMirrorsConfig } from "../components/schedule-mirrors-config";
|
||||||
import { cn } from "~/client/lib/utils";
|
import { cn } from "~/client/lib/utils";
|
||||||
|
|
||||||
export const handle = {
|
export const handle = {
|
||||||
@@ -53,10 +54,11 @@ export function meta(_: Route.MetaArgs) {
|
|||||||
export const clientLoader = async ({ params }: Route.LoaderArgs) => {
|
export const clientLoader = async ({ params }: Route.LoaderArgs) => {
|
||||||
const schedule = await getBackupSchedule({ path: { scheduleId: params.id } });
|
const schedule = await getBackupSchedule({ path: { scheduleId: params.id } });
|
||||||
const notifs = await listNotificationDestinations();
|
const notifs = await listNotificationDestinations();
|
||||||
|
const repos = await listRepositories();
|
||||||
|
|
||||||
if (!schedule.data) return redirect("/backups");
|
if (!schedule.data) return redirect("/backups");
|
||||||
|
|
||||||
return { schedule: schedule.data, notifs: notifs.data };
|
return { schedule: schedule.data, notifs: notifs.data, repos: repos.data };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ScheduleDetailsPage({ params, loaderData }: Route.ComponentProps) {
|
export default function ScheduleDetailsPage({ params, loaderData }: Route.ComponentProps) {
|
||||||
@@ -226,6 +228,13 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
|||||||
<div className={cn({ hidden: !loaderData.notifs?.length })}>
|
<div className={cn({ hidden: !loaderData.notifs?.length })}>
|
||||||
<ScheduleNotificationsConfig scheduleId={schedule.id} destinations={loaderData.notifs ?? []} />
|
<ScheduleNotificationsConfig scheduleId={schedule.id} destinations={loaderData.notifs ?? []} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className={cn({ hidden: !loaderData.repos?.length || loaderData.repos.length < 2 })}>
|
||||||
|
<ScheduleMirrorsConfig
|
||||||
|
scheduleId={schedule.id}
|
||||||
|
primaryRepositoryId={schedule.repositoryId}
|
||||||
|
repositories={loaderData.repos ?? []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<SnapshotTimeline
|
<SnapshotTimeline
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
snapshots={snapshots ?? []}
|
snapshots={snapshots ?? []}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Database } from "lucide-react";
|
import { Database } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { listSnapshotsOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
import { listBackupSchedulesOptions, listSnapshotsOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import { SnapshotsTable } from "~/client/components/snapshots-table";
|
import { SnapshotsTable } from "~/client/components/snapshots-table";
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||||
@@ -21,6 +21,10 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
|
|||||||
initialData: [],
|
initialData: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const schedules = useQuery({
|
||||||
|
...listBackupSchedulesOptions(),
|
||||||
|
});
|
||||||
|
|
||||||
const filteredSnapshots = data.filter((snapshot: Snapshot) => {
|
const filteredSnapshots = data.filter((snapshot: Snapshot) => {
|
||||||
if (!searchQuery) return true;
|
if (!searchQuery) return true;
|
||||||
const searchLower = searchQuery.toLowerCase();
|
const searchLower = searchQuery.toLowerCase();
|
||||||
@@ -132,7 +136,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
) : (
|
) : (
|
||||||
<SnapshotsTable snapshots={filteredSnapshots} repositoryName={repository.name} />
|
<SnapshotsTable snapshots={filteredSnapshots} repositoryName={repository.name} backups={schedules.data ?? []} />
|
||||||
)}
|
)}
|
||||||
<div className="px-4 py-2 text-sm text-muted-foreground bg-card-header flex justify-between border-t">
|
<div className="px-4 py-2 text-sm text-muted-foreground bg-card-header flex justify-between border-t">
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
139
app/drizzle/0018_bizarre_zzzax.sql
Normal file
139
app/drizzle/0018_bizarre_zzzax.sql
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
DROP TABLE IF EXISTS `backup_schedule_mirrors_table`;--> statement-breakpoint
|
||||||
|
CREATE TABLE `backup_schedule_mirrors_table` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`schedule_id` integer NOT NULL,
|
||||||
|
`repository_id` text NOT NULL,
|
||||||
|
`enabled` integer DEFAULT true NOT NULL,
|
||||||
|
`last_copy_at` integer,
|
||||||
|
`last_copy_status` text,
|
||||||
|
`last_copy_error` text,
|
||||||
|
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
FOREIGN KEY (`schedule_id`) REFERENCES `backup_schedules_table`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (`repository_id`) REFERENCES `repositories_table`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_app_metadata` (
|
||||||
|
`key` text PRIMARY KEY NOT NULL,
|
||||||
|
`value` text NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_app_metadata`("key", "value", "created_at", "updated_at") SELECT "key", "value", "created_at", "updated_at" FROM `app_metadata`;--> statement-breakpoint
|
||||||
|
DROP TABLE `app_metadata`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_app_metadata` RENAME TO `app_metadata`;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_backup_schedule_notifications_table` (
|
||||||
|
`schedule_id` integer NOT NULL,
|
||||||
|
`destination_id` integer NOT NULL,
|
||||||
|
`notify_on_start` integer DEFAULT false NOT NULL,
|
||||||
|
`notify_on_success` integer DEFAULT false NOT NULL,
|
||||||
|
`notify_on_failure` integer DEFAULT true NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
PRIMARY KEY(`schedule_id`, `destination_id`),
|
||||||
|
FOREIGN KEY (`schedule_id`) REFERENCES `backup_schedules_table`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (`destination_id`) REFERENCES `notification_destinations_table`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_backup_schedule_notifications_table`("schedule_id", "destination_id", "notify_on_start", "notify_on_success", "notify_on_failure", "created_at") SELECT "schedule_id", "destination_id", "notify_on_start", "notify_on_success", "notify_on_failure", "created_at" FROM `backup_schedule_notifications_table`;--> statement-breakpoint
|
||||||
|
DROP TABLE `backup_schedule_notifications_table`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_backup_schedule_notifications_table` RENAME TO `backup_schedule_notifications_table`;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_backup_schedules_table` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`volume_id` integer NOT NULL,
|
||||||
|
`repository_id` text NOT NULL,
|
||||||
|
`enabled` integer DEFAULT true NOT NULL,
|
||||||
|
`cron_expression` text NOT NULL,
|
||||||
|
`retention_policy` text,
|
||||||
|
`exclude_patterns` text DEFAULT '[]',
|
||||||
|
`include_patterns` text DEFAULT '[]',
|
||||||
|
`last_backup_at` integer,
|
||||||
|
`last_backup_status` text,
|
||||||
|
`last_backup_error` text,
|
||||||
|
`next_backup_at` integer,
|
||||||
|
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
FOREIGN KEY (`volume_id`) REFERENCES `volumes_table`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (`repository_id`) REFERENCES `repositories_table`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_backup_schedules_table`("id", "volume_id", "repository_id", "enabled", "cron_expression", "retention_policy", "exclude_patterns", "include_patterns", "last_backup_at", "last_backup_status", "last_backup_error", "next_backup_at", "created_at", "updated_at") SELECT "id", "volume_id", "repository_id", "enabled", "cron_expression", "retention_policy", "exclude_patterns", "include_patterns", "last_backup_at", "last_backup_status", "last_backup_error", "next_backup_at", "created_at", "updated_at" FROM `backup_schedules_table`;--> statement-breakpoint
|
||||||
|
DROP TABLE `backup_schedules_table`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_backup_schedules_table` RENAME TO `backup_schedules_table`;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_notification_destinations_table` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`enabled` integer DEFAULT true NOT NULL,
|
||||||
|
`type` text NOT NULL,
|
||||||
|
`config` text NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_notification_destinations_table`("id", "name", "enabled", "type", "config", "created_at", "updated_at") SELECT "id", "name", "enabled", "type", "config", "created_at", "updated_at" FROM `notification_destinations_table`;--> statement-breakpoint
|
||||||
|
DROP TABLE `notification_destinations_table`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_notification_destinations_table` RENAME TO `notification_destinations_table`;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `notification_destinations_table_name_unique` ON `notification_destinations_table` (`name`);--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_repositories_table` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`short_id` text NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`type` text NOT NULL,
|
||||||
|
`config` text NOT NULL,
|
||||||
|
`compression_mode` text DEFAULT 'auto',
|
||||||
|
`status` text DEFAULT 'unknown',
|
||||||
|
`last_checked` integer,
|
||||||
|
`last_error` text,
|
||||||
|
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_repositories_table`("id", "short_id", "name", "type", "config", "compression_mode", "status", "last_checked", "last_error", "created_at", "updated_at") SELECT "id", "short_id", "name", "type", "config", "compression_mode", "status", "last_checked", "last_error", "created_at", "updated_at" FROM `repositories_table`;--> statement-breakpoint
|
||||||
|
DROP TABLE `repositories_table`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_repositories_table` RENAME TO `repositories_table`;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `repositories_table_short_id_unique` ON `repositories_table` (`short_id`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `repositories_table_name_unique` ON `repositories_table` (`name`);--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_sessions_table` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`user_id` integer NOT NULL,
|
||||||
|
`expires_at` integer NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users_table`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_sessions_table`("id", "user_id", "expires_at", "created_at") SELECT "id", "user_id", "expires_at", "created_at" FROM `sessions_table`;--> statement-breakpoint
|
||||||
|
DROP TABLE `sessions_table`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_sessions_table` RENAME TO `sessions_table`;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_users_table` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`username` text NOT NULL,
|
||||||
|
`password_hash` text NOT NULL,
|
||||||
|
`has_downloaded_restic_password` integer DEFAULT false NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_users_table`("id", "username", "password_hash", "has_downloaded_restic_password", "created_at", "updated_at") SELECT "id", "username", "password_hash", "has_downloaded_restic_password", "created_at", "updated_at" FROM `users_table`;--> statement-breakpoint
|
||||||
|
DROP TABLE `users_table`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_users_table` RENAME TO `users_table`;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `users_table_username_unique` ON `users_table` (`username`);--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_volumes_table` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`short_id` text NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`type` text NOT NULL,
|
||||||
|
`status` text DEFAULT 'unmounted' NOT NULL,
|
||||||
|
`last_error` text,
|
||||||
|
`last_health_check` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
`config` text NOT NULL,
|
||||||
|
`auto_remount` integer DEFAULT true NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_volumes_table`("id", "short_id", "name", "type", "status", "last_error", "last_health_check", "created_at", "updated_at", "config", "auto_remount") SELECT "id", "short_id", "name", "type", "status", "last_error", "last_health_check", "created_at", "updated_at", "config", "auto_remount" FROM `volumes_table`;--> statement-breakpoint
|
||||||
|
DROP TABLE `volumes_table`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_volumes_table` RENAME TO `volumes_table`;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `volumes_table_short_id_unique` ON `volumes_table` (`short_id`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `volumes_table_name_unique` ON `volumes_table` (`name`);--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||||
1
app/drizzle/0019_heavy_shen.sql
Normal file
1
app/drizzle/0019_heavy_shen.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CREATE UNIQUE INDEX `backup_schedule_mirrors_table_schedule_id_repository_id_unique` ON `backup_schedule_mirrors_table` (`schedule_id`,`repository_id`);
|
||||||
740
app/drizzle/meta/0018_snapshot.json
Normal file
740
app/drizzle/meta/0018_snapshot.json
Normal file
@@ -0,0 +1,740 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "121ef03c-eb5a-4b97-b2f1-4add6adfb080",
|
||||||
|
"prevId": "d0bfd316-b8f5-459b-ab17-0ce679479321",
|
||||||
|
"tables": {
|
||||||
|
"app_metadata": {
|
||||||
|
"name": "app_metadata",
|
||||||
|
"columns": {
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch() * 1000)"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch() * 1000)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"backup_schedule_mirrors_table": {
|
||||||
|
"name": "backup_schedule_mirrors_table",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"schedule_id": {
|
||||||
|
"name": "schedule_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"repository_id": {
|
||||||
|
"name": "repository_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"name": "enabled",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"last_copy_at": {
|
||||||
|
"name": "last_copy_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"last_copy_status": {
|
||||||
|
"name": "last_copy_status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"last_copy_error": {
|
||||||
|
"name": "last_copy_error",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch() * 1000)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk": {
|
||||||
|
"name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk",
|
||||||
|
"tableFrom": "backup_schedule_mirrors_table",
|
||||||
|
"tableTo": "backup_schedules_table",
|
||||||
|
"columnsFrom": ["schedule_id"],
|
||||||
|
"columnsTo": ["id"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"backup_schedule_mirrors_table_repository_id_repositories_table_id_fk": {
|
||||||
|
"name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk",
|
||||||
|
"tableFrom": "backup_schedule_mirrors_table",
|
||||||
|
"tableTo": "repositories_table",
|
||||||
|
"columnsFrom": ["repository_id"],
|
||||||
|
"columnsTo": ["id"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"backup_schedule_notifications_table": {
|
||||||
|
"name": "backup_schedule_notifications_table",
|
||||||
|
"columns": {
|
||||||
|
"schedule_id": {
|
||||||
|
"name": "schedule_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"destination_id": {
|
||||||
|
"name": "destination_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"notify_on_start": {
|
||||||
|
"name": "notify_on_start",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"notify_on_success": {
|
||||||
|
"name": "notify_on_success",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"notify_on_failure": {
|
||||||
|
"name": "notify_on_failure",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch() * 1000)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": {
|
||||||
|
"name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk",
|
||||||
|
"tableFrom": "backup_schedule_notifications_table",
|
||||||
|
"tableTo": "backup_schedules_table",
|
||||||
|
"columnsFrom": ["schedule_id"],
|
||||||
|
"columnsTo": ["id"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": {
|
||||||
|
"name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk",
|
||||||
|
"tableFrom": "backup_schedule_notifications_table",
|
||||||
|
"tableTo": "notification_destinations_table",
|
||||||
|
"columnsFrom": ["destination_id"],
|
||||||
|
"columnsTo": ["id"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"backup_schedule_notifications_table_schedule_id_destination_id_pk": {
|
||||||
|
"columns": ["schedule_id", "destination_id"],
|
||||||
|
"name": "backup_schedule_notifications_table_schedule_id_destination_id_pk"
|
||||||
|
|||||||