diff --git a/README.md b/README.md index 498e858..f2f3652 100644 --- a/README.md +++ b/README.md @@ -265,7 +265,7 @@ docker compose up -d Your Zerobyte volumes will now be available as Docker volumes that you can mount into other containers using the `--volume` flag: ```bash -docker run -v im-nfs:/path/in/container nginx:latest +docker run -v zb-abc12:/path/in/container nginx:latest ``` Or using Docker Compose: @@ -275,13 +275,13 @@ services: myservice: image: nginx:latest volumes: - - im-nfs:/path/in/container + - zb-abc12:/path/in/container volumes: - im-nfs: + zb-abc12: external: true ``` -The volume name format is `im-` where `` is the name you assigned to the volume in Zerobyte. You can verify that the volume is available by running: +The volume name format is `zb-` where `` is the unique identifier shown on the volume's Docker tab in Zerobyte. This short ID remains stable even if you rename the volume. You can verify that the volume is available by running: ```bash docker volume ls diff --git a/app/client/api-client/@tanstack/react-query.gen.ts b/app/client/api-client/@tanstack/react-query.gen.ts index b747bfb..5beda44 100644 --- a/app/client/api-client/@tanstack/react-query.gen.ts +++ b/app/client/api-client/@tanstack/react-query.gen.ts @@ -3,8 +3,8 @@ import { type DefaultError, queryOptions, type UseMutationOptions } from '@tanstack/react-query'; 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, 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, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateVolumeData, UpdateVolumeResponse } from '../types.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 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'; /** * Register a new user @@ -442,6 +442,23 @@ export const getRepositoryOptions = (options: Options) => que queryKey: getRepositoryQueryKey(options) }); +/** + * Update a repository's name or settings + */ +export const updateRepositoryMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await updateRepository({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + export const listSnapshotsQueryKey = (options: Options) => createQueryKey("listSnapshots", options); /** diff --git a/app/client/api-client/sdk.gen.ts b/app/client/api-client/sdk.gen.ts index df14961..9f5afe4 100644 --- a/app/client/api-client/sdk.gen.ts +++ b/app/client/api-client/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; 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, 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, 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'; export type Options = Options2 & { /** @@ -276,6 +276,20 @@ export const getRepository = (options: Opt }); }; +/** + * Update a repository's name or settings + */ +export const updateRepository = (options: Options) => { + return (options.client ?? client).patch({ + url: '/api/v1/repositories/{name}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + /** * List all snapshots in a repository */ diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index fb47740..a6d86d1 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -189,6 +189,7 @@ export type ListVolumesResponses = { lastError: string | null; lastHealthCheck: number; name: string; + shortId: string; status: 'error' | 'mounted' | 'unmounted'; type: 'directory' | 'nfs' | 'smb' | 'webdav'; updatedAt: number; @@ -279,6 +280,7 @@ export type CreateVolumeResponses = { lastError: string | null; lastHealthCheck: number; name: string; + shortId: string; status: 'error' | 'mounted' | 'unmounted'; type: 'directory' | 'nfs' | 'smb' | 'webdav'; updatedAt: number; @@ -422,6 +424,7 @@ export type GetVolumeResponses = { lastError: string | null; lastHealthCheck: number; name: string; + shortId: string; status: 'error' | 'mounted' | 'unmounted'; type: 'directory' | 'nfs' | 'smb' | 'webdav'; updatedAt: number; @@ -465,6 +468,7 @@ export type UpdateVolumeData = { ssl?: boolean; username?: string; }; + name?: string; }; path: { name: string; @@ -522,6 +526,7 @@ export type UpdateVolumeResponses = { lastError: string | null; lastHealthCheck: number; name: string; + shortId: string; status: 'error' | 'mounted' | 'unmounted'; type: 'directory' | 'nfs' | 'smb' | 'webdav'; updatedAt: number; @@ -771,6 +776,7 @@ export type ListRepositoriesResponses = { 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; @@ -985,6 +991,7 @@ export type GetRepositoryResponses = { 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; @@ -993,6 +1000,110 @@ export type GetRepositoryResponses = { export type GetRepositoryResponse = GetRepositoryResponses[keyof GetRepositoryResponses]; +export type UpdateRepositoryData = { + body?: { + compressionMode?: 'auto' | 'better' | 'fastest' | 'max' | 'off'; + name?: string; + }; + path: { + name: string; + }; + query?: never; + url: '/api/v1/repositories/{name}'; +}; + +export type UpdateRepositoryErrors = { + /** + * Repository not found + */ + 404: unknown; + /** + * Repository with this name already exists + */ + 409: unknown; +}; + +export type UpdateRepositoryResponses = { + /** + * Repository updated successfully + */ + 200: { + compressionMode: 'auto' | 'better' | 'fastest' | '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; + }; +}; + +export type UpdateRepositoryResponse = UpdateRepositoryResponses[keyof UpdateRepositoryResponses]; + export type ListSnapshotsData = { body?: never; path: { @@ -1251,6 +1362,7 @@ export type ListBackupSchedulesResponses = { 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; @@ -1304,6 +1416,7 @@ export type ListBackupSchedulesResponses = { lastError: string | null; lastHealthCheck: number; name: string; + shortId: string; status: 'error' | 'mounted' | 'unmounted'; type: 'directory' | 'nfs' | 'smb' | 'webdav'; updatedAt: number; @@ -1482,6 +1595,7 @@ export type GetBackupScheduleResponses = { 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; @@ -1535,6 +1649,7 @@ export type GetBackupScheduleResponses = { lastError: string | null; lastHealthCheck: number; name: string; + shortId: string; status: 'error' | 'mounted' | 'unmounted'; type: 'directory' | 'nfs' | 'smb' | 'webdav'; updatedAt: number; @@ -1694,6 +1809,7 @@ export type GetBackupScheduleForVolumeResponses = { 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; @@ -1747,6 +1863,7 @@ export type GetBackupScheduleForVolumeResponses = { lastError: string | null; lastHealthCheck: number; name: string; + shortId: string; status: 'error' | 'mounted' | 'unmounted'; type: 'directory' | 'nfs' | 'smb' | 'webdav'; updatedAt: number; diff --git a/app/client/modules/volumes/tabs/docker.tsx b/app/client/modules/volumes/tabs/docker.tsx index b942479..c03e568 100644 --- a/app/client/modules/volumes/tabs/docker.tsx +++ b/app/client/modules/volumes/tabs/docker.tsx @@ -16,17 +16,17 @@ export const DockerTabContent = ({ volume }: Props) => { services: { nginx: { image: "nginx:latest", - volumes: [`im-${volume.name}:/path/in/container`], + volumes: [`zb-${volume.shortId}:/path/in/container`], }, }, volumes: { - [`im-${volume.name}`]: { + [`zb-${volume.shortId}`]: { external: true, }, }, }); - const dockerRunCommand = `docker run -v im-${volume.name}:/path/in/container nginx:latest`; + const dockerRunCommand = `docker run -v zb-${volume.shortId}:/path/in/container nginx:latest`; const { data: containersData, diff --git a/app/drizzle/0012_add_short_ids.sql b/app/drizzle/0012_add_short_ids.sql new file mode 100644 index 0000000..e4fc6eb --- /dev/null +++ b/app/drizzle/0012_add_short_ids.sql @@ -0,0 +1,7 @@ +ALTER TABLE `repositories_table` ADD `short_id` text;--> statement-breakpoint +UPDATE `repositories_table` SET `short_id` = lower(hex(randomblob(3))) WHERE `short_id` IS NULL;--> statement-breakpoint +CREATE UNIQUE INDEX `repositories_table_short_id_unique` ON `repositories_table` (`short_id`);--> statement-breakpoint + +ALTER TABLE `volumes_table` ADD `short_id` text;--> statement-breakpoint +UPDATE `volumes_table` SET `short_id` = lower(hex(randomblob(3))) WHERE `short_id` IS NULL;--> statement-breakpoint +CREATE UNIQUE INDEX `volumes_table_short_id_unique` ON `volumes_table` (`short_id`); diff --git a/app/drizzle/0013_elite_sprite.sql b/app/drizzle/0013_elite_sprite.sql new file mode 100644 index 0000000..723e3e2 --- /dev/null +++ b/app/drizzle/0013_elite_sprite.sql @@ -0,0 +1,6 @@ +CREATE TABLE `app_metadata` ( + `key` text PRIMARY KEY NOT NULL, + `value` text NOT NULL, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) NOT NULL +); diff --git a/app/drizzle/0014_wild_echo.sql b/app/drizzle/0014_wild_echo.sql new file mode 100644 index 0000000..62f224d --- /dev/null +++ b/app/drizzle/0014_wild_echo.sql @@ -0,0 +1,40 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_repositories_table` ( + `id` text PRIMARY KEY NOT NULL, + `short_id` text, + `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()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) 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 +PRAGMA foreign_keys=ON;--> 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_volumes_table` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `short_id` text, + `name` text NOT NULL, + `type` text NOT NULL, + `status` text DEFAULT 'unmounted' NOT NULL, + `last_error` text, + `last_health_check` integer DEFAULT (unixepoch()) NOT NULL, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) 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`); \ No newline at end of file diff --git a/app/drizzle/0015_jazzy_sersi.sql b/app/drizzle/0015_jazzy_sersi.sql new file mode 100644 index 0000000..740f34c --- /dev/null +++ b/app/drizzle/0015_jazzy_sersi.sql @@ -0,0 +1,40 @@ +PRAGMA foreign_keys=OFF;--> 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()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) 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 +PRAGMA foreign_keys=ON;--> 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_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()) NOT NULL, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) 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`); \ No newline at end of file diff --git a/app/drizzle/meta/0012_snapshot.json b/app/drizzle/meta/0012_snapshot.json new file mode 100644 index 0000000..02bff3e --- /dev/null +++ b/app/drizzle/meta/0012_snapshot.json @@ -0,0 +1,648 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "bbca8451-3894-4556-9824-c309b5105628", + "prevId": "67552135-fa49-478f-9333-107d3dbd7610", + "tables": { + "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())" + } + }, + "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" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedules_table": { + "name": "backup_schedules_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "volume_id": { + "name": "volume_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 + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retention_policy": { + "name": "retention_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exclude_patterns": { + "name": "exclude_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "include_patterns": { + "name": "include_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "last_backup_at": { + "name": "last_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_status": { + "name": "last_backup_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_error": { + "name": "last_backup_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_backup_at": { + "name": "next_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedules_table_volume_id_volumes_table_id_fk": { + "name": "backup_schedules_table_volume_id_volumes_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "volumes_table", + "columnsFrom": [ + "volume_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedules_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedules_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "repositories_table", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_destinations_table": { + "name": "notification_destinations_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "notification_destinations_table_name_unique": { + "name": "notification_destinations_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repositories_table": { + "name": "repositories_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compression_mode": { + "name": "compression_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'auto'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'unknown'" + }, + "last_checked": { + "name": "last_checked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "repositories_table_short_id_unique": { + "name": "repositories_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "repositories_table_name_unique": { + "name": "repositories_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions_table": { + "name": "sessions_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_table_user_id_users_table_id_fk": { + "name": "sessions_table_user_id_users_table_id_fk", + "tableFrom": "sessions_table", + "tableTo": "users_table", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users_table": { + "name": "users_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "has_downloaded_restic_password": { + "name": "has_downloaded_restic_password", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "users_table_username_unique": { + "name": "users_table_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "volumes_table": { + "name": "volumes_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unmounted'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_health_check": { + "name": "last_health_check", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auto_remount": { + "name": "auto_remount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "volumes_table_short_id_unique": { + "name": "volumes_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "volumes_table_name_unique": { + "name": "volumes_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/app/drizzle/meta/0013_snapshot.json b/app/drizzle/meta/0013_snapshot.json new file mode 100644 index 0000000..b67ff0a --- /dev/null +++ b/app/drizzle/meta/0013_snapshot.json @@ -0,0 +1,654 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "794bddf6-1978-46e4-88d5-051d76cfa2f6", + "prevId": "bbca8451-3894-4556-9824-c309b5105628", + "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())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "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())" + } + }, + "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" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedules_table": { + "name": "backup_schedules_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "volume_id": { + "name": "volume_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 + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retention_policy": { + "name": "retention_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exclude_patterns": { + "name": "exclude_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "include_patterns": { + "name": "include_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "last_backup_at": { + "name": "last_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_status": { + "name": "last_backup_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_error": { + "name": "last_backup_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_backup_at": { + "name": "next_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedules_table_volume_id_volumes_table_id_fk": { + "name": "backup_schedules_table_volume_id_volumes_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "volumes_table", + "columnsFrom": ["volume_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedules_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedules_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "repositories_table", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_destinations_table": { + "name": "notification_destinations_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "notification_destinations_table_name_unique": { + "name": "notification_destinations_table_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repositories_table": { + "name": "repositories_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compression_mode": { + "name": "compression_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'auto'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'unknown'" + }, + "last_checked": { + "name": "last_checked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "repositories_table_short_id_unique": { + "name": "repositories_table_short_id_unique", + "columns": ["short_id"], + "isUnique": true + }, + "repositories_table_name_unique": { + "name": "repositories_table_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions_table": { + "name": "sessions_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_table_user_id_users_table_id_fk": { + "name": "sessions_table_user_id_users_table_id_fk", + "tableFrom": "sessions_table", + "tableTo": "users_table", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users_table": { + "name": "users_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "has_downloaded_restic_password": { + "name": "has_downloaded_restic_password", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "users_table_username_unique": { + "name": "users_table_username_unique", + "columns": ["username"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "volumes_table": { + "name": "volumes_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unmounted'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_health_check": { + "name": "last_health_check", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auto_remount": { + "name": "auto_remount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "volumes_table_short_id_unique": { + "name": "volumes_table_short_id_unique", + "columns": ["short_id"], + "isUnique": true + }, + "volumes_table_name_unique": { + "name": "volumes_table_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} + diff --git a/app/drizzle/meta/0014_snapshot.json b/app/drizzle/meta/0014_snapshot.json new file mode 100644 index 0000000..89f45cd --- /dev/null +++ b/app/drizzle/meta/0014_snapshot.json @@ -0,0 +1,688 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "05309ea5-8ef2-4d63-b3d2-9842b2b4111b", + "prevId": "794bddf6-1978-46e4-88d5-051d76cfa2f6", + "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())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "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())" + } + }, + "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" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedules_table": { + "name": "backup_schedules_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "volume_id": { + "name": "volume_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 + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retention_policy": { + "name": "retention_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exclude_patterns": { + "name": "exclude_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "include_patterns": { + "name": "include_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "last_backup_at": { + "name": "last_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_status": { + "name": "last_backup_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_error": { + "name": "last_backup_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_backup_at": { + "name": "next_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedules_table_volume_id_volumes_table_id_fk": { + "name": "backup_schedules_table_volume_id_volumes_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "volumes_table", + "columnsFrom": [ + "volume_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedules_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedules_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "repositories_table", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_destinations_table": { + "name": "notification_destinations_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "notification_destinations_table_name_unique": { + "name": "notification_destinations_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repositories_table": { + "name": "repositories_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compression_mode": { + "name": "compression_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'auto'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'unknown'" + }, + "last_checked": { + "name": "last_checked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "repositories_table_short_id_unique": { + "name": "repositories_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "repositories_table_name_unique": { + "name": "repositories_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions_table": { + "name": "sessions_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_table_user_id_users_table_id_fk": { + "name": "sessions_table_user_id_users_table_id_fk", + "tableFrom": "sessions_table", + "tableTo": "users_table", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users_table": { + "name": "users_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "has_downloaded_restic_password": { + "name": "has_downloaded_restic_password", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "users_table_username_unique": { + "name": "users_table_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "volumes_table": { + "name": "volumes_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unmounted'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_health_check": { + "name": "last_health_check", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auto_remount": { + "name": "auto_remount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "volumes_table_short_id_unique": { + "name": "volumes_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "volumes_table_name_unique": { + "name": "volumes_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/app/drizzle/meta/0015_snapshot.json b/app/drizzle/meta/0015_snapshot.json new file mode 100644 index 0000000..745a2bf --- /dev/null +++ b/app/drizzle/meta/0015_snapshot.json @@ -0,0 +1,688 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "e52fe10a-3f36-4b21-abef-c15990d28363", + "prevId": "05309ea5-8ef2-4d63-b3d2-9842b2b4111b", + "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())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "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())" + } + }, + "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" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedules_table": { + "name": "backup_schedules_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "volume_id": { + "name": "volume_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 + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retention_policy": { + "name": "retention_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exclude_patterns": { + "name": "exclude_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "include_patterns": { + "name": "include_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "last_backup_at": { + "name": "last_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_status": { + "name": "last_backup_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_error": { + "name": "last_backup_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_backup_at": { + "name": "next_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedules_table_volume_id_volumes_table_id_fk": { + "name": "backup_schedules_table_volume_id_volumes_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "volumes_table", + "columnsFrom": [ + "volume_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedules_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedules_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "repositories_table", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_destinations_table": { + "name": "notification_destinations_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "notification_destinations_table_name_unique": { + "name": "notification_destinations_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repositories_table": { + "name": "repositories_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compression_mode": { + "name": "compression_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'auto'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'unknown'" + }, + "last_checked": { + "name": "last_checked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "repositories_table_short_id_unique": { + "name": "repositories_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "repositories_table_name_unique": { + "name": "repositories_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions_table": { + "name": "sessions_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_table_user_id_users_table_id_fk": { + "name": "sessions_table_user_id_users_table_id_fk", + "tableFrom": "sessions_table", + "tableTo": "users_table", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users_table": { + "name": "users_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "has_downloaded_restic_password": { + "name": "has_downloaded_restic_password", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "users_table_username_unique": { + "name": "users_table_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "volumes_table": { + "name": "volumes_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unmounted'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_health_check": { + "name": "last_health_check", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auto_remount": { + "name": "auto_remount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "volumes_table_short_id_unique": { + "name": "volumes_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "volumes_table_name_unique": { + "name": "volumes_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/app/drizzle/meta/_journal.json b/app/drizzle/meta/_journal.json index 6310637..d69d66b 100644 --- a/app/drizzle/meta/_journal.json +++ b/app/drizzle/meta/_journal.json @@ -85,6 +85,34 @@ "when": 1763644043601, "tag": "0011_familiar_stone_men", "breakpoints": true + }, + { + "idx": 12, + "version": "6", + "when": 1764100562084, + "tag": "0012_add_short_ids", + "breakpoints": true + }, + { + "idx": 13, + "version": "6", + "when": 1764182159797, + "tag": "0013_elite_sprite", + "breakpoints": true + }, + { + "idx": 14, + "version": "6", + "when": 1764182405089, + "tag": "0014_wild_echo", + "breakpoints": true + }, + { + "idx": 15, + "version": "6", + "when": 1764182465287, + "tag": "0015_jazzy_sersi", + "breakpoints": true } ] } \ No newline at end of file diff --git a/app/server/core/constants.ts b/app/server/core/constants.ts index 5b970ff..f0e5423 100644 --- a/app/server/core/constants.ts +++ b/app/server/core/constants.ts @@ -4,3 +4,5 @@ export const REPOSITORY_BASE = "/var/lib/zerobyte/repositories"; export const DATABASE_URL = "/var/lib/zerobyte/data/ironmount.db"; export const RESTIC_PASS_FILE = "/var/lib/zerobyte/data/restic.pass"; export const SOCKET_PATH = "/run/docker/plugins/zerobyte.sock"; + +export const REQUIRED_MIGRATIONS = ["v0.14.0"]; diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts index f4842d6..686c435 100644 --- a/app/server/db/schema.ts +++ b/app/server/db/schema.ts @@ -9,6 +9,7 @@ import type { NotificationType, notificationConfigSchema } from "~/schemas/notif */ export const volumesTable = sqliteTable("volumes_table", { id: int().primaryKey({ autoIncrement: true }), + shortId: text("short_id").notNull().unique(), name: text().notNull().unique(), type: text().$type().notNull(), status: text().$type().notNull().default("unmounted"), @@ -48,6 +49,7 @@ export type Session = typeof sessionsTable.$inferSelect; */ export const repositoriesTable = sqliteTable("repositories_table", { id: text().primaryKey(), + shortId: text("short_id").notNull().unique(), name: text().notNull().unique(), type: text().$type().notNull(), config: text("config", { mode: "json" }).$type().notNull(), @@ -151,3 +153,15 @@ export const backupScheduleNotificationRelations = relations(backupScheduleNotif }), })); export type BackupScheduleNotification = typeof backupScheduleNotificationsTable.$inferSelect; + +/** + * App Metadata Table + * Used for storing key-value pairs like migration checkpoints + */ +export const appMetadataTable = sqliteTable("app_metadata", { + key: text().primaryKey(), + value: text().notNull(), + createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch())`), + updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch())`), +}); +export type AppMetadata = typeof appMetadataTable.$inferSelect; diff --git a/app/server/index.ts b/app/server/index.ts index cf741e4..09e8045 100644 --- a/app/server/index.ts +++ b/app/server/index.ts @@ -10,6 +10,7 @@ import { authController } from "./modules/auth/auth.controller"; import { requireAuth } from "./modules/auth/auth.middleware"; import { driverController } from "./modules/driver/driver.controller"; import { startup } from "./modules/lifecycle/startup"; +import { migrateToShortIds } from "./modules/lifecycle/migration"; import { repositoriesController } from "./modules/repositories/repositories.controller"; import { systemController } from "./modules/system/system.controller"; import { volumeController } from "./modules/volumes/volume.controller"; @@ -19,7 +20,8 @@ import { notificationsController } from "./modules/notifications/notifications.c import { handleServiceError } from "./utils/errors"; import { logger } from "./utils/logger"; import { shutdown } from "./modules/lifecycle/shutdown"; -import { SOCKET_PATH } from "./core/constants"; +import { REQUIRED_MIGRATIONS, SOCKET_PATH } from "./core/constants"; +import { validateRequiredMigrations } from "./modules/lifecycle/checkpoint"; export const generalDescriptor = (app: Hono) => openAPIRouteHandler(app, { @@ -68,6 +70,9 @@ app.onError((err, c) => { runDbMigrations(); +await migrateToShortIds(); +await validateRequiredMigrations(REQUIRED_MIGRATIONS); + const { docker } = await getCapabilities(); if (docker) { diff --git a/app/server/modules/driver/driver.controller.ts b/app/server/modules/driver/driver.controller.ts index 6418d95..72a1feb 100644 --- a/app/server/modules/driver/driver.controller.ts +++ b/app/server/modules/driver/driver.controller.ts @@ -1,6 +1,9 @@ import { Hono } from "hono"; import { volumeService } from "../volumes/volume.service"; import { getVolumePath } from "../volumes/helpers"; +import { eq } from "drizzle-orm"; +import { db } from "../../db/db"; +import { volumesTable } from "../../db/schema"; export const driverController = new Hono() .post("/VolumeDriver.Capabilities", (c) => { @@ -30,10 +33,18 @@ export const driverController = new Hono() return c.json({ Err: "Volume name is required" }, 400); } - const volumeName = body.Name.replace(/^zb-/, ""); + const shortId = body.Name.replace(/^zb-/, ""); + + const volume = await db.query.volumesTable.findFirst({ + where: eq(volumesTable.shortId, shortId), + }); + + if (!volume) { + return c.json({ Err: `Volume with shortId ${shortId} not found` }, 404); + } return c.json({ - Mountpoint: getVolumePath(volumeName), + Mountpoint: getVolumePath(volume), }); }) .post("/VolumeDriver.Unmount", (c) => { @@ -48,7 +59,15 @@ export const driverController = new Hono() return c.json({ Err: "Volume name is required" }, 400); } - const { volume } = await volumeService.getVolume(body.Name.replace(/^zb-/, "")); + const shortId = body.Name.replace(/^zb-/, ""); + + const volume = await db.query.volumesTable.findFirst({ + where: eq(volumesTable.shortId, shortId), + }); + + if (!volume) { + return c.json({ Err: `Volume with shortId ${shortId} not found` }, 404); + } return c.json({ Mountpoint: getVolumePath(volume), @@ -61,11 +80,19 @@ export const driverController = new Hono() return c.json({ Err: "Volume name is required" }, 400); } - const { volume } = await volumeService.getVolume(body.Name.replace(/^zb-/, "")); + const shortId = body.Name.replace(/^zb-/, ""); + + const volume = await db.query.volumesTable.findFirst({ + where: eq(volumesTable.shortId, shortId), + }); + + if (!volume) { + return c.json({ Err: `Volume with shortId ${shortId} not found` }, 404); + } return c.json({ Volume: { - Name: `zb-${volume.name}`, + Name: `zb-${volume.shortId}`, Mountpoint: getVolumePath(volume), Status: {}, }, @@ -76,7 +103,7 @@ export const driverController = new Hono() const volumes = await volumeService.listVolumes(); const res = volumes.map((volume) => ({ - Name: `zb-${volume.name}`, + Name: `zb-${volume.shortId}`, Mountpoint: getVolumePath(volume), Status: {}, })); diff --git a/app/server/modules/lifecycle/checkpoint.ts b/app/server/modules/lifecycle/checkpoint.ts new file mode 100644 index 0000000..6b15ba3 --- /dev/null +++ b/app/server/modules/lifecycle/checkpoint.ts @@ -0,0 +1,89 @@ +import { eq, sql } from "drizzle-orm"; +import { db } from "../../db/db"; +import { appMetadataTable, usersTable } from "../../db/schema"; +import { logger } from "../../utils/logger"; +import { REQUIRED_MIGRATIONS } from "~/server/core/constants"; + +const MIGRATION_KEY_PREFIX = "migration:"; + +export const recordMigrationCheckpoint = async (version: string): Promise => { + const key = `${MIGRATION_KEY_PREFIX}${version}`; + const now = Math.floor(Date.now() / 1000); + + await db + .insert(appMetadataTable) + .values({ + key, + value: JSON.stringify({ completedAt: new Date().toISOString() }), + createdAt: now, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: appMetadataTable.key, + set: { + value: JSON.stringify({ completedAt: new Date().toISOString() }), + updatedAt: now, + }, + }); + + logger.info(`Recorded migration checkpoint for ${version}`); +}; + +export const hasMigrationCheckpoint = async (version: string): Promise => { + const key = `${MIGRATION_KEY_PREFIX}${version}`; + const result = await db.query.appMetadataTable.findFirst({ + where: eq(appMetadataTable.key, key), + }); + return result !== undefined; +}; + +export const validateRequiredMigrations = async (requiredVersions: string[]): Promise => { + const userCount = await db.select({ count: sql`count(*)` }).from(usersTable); + const isFreshInstall = userCount[0]?.count === 0; + + if (isFreshInstall) { + logger.info("Fresh installation detected, skipping migration checkpoint validation."); + + for (const version of requiredVersions) { + const hasCheckpoint = await hasMigrationCheckpoint(version); + if (!hasCheckpoint) { + await recordMigrationCheckpoint(version); + } + } + + return; + } + + for (const version of requiredVersions) { + const hasCheckpoint = await hasMigrationCheckpoint(version); + if (!hasCheckpoint) { + logger.error(` +================================================================================ +MIGRATION ERROR: Required migration ${version} has not been run. + +You are attempting to start a version of Zerobyte that requires migration +checkpoints from previous versions. This typically happens when you skip +versions during an upgrade. + +To fix this: +1. First upgrade to version ${version} and run the application once +2. Validate that everything is still working correctly +3. Then upgrade to the current version + +================================================================================ +`); + process.exit(1); + } + } +}; + +export const getMigrationCheckpoints = async (): Promise<{ version: string; completedAt: string }[]> => { + const results = await db.query.appMetadataTable.findMany({ + where: (table, { like }) => like(table.key, `${MIGRATION_KEY_PREFIX}%`), + }); + + return results.map((r) => ({ + version: r.key.replace(MIGRATION_KEY_PREFIX, ""), + completedAt: JSON.parse(r.value).completedAt, + })); +}; diff --git a/app/server/modules/lifecycle/migration.ts b/app/server/modules/lifecycle/migration.ts new file mode 100644 index 0000000..5f1cabf --- /dev/null +++ b/app/server/modules/lifecycle/migration.ts @@ -0,0 +1,193 @@ +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { eq } from "drizzle-orm"; +import { db } from "../../db/db"; +import { repositoriesTable } from "../../db/schema"; +import { VOLUME_MOUNT_BASE, REPOSITORY_BASE } from "../../core/constants"; +import { logger } from "../../utils/logger"; +import { hasMigrationCheckpoint, recordMigrationCheckpoint } from "./checkpoint"; +import type { RepositoryConfig } from "~/schemas/restic"; + +const MIGRATION_VERSION = "v0.14.0"; + +interface MigrationResult { + success: boolean; + errors: Array<{ name: string; error: string }>; +} + +export class MigrationError extends Error { + version: string; + failedItems: Array<{ name: string; error: string }>; + + constructor(version: string, failedItems: Array<{ name: string; error: string }>) { + const itemNames = failedItems.map((e) => e.name).join(", "); + super(`Migration ${version} failed for: ${itemNames}`); + this.version = version; + this.failedItems = failedItems; + this.name = "MigrationError"; + } +} + +export const migrateToShortIds = async () => { + const alreadyMigrated = await hasMigrationCheckpoint(MIGRATION_VERSION); + if (alreadyMigrated) { + logger.debug(`Migration ${MIGRATION_VERSION} already completed, skipping.`); + return; + } + + logger.info(`Starting short ID migration (${MIGRATION_VERSION})...`); + + const volumeResult = await migrateVolumeFolders(); + const repoResult = await migrateRepositoryFolders(); + + const allErrors = [...volumeResult.errors, ...repoResult.errors]; + + if (allErrors.length > 0) { + for (const err of allErrors) { + logger.error(`Migration failure - ${err.name}: ${err.error}`); + } + throw new MigrationError(MIGRATION_VERSION, allErrors); + } + + await recordMigrationCheckpoint(MIGRATION_VERSION); + + logger.info(`Short ID migration (${MIGRATION_VERSION}) complete.`); +}; + +const migrateVolumeFolders = async (): Promise => { + const errors: Array<{ name: string; error: string }> = []; + const volumes = await db.query.volumesTable.findMany({}); + + for (const volume of volumes) { + if (volume.config.backend === "directory") { + continue; + } + + const oldPath = path.join(VOLUME_MOUNT_BASE, volume.name); + const newPath = path.join(VOLUME_MOUNT_BASE, volume.shortId); + + const oldExists = await pathExists(oldPath); + const newExists = await pathExists(newPath); + + if (oldExists && !newExists) { + try { + logger.info(`Migrating volume folder: ${oldPath} -> ${newPath}`); + await fs.rename(oldPath, newPath); + logger.info(`Successfully migrated volume folder for "${volume.name}"`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + errors.push({ name: `volume:${volume.name}`, error: errorMessage }); + } + } else if (oldExists && newExists) { + logger.warn( + `Both old (${oldPath}) and new (${newPath}) paths exist for volume "${volume.name}". Manual intervention may be required.`, + ); + } + } + + return { success: errors.length === 0, errors }; +}; + +const migrateRepositoryFolders = async (): Promise => { + const errors: Array<{ name: string; error: string }> = []; + const repositories = await db.query.repositoriesTable.findMany({}); + + for (const repo of repositories) { + if (repo.config.backend !== "local") { + continue; + } + + const config = repo.config as Extract; + + if (config.name === repo.shortId) { + continue; + } + + const basePath = config.path || REPOSITORY_BASE; + const oldPath = path.join(basePath, config.name); + const newPath = path.join(basePath, repo.shortId); + + const oldExists = await pathExists(oldPath); + const newExists = await pathExists(newPath); + + if (oldExists && !newExists) { + try { + logger.info(`Migrating repository folder: ${oldPath} -> ${newPath}`); + await fs.rename(oldPath, newPath); + + const updatedConfig: RepositoryConfig = { + ...config, + name: repo.shortId, + }; + + await db + .update(repositoriesTable) + .set({ + config: updatedConfig, + updatedAt: Math.floor(Date.now() / 1000), + }) + .where(eq(repositoriesTable.id, repo.id)); + + logger.info(`Successfully migrated repository folder and config for "${repo.name}"`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + errors.push({ name: `repository:${repo.name}`, error: errorMessage }); + } + } else if (oldExists && newExists) { + logger.warn( + `Both old (${oldPath}) and new (${newPath}) paths exist for repository "${repo.name}". Manual intervention may be required.`, + ); + } else if (!oldExists && !newExists) { + try { + logger.info(`Updating config.name for repository "${repo.name}" (no folder exists yet)`); + + const updatedConfig: RepositoryConfig = { + ...config, + name: repo.shortId, + }; + + await db + .update(repositoriesTable) + .set({ + config: updatedConfig, + updatedAt: Math.floor(Date.now() / 1000), + }) + .where(eq(repositoriesTable.id, repo.id)); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + errors.push({ name: `repository:${repo.name}`, error: errorMessage }); + } + } else if (newExists && !oldExists && config.name !== repo.shortId) { + try { + logger.info(`Folder already at new path, updating config.name for repository "${repo.name}"`); + + const updatedConfig: RepositoryConfig = { + ...config, + name: repo.shortId, + }; + + await db + .update(repositoriesTable) + .set({ + config: updatedConfig, + updatedAt: Math.floor(Date.now() / 1000), + }) + .where(eq(repositoriesTable.id, repo.id)); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + errors.push({ name: `repository:${repo.name}`, error: errorMessage }); + } + } + } + + return { success: errors.length === 0, errors }; +}; + +const pathExists = async (p: string): Promise => { + try { + await fs.access(p); + return true; + } catch { + return false; + } +}; diff --git a/app/server/modules/notifications/builders/pushover.ts b/app/server/modules/notifications/builders/pushover.ts index 5f735a0..75c0492 100644 --- a/app/server/modules/notifications/builders/pushover.ts +++ b/app/server/modules/notifications/builders/pushover.ts @@ -1,8 +1,6 @@ import type { NotificationConfig } from "~/schemas/notifications"; -export function buildPushoverShoutrrrUrl( - config: Extract, -): string { +export function buildPushoverShoutrrrUrl(config: Extract): string { const params = new URLSearchParams(); if (config.devices) { diff --git a/app/server/modules/notifications/notifications.service.ts b/app/server/modules/notifications/notifications.service.ts index a3a83c5..13932fa 100644 --- a/app/server/modules/notifications/notifications.service.ts +++ b/app/server/modules/notifications/notifications.service.ts @@ -1,4 +1,4 @@ -import { eq, and } from "drizzle-orm"; +import { eq, and, ne } from "drizzle-orm"; import { ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced"; import slugify from "slugify"; import { db } from "../../db/db"; @@ -164,10 +164,10 @@ const updateDestination = async ( const slug = slugify(updates.name, { lower: true, strict: true }); const conflict = await db.query.notificationDestinationsTable.findFirst({ - where: and(eq(notificationDestinationsTable.name, slug), eq(notificationDestinationsTable.id, id)), + where: and(eq(notificationDestinationsTable.name, slug), ne(notificationDestinationsTable.id, id)), }); - if (conflict && conflict.id !== id) { + if (conflict) { throw new ConflictError("Notification destination with this name already exists"); } updateData.name = slug; diff --git a/app/server/modules/repositories/repositories.controller.ts b/app/server/modules/repositories/repositories.controller.ts index e4ed4fc..c629dec 100644 --- a/app/server/modules/repositories/repositories.controller.ts +++ b/app/server/modules/repositories/repositories.controller.ts @@ -16,6 +16,8 @@ import { listSnapshotsFilters, restoreSnapshotBody, restoreSnapshotDto, + updateRepositoryBody, + updateRepositoryDto, type DeleteRepositoryDto, type DeleteSnapshotDto, type DoctorRepositoryDto, @@ -25,6 +27,7 @@ import { type ListSnapshotFilesDto, type ListSnapshotsDto, type RestoreSnapshotDto, + type UpdateRepositoryDto, } from "./repositories.dto"; import { repositoriesService } from "./repositories.service"; import { getRcloneRemoteInfo, listRcloneRemotes } from "../../utils/rclone"; @@ -152,4 +155,12 @@ export const repositoriesController = new Hono() await repositoriesService.deleteSnapshot(name, snapshotId); return c.json({ message: "Snapshot deleted" }, 200); + }) + .patch("/:name", updateRepositoryDto, validator("json", updateRepositoryBody), async (c) => { + const { name } = c.req.param(); + const body = c.req.valid("json"); + + const res = await repositoriesService.updateRepository(name, body); + + return c.json(res.repository, 200); }); diff --git a/app/server/modules/repositories/repositories.dto.ts b/app/server/modules/repositories/repositories.dto.ts index 8d85808..e54af33 100644 --- a/app/server/modules/repositories/repositories.dto.ts +++ b/app/server/modules/repositories/repositories.dto.ts @@ -4,6 +4,7 @@ import { COMPRESSION_MODES, REPOSITORY_BACKENDS, REPOSITORY_STATUS, repositoryCo export const repositorySchema = type({ id: "string", + shortId: "string", name: "string", type: type.valueOf(REPOSITORY_BACKENDS), config: repositoryConfigSchema, @@ -123,6 +124,41 @@ export const deleteRepositoryDto = describeRoute({ }, }); +/** + * Update a repository + */ +export const updateRepositoryBody = type({ + name: "string?", + compressionMode: type.valueOf(COMPRESSION_MODES).optional(), +}); + +export type UpdateRepositoryBody = typeof updateRepositoryBody.infer; + +export const updateRepositoryResponse = repositorySchema; +export type UpdateRepositoryDto = typeof updateRepositoryResponse.infer; + +export const updateRepositoryDto = describeRoute({ + description: "Update a repository's name or settings", + tags: ["Repositories"], + operationId: "updateRepository", + responses: { + 200: { + description: "Repository updated successfully", + content: { + "application/json": { + schema: resolver(updateRepositoryResponse), + }, + }, + }, + 404: { + description: "Repository not found", + }, + 409: { + description: "Repository with this name already exists", + }, + }, +}); + /** * List snapshots in a repository */ diff --git a/app/server/modules/repositories/repositories.service.ts b/app/server/modules/repositories/repositories.service.ts index 4c711d5..3ce1be2 100644 --- a/app/server/modules/repositories/repositories.service.ts +++ b/app/server/modules/repositories/repositories.service.ts @@ -1,10 +1,11 @@ import crypto from "node:crypto"; -import { eq } from "drizzle-orm"; +import { and, eq, ne } from "drizzle-orm"; import { ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced"; import slugify from "slugify"; import { db } from "../../db/db"; import { repositoriesTable } from "../../db/schema"; import { toMessage } from "../../utils/errors"; +import { generateShortId } from "../../utils/id"; import { restic } from "../../utils/restic"; import { cryptoUtils } from "../../utils/crypto"; import type { CompressionMode, RepositoryConfig } from "~/schemas/restic"; @@ -61,13 +62,20 @@ const createRepository = async (name: string, config: RepositoryConfig, compress } const id = crypto.randomUUID(); + const shortId = generateShortId(); - const encryptedConfig = await encryptConfig(config); + let processedConfig = config; + if (config.backend === "local") { + processedConfig = { ...config, name: shortId }; + } + + const encryptedConfig = await encryptConfig(processedConfig); const [created] = await db .insert(repositoriesTable) .values({ id, + shortId, name: slug, type: config.backend, config: encryptedConfig, @@ -350,11 +358,53 @@ const deleteSnapshot = async (name: string, snapshotId: string) => { await restic.deleteSnapshot(repository.config, snapshotId); }; +const updateRepository = async (name: string, updates: { name?: string; compressionMode?: CompressionMode }) => { + const existing = await db.query.repositoriesTable.findFirst({ + where: eq(repositoriesTable.name, name), + }); + + if (!existing) { + throw new NotFoundError("Repository not found"); + } + + let newName = existing.name; + if (updates.name !== undefined && updates.name !== existing.name) { + const newSlug = slugify(updates.name, { lower: true, strict: true }); + + const conflict = await db.query.repositoriesTable.findFirst({ + where: and(eq(repositoriesTable.name, newSlug), ne(repositoriesTable.id, existing.id)), + }); + + if (conflict) { + throw new ConflictError("A repository with this name already exists"); + } + + newName = newSlug; + } + + const [updated] = await db + .update(repositoriesTable) + .set({ + name: newName, + compressionMode: updates.compressionMode ?? existing.compressionMode, + updatedAt: Math.floor(Date.now() / 1000), + }) + .where(eq(repositoriesTable.id, existing.id)) + .returning(); + + if (!updated) { + throw new InternalServerError("Failed to update repository"); + } + + return { repository: updated }; +}; + export const repositoriesService = { listRepositories, createRepository, getRepository, deleteRepository, + updateRepository, listSnapshots, listSnapshotFiles, restoreSnapshot, diff --git a/app/server/modules/volumes/helpers.ts b/app/server/modules/volumes/helpers.ts index bb27265..a9f8207 100644 --- a/app/server/modules/volumes/helpers.ts +++ b/app/server/modules/volumes/helpers.ts @@ -6,5 +6,5 @@ export const getVolumePath = (volume: Volume) => { return volume.config.path; } - return `${VOLUME_MOUNT_BASE}/${volume.name}/_data`; + return `${VOLUME_MOUNT_BASE}/${volume.shortId}/_data`; }; diff --git a/app/server/modules/volumes/volume.dto.ts b/app/server/modules/volumes/volume.dto.ts index 7f2ea40..03d6198 100644 --- a/app/server/modules/volumes/volume.dto.ts +++ b/app/server/modules/volumes/volume.dto.ts @@ -4,6 +4,7 @@ import { BACKEND_STATUS, BACKEND_TYPES, volumeConfigSchema } from "~/schemas/vol export const volumeSchema = type({ id: "number", + shortId: "string", name: "string", type: type.valueOf(BACKEND_TYPES), status: type.valueOf(BACKEND_STATUS), @@ -128,6 +129,7 @@ export const getVolumeDto = describeRoute({ * Update a volume */ export const updateVolumeBody = type({ + name: "string?", autoRemount: "boolean?", config: volumeConfigSchema.optional(), }); diff --git a/app/server/modules/volumes/volume.service.ts b/app/server/modules/volumes/volume.service.ts index 269b40f..7169d6f 100644 --- a/app/server/modules/volumes/volume.service.ts +++ b/app/server/modules/volumes/volume.service.ts @@ -2,13 +2,14 @@ import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; import Docker from "dockerode"; -import { eq } from "drizzle-orm"; +import { and, eq, ne } from "drizzle-orm"; import { ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced"; import slugify from "slugify"; import { getCapabilities } from "../../core/capabilities"; import { db } from "../../db/db"; import { volumesTable } from "../../db/schema"; import { toMessage } from "../../utils/errors"; +import { generateShortId } from "../../utils/id"; import { getStatFs, type StatFs } from "../../utils/mountinfo"; import { withTimeout } from "../../utils/timeout"; import { createVolumeBackend } from "../backends/backend"; @@ -35,9 +36,12 @@ const createVolume = async (name: string, backendConfig: BackendConfig) => { throw new ConflictError("Volume already exists"); } + const shortId = generateShortId(); + const [created] = await db .insert(volumesTable) .values({ + shortId, name: slug, config: backendConfig, type: backendConfig.backend, @@ -147,6 +151,21 @@ const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => { throw new NotFoundError("Volume not found"); } + let newName = existing.name; + if (volumeData.name !== undefined && volumeData.name !== existing.name) { + const newSlug = slugify(volumeData.name, { lower: true, strict: true }); + + const conflict = await db.query.volumesTable.findFirst({ + where: and(eq(volumesTable.name, newSlug), ne(volumesTable.id, existing.id)), + }); + + if (conflict) { + throw new ConflictError("A volume with this name already exists"); + } + + newName = newSlug; + } + const configChanged = JSON.stringify(existing.config) !== JSON.stringify(volumeData.config) && volumeData.config !== undefined; @@ -159,12 +178,13 @@ const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => { const [updated] = await db .update(volumesTable) .set({ + name: newName, config: volumeData.config, type: volumeData.config?.backend, autoRemount: volumeData.autoRemount, updatedAt: Date.now(), }) - .where(eq(volumesTable.name, name)) + .where(eq(volumesTable.id, existing.id)) .returning(); if (!updated) { @@ -177,9 +197,9 @@ const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => { await db .update(volumesTable) .set({ status, lastError: error ?? null, lastHealthCheck: Date.now() }) - .where(eq(volumesTable.name, name)); + .where(eq(volumesTable.id, existing.id)); - serverEvents.emit("volume:updated", { volumeName: name }); + serverEvents.emit("volume:updated", { volumeName: updated.name }); } return { volume: updated }; @@ -190,6 +210,7 @@ const testConnection = async (backendConfig: BackendConfig) => { const mockVolume = { id: 0, + shortId: "test", name: "test-connection", path: tempDir, config: backendConfig, @@ -264,7 +285,7 @@ const getContainersUsingVolume = async (name: string) => { const container = docker.getContainer(info.Id); const inspect = await container.inspect(); const mounts = inspect.Mounts || []; - const usesVolume = mounts.some((mount) => mount.Type === "volume" && mount.Name === `im-${volume.name}`); + const usesVolume = mounts.some((mount) => mount.Type === "volume" && mount.Name === `zb-${volume.shortId}`); if (usesVolume) { usingContainers.push({ id: inspect.Id, diff --git a/app/server/utils/id.ts b/app/server/utils/id.ts new file mode 100644 index 0000000..4ecfb66 --- /dev/null +++ b/app/server/utils/id.ts @@ -0,0 +1,6 @@ +import crypto from "node:crypto"; + +export const generateShortId = (length = 5): string => { + const bytesNeeded = Math.ceil((length * 3) / 4); + return crypto.randomBytes(bytesNeeded).toString("base64url").slice(0, length); +};