From 55dbcf0eb369b89dfb78eeb8660edc0d8d97ac3c Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:27:03 +0100 Subject: [PATCH] refactor: move from arktype to zod (#637) chore: gen api-client --- .oxlintrc.json | 1 + .../api-client/@tanstack/react-query.gen.ts | 4 +- app/client/api-client/client/client.gen.ts | 8 +- app/client/api-client/client/types.gen.ts | 2 +- .../api-client/core/bodySerializer.gen.ts | 14 +- app/client/api-client/types.gen.ts | 5063 ++++++++--------- .../file-browsers/snapshot-tree-browser.tsx | 12 +- .../file-browsers/volume-file-browser.tsx | 2 +- app/client/modules/auth/routes/login.tsx | 17 +- app/client/modules/auth/routes/onboarding.tsx | 21 +- .../components/create-schedule-form/index.tsx | 6 +- .../retention-section.tsx | 12 +- .../components/create-schedule-form/types.ts | 47 +- .../components/create-notification-form.tsx | 38 +- .../components/create-repository-form.tsx | 41 +- .../repositories/routes/create-repository.tsx | 9 +- .../repositories/routes/edit-repository.tsx | 15 +- .../components/create-user-dialog.tsx | 20 +- .../sso/routes/create-sso-provider.tsx | 24 +- .../volumes/components/create-volume-form.tsx | 48 +- .../modules/volumes/routes/create-volume.tsx | 8 +- app/client/modules/volumes/tabs/info.tsx | 6 +- app/routes/(auth)/login.error.tsx | 4 +- app/routes/(auth)/login.tsx | 4 +- app/routes/(dashboard)/admin/index.tsx | 4 +- .../(dashboard)/backups/$backupId/index.tsx | 4 +- .../repositories/$repositoryId/index.tsx | 4 +- app/routes/(dashboard)/settings/index.tsx | 6 +- app/routes/(dashboard)/volumes/$volumeId.tsx | 4 +- app/schemas/events-dto.ts | 113 +- app/schemas/notifications.ts | 144 +- app/schemas/restic-dto.ts | 135 +- app/schemas/restic.ts | 194 +- app/schemas/volumes.ts | 130 +- app/server/core/config.ts | 77 +- app/server/db/schema.ts | 12 +- app/server/modules/auth/auth.dto.ts | 94 +- app/server/modules/backends/backend.ts | 3 + app/server/modules/backups/backups.dto.ts | 253 +- .../migrations/00004-concat-path-name.ts | 8 +- .../notifications/notifications.dto.ts | 120 +- .../notifications/notifications.service.ts | 6 +- .../modules/repositories/helpers/doctor.ts | 15 +- .../repositories/repositories.controller.ts | 4 +- .../modules/repositories/repositories.dto.ts | 325 +- .../repositories/repositories.service.ts | 13 +- app/server/modules/sso/sso.dto.ts | 58 +- app/server/modules/system/system.dto.ts | 52 +- .../modules/volumes/volume.controller.ts | 4 +- app/server/modules/volumes/volume.dto.ts | 167 +- app/server/modules/volumes/volume.service.ts | 6 +- app/server/utils/restic/commands/backup.ts | 17 +- app/server/utils/restic/commands/ls.ts | 84 +- app/server/utils/restic/commands/restore.ts | 38 +- app/server/utils/restic/commands/snapshots.ts | 36 +- app/server/utils/restic/commands/stats.ts | 11 +- bun.lock | 2 +- package.json | 4 +- 58 files changed, 3776 insertions(+), 3797 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index f2e5a583..0bb6f8ba 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -50,6 +50,7 @@ "no-unused-vars": [ "warn", { + "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_", "argsIgnorePattern": "^_" } diff --git a/app/client/api-client/@tanstack/react-query.gen.ts b/app/client/api-client/@tanstack/react-query.gen.ts index db975ee0..d6510ffe 100644 --- a/app/client/api-client/@tanstack/react-query.gen.ts +++ b/app/client/api-client/@tanstack/react-query.gen.ts @@ -457,7 +457,7 @@ export const listFilesInfiniteQueryKey = (options: Options): Quer /** * List files in a volume directory */ -export const listFilesInfiniteOptions = (options: Options) => infiniteQueryOptions, QueryKey>, string | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>( +export const listFilesInfiniteOptions = (options: Options) => infiniteQueryOptions, QueryKey>, number | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>( // @ts-ignore { queryFn: async ({ pageParam, queryKey, signal }) => { @@ -747,7 +747,7 @@ export const listSnapshotFilesInfiniteQueryKey = (options: Options) => infiniteQueryOptions, QueryKey>, string | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>( +export const listSnapshotFilesInfiniteOptions = (options: Options) => infiniteQueryOptions, QueryKey>, number | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>( // @ts-ignore { queryFn: async ({ pageParam, queryKey, signal }) => { diff --git a/app/client/api-client/client/client.gen.ts b/app/client/api-client/client/client.gen.ts index d9a078b1..7bddcbe1 100644 --- a/app/client/api-client/client/client.gen.ts +++ b/app/client/api-client/client/client.gen.ts @@ -38,7 +38,7 @@ export const createClient = (config: Config = {}): Client => { ...options, fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, headers: mergeHeaders(_config.headers, options.headers), - serializedBody: undefined, + serializedBody: undefined as string | undefined, }; if (opts.security) { @@ -53,7 +53,7 @@ export const createClient = (config: Config = {}): Client => { } if (opts.body !== undefined && opts.bodySerializer) { - opts.serializedBody = opts.bodySerializer(opts.body); + opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; } // remove Content-Type header if body is empty to avoid sending invalid requests @@ -259,8 +259,10 @@ export const createClient = (config: Config = {}): Client => { }); }; + const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); + return { - buildUrl, + buildUrl: _buildUrl, connect: makeMethodFn('CONNECT'), delete: makeMethodFn('DELETE'), get: makeMethodFn('GET'), diff --git a/app/client/api-client/client/types.gen.ts b/app/client/api-client/client/types.gen.ts index 701c3630..92f86372 100644 --- a/app/client/api-client/client/types.gen.ts +++ b/app/client/api-client/client/types.gen.ts @@ -191,7 +191,7 @@ export type Client = CoreClient */ export type CreateClientConfig = ( override?: Config, -) => Config & T> | Promise & T>>; +) => Config & T>; export interface TDataShape { body?: unknown; diff --git a/app/client/api-client/core/bodySerializer.gen.ts b/app/client/api-client/core/bodySerializer.gen.ts index bbfd8727..2c7177c5 100644 --- a/app/client/api-client/core/bodySerializer.gen.ts +++ b/app/client/api-client/core/bodySerializer.gen.ts @@ -5,7 +5,7 @@ import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerialize export type QuerySerializer = (query: Record) => string; -export type BodySerializer = (body: any) => any; +export type BodySerializer = (body: unknown) => unknown; type QuerySerializerOptionsObject = { allowReserved?: boolean; @@ -40,12 +40,10 @@ const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: }; export const formDataBodySerializer = { - bodySerializer: | Array>>( - body: T, - ): FormData => { + bodySerializer: (body: unknown): FormData => { const data = new FormData(); - Object.entries(body).forEach(([key, value]) => { + Object.entries(body as Record).forEach(([key, value]) => { if (value === undefined || value === null) { return; } @@ -61,15 +59,15 @@ export const formDataBodySerializer = { }; export const jsonBodySerializer = { - bodySerializer: (body: T): string => + bodySerializer: (body: unknown): string => JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), }; export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>(body: T): string => { + bodySerializer: (body: unknown): string => { const data = new URLSearchParams(); - Object.entries(body).forEach(([key, value]) => { + Object.entries(body as Record).forEach(([key, value]) => { if (value === undefined || value === null) { return; } diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index d319c8f7..1198641c 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -35,18 +35,18 @@ export type GetAdminUsersResponses = { * List of users with roles and status */ 200: { - total: number; users: Array<{ + id: string; + name: string | null; + email: string; + role: string; + banned: boolean; accounts: Array<{ id: string; providerId: string; }>; - banned: boolean; - email: string; - id: string; - name: string | null; - role: string; }>; + total: number; }; }; @@ -98,9 +98,9 @@ export type GetUserDeletionImpactResponses = { id: string; name: string; resources: { - backupSchedulesCount: number; - repositoriesCount: number; volumesCount: number; + repositoriesCount: number; + backupSchedulesCount: number; }; }>; }; @@ -121,14 +121,14 @@ export type GetOrgMembersResponses = { */ 200: { members: Array<{ - createdAt: string; id: string; - role: string; - user: { - email: string; - name: string | null; - }; userId: string; + role: string; + createdAt: string; + user: { + name: string | null; + email: string; + }; }>; }; }; @@ -137,7 +137,7 @@ export type GetOrgMembersResponse = GetOrgMembersResponses[keyof GetOrgMembersRe export type UpdateMemberRoleData = { body: { - role: 'admin' | 'member'; + role: 'member' | 'admin'; }; path: { memberId: string; @@ -335,64 +335,64 @@ export type ListVolumesResponses = { * A list of volumes */ 200: Array<{ - autoRemount: boolean; + id: number; + shortId: string; + name: string; + type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp'; + status: 'mounted' | 'unmounted' | 'error'; + lastError: string | null; + createdAt: number; + updatedAt: number; + lastHealthCheck: number; config: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; - shortId: string; - status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; - updatedAt: number; + autoRemount: boolean; }>; }; @@ -400,55 +400,55 @@ export type ListVolumesResponse = ListVolumesResponses[keyof ListVolumesResponse export type CreateVolumeData = { body: { + name: string; config: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; - name: string; }; path?: never; query?: never; @@ -460,64 +460,64 @@ export type CreateVolumeResponses = { * Volume created successfully */ 201: { - autoRemount: boolean; + id: number; + shortId: string; + name: string; + type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp'; + status: 'mounted' | 'unmounted' | 'error'; + lastError: string | null; + createdAt: number; + updatedAt: number; + lastHealthCheck: number; config: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; - shortId: string; - status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; - updatedAt: number; + autoRemount: boolean; }; }; @@ -526,52 +526,52 @@ export type CreateVolumeResponse = CreateVolumeResponses[keyof CreateVolumeRespo export type TestConnectionData = { body: { config: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; }; path?: never; @@ -584,8 +584,8 @@ export type TestConnectionResponses = { * Connection test result */ 200: { - message: string; success: boolean; + message: string; }; }; @@ -632,70 +632,70 @@ export type GetVolumeResponses = { * Volume details */ 200: { - statfs: { - free: number; - total: number; - used: number; - }; volume: { - autoRemount: boolean; + id: number; + shortId: string; + name: string; + type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp'; + status: 'mounted' | 'unmounted' | 'error'; + lastError: string | null; + createdAt: number; + updatedAt: number; + lastHealthCheck: number; config: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; - shortId: string; - status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; - updatedAt: number; + autoRemount: boolean; + }; + statfs: { + total: number; + used: number; + free: number; }; }; }; @@ -704,56 +704,56 @@ export type GetVolumeResponse = GetVolumeResponses[keyof GetVolumeResponses]; export type UpdateVolumeData = { body: { + name?: string; autoRemount?: boolean; config?: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; - name?: string; }; path: { shortId: string; @@ -774,64 +774,64 @@ export type UpdateVolumeResponses = { * Volume updated successfully */ 200: { - autoRemount: boolean; + id: number; + shortId: string; + name: string; + type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp'; + status: 'mounted' | 'unmounted' | 'error'; + lastError: string | null; + createdAt: number; + updatedAt: number; + lastHealthCheck: number; config: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; - shortId: string; - status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; - updatedAt: number; + autoRemount: boolean; }; }; @@ -851,8 +851,8 @@ export type MountVolumeResponses = { * Volume mounted successfully */ 200: { - status: 'error' | 'mounted' | 'unmounted'; error?: string; + status: 'mounted' | 'unmounted' | 'error'; }; }; @@ -872,8 +872,8 @@ export type UnmountVolumeResponses = { * Volume unmounted successfully */ 200: { - status: 'error' | 'mounted' | 'unmounted'; error?: string; + status: 'mounted' | 'unmounted' | 'error'; }; }; @@ -900,8 +900,8 @@ export type HealthCheckVolumeResponses = { * Volume health check result */ 200: { - status: 'error' | 'mounted' | 'unmounted'; error?: string; + status: 'mounted' | 'unmounted' | 'error'; }; }; @@ -913,9 +913,9 @@ export type ListFilesData = { shortId: string; }; query?: { - limit?: string; - offset?: string; path?: string; + offset?: number; + limit?: number; }; url: '/api/v1/volumes/{shortId}/files'; }; @@ -928,15 +928,15 @@ export type ListFilesResponses = { files: Array<{ name: string; path: string; - type: 'directory' | 'file'; - modifiedAt?: number; + type: 'file' | 'directory'; size?: number; + modifiedAt?: number; }>; - hasMore: boolean; - limit: number; - offset: number; path: string; + offset: number; + limit: number; total: number; + hasMore: boolean; }; }; @@ -962,9 +962,9 @@ export type BrowseFilesystemResponses = { directories: Array<{ name: string; path: string; - type: 'directory' | 'file'; - modifiedAt?: number; + type: 'file' | 'directory'; size?: number; + modifiedAt?: number; }>; path: string; }; @@ -984,183 +984,183 @@ export type ListRepositoriesResponses = { * List of repositories */ 200: Array<{ - compressionMode: 'auto' | 'max' | 'off' | null; + id: string; + shortId: string; + name: string; + type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp'; config: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accessKeyId: string; backend: 's3'; - bucket: string; endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accountKey: string; - accountName: string; - backend: 'azure'; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'gcs'; bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + accessKeyId: string; + secretAccessKey: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'local'; path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rclone'; - path: string; remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + path: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rest'; url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; + username?: string; password?: string; path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; - username?: string; } | { backend: 'sftp'; host: string; + port?: string | number; + user: string; path: string; privateKey: string; - user: string; - port?: number; skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; + compressionMode: 'off' | 'auto' | 'max' | null; + status: 'healthy' | 'error' | 'unknown' | 'doctor' | 'cancelled' | null; lastChecked: number | null; lastError: string | null; - name: string; - shortId: string; - status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + doctorResult: { + success: boolean; + steps: Array<{ + step: string; + success: boolean; + output: string | null; + error: string | null; + }>; + completedAt: number; + } | null; + createdAt: number; updatedAt: number; }>; }; @@ -1169,166 +1169,166 @@ export type ListRepositoriesResponse = ListRepositoriesResponses[keyof ListRepos export type CreateRepositoryData = { body: { + name: string; + compressionMode?: 'off' | 'auto' | 'max'; config: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accessKeyId: string; backend: 's3'; - bucket: string; endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accountKey: string; - accountName: string; - backend: 'azure'; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'gcs'; bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + accessKeyId: string; + secretAccessKey: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'local'; path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rclone'; - path: string; remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + path: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rest'; url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; + username?: string; password?: string; path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; - username?: string; } | { backend: 'sftp'; host: string; + port?: string | number; + user: string; path: string; privateKey: string; - user: string; - port?: number; skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; }; - name: string; - compressionMode?: 'auto' | 'max' | 'off'; }; path?: never; query?: never; @@ -1343,8 +1343,8 @@ export type CreateRepositoryResponses = { message: string; repository: { id: string; - name: string; shortId: string; + name: string; }; }; }; @@ -1404,183 +1404,183 @@ export type GetRepositoryResponses = { * Repository details */ 200: { - compressionMode: 'auto' | 'max' | 'off' | null; + id: string; + shortId: string; + name: string; + type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp'; config: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accessKeyId: string; backend: 's3'; - bucket: string; endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accountKey: string; - accountName: string; - backend: 'azure'; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'gcs'; bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + accessKeyId: string; + secretAccessKey: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'local'; path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rclone'; - path: string; remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + path: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rest'; url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; + username?: string; password?: string; path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; - username?: string; } | { backend: 'sftp'; host: string; + port?: string | number; + user: string; path: string; privateKey: string; - user: string; - port?: number; skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; + compressionMode: 'off' | 'auto' | 'max' | null; + status: 'healthy' | 'error' | 'unknown' | 'doctor' | 'cancelled' | null; lastChecked: number | null; lastError: string | null; - name: string; - shortId: string; - status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + doctorResult: { + success: boolean; + steps: Array<{ + step: string; + success: boolean; + output: string | null; + error: string | null; + }>; + completedAt: number; + } | null; + createdAt: number; updatedAt: number; }; }; @@ -1589,166 +1589,166 @@ export type GetRepositoryResponse = GetRepositoryResponses[keyof GetRepositoryRe export type UpdateRepositoryData = { body: { - compressionMode?: 'auto' | 'max' | 'off'; + name?: string; + compressionMode?: 'off' | 'auto' | 'max'; config?: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accessKeyId: string; backend: 's3'; - bucket: string; endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accountKey: string; - accountName: string; - backend: 'azure'; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'gcs'; bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + accessKeyId: string; + secretAccessKey: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'local'; path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rclone'; - path: string; remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + path: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rest'; url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; + username?: string; password?: string; path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; - username?: string; } | { backend: 'sftp'; host: string; + port?: string | number; + user: string; path: string; privateKey: string; - user: string; - port?: number; skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; }; - name?: string; }; path: { shortId: string; @@ -1777,183 +1777,183 @@ export type UpdateRepositoryResponses = { * Repository updated successfully */ 200: { - compressionMode: 'auto' | 'max' | 'off' | null; + id: string; + shortId: string; + name: string; + type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp'; config: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accessKeyId: string; backend: 's3'; - bucket: string; endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accountKey: string; - accountName: string; - backend: 'azure'; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'gcs'; bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + accessKeyId: string; + secretAccessKey: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'local'; path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rclone'; - path: string; remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + path: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rest'; url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; + username?: string; password?: string; path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; - username?: string; } | { backend: 'sftp'; host: string; + port?: string | number; + user: string; path: string; privateKey: string; - user: string; - port?: number; skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; + compressionMode: 'off' | 'auto' | 'max' | null; + status: 'healthy' | 'error' | 'unknown' | 'doctor' | 'cancelled' | null; lastChecked: number | null; lastError: string | null; - name: string; - shortId: string; - status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + doctorResult: { + success: boolean; + steps: Array<{ + step: string; + success: boolean; + output: string | null; + error: string | null; + }>; + completedAt: number; + } | null; + createdAt: number; updatedAt: number; }; }; @@ -1974,12 +1974,12 @@ export type GetRepositoryStatsResponses = { * Repository statistics */ 200: { - compression_progress?: number; - compression_ratio?: number; - compression_space_saving?: number; - snapshots_count?: number; total_size?: number; total_uncompressed_size?: number; + compression_ratio?: number; + compression_progress?: number; + compression_space_saving?: number; + snapshots_count?: number; }; }; @@ -1999,12 +1999,12 @@ export type RefreshRepositoryStatsResponses = { * Refreshed repository statistics */ 200: { - compression_progress?: number; - compression_ratio?: number; - compression_space_saving?: number; - snapshots_count?: number; total_size?: number; total_uncompressed_size?: number; + compression_ratio?: number; + compression_progress?: number; + compression_space_saving?: number; + snapshots_count?: number; }; }; @@ -2048,29 +2048,29 @@ export type ListSnapshotsResponses = { * List of snapshots */ 200: Array<{ - duration: number; - paths: Array; - retentionCategories: Array; short_id: string; - size: number; - tags: Array; time: number; + paths: Array; + size: number; + duration: number; + tags: Array; + retentionCategories: Array; hostname?: string; summary?: { - backup_end: string; - backup_start: string; - data_added: number; - data_blobs: number; - dirs_changed: number; - dirs_new: number; - dirs_unmodified: number; - files_changed: number; files_new: number; + files_changed: number; files_unmodified: number; - total_bytes_processed: number; - total_files_processed: number; + dirs_new: number; + dirs_changed: number; + dirs_unmodified: number; + data_blobs: number; tree_blobs: number; + data_added: number; data_added_packed?: number; + total_files_processed: number; + total_bytes_processed: number; + backup_start: string; + backup_end: string; }; }>; }; @@ -2091,8 +2091,8 @@ export type RefreshSnapshotsResponses = { * Snapshot cache cleared and refreshed */ 200: { - count: number; message: string; + count: number; }; }; @@ -2134,29 +2134,29 @@ export type GetSnapshotDetailsResponses = { * Snapshot details */ 200: { - duration: number; - paths: Array; - retentionCategories: Array; short_id: string; - size: number; - tags: Array; time: number; + paths: Array; + size: number; + duration: number; + tags: Array; + retentionCategories: Array; hostname?: string; summary?: { - backup_end: string; - backup_start: string; - data_added: number; - data_blobs: number; - dirs_changed: number; - dirs_new: number; - dirs_unmodified: number; - files_changed: number; files_new: number; + files_changed: number; files_unmodified: number; - total_bytes_processed: number; - total_files_processed: number; + dirs_new: number; + dirs_changed: number; + dirs_unmodified: number; + data_blobs: number; tree_blobs: number; + data_added: number; data_added_packed?: number; + total_files_processed: number; + total_bytes_processed: number; + backup_start: string; + backup_end: string; }; }; }; @@ -2170,9 +2170,9 @@ export type ListSnapshotFilesData = { snapshotId: string; }; query?: { - limit?: string; - offset?: string; path?: string; + offset?: number; + limit?: number; }; url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}/files'; }; @@ -2182,29 +2182,29 @@ export type ListSnapshotFilesResponses = { * List of files and directories in the snapshot */ 200: { - files: Array<{ - name: string; - path: string; - type: string; - atime?: string; - ctime?: string; - gid?: number; - mode?: number; - mtime?: string; - size?: number; - uid?: number; - }>; - hasMore: boolean; - limit: number; - offset: number; snapshot: { - hostname: string; id: string; - paths: Array; short_id: string; time: string; + hostname: string; + paths: Array; }; + files: Array<{ + name: string; + type: string; + path: string; + uid?: number; + gid?: number; + size?: number; + mode?: number; + mtime?: string; + atime?: string; + ctime?: string; + }>; + offset: number; + limit: number; total: number; + hasMore: boolean; }; }; @@ -2217,8 +2217,8 @@ export type DumpSnapshotData = { snapshotId: string; }; query?: { - kind?: 'dir' | 'file'; path?: string; + kind?: 'file' | 'dir'; }; url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}/dump'; }; @@ -2235,13 +2235,13 @@ export type DumpSnapshotResponse = DumpSnapshotResponses[keyof DumpSnapshotRespo export type RestoreSnapshotData = { body: { snapshotId: string; - delete?: boolean; + include?: Array; + selectedItemKind?: 'file' | 'dir'; exclude?: Array; excludeXattr?: Array; - include?: Array; - overwrite?: 'always' | 'if-changed' | 'if-newer' | 'never'; - selectedItemKind?: 'dir' | 'file'; + delete?: boolean; targetPath?: string; + overwrite?: 'always' | 'if-changed' | 'if-newer' | 'never'; }; path: { shortId: string; @@ -2255,10 +2255,10 @@ export type RestoreSnapshotResponses = { * Snapshot restored successfully */ 200: { + success: boolean; + message: string; filesRestored: number; filesSkipped: number; - message: string; - success: boolean; }; }; @@ -2333,8 +2333,8 @@ export type UnlockRepositoryResponses = { * Repository unlocked successfully */ 200: { - message: string; success: boolean; + message: string; }; }; @@ -2405,273 +2405,273 @@ export type ListBackupSchedulesResponses = { * List of backup schedules */ 200: Array<{ - createdAt: number; - cronExpression: string; - customResticParams: Array | null; - enabled: boolean; - excludeIfPresent: Array | null; - excludePatterns: Array | null; id: number; - includePatterns: Array | null; - lastBackupAt: number | null; - lastBackupError: string | null; - lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; - name: string; - nextBackupAt: number | null; - oneFileSystem: boolean; - repository: { - compressionMode: 'auto' | 'max' | 'off' | null; - config: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accessKeyId: string; - backend: 's3'; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accountKey: string; - accountName: string; - backend: 'azure'; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'gcs'; - bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'local'; - path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'rclone'; - path: string; - remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'rest'; - url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - password?: string; - path?: string; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - username?: string; - } | { - backend: 'sftp'; - host: string; - path: string; - privateKey: string; - user: string; - port?: number; - skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - knownHosts?: string; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; - lastChecked: number | null; - lastError: string | null; - name: string; - shortId: string; - status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; - updatedAt: number; - }; - repositoryId: string; - retentionPolicy: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - } | null; shortId: string; + name: string; + volumeId: number; + repositoryId: string; + enabled: boolean; + cronExpression: string; + retentionPolicy: { + keepLast?: number; + keepHourly?: number; + keepDaily?: number; + keepWeekly?: number; + keepMonthly?: number; + keepYearly?: number; + keepWithinDuration?: string; + } | null; + excludePatterns: Array | null; + excludeIfPresent: Array | null; + includePatterns: Array | null; + oneFileSystem: boolean; + customResticParams: Array | null; + lastBackupAt: number | null; + lastBackupStatus: 'success' | 'error' | 'in_progress' | 'warning' | null; + lastBackupError: string | null; + nextBackupAt: number | null; + createdAt: number; updatedAt: number; volume: { - autoRemount: boolean; + id: number; + shortId: string; + name: string; + type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp'; + status: 'mounted' | 'unmounted' | 'error'; + lastError: string | null; + createdAt: number; + updatedAt: number; + lastHealthCheck: number; config: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; + autoRemount: boolean; + }; + repository: { + id: string; shortId: string; - status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; + name: string; + type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp'; + config: { + backend: 's3'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'local'; + path: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'rclone'; + remote: string; + path: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'rest'; + url: string; + username?: string; + password?: string; + path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + user: string; + path: string; + privateKey: string; + skipHostKeyCheck?: boolean; + knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + }; + compressionMode: 'off' | 'auto' | 'max' | null; + status: 'healthy' | 'error' | 'unknown' | 'doctor' | 'cancelled' | null; + lastChecked: number | null; + lastError: string | null; + doctorResult: { + success: boolean; + steps: Array<{ + step: string; + success: boolean; + output: string | null; + error: string | null; + }>; + completedAt: number; + } | null; + createdAt: number; updatedAt: number; }; - volumeId: number; }>; }; @@ -2679,26 +2679,26 @@ export type ListBackupSchedulesResponse = ListBackupSchedulesResponses[keyof Lis export type CreateBackupScheduleData = { body: { - cronExpression: string; - enabled: boolean; name: string; + volumeId: string | number; repositoryId: string; - volumeId: number | string; - customResticParams?: Array; - excludeIfPresent?: Array; + enabled: boolean; + cronExpression: string; + retentionPolicy?: { + keepLast?: number; + keepHourly?: number; + keepDaily?: number; + keepWeekly?: number; + keepMonthly?: number; + keepYearly?: number; + keepWithinDuration?: string; + }; excludePatterns?: Array; + excludeIfPresent?: Array; includePatterns?: Array; oneFileSystem?: boolean; - retentionPolicy?: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - }; tags?: Array; + customResticParams?: Array; }; path?: never; query?: never; @@ -2710,33 +2710,33 @@ export type CreateBackupScheduleResponses = { * Backup schedule created successfully */ 201: { - createdAt: number; - cronExpression: string; - customResticParams: Array | null; - enabled: boolean; - excludeIfPresent: Array | null; - excludePatterns: Array | null; id: number; - includePatterns: Array | null; - lastBackupAt: number | null; - lastBackupError: string | null; - lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; - name: string; - nextBackupAt: number | null; - oneFileSystem: boolean; - repositoryId: string; - retentionPolicy: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - } | null; shortId: string; - updatedAt: number; + name: string; volumeId: number; + repositoryId: string; + enabled: boolean; + cronExpression: string; + retentionPolicy: { + keepLast?: number; + keepHourly?: number; + keepDaily?: number; + keepWeekly?: number; + keepMonthly?: number; + keepYearly?: number; + keepWithinDuration?: string; + } | null; + excludePatterns: Array | null; + excludeIfPresent: Array | null; + includePatterns: Array | null; + oneFileSystem: boolean; + customResticParams: Array | null; + lastBackupAt: number | null; + lastBackupStatus: 'success' | 'error' | 'in_progress' | 'warning' | null; + lastBackupError: string | null; + nextBackupAt: number | null; + createdAt: number; + updatedAt: number; }; }; @@ -2776,273 +2776,273 @@ export type GetBackupScheduleResponses = { * Backup schedule details */ 200: { - createdAt: number; - cronExpression: string; - customResticParams: Array | null; - enabled: boolean; - excludeIfPresent: Array | null; - excludePatterns: Array | null; id: number; - includePatterns: Array | null; - lastBackupAt: number | null; - lastBackupError: string | null; - lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; - name: string; - nextBackupAt: number | null; - oneFileSystem: boolean; - repository: { - compressionMode: 'auto' | 'max' | 'off' | null; - config: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accessKeyId: string; - backend: 's3'; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accountKey: string; - accountName: string; - backend: 'azure'; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'gcs'; - bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'local'; - path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'rclone'; - path: string; - remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'rest'; - url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - password?: string; - path?: string; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - username?: string; - } | { - backend: 'sftp'; - host: string; - path: string; - privateKey: string; - user: string; - port?: number; - skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - knownHosts?: string; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; - lastChecked: number | null; - lastError: string | null; - name: string; - shortId: string; - status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; - updatedAt: number; - }; - repositoryId: string; - retentionPolicy: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - } | null; shortId: string; + name: string; + volumeId: number; + repositoryId: string; + enabled: boolean; + cronExpression: string; + retentionPolicy: { + keepLast?: number; + keepHourly?: number; + keepDaily?: number; + keepWeekly?: number; + keepMonthly?: number; + keepYearly?: number; + keepWithinDuration?: string; + } | null; + excludePatterns: Array | null; + excludeIfPresent: Array | null; + includePatterns: Array | null; + oneFileSystem: boolean; + customResticParams: Array | null; + lastBackupAt: number | null; + lastBackupStatus: 'success' | 'error' | 'in_progress' | 'warning' | null; + lastBackupError: string | null; + nextBackupAt: number | null; + createdAt: number; updatedAt: number; volume: { - autoRemount: boolean; + id: number; + shortId: string; + name: string; + type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp'; + status: 'mounted' | 'unmounted' | 'error'; + lastError: string | null; + createdAt: number; + updatedAt: number; + lastHealthCheck: number; config: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; + autoRemount: boolean; + }; + repository: { + id: string; shortId: string; - status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; + name: string; + type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp'; + config: { + backend: 's3'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'local'; + path: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'rclone'; + remote: string; + path: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'rest'; + url: string; + username?: string; + password?: string; + path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + user: string; + path: string; + privateKey: string; + skipHostKeyCheck?: boolean; + knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + }; + compressionMode: 'off' | 'auto' | 'max' | null; + status: 'healthy' | 'error' | 'unknown' | 'doctor' | 'cancelled' | null; + lastChecked: number | null; + lastError: string | null; + doctorResult: { + success: boolean; + steps: Array<{ + step: string; + success: boolean; + output: string | null; + error: string | null; + }>; + completedAt: number; + } | null; + createdAt: number; updatedAt: number; }; - volumeId: number; }; }; @@ -3050,25 +3050,25 @@ export type GetBackupScheduleResponse = GetBackupScheduleResponses[keyof GetBack export type UpdateBackupScheduleData = { body: { - cronExpression: string; - repositoryId: string; - customResticParams?: Array; - enabled?: boolean; - excludeIfPresent?: Array; - excludePatterns?: Array; - includePatterns?: Array; name?: string; - oneFileSystem?: boolean; + repositoryId: string; + enabled?: boolean; + cronExpression: string; retentionPolicy?: { - keepDaily?: number; - keepHourly?: number; keepLast?: number; - keepMonthly?: number; + keepHourly?: number; + keepDaily?: number; keepWeekly?: number; - keepWithinDuration?: string; + keepMonthly?: number; keepYearly?: number; + keepWithinDuration?: string; }; + excludePatterns?: Array; + excludeIfPresent?: Array; + includePatterns?: Array; + oneFileSystem?: boolean; tags?: Array; + customResticParams?: Array; }; path: { shortId: string; @@ -3082,33 +3082,33 @@ export type UpdateBackupScheduleResponses = { * Backup schedule updated successfully */ 200: { - createdAt: number; - cronExpression: string; - customResticParams: Array | null; - enabled: boolean; - excludeIfPresent: Array | null; - excludePatterns: Array | null; id: number; - includePatterns: Array | null; - lastBackupAt: number | null; - lastBackupError: string | null; - lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; - name: string; - nextBackupAt: number | null; - oneFileSystem: boolean; - repositoryId: string; - retentionPolicy: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - } | null; shortId: string; - updatedAt: number; + name: string; volumeId: number; + repositoryId: string; + enabled: boolean; + cronExpression: string; + retentionPolicy: { + keepLast?: number; + keepHourly?: number; + keepDaily?: number; + keepWeekly?: number; + keepMonthly?: number; + keepYearly?: number; + keepWithinDuration?: string; + } | null; + excludePatterns: Array | null; + excludeIfPresent: Array | null; + includePatterns: Array | null; + oneFileSystem: boolean; + customResticParams: Array | null; + lastBackupAt: number | null; + lastBackupStatus: 'success' | 'error' | 'in_progress' | 'warning' | null; + lastBackupError: string | null; + nextBackupAt: number | null; + createdAt: number; + updatedAt: number; }; }; @@ -3128,273 +3128,273 @@ export type GetBackupScheduleForVolumeResponses = { * Backup schedule details for the volume */ 200: { - createdAt: number; - cronExpression: string; - customResticParams: Array | null; - enabled: boolean; - excludeIfPresent: Array | null; - excludePatterns: Array | null; id: number; - includePatterns: Array | null; - lastBackupAt: number | null; - lastBackupError: string | null; - lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; - name: string; - nextBackupAt: number | null; - oneFileSystem: boolean; - repository: { - compressionMode: 'auto' | 'max' | 'off' | null; - config: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accessKeyId: string; - backend: 's3'; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accountKey: string; - accountName: string; - backend: 'azure'; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'gcs'; - bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'local'; - path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'rclone'; - path: string; - remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'rest'; - url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - password?: string; - path?: string; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - username?: string; - } | { - backend: 'sftp'; - host: string; - path: string; - privateKey: string; - user: string; - port?: number; - skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - knownHosts?: string; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; - lastChecked: number | null; - lastError: string | null; - name: string; - shortId: string; - status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; - updatedAt: number; - }; - repositoryId: string; - retentionPolicy: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - } | null; shortId: string; + name: string; + volumeId: number; + repositoryId: string; + enabled: boolean; + cronExpression: string; + retentionPolicy: { + keepLast?: number; + keepHourly?: number; + keepDaily?: number; + keepWeekly?: number; + keepMonthly?: number; + keepYearly?: number; + keepWithinDuration?: string; + } | null; + excludePatterns: Array | null; + excludeIfPresent: Array | null; + includePatterns: Array | null; + oneFileSystem: boolean; + customResticParams: Array | null; + lastBackupAt: number | null; + lastBackupStatus: 'success' | 'error' | 'in_progress' | 'warning' | null; + lastBackupError: string | null; + nextBackupAt: number | null; + createdAt: number; updatedAt: number; volume: { - autoRemount: boolean; + id: number; + shortId: string; + name: string; + type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp'; + status: 'mounted' | 'unmounted' | 'error'; + lastError: string | null; + createdAt: number; + updatedAt: number; + lastHealthCheck: number; config: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; + autoRemount: boolean; + }; + repository: { + id: string; shortId: string; - status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; + name: string; + type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp'; + config: { + backend: 's3'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'local'; + path: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'rclone'; + remote: string; + path: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'rest'; + url: string; + username?: string; + password?: string; + path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + user: string; + path: string; + privateKey: string; + skipHostKeyCheck?: boolean; + knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + }; + compressionMode: 'off' | 'auto' | 'max' | null; + status: 'healthy' | 'error' | 'unknown' | 'doctor' | 'cancelled' | null; + lastChecked: number | null; + lastError: string | null; + doctorResult: { + success: boolean; + steps: Array<{ + step: string; + success: boolean; + output: string | null; + error: string | null; + }>; + completedAt: number; + } | null; + createdAt: number; updatedAt: number; }; - volumeId: number; } | null; }; @@ -3481,81 +3481,81 @@ export type GetScheduleNotificationsResponses = { * List of notification assignments for the schedule */ 200: Array<{ + scheduleId: number; + destinationId: number; + notifyOnStart: boolean; + notifyOnSuccess: boolean; + notifyOnWarning: boolean; + notifyOnFailure: boolean; createdAt: number; destination: { + id: number; + name: string; + enabled: boolean; + type: 'email' | 'slack' | 'discord' | 'gotify' | 'ntfy' | 'pushover' | 'telegram' | 'generic' | 'custom'; config: { - apiToken: string; - priority: -1 | 0 | 1; - type: 'pushover'; - userKey: string; - devices?: string; - } | { - botToken: string; - chatId: string; - type: 'telegram'; - threadId?: string; - } | { - from: string; + type: 'email'; smtpHost: string; smtpPort: number; - to: Array; - type: 'email'; - useTLS: boolean; + username?: string; + password?: string; + from: string; fromName?: string; - password?: string; - username?: string; - } | { - method: 'GET' | 'POST'; - type: 'generic'; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } | { - priority: 'default' | 'high' | 'low' | 'max' | 'min'; - topic: string; - type: 'ntfy'; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } | { - priority: number; - serverUrl: string; - token: string; - type: 'gotify'; - path?: string; - } | { - shoutrrrUrl: string; - type: 'custom'; - } | { - type: 'discord'; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; + to: Array; + useTLS: boolean; } | { type: 'slack'; webhookUrl: string; channel?: string; - iconEmoji?: string; username?: string; + iconEmoji?: string; + } | { + type: 'discord'; + webhookUrl: string; + username?: string; + avatarUrl?: string; + threadId?: string; + } | { + type: 'gotify'; + serverUrl: string; + token: string; + path?: string; + priority: number; + } | { + type: 'ntfy'; + serverUrl?: string; + topic: string; + priority: 'max' | 'high' | 'default' | 'low' | 'min'; + username?: string; + password?: string; + accessToken?: string; + } | { + type: 'pushover'; + userKey: string; + apiToken: string; + devices?: string; + priority: -1 | 0 | 1; + } | { + type: 'telegram'; + botToken: string; + chatId: string; + threadId?: string; + } | { + type: 'generic'; + url: string; + method: 'GET' | 'POST'; + contentType?: string; + headers?: Array; + useJson?: boolean; + titleKey?: string; + messageKey?: string; + } | { + type: 'custom'; + shoutrrrUrl: string; }; createdAt: number; - enabled: boolean; - id: number; - name: string; - type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; updatedAt: number; }; - destinationId: number; - notifyOnFailure: boolean; - notifyOnStart: boolean; - notifyOnSuccess: boolean; - notifyOnWarning: boolean; - scheduleId: number; }>; }; @@ -3565,10 +3565,10 @@ export type UpdateScheduleNotificationsData = { body: { assignments: Array<{ destinationId: number; - notifyOnFailure: boolean; notifyOnStart: boolean; notifyOnSuccess: boolean; notifyOnWarning: boolean; + notifyOnFailure: boolean; }>; }; path: { @@ -3583,81 +3583,81 @@ export type UpdateScheduleNotificationsResponses = { * Notification assignments updated successfully */ 200: Array<{ + scheduleId: number; + destinationId: number; + notifyOnStart: boolean; + notifyOnSuccess: boolean; + notifyOnWarning: boolean; + notifyOnFailure: boolean; createdAt: number; destination: { + id: number; + name: string; + enabled: boolean; + type: 'email' | 'slack' | 'discord' | 'gotify' | 'ntfy' | 'pushover' | 'telegram' | 'generic' | 'custom'; config: { - apiToken: string; - priority: -1 | 0 | 1; - type: 'pushover'; - userKey: string; - devices?: string; - } | { - botToken: string; - chatId: string; - type: 'telegram'; - threadId?: string; - } | { - from: string; + type: 'email'; smtpHost: string; smtpPort: number; - to: Array; - type: 'email'; - useTLS: boolean; + username?: string; + password?: string; + from: string; fromName?: string; - password?: string; - username?: string; - } | { - method: 'GET' | 'POST'; - type: 'generic'; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } | { - priority: 'default' | 'high' | 'low' | 'max' | 'min'; - topic: string; - type: 'ntfy'; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } | { - priority: number; - serverUrl: string; - token: string; - type: 'gotify'; - path?: string; - } | { - shoutrrrUrl: string; - type: 'custom'; - } | { - type: 'discord'; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; + to: Array; + useTLS: boolean; } | { type: 'slack'; webhookUrl: string; channel?: string; - iconEmoji?: string; username?: string; + iconEmoji?: string; + } | { + type: 'discord'; + webhookUrl: string; + username?: string; + avatarUrl?: string; + threadId?: string; + } | { + type: 'gotify'; + serverUrl: string; + token: string; + path?: string; + priority: number; + } | { + type: 'ntfy'; + serverUrl?: string; + topic: string; + priority: 'max' | 'high' | 'default' | 'low' | 'min'; + username?: string; + password?: string; + accessToken?: string; + } | { + type: 'pushover'; + userKey: string; + apiToken: string; + devices?: string; + priority: -1 | 0 | 1; + } | { + type: 'telegram'; + botToken: string; + chatId: string; + threadId?: string; + } | { + type: 'generic'; + url: string; + method: 'GET' | 'POST'; + contentType?: string; + headers?: Array; + useJson?: boolean; + titleKey?: string; + messageKey?: string; + } | { + type: 'custom'; + shoutrrrUrl: string; }; createdAt: number; - enabled: boolean; - id: number; - name: string; - type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; updatedAt: number; }; - destinationId: number; - notifyOnFailure: boolean; - notifyOnStart: boolean; - notifyOnSuccess: boolean; - notifyOnWarning: boolean; - scheduleId: number; }>; }; @@ -3677,193 +3677,193 @@ export type GetScheduleMirrorsResponses = { * List of mirror repository assignments for the schedule */ 200: Array<{ - createdAt: number; + scheduleId: string; + repositoryId: string; enabled: boolean; lastCopyAt: number | null; + lastCopyStatus: 'success' | 'error' | 'in_progress' | null; lastCopyError: string | null; - lastCopyStatus: 'error' | 'in_progress' | 'success' | null; + createdAt: number; repository: { - compressionMode: 'auto' | 'max' | 'off' | null; + id: string; + shortId: string; + name: string; + type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp'; config: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accessKeyId: string; backend: 's3'; - bucket: string; endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accountKey: string; - accountName: string; - backend: 'azure'; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'gcs'; bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + accessKeyId: string; + secretAccessKey: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'local'; path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rclone'; - path: string; remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + path: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rest'; url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; + username?: string; password?: string; path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; - username?: string; } | { backend: 'sftp'; host: string; + port?: string | number; + user: string; path: string; privateKey: string; - user: string; - port?: number; skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; + compressionMode: 'off' | 'auto' | 'max' | null; + status: 'healthy' | 'error' | 'unknown' | 'doctor' | 'cancelled' | null; lastChecked: number | null; lastError: string | null; - name: string; - shortId: string; - status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + doctorResult: { + success: boolean; + steps: Array<{ + step: string; + success: boolean; + output: string | null; + error: string | null; + }>; + completedAt: number; + } | null; + createdAt: number; updatedAt: number; }; - repositoryId: string; - scheduleId: string; }>; }; @@ -3872,8 +3872,8 @@ export type GetScheduleMirrorsResponse = GetScheduleMirrorsResponses[keyof GetSc export type UpdateScheduleMirrorsData = { body: { mirrors: Array<{ - enabled: boolean; repositoryId: string; + enabled: boolean; }>; }; path: { @@ -3888,193 +3888,193 @@ export type UpdateScheduleMirrorsResponses = { * Mirror assignments updated successfully */ 200: Array<{ - createdAt: number; + scheduleId: string; + repositoryId: string; enabled: boolean; lastCopyAt: number | null; + lastCopyStatus: 'success' | 'error' | 'in_progress' | null; lastCopyError: string | null; - lastCopyStatus: 'error' | 'in_progress' | 'success' | null; + createdAt: number; repository: { - compressionMode: 'auto' | 'max' | 'off' | null; + id: string; + shortId: string; + name: string; + type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp'; config: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accessKeyId: string; backend: 's3'; - bucket: string; endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - accountKey: string; - accountName: string; - backend: 'azure'; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - } | { - backend: 'gcs'; bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + accessKeyId: string; + secretAccessKey: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'local'; path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rclone'; - path: string; remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + path: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rest'; url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; + username?: string; password?: string; path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; - username?: string; } | { backend: 'sftp'; host: string; + port?: string | number; + user: string; path: string; privateKey: string; - user: string; - port?: number; skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; + compressionMode: 'off' | 'auto' | 'max' | null; + status: 'healthy' | 'error' | 'unknown' | 'doctor' | 'cancelled' | null; lastChecked: number | null; lastError: string | null; - name: string; - shortId: string; - status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + doctorResult: { + success: boolean; + steps: Array<{ + step: string; + success: boolean; + output: string | null; + error: string | null; + }>; + completedAt: number; + } | null; + createdAt: number; updatedAt: number; }; - repositoryId: string; - scheduleId: string; }>; }; @@ -4094,9 +4094,9 @@ export type GetMirrorCompatibilityResponses = { * List of repositories with their mirror compatibility status */ 200: Array<{ + repositoryId: string; compatible: boolean; reason: string | null; - repositoryId: string; }>; }; @@ -4136,16 +4136,15 @@ export type GetBackupProgressResponses = { * Current backup progress or null if not yet available */ 200: { - bytes_done: number; - files_done: number; - percent_done: number; - repositoryName: string; scheduleId: string; - seconds_elapsed: number; - seconds_remaining: number; - total_bytes: number; - total_files: number; volumeName: string; + repositoryName: string; + seconds_elapsed: number; + percent_done: number; + total_files: number; + files_done: number; + total_bytes: number; + bytes_done: number; current_files?: Array; } | null; }; @@ -4164,71 +4163,71 @@ export type ListNotificationDestinationsResponses = { * A list of notification destinations */ 200: Array<{ + id: number; + name: string; + enabled: boolean; + type: 'email' | 'slack' | 'discord' | 'gotify' | 'ntfy' | 'pushover' | 'telegram' | 'generic' | 'custom'; config: { - apiToken: string; - priority: -1 | 0 | 1; - type: 'pushover'; - userKey: string; - devices?: string; - } | { - botToken: string; - chatId: string; - type: 'telegram'; - threadId?: string; - } | { - from: string; + type: 'email'; smtpHost: string; smtpPort: number; - to: Array; - type: 'email'; - useTLS: boolean; + username?: string; + password?: string; + from: string; fromName?: string; - password?: string; - username?: string; - } | { - method: 'GET' | 'POST'; - type: 'generic'; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } | { - priority: 'default' | 'high' | 'low' | 'max' | 'min'; - topic: string; - type: 'ntfy'; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } | { - priority: number; - serverUrl: string; - token: string; - type: 'gotify'; - path?: string; - } | { - shoutrrrUrl: string; - type: 'custom'; - } | { - type: 'discord'; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; + to: Array; + useTLS: boolean; } | { type: 'slack'; webhookUrl: string; channel?: string; - iconEmoji?: string; username?: string; + iconEmoji?: string; + } | { + type: 'discord'; + webhookUrl: string; + username?: string; + avatarUrl?: string; + threadId?: string; + } | { + type: 'gotify'; + serverUrl: string; + token: string; + path?: string; + priority: number; + } | { + type: 'ntfy'; + serverUrl?: string; + topic: string; + priority: 'max' | 'high' | 'default' | 'low' | 'min'; + username?: string; + password?: string; + accessToken?: string; + } | { + type: 'pushover'; + userKey: string; + apiToken: string; + devices?: string; + priority: -1 | 0 | 1; + } | { + type: 'telegram'; + botToken: string; + chatId: string; + threadId?: string; + } | { + type: 'generic'; + url: string; + method: 'GET' | 'POST'; + contentType?: string; + headers?: Array; + useJson?: boolean; + titleKey?: string; + messageKey?: string; + } | { + type: 'custom'; + shoutrrrUrl: string; }; createdAt: number; - enabled: boolean; - id: number; - name: string; - type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; updatedAt: number; }>; }; @@ -4237,67 +4236,67 @@ export type ListNotificationDestinationsResponse = ListNotificationDestinationsR export type CreateNotificationDestinationData = { body: { + name: string; config: { - apiToken: string; - priority: -1 | 0 | 1; - type: 'pushover'; - userKey: string; - devices?: string; - } | { - botToken: string; - chatId: string; - type: 'telegram'; - threadId?: string; - } | { - from: string; + type: 'email'; smtpHost: string; smtpPort: number; - to: Array; - type: 'email'; - useTLS: boolean; + username?: string; + password?: string; + from: string; fromName?: string; - password?: string; - username?: string; - } | { - method: 'GET' | 'POST'; - type: 'generic'; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } | { - priority: 'default' | 'high' | 'low' | 'max' | 'min'; - topic: string; - type: 'ntfy'; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } | { - priority: number; - serverUrl: string; - token: string; - type: 'gotify'; - path?: string; - } | { - shoutrrrUrl: string; - type: 'custom'; - } | { - type: 'discord'; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; + to: Array; + useTLS: boolean; } | { type: 'slack'; webhookUrl: string; channel?: string; - iconEmoji?: string; username?: string; + iconEmoji?: string; + } | { + type: 'discord'; + webhookUrl: string; + username?: string; + avatarUrl?: string; + threadId?: string; + } | { + type: 'gotify'; + serverUrl: string; + token: string; + path?: string; + priority: number; + } | { + type: 'ntfy'; + serverUrl?: string; + topic: string; + priority: 'max' | 'high' | 'default' | 'low' | 'min'; + username?: string; + password?: string; + accessToken?: string; + } | { + type: 'pushover'; + userKey: string; + apiToken: string; + devices?: string; + priority: -1 | 0 | 1; + } | { + type: 'telegram'; + botToken: string; + chatId: string; + threadId?: string; + } | { + type: 'generic'; + url: string; + method: 'GET' | 'POST'; + contentType?: string; + headers?: Array; + useJson?: boolean; + titleKey?: string; + messageKey?: string; + } | { + type: 'custom'; + shoutrrrUrl: string; }; - name: string; }; path?: never; query?: never; @@ -4309,71 +4308,71 @@ export type CreateNotificationDestinationResponses = { * Notification destination created successfully */ 201: { + id: number; + name: string; + enabled: boolean; + type: 'email' | 'slack' | 'discord' | 'gotify' | 'ntfy' | 'pushover' | 'telegram' | 'generic' | 'custom'; config: { - apiToken: string; - priority: -1 | 0 | 1; - type: 'pushover'; - userKey: string; - devices?: string; - } | { - botToken: string; - chatId: string; - type: 'telegram'; - threadId?: string; - } | { - from: string; + type: 'email'; smtpHost: string; smtpPort: number; - to: Array; - type: 'email'; - useTLS: boolean; + username?: string; + password?: string; + from: string; fromName?: string; - password?: string; - username?: string; - } | { - method: 'GET' | 'POST'; - type: 'generic'; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } | { - priority: 'default' | 'high' | 'low' | 'max' | 'min'; - topic: string; - type: 'ntfy'; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } | { - priority: number; - serverUrl: string; - token: string; - type: 'gotify'; - path?: string; - } | { - shoutrrrUrl: string; - type: 'custom'; - } | { - type: 'discord'; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; + to: Array; + useTLS: boolean; } | { type: 'slack'; webhookUrl: string; channel?: string; - iconEmoji?: string; username?: string; + iconEmoji?: string; + } | { + type: 'discord'; + webhookUrl: string; + username?: string; + avatarUrl?: string; + threadId?: string; + } | { + type: 'gotify'; + serverUrl: string; + token: string; + path?: string; + priority: number; + } | { + type: 'ntfy'; + serverUrl?: string; + topic: string; + priority: 'max' | 'high' | 'default' | 'low' | 'min'; + username?: string; + password?: string; + accessToken?: string; + } | { + type: 'pushover'; + userKey: string; + apiToken: string; + devices?: string; + priority: -1 | 0 | 1; + } | { + type: 'telegram'; + botToken: string; + chatId: string; + threadId?: string; + } | { + type: 'generic'; + url: string; + method: 'GET' | 'POST'; + contentType?: string; + headers?: Array; + useJson?: boolean; + titleKey?: string; + messageKey?: string; + } | { + type: 'custom'; + shoutrrrUrl: string; }; createdAt: number; - enabled: boolean; - id: number; - name: string; - type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; updatedAt: number; }; }; @@ -4428,71 +4427,71 @@ export type GetNotificationDestinationResponses = { * Notification destination details */ 200: { + id: number; + name: string; + enabled: boolean; + type: 'email' | 'slack' | 'discord' | 'gotify' | 'ntfy' | 'pushover' | 'telegram' | 'generic' | 'custom'; config: { - apiToken: string; - priority: -1 | 0 | 1; - type: 'pushover'; - userKey: string; - devices?: string; - } | { - botToken: string; - chatId: string; - type: 'telegram'; - threadId?: string; - } | { - from: string; + type: 'email'; smtpHost: string; smtpPort: number; - to: Array; - type: 'email'; - useTLS: boolean; + username?: string; + password?: string; + from: string; fromName?: string; - password?: string; - username?: string; - } | { - method: 'GET' | 'POST'; - type: 'generic'; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } | { - priority: 'default' | 'high' | 'low' | 'max' | 'min'; - topic: string; - type: 'ntfy'; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } | { - priority: number; - serverUrl: string; - token: string; - type: 'gotify'; - path?: string; - } | { - shoutrrrUrl: string; - type: 'custom'; - } | { - type: 'discord'; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; + to: Array; + useTLS: boolean; } | { type: 'slack'; webhookUrl: string; channel?: string; - iconEmoji?: string; username?: string; + iconEmoji?: string; + } | { + type: 'discord'; + webhookUrl: string; + username?: string; + avatarUrl?: string; + threadId?: string; + } | { + type: 'gotify'; + serverUrl: string; + token: string; + path?: string; + priority: number; + } | { + type: 'ntfy'; + serverUrl?: string; + topic: string; + priority: 'max' | 'high' | 'default' | 'low' | 'min'; + username?: string; + password?: string; + accessToken?: string; + } | { + type: 'pushover'; + userKey: string; + apiToken: string; + devices?: string; + priority: -1 | 0 | 1; + } | { + type: 'telegram'; + botToken: string; + chatId: string; + threadId?: string; + } | { + type: 'generic'; + url: string; + method: 'GET' | 'POST'; + contentType?: string; + headers?: Array; + useJson?: boolean; + titleKey?: string; + messageKey?: string; + } | { + type: 'custom'; + shoutrrrUrl: string; }; createdAt: number; - enabled: boolean; - id: number; - name: string; - type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; updatedAt: number; }; }; @@ -4501,68 +4500,68 @@ export type GetNotificationDestinationResponse = GetNotificationDestinationRespo export type UpdateNotificationDestinationData = { body: { + name?: string; + enabled?: boolean; config?: { - apiToken: string; - priority: -1 | 0 | 1; - type: 'pushover'; - userKey: string; - devices?: string; - } | { - botToken: string; - chatId: string; - type: 'telegram'; - threadId?: string; - } | { - from: string; + type: 'email'; smtpHost: string; smtpPort: number; - to: Array; - type: 'email'; - useTLS: boolean; + username?: string; + password?: string; + from: string; fromName?: string; - password?: string; - username?: string; - } | { - method: 'GET' | 'POST'; - type: 'generic'; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } | { - priority: 'default' | 'high' | 'low' | 'max' | 'min'; - topic: string; - type: 'ntfy'; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } | { - priority: number; - serverUrl: string; - token: string; - type: 'gotify'; - path?: string; - } | { - shoutrrrUrl: string; - type: 'custom'; - } | { - type: 'discord'; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; + to: Array; + useTLS: boolean; } | { type: 'slack'; webhookUrl: string; channel?: string; - iconEmoji?: string; username?: string; + iconEmoji?: string; + } | { + type: 'discord'; + webhookUrl: string; + username?: string; + avatarUrl?: string; + threadId?: string; + } | { + type: 'gotify'; + serverUrl: string; + token: string; + path?: string; + priority: number; + } | { + type: 'ntfy'; + serverUrl?: string; + topic: string; + priority: 'max' | 'high' | 'default' | 'low' | 'min'; + username?: string; + password?: string; + accessToken?: string; + } | { + type: 'pushover'; + userKey: string; + apiToken: string; + devices?: string; + priority: -1 | 0 | 1; + } | { + type: 'telegram'; + botToken: string; + chatId: string; + threadId?: string; + } | { + type: 'generic'; + url: string; + method: 'GET' | 'POST'; + contentType?: string; + headers?: Array; + useJson?: boolean; + titleKey?: string; + messageKey?: string; + } | { + type: 'custom'; + shoutrrrUrl: string; }; - enabled?: boolean; - name?: string; }; path: { id: string; @@ -4583,71 +4582,71 @@ export type UpdateNotificationDestinationResponses = { * Notification destination updated successfully */ 200: { + id: number; + name: string; + enabled: boolean; + type: 'email' | 'slack' | 'discord' | 'gotify' | 'ntfy' | 'pushover' | 'telegram' | 'generic' | 'custom'; config: { - apiToken: string; - priority: -1 | 0 | 1; - type: 'pushover'; - userKey: string; - devices?: string; - } | { - botToken: string; - chatId: string; - type: 'telegram'; - threadId?: string; - } | { - from: string; + type: 'email'; smtpHost: string; smtpPort: number; - to: Array; - type: 'email'; - useTLS: boolean; + username?: string; + password?: string; + from: string; fromName?: string; - password?: string; - username?: string; - } | { - method: 'GET' | 'POST'; - type: 'generic'; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } | { - priority: 'default' | 'high' | 'low' | 'max' | 'min'; - topic: string; - type: 'ntfy'; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } | { - priority: number; - serverUrl: string; - token: string; - type: 'gotify'; - path?: string; - } | { - shoutrrrUrl: string; - type: 'custom'; - } | { - type: 'discord'; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; + to: Array; + useTLS: boolean; } | { type: 'slack'; webhookUrl: string; channel?: string; - iconEmoji?: string; username?: string; + iconEmoji?: string; + } | { + type: 'discord'; + webhookUrl: string; + username?: string; + avatarUrl?: string; + threadId?: string; + } | { + type: 'gotify'; + serverUrl: string; + token: string; + path?: string; + priority: number; + } | { + type: 'ntfy'; + serverUrl?: string; + topic: string; + priority: 'max' | 'high' | 'default' | 'low' | 'min'; + username?: string; + password?: string; + accessToken?: string; + } | { + type: 'pushover'; + userKey: string; + apiToken: string; + devices?: string; + priority: -1 | 0 | 1; + } | { + type: 'telegram'; + botToken: string; + chatId: string; + threadId?: string; + } | { + type: 'generic'; + url: string; + method: 'GET' | 'POST'; + contentType?: string; + headers?: Array; + useJson?: boolean; + titleKey?: string; + messageKey?: string; + } | { + type: 'custom'; + shoutrrrUrl: string; }; createdAt: number; - enabled: boolean; - id: number; - name: string; - type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; updatedAt: number; }; }; @@ -4723,13 +4722,13 @@ export type GetUpdatesResponses = { */ 200: { currentVersion: string; - hasUpdate: boolean; latestVersion: string; + hasUpdate: boolean; missedReleases: Array<{ - body: string; - publishedAt: string; - url: string; version: string; + url: string; + publishedAt: string; + body: string; }>; }; }; diff --git a/app/client/components/file-browsers/snapshot-tree-browser.tsx b/app/client/components/file-browsers/snapshot-tree-browser.tsx index f9983dfd..d660c4c2 100644 --- a/app/client/components/file-browsers/snapshot-tree-browser.tsx +++ b/app/client/components/file-browsers/snapshot-tree-browser.tsx @@ -74,11 +74,7 @@ export const SnapshotTreeBrowser = ({ return await queryClient.ensureQueryData( listSnapshotFilesOptions({ path: { shortId: repositoryId, snapshotId }, - query: { - path, - offset: offset.toString(), - limit: pageSize.toString(), - }, + query: { path, offset: offset, limit: pageSize }, }), ); }, @@ -86,11 +82,7 @@ export const SnapshotTreeBrowser = ({ void queryClient.prefetchQuery( listSnapshotFilesOptions({ path: { shortId: repositoryId, snapshotId }, - query: { - path, - offset: "0", - limit: pageSize.toString(), - }, + query: { path, offset: 0, limit: pageSize }, }), ); }, diff --git a/app/client/components/file-browsers/volume-file-browser.tsx b/app/client/components/file-browsers/volume-file-browser.tsx index 2b39d03f..902b191a 100644 --- a/app/client/components/file-browsers/volume-file-browser.tsx +++ b/app/client/components/file-browsers/volume-file-browser.tsx @@ -24,7 +24,7 @@ export const VolumeFileBrowser = ({ volumeId, enabled = true, ...uiProps }: Volu return await queryClient.ensureQueryData( listFilesOptions({ path: { shortId: volumeId }, - query: { path, offset: offset?.toString() }, + query: { path, offset: offset }, }), ); }, diff --git a/app/client/modules/auth/routes/login.tsx b/app/client/modules/auth/routes/login.tsx index 83317925..58663c1b 100644 --- a/app/client/modules/auth/routes/login.tsx +++ b/app/client/modules/auth/routes/login.tsx @@ -1,5 +1,4 @@ -import { arktypeResolver } from "@hookform/resolvers/arktype"; -import { type } from "arktype"; +import { zodResolver } from "@hookform/resolvers/zod"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -16,13 +15,17 @@ import { useNavigate } from "@tanstack/react-router"; import { normalizeUsername } from "~/lib/username"; import { cn } from "~/client/lib/utils"; import { SsoLoginSection } from "~/client/modules/sso/components/sso-login-section"; +import { z } from "zod"; -const loginSchema = type({ - username: "2<=string<=50", - password: "string>=1", +const loginSchema = z.object({ + username: z + .string() + .min(2, "Username must be at least 2 characters") + .max(50, "Username must be at most 50 characters"), + password: z.string().min(1, "Password is required"), }); -type LoginFormValues = typeof loginSchema.inferIn; +type LoginFormValues = z.input; type LoginPageProps = { error?: string; @@ -40,7 +43,7 @@ export function LoginPage({ error }: LoginPageProps = {}) { const errorDescription = errorCode ? getLoginErrorDescription(errorCode) : null; const form = useForm({ - resolver: arktypeResolver(loginSchema), + resolver: zodResolver(loginSchema), defaultValues: { username: "", password: "", diff --git a/app/client/modules/auth/routes/onboarding.tsx b/app/client/modules/auth/routes/onboarding.tsx index 8eddace4..e477837e 100644 --- a/app/client/modules/auth/routes/onboarding.tsx +++ b/app/client/modules/auth/routes/onboarding.tsx @@ -1,5 +1,4 @@ -import { arktypeResolver } from "@hookform/resolvers/arktype"; -import { type } from "arktype"; +import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { @@ -19,24 +18,28 @@ import { authClient } from "~/client/lib/auth-client"; import { useState } from "react"; import { useNavigate } from "@tanstack/react-router"; import { normalizeUsername } from "~/lib/username"; +import { z } from "zod"; export const clientMiddleware = [authMiddleware]; -const onboardingSchema = type({ - username: type("2<=string<=30").pipe(normalizeUsername), - email: type("string.email").pipe((str) => str.trim().toLowerCase()), - password: "string>=8", - confirmPassword: "string>=1", +const onboardingSchema = z.object({ + username: z.string().min(2).max(30).transform(normalizeUsername), + email: z + .string() + .email() + .transform((str) => str.trim().toLowerCase()), + password: z.string().min(8), + confirmPassword: z.string().min(1), }); -type OnboardingFormValues = typeof onboardingSchema.inferIn; +type OnboardingFormValues = z.input; export function OnboardingPage() { const navigate = useNavigate(); const [submitting, setSubmitting] = useState(false); const form = useForm({ - resolver: arktypeResolver(onboardingSchema), + resolver: zodResolver(onboardingSchema), defaultValues: { username: "", password: "", diff --git a/app/client/modules/backups/components/create-schedule-form/index.tsx b/app/client/modules/backups/components/create-schedule-form/index.tsx index 242b4d4d..d5089233 100644 --- a/app/client/modules/backups/components/create-schedule-form/index.tsx +++ b/app/client/modules/backups/components/create-schedule-form/index.tsx @@ -1,4 +1,4 @@ -import { arktypeResolver } from "@hookform/resolvers/arktype"; +import { zodResolver } from "@hookform/resolvers/zod"; import { useCallback, useState } from "react"; import { useForm } from "react-hook-form"; import { useScrollToFormError } from "~/client/hooks/use-scroll-to-form-error"; @@ -13,7 +13,7 @@ import { FrequencySection } from "./frequency-section"; import { PathsSection } from "./paths-section"; import { RetentionSection } from "./retention-section"; import { SummarySection } from "./summary-section"; -import { cleanSchema, type BackupScheduleFormValues, type InternalFormValues } from "./types"; +import { internalFormSchema, type BackupScheduleFormValues, type InternalFormValues } from "./types"; import { backupScheduleToFormValues } from "./utils"; export type { BackupScheduleFormValues }; @@ -29,7 +29,7 @@ type Props = { export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }: Props) => { const form = useForm({ - resolver: arktypeResolver(cleanSchema as unknown as typeof import("./types").internalFormSchema), + resolver: zodResolver(internalFormSchema), defaultValues: backupScheduleToFormValues(initialValues), }); diff --git a/app/client/modules/backups/components/create-schedule-form/retention-section.tsx b/app/client/modules/backups/components/create-schedule-form/retention-section.tsx index c28de050..ccad9aad 100644 --- a/app/client/modules/backups/components/create-schedule-form/retention-section.tsx +++ b/app/client/modules/backups/components/create-schedule-form/retention-section.tsx @@ -22,7 +22,7 @@ export const RetentionSection = ({ form }: RetentionSectionProps) => { type="number" min={0} placeholder="Optional" - onChange={(v) => field.onChange(Number(v.target.value))} + onChange={(v) => field.onChange(v.target.value === "" ? undefined : Number(v.target.value))} /> Keep the N most recent snapshots. @@ -43,7 +43,7 @@ export const RetentionSection = ({ form }: RetentionSectionProps) => { min={0} placeholder="Optional" {...field} - onChange={(v) => field.onChange(Number(v.target.value))} + onChange={(v) => field.onChange(v.target.value === "" ? undefined : Number(v.target.value))} /> Keep the last N hourly snapshots. @@ -64,7 +64,7 @@ export const RetentionSection = ({ form }: RetentionSectionProps) => { min={0} placeholder="e.g., 7" {...field} - onChange={(v) => field.onChange(Number(v.target.value))} + onChange={(v) => field.onChange(v.target.value === "" ? undefined : Number(v.target.value))} /> Keep the last N daily snapshots. @@ -85,7 +85,7 @@ export const RetentionSection = ({ form }: RetentionSectionProps) => { min={0} placeholder="e.g., 4" {...field} - onChange={(v) => field.onChange(Number(v.target.value))} + onChange={(v) => field.onChange(v.target.value === "" ? undefined : Number(v.target.value))} /> Keep the last N weekly snapshots. @@ -106,7 +106,7 @@ export const RetentionSection = ({ form }: RetentionSectionProps) => { min={0} placeholder="e.g., 6" {...field} - onChange={(v) => field.onChange(Number(v.target.value))} + onChange={(v) => field.onChange(v.target.value === "" ? undefined : Number(v.target.value))} /> Keep the last N monthly snapshots. @@ -127,7 +127,7 @@ export const RetentionSection = ({ form }: RetentionSectionProps) => { min={0} placeholder="Optional" {...field} - onChange={(v) => field.onChange(Number(v.target.value))} + onChange={(v) => field.onChange(v.target.value === "" ? undefined : Number(v.target.value))} /> Keep the last N yearly snapshots. diff --git a/app/client/modules/backups/components/create-schedule-form/types.ts b/app/client/modules/backups/components/create-schedule-form/types.ts index 3467efb2..bad646ff 100644 --- a/app/client/modules/backups/components/create-schedule-form/types.ts +++ b/app/client/modules/backups/components/create-schedule-form/types.ts @@ -1,30 +1,27 @@ -import { type } from "arktype"; -import { deepClean } from "~/utils/object"; +import { z } from "zod"; -export const internalFormSchema = type({ - name: "1 <= string <= 128", - repositoryId: "string", - excludePatternsText: "string?", - excludeIfPresentText: "string?", - includePatternsText: "string?", - includePatterns: "string[]?", - frequency: "string", - dailyTime: "string?", - weeklyDay: "string?", - monthlyDays: "string[]?", - cronExpression: "string?", - keepLast: "number?", - keepHourly: "number?", - keepDaily: "number?", - keepWeekly: "number?", - keepMonthly: "number?", - keepYearly: "number?", - oneFileSystem: "boolean?", - customResticParamsText: "string?", +export const internalFormSchema = z.object({ + name: z.string().min(1).max(128), + repositoryId: z.string(), + excludePatternsText: z.string().optional(), + excludeIfPresentText: z.string().optional(), + includePatternsText: z.string().optional(), + includePatterns: z.array(z.string()).optional(), + frequency: z.string(), + dailyTime: z.string().optional(), + weeklyDay: z.string().optional(), + monthlyDays: z.array(z.string()).optional(), + cronExpression: z.string().optional(), + keepLast: z.number().optional(), + keepHourly: z.number().optional(), + keepDaily: z.number().optional(), + keepWeekly: z.number().optional(), + keepMonthly: z.number().optional(), + keepYearly: z.number().optional(), + oneFileSystem: z.boolean().optional(), + customResticParamsText: z.string().optional(), }); -export const cleanSchema = type.pipe((d) => internalFormSchema(deepClean(d))); - export const weeklyDays = [ { label: "Monday", value: "1" }, { label: "Tuesday", value: "2" }, @@ -35,7 +32,7 @@ export const weeklyDays = [ { label: "Sunday", value: "0" }, ]; -export type InternalFormValues = typeof internalFormSchema.infer; +export type InternalFormValues = z.infer; export type BackupScheduleFormValues = Omit< InternalFormValues, diff --git a/app/client/modules/notifications/components/create-notification-form.tsx b/app/client/modules/notifications/components/create-notification-form.tsx index 684740e2..4c51f02f 100644 --- a/app/client/modules/notifications/components/create-notification-form.tsx +++ b/app/client/modules/notifications/components/create-notification-form.tsx @@ -1,8 +1,7 @@ -import { arktypeResolver } from "@hookform/resolvers/arktype"; -import { type } from "arktype"; +import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; +import { z } from "zod"; import { cn } from "~/client/lib/utils"; -import { deepClean } from "~/utils/object"; import { Form, FormControl, @@ -14,7 +13,17 @@ import { } from "~/client/components/ui/form"; import { Input } from "~/client/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; -import { notificationConfigSchemaBase } from "~/schemas/notifications"; +import { + customNotificationConfigSchema, + discordNotificationConfigSchema, + emailNotificationConfigSchema, + genericNotificationConfigSchema, + gotifyNotificationConfigSchema, + ntfyNotificationConfigSchema, + pushoverNotificationConfigSchema, + slackNotificationConfigSchema, + telegramNotificationConfigSchema, +} from "~/schemas/notifications"; import { useScrollToFormError } from "~/client/hooks/use-scroll-to-form-error"; import { CustomForm, @@ -28,12 +37,21 @@ import { TelegramForm, } from "./notification-forms"; -export const formSchema = type({ - name: "2<=string<=32", -}).and(notificationConfigSchemaBase); -const cleanSchema = type.pipe((d) => formSchema(deepClean(d))); +const baseFields = { name: z.string().min(2).max(32) }; -export type NotificationFormValues = typeof formSchema.inferIn; +export const formSchema = z.discriminatedUnion("type", [ + emailNotificationConfigSchema.extend(baseFields), + slackNotificationConfigSchema.extend(baseFields), + discordNotificationConfigSchema.extend(baseFields), + gotifyNotificationConfigSchema.extend(baseFields), + ntfyNotificationConfigSchema.extend(baseFields), + pushoverNotificationConfigSchema.extend(baseFields), + telegramNotificationConfigSchema.extend(baseFields), + genericNotificationConfigSchema.extend(baseFields), + customNotificationConfigSchema.extend(baseFields), +]); + +export type NotificationFormValues = z.input; type Props = { onSubmit: (values: NotificationFormValues) => void; @@ -106,7 +124,7 @@ const defaultValuesForType = { export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValues, formId, className }: Props) => { const form = useForm({ - resolver: arktypeResolver(cleanSchema as unknown as typeof formSchema), + resolver: zodResolver(formSchema, undefined, { raw: true }), defaultValues: initialValues || { name: "", }, diff --git a/app/client/modules/repositories/components/create-repository-form.tsx b/app/client/modules/repositories/components/create-repository-form.tsx index 5af7a393..3bfcc7af 100644 --- a/app/client/modules/repositories/components/create-repository-form.tsx +++ b/app/client/modules/repositories/components/create-repository-form.tsx @@ -1,10 +1,9 @@ -import { arktypeResolver } from "@hookform/resolvers/arktype"; -import { type } from "arktype"; +import { zodResolver } from "@hookform/resolvers/zod"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { Save } from "lucide-react"; +import { z } from "zod"; import { cn } from "~/client/lib/utils"; -import { deepClean } from "~/utils/object"; import { Button } from "../../../components/ui/button"; import { Form, @@ -21,7 +20,17 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ". import { Tooltip, TooltipContent, TooltipTrigger } from "../../../components/ui/tooltip"; import { useSystemInfo } from "~/client/hooks/use-system-info"; import { useScrollToFormError } from "~/client/hooks/use-scroll-to-form-error"; -import { COMPRESSION_MODES, repositoryConfigSchemaBase } from "~/schemas/restic"; +import { + COMPRESSION_MODES, + azureRepositoryConfigSchema, + gcsRepositoryConfigSchema, + localRepositoryConfigSchema, + r2RepositoryConfigSchema, + rcloneRepositoryConfigSchema, + restRepositoryConfigSchema, + s3RepositoryConfigSchema, + sftpRepositoryConfigSchema, +} from "~/schemas/restic"; import { Checkbox } from "../../../components/ui/checkbox"; import { LocalRepositoryForm, @@ -38,13 +47,23 @@ import { useServerFn } from "@tanstack/react-start"; import { getServerConstants } from "~/server/lib/functions/server-constants"; import { useSuspenseQuery } from "@tanstack/react-query"; -export const formSchema = type({ - name: "2<=string<=32", - compressionMode: type.valueOf(COMPRESSION_MODES).optional(), -}).and(repositoryConfigSchemaBase); -const cleanSchema = type.pipe((d) => formSchema(deepClean(d))); +const formBaseFields = { + name: z.string().min(2).max(32), + compressionMode: z.enum(COMPRESSION_MODES).optional(), +}; -export type RepositoryFormValues = typeof formSchema.inferIn; +export const formSchema = z.discriminatedUnion("backend", [ + localRepositoryConfigSchema.extend(formBaseFields), + s3RepositoryConfigSchema.extend(formBaseFields), + r2RepositoryConfigSchema.extend(formBaseFields), + gcsRepositoryConfigSchema.extend(formBaseFields), + azureRepositoryConfigSchema.extend(formBaseFields), + rcloneRepositoryConfigSchema.extend(formBaseFields), + restRepositoryConfigSchema.extend(formBaseFields), + sftpRepositoryConfigSchema.extend(formBaseFields), +]); + +export type RepositoryFormValues = z.input; type Props = { onSubmit: (values: RepositoryFormValues) => void; @@ -81,7 +100,7 @@ export const CreateRepositoryForm = ({ }); const form = useForm({ - resolver: arktypeResolver(cleanSchema as unknown as typeof formSchema), + resolver: zodResolver(formSchema, undefined, { raw: true }), defaultValues: initialValues, resetOptions: { keepDefaultValues: true, diff --git a/app/client/modules/repositories/routes/create-repository.tsx b/app/client/modules/repositories/routes/create-repository.tsx index db450a63..92912c49 100644 --- a/app/client/modules/repositories/routes/create-repository.tsx +++ b/app/client/modules/repositories/routes/create-repository.tsx @@ -5,6 +5,7 @@ import { toast } from "sonner"; import { createRepositoryMutation } from "~/client/api-client/@tanstack/react-query.gen"; import { CreateRepositoryForm, + formSchema, type RepositoryFormValues, } from "~/client/modules/repositories/components/create-repository-form"; import { Button } from "~/client/components/ui/button"; @@ -26,11 +27,13 @@ export function CreateRepositoryPage() { }); const handleSubmit = (values: RepositoryFormValues) => { + const { name, compressionMode, ...config } = formSchema.parse(values); + createRepository.mutate({ body: { - config: values, - name: values.name, - compressionMode: values.compressionMode, + config, + name, + compressionMode, }, }); }; diff --git a/app/client/modules/repositories/routes/edit-repository.tsx b/app/client/modules/repositories/routes/edit-repository.tsx index ad949104..d448f4a2 100644 --- a/app/client/modules/repositories/routes/edit-repository.tsx +++ b/app/client/modules/repositories/routes/edit-repository.tsx @@ -5,6 +5,7 @@ import { toast } from "sonner"; import { getRepositoryOptions, updateRepositoryMutation } from "~/client/api-client/@tanstack/react-query.gen"; import { CreateRepositoryForm, + formSchema, type RepositoryFormValues, } from "~/client/modules/repositories/components/create-repository-form"; import { @@ -35,11 +36,11 @@ const riskyLocationFieldsByBackend = { rclone: ["remote", "path"], } as const; -const hasRiskyLocationChange = (initialConfig: RepositoryConfig, nextConfig: RepositoryFormValues): boolean => { +const hasRiskyLocationChange = (initialConfig: RepositoryConfig, nextConfig: RepositoryConfig): boolean => { const fields = riskyLocationFieldsByBackend[initialConfig.backend] ?? []; return fields.some( - (field) => initialConfig[field as keyof RepositoryConfig] !== nextConfig[field as keyof RepositoryFormValues], + (field) => initialConfig[field as keyof RepositoryConfig] !== nextConfig[field as keyof RepositoryConfig], ); }; @@ -75,18 +76,20 @@ export function EditRepositoryPage({ repositoryId }: { repositoryId: string }) { }; const submitUpdate = (values: RepositoryFormValues) => { + const { name, compressionMode, ...config } = formSchema.parse(values); + updateRepository.mutate({ path: { shortId: repositoryId }, body: { - name: values.name, - compressionMode: values.compressionMode, - config: values, + name, + compressionMode, + config, }, }); }; const handleSubmit = (values: RepositoryFormValues) => { - const nextConfig = values; + const { name: _name, compressionMode: _compressionMode, ...nextConfig } = formSchema.parse(values); if (hasRiskyLocationChange(initialConfig, nextConfig)) { setPendingValues(values); diff --git a/app/client/modules/settings/components/create-user-dialog.tsx b/app/client/modules/settings/components/create-user-dialog.tsx index 25ba8533..8c669903 100644 --- a/app/client/modules/settings/components/create-user-dialog.tsx +++ b/app/client/modules/settings/components/create-user-dialog.tsx @@ -1,10 +1,10 @@ -import { arktypeResolver } from "@hookform/resolvers/arktype"; +import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation } from "@tanstack/react-query"; -import { type } from "arktype"; import { Plus, UserPlus } from "lucide-react"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; +import { z } from "zod"; import { Button } from "~/client/components/ui/button"; import { Dialog, @@ -20,15 +20,15 @@ import { Input } from "~/client/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; import { authClient } from "~/client/lib/auth-client"; -const createUserSchema = type({ - name: "string>=1", - username: "string>=1", - email: "string", - password: "string>=8", - role: "'user'|'admin'", +const createUserSchema = z.object({ + name: z.string().min(1), + username: z.string().min(1), + email: z.string().email(), + password: z.string().min(8), + role: z.enum(["user", "admin"]), }); -type CreateUserFormValues = typeof createUserSchema.infer; +type CreateUserFormValues = z.infer; interface CreateUserDialogProps { onUserCreated?: () => void; @@ -38,7 +38,7 @@ export function CreateUserDialog({ onUserCreated }: CreateUserDialogProps) { const [isOpen, setIsOpen] = useState(false); const form = useForm({ - resolver: arktypeResolver(createUserSchema), + resolver: zodResolver(createUserSchema), defaultValues: { name: "", username: "", diff --git a/app/client/modules/sso/routes/create-sso-provider.tsx b/app/client/modules/sso/routes/create-sso-provider.tsx index 8b13f41a..f293dd64 100644 --- a/app/client/modules/sso/routes/create-sso-provider.tsx +++ b/app/client/modules/sso/routes/create-sso-provider.tsx @@ -1,11 +1,11 @@ -import { arktypeResolver } from "@hookform/resolvers/arktype"; +import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation } from "@tanstack/react-query"; import { useNavigate } from "@tanstack/react-router"; -import { type } from "arktype"; import { ShieldCheck, Plus } from "lucide-react"; import { useId } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; +import { z } from "zod"; import { updateSsoProviderAutoLinkingMutation } from "~/client/api-client/@tanstack/react-query.gen"; import { Button } from "~/client/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card"; @@ -24,17 +24,17 @@ import { authClient } from "~/client/lib/auth-client"; import { parseError } from "~/client/lib/errors"; import { useOrganizationContext } from "~/client/hooks/use-org-context"; -const ssoProviderSchema = type({ - providerId: "string>=1", - issuer: "string>=1", - domain: "string>=1", - clientId: "string>=1", - clientSecret: "string>=1", - discoveryEndpoint: "string>=1", - linkMatchingEmails: "boolean", +const ssoProviderSchema = z.object({ + providerId: z.string().min(1), + issuer: z.string().min(1), + domain: z.string().min(1), + clientId: z.string().min(1), + clientSecret: z.string().min(1), + discoveryEndpoint: z.string().min(1), + linkMatchingEmails: z.boolean(), }); -type ProviderForm = typeof ssoProviderSchema.infer; +type ProviderForm = z.infer; export function CreateSsoProviderPage() { const navigate = useNavigate(); @@ -42,7 +42,7 @@ export function CreateSsoProviderPage() { const { activeOrganization } = useOrganizationContext(); const form = useForm({ - resolver: arktypeResolver(ssoProviderSchema), + resolver: zodResolver(ssoProviderSchema), defaultValues: { providerId: "", issuer: "", diff --git a/app/client/modules/volumes/components/create-volume-form.tsx b/app/client/modules/volumes/components/create-volume-form.tsx index 598d47c8..0b1d51f9 100644 --- a/app/client/modules/volumes/components/create-volume-form.tsx +++ b/app/client/modules/volumes/components/create-volume-form.tsx @@ -1,11 +1,10 @@ -import { arktypeResolver } from "@hookform/resolvers/arktype"; +import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation } from "@tanstack/react-query"; -import { type } from "arktype"; import { CheckCircle, Loader2, Plug, Save, XCircle } from "lucide-react"; import { useState } from "react"; import { useForm } from "react-hook-form"; +import { z } from "zod"; import { cn } from "~/client/lib/utils"; -import { deepClean } from "~/utils/object"; import { Button } from "../../../components/ui/button"; import { Form, @@ -18,19 +17,31 @@ import { } from "../../../components/ui/form"; import { Input } from "../../../components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../components/ui/select"; -import { volumeConfigSchemaBase } from "~/schemas/volumes"; +import { + directoryConfigSchema, + nfsConfigSchema, + rcloneConfigSchema, + sftpConfigSchema, + smbConfigSchema, + volumeConfigSchema, + webdavConfigSchema, +} from "~/schemas/volumes"; import { testConnectionMutation } from "../../../api-client/@tanstack/react-query.gen"; import { Tooltip, TooltipContent, TooltipTrigger } from "../../../components/ui/tooltip"; import { useSystemInfo } from "~/client/hooks/use-system-info"; import { useScrollToFormError } from "~/client/hooks/use-scroll-to-form-error"; import { DirectoryForm, NFSForm, SMBForm, WebDAVForm, RcloneForm, SFTPForm } from "./volume-forms"; -export const formSchema = type({ - name: "2<=string<=32", -}).and(volumeConfigSchemaBase); -const cleanSchema = type.pipe((d) => formSchema(deepClean(d))); +export const formSchema = z.discriminatedUnion("backend", [ + directoryConfigSchema.extend({ name: z.string().min(2).max(32) }), + nfsConfigSchema.extend({ name: z.string().min(2).max(32) }), + smbConfigSchema.extend({ name: z.string().min(2).max(32) }), + webdavConfigSchema.extend({ name: z.string().min(2).max(32) }), + rcloneConfigSchema.extend({ name: z.string().min(2).max(32) }), + sftpConfigSchema.extend({ name: z.string().min(2).max(32) }), +]); -export type FormValues = typeof formSchema.inferIn; +export type FormValues = z.input; type Props = { onSubmit: (values: FormValues) => void; @@ -52,7 +63,7 @@ const defaultValuesForType = { export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, formId, loading, className }: Props) => { const form = useForm({ - resolver: arktypeResolver(cleanSchema as unknown as typeof formSchema), + resolver: zodResolver(formSchema, undefined, { raw: true }), defaultValues: initialValues || { name: "", backend: "directory", @@ -89,15 +100,22 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for const handleTestConnection = async () => { const formValues = getValues(); + const { name: _, ...configCandidate } = formValues; + const parsedConfig = volumeConfigSchema.safeParse(configCandidate); + + if (!parsedConfig.success) { + setTestMessage({ success: false, message: "Please fix validation errors before testing the connection." }); + return; + } if ( - formValues.backend === "nfs" || - formValues.backend === "smb" || - formValues.backend === "webdav" || - formValues.backend === "sftp" + parsedConfig.data.backend === "nfs" || + parsedConfig.data.backend === "smb" || + parsedConfig.data.backend === "webdav" || + parsedConfig.data.backend === "sftp" ) { testBackendConnection.mutate({ - body: { config: formValues }, + body: { config: parsedConfig.data }, }); } }; diff --git a/app/client/modules/volumes/routes/create-volume.tsx b/app/client/modules/volumes/routes/create-volume.tsx index a6bb98c2..db00d37a 100644 --- a/app/client/modules/volumes/routes/create-volume.tsx +++ b/app/client/modules/volumes/routes/create-volume.tsx @@ -3,7 +3,7 @@ import { HardDrive, Plus } from "lucide-react"; import { useId } from "react"; import { toast } from "sonner"; import { createVolumeMutation } from "~/client/api-client/@tanstack/react-query.gen"; -import { CreateVolumeForm, type FormValues } from "~/client/modules/volumes/components/create-volume-form"; +import { CreateVolumeForm, formSchema, type FormValues } from "~/client/modules/volumes/components/create-volume-form"; import { Button } from "~/client/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card"; import { parseError } from "~/client/lib/errors"; @@ -23,10 +23,12 @@ export function CreateVolumePage() { }); const handleSubmit = (values: FormValues) => { + const { name, ...config } = formSchema.parse(values); + createVolume.mutate({ body: { - config: values, - name: values.name, + config, + name, }, }); }; diff --git a/app/client/modules/volumes/tabs/info.tsx b/app/client/modules/volumes/tabs/info.tsx index be9221be..29dbc6e7 100644 --- a/app/client/modules/volumes/tabs/info.tsx +++ b/app/client/modules/volumes/tabs/info.tsx @@ -2,7 +2,7 @@ import { useMutation } from "@tanstack/react-query"; import { useState } from "react"; import { toast } from "sonner"; import { Check, Plug, Trash2, Unplug } from "lucide-react"; -import { CreateVolumeForm, type FormValues } from "~/client/modules/volumes/components/create-volume-form"; +import { CreateVolumeForm, formSchema, type FormValues } from "~/client/modules/volumes/components/create-volume-form"; import { AlertDialog, AlertDialogAction, @@ -102,9 +102,11 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => { const confirmUpdate = () => { if (pendingValues) { + const { name, ...config } = formSchema.parse(pendingValues); + updateMutation.mutate({ path: { shortId: volume.shortId }, - body: { name: pendingValues.name, config: pendingValues }, + body: { name, config }, }); } }; diff --git a/app/routes/(auth)/login.error.tsx b/app/routes/(auth)/login.error.tsx index 36f3b2d8..e977d09b 100644 --- a/app/routes/(auth)/login.error.tsx +++ b/app/routes/(auth)/login.error.tsx @@ -1,11 +1,11 @@ import { createFileRoute } from "@tanstack/react-router"; -import { type } from "arktype"; +import { z } from "zod"; import { LoginPage } from "~/client/modules/auth/routes/login"; export const Route = createFileRoute("/(auth)/login/error")({ component: RouteComponent, errorComponent: () =>
Failed to load login error
, - validateSearch: type({ error: "string?" }), + validateSearch: z.object({ error: z.string().optional() }), head: () => ({ meta: [ { title: "Zerobyte - Login Error" }, diff --git a/app/routes/(auth)/login.tsx b/app/routes/(auth)/login.tsx index d3a0259c..a2c39764 100644 --- a/app/routes/(auth)/login.tsx +++ b/app/routes/(auth)/login.tsx @@ -1,11 +1,11 @@ import { Outlet, createFileRoute, useRouterState } from "@tanstack/react-router"; -import { type } from "arktype"; +import { z } from "zod"; import { LoginPage } from "~/client/modules/auth/routes/login"; export const Route = createFileRoute("/(auth)/login")({ component: RouteComponent, errorComponent: () =>
Failed to load login
, - validateSearch: type({ error: "string?" }), + validateSearch: z.object({ error: z.string().optional() }), head: () => ({ meta: [ { title: "Zerobyte - Login" }, diff --git a/app/routes/(dashboard)/admin/index.tsx b/app/routes/(dashboard)/admin/index.tsx index ff953ba4..b21b38d1 100644 --- a/app/routes/(dashboard)/admin/index.tsx +++ b/app/routes/(dashboard)/admin/index.tsx @@ -1,12 +1,12 @@ import { createFileRoute, redirect } from "@tanstack/react-router"; -import { type } from "arktype"; +import { z } from "zod"; import { fetchUser } from "../route"; import type { AppContext } from "~/context"; import { AdminPage } from "~/client/modules/admin/routes/admin-page"; import { getAdminUsersOptions, getRegistrationStatusOptions } from "~/client/api-client/@tanstack/react-query.gen"; export const Route = createFileRoute("/(dashboard)/admin/")({ - validateSearch: type({ tab: "string?" }), + validateSearch: z.object({ tab: z.string().optional() }), component: RouteComponent, errorComponent: () =>
Failed to load admin
, loader: async ({ context }) => { diff --git a/app/routes/(dashboard)/backups/$backupId/index.tsx b/app/routes/(dashboard)/backups/$backupId/index.tsx index 842a17de..7fd35a54 100644 --- a/app/routes/(dashboard)/backups/$backupId/index.tsx +++ b/app/routes/(dashboard)/backups/$backupId/index.tsx @@ -1,5 +1,5 @@ import { createFileRoute } from "@tanstack/react-router"; -import { type } from "arktype"; +import { z } from "zod"; import { getBackupProgressOptions, getBackupScheduleOptions, @@ -15,7 +15,7 @@ import { prefetchOrSkip } from "~/utils/prefetch"; export const Route = createFileRoute("/(dashboard)/backups/$backupId/")({ component: RouteComponent, errorComponent: () =>
Failed to load backup
, - validateSearch: type({ snapshot: "string?" }), + validateSearch: z.object({ snapshot: z.string().optional() }), loader: async ({ params, context }) => { const { backupId } = params; diff --git a/app/routes/(dashboard)/repositories/$repositoryId/index.tsx b/app/routes/(dashboard)/repositories/$repositoryId/index.tsx index 0e7158f6..82368797 100644 --- a/app/routes/(dashboard)/repositories/$repositoryId/index.tsx +++ b/app/routes/(dashboard)/repositories/$repositoryId/index.tsx @@ -1,5 +1,5 @@ import { createFileRoute } from "@tanstack/react-router"; -import { type } from "arktype"; +import { z } from "zod"; import { getRepositoryOptions, getRepositoryStatsOptions, @@ -30,7 +30,7 @@ export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/") stats: context.queryClient.getQueryData(statsOptions.queryKey), }; }, - validateSearch: type({ tab: "string?" }), + validateSearch: z.object({ tab: z.string().optional() }), staticData: { breadcrumb: (match) => [ { label: "Repositories", href: "/repositories" }, diff --git a/app/routes/(dashboard)/settings/index.tsx b/app/routes/(dashboard)/settings/index.tsx index 9034d40b..b97f0569 100644 --- a/app/routes/(dashboard)/settings/index.tsx +++ b/app/routes/(dashboard)/settings/index.tsx @@ -2,14 +2,14 @@ import { createFileRoute } from "@tanstack/react-router"; import { fetchUser } from "../route"; import type { AppContext } from "~/context"; import { SettingsPage } from "~/client/modules/settings/routes/settings"; -import { type } from "arktype"; +import { z } from "zod"; +import { getOrganizationContext } from "~/server/lib/functions/organization-context"; import { getOrgMembersOptions, getSsoSettingsOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { getOrigin } from "~/client/functions/get-origin"; -import { getOrganizationContext } from "~/server/lib/functions/organization-context"; export const Route = createFileRoute("/(dashboard)/settings/")({ component: RouteComponent, - validateSearch: type({ tab: "string?" }), + validateSearch: z.object({ tab: z.string().optional() }), errorComponent: () =>
Failed to load settings
, loader: async ({ context }) => { const [authContext, orgContext] = await Promise.all([ diff --git a/app/routes/(dashboard)/volumes/$volumeId.tsx b/app/routes/(dashboard)/volumes/$volumeId.tsx index c88d10d0..7dfc9610 100644 --- a/app/routes/(dashboard)/volumes/$volumeId.tsx +++ b/app/routes/(dashboard)/volumes/$volumeId.tsx @@ -1,5 +1,5 @@ import { createFileRoute } from "@tanstack/react-router"; -import { type } from "arktype"; +import { z } from "zod"; import { getVolumeOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { VolumeDetails } from "~/client/modules/volumes/routes/volume-details"; @@ -13,7 +13,7 @@ export const Route = createFileRoute("/(dashboard)/volumes/$volumeId")({ return res; }, - validateSearch: type({ tab: "string?" }), + validateSearch: z.object({ tab: z.string().optional() }), staticData: { breadcrumb: (match) => [ { label: "Volumes", href: "/volumes" }, diff --git a/app/schemas/events-dto.ts b/app/schemas/events-dto.ts index 17a2e5f3..02e892b8 100644 --- a/app/schemas/events-dto.ts +++ b/app/schemas/events-dto.ts @@ -1,85 +1,84 @@ -import { type } from "arktype"; +import { z } from "zod"; import { resticBackupProgressMetricsSchema, resticBackupRunSummarySchema } from "~/schemas/restic-dto"; -export const backupEventStatusSchema = type("'success' | 'error' | 'stopped' | 'warning'"); -export const restoreEventStatusSchema = type("'success' | 'error'"); +export const backupEventStatusSchema = z.enum(["success", "error", "stopped", "warning"]); +export const restoreEventStatusSchema = z.enum(["success", "error"]); -const backupEventBaseSchema = type({ - scheduleId: "string", - volumeName: "string", - repositoryName: "string", +const backupEventBaseSchema = z.object({ + scheduleId: z.string(), + volumeName: z.string(), + repositoryName: z.string(), }); -const organizationScopedSchema = type({ - organizationId: "string", +const organizationScopedSchema = z.object({ + organizationId: z.string(), }); -const restoreEventBaseSchema = type({ - repositoryId: "string", - snapshotId: "string", +const restoreEventBaseSchema = z.object({ + repositoryId: z.string(), + snapshotId: z.string(), }); -const dumpStartedEventSchema = type({ - repositoryId: "string", - snapshotId: "string", - path: "string", - filename: "string", +const dumpStartedEventSchema = z.object({ + repositoryId: z.string(), + snapshotId: z.string(), + path: z.string(), + filename: z.string(), }); -const restoreProgressMetricsSchema = type({ - seconds_elapsed: "number", - percent_done: "number", - total_files: "number", - files_restored: "number", - total_bytes: "number", - bytes_restored: "number", +const restoreProgressMetricsSchema = z.object({ + seconds_elapsed: z.number(), + percent_done: z.number(), + total_files: z.number(), + files_restored: z.number(), + total_bytes: z.number(), + bytes_restored: z.number(), }); export const backupStartedEventSchema = backupEventBaseSchema; -export const backupProgressEventSchema = backupEventBaseSchema.and(resticBackupProgressMetricsSchema); +export const backupProgressEventSchema = backupEventBaseSchema.extend(resticBackupProgressMetricsSchema.shape); -export const backupCompletedEventSchema = backupEventBaseSchema.and( - type({ - status: backupEventStatusSchema, - summary: resticBackupRunSummarySchema.optional(), - }), -); +export const backupCompletedEventSchema = backupEventBaseSchema.extend({ + status: backupEventStatusSchema, + summary: resticBackupRunSummarySchema.optional(), +}); export const restoreStartedEventSchema = restoreEventBaseSchema; -export const restoreProgressEventSchema = restoreEventBaseSchema.and(restoreProgressMetricsSchema); +export const restoreProgressEventSchema = restoreEventBaseSchema.extend(restoreProgressMetricsSchema.shape); -export const restoreCompletedEventSchema = restoreEventBaseSchema.and( - type({ status: restoreEventStatusSchema, error: "string?" }), -); +export const restoreCompletedEventSchema = restoreEventBaseSchema.extend({ + status: restoreEventStatusSchema, + error: z.string().optional(), +}); -export const serverBackupStartedEventSchema = organizationScopedSchema.and(backupStartedEventSchema); +export const serverBackupStartedEventSchema = organizationScopedSchema.extend(backupStartedEventSchema.shape); -export const serverBackupProgressEventSchema = organizationScopedSchema.and(backupProgressEventSchema); +export const serverBackupProgressEventSchema = organizationScopedSchema.extend(backupProgressEventSchema.shape); -export const serverBackupCompletedEventSchema = organizationScopedSchema.and(backupCompletedEventSchema); +export const serverBackupCompletedEventSchema = organizationScopedSchema.extend(backupCompletedEventSchema.shape); -export const serverRestoreStartedEventSchema = organizationScopedSchema.and(restoreStartedEventSchema); +export const serverRestoreStartedEventSchema = organizationScopedSchema.extend(restoreStartedEventSchema.shape); -export const serverRestoreProgressEventSchema = organizationScopedSchema.and(restoreProgressEventSchema); +export const serverRestoreProgressEventSchema = organizationScopedSchema.extend(restoreProgressEventSchema.shape); -export const serverRestoreCompletedEventSchema = organizationScopedSchema.and(restoreCompletedEventSchema); +export const serverRestoreCompletedEventSchema = organizationScopedSchema.extend(restoreCompletedEventSchema.shape); -export const serverDumpStartedEventSchema = organizationScopedSchema.and(dumpStartedEventSchema); +export const serverDumpStartedEventSchema = organizationScopedSchema.extend(dumpStartedEventSchema.shape); -export type BackupEventStatusDto = typeof backupEventStatusSchema.infer; -export type BackupStartedEventDto = typeof backupStartedEventSchema.infer; -export type BackupProgressEventDto = typeof backupProgressEventSchema.infer; -export type BackupCompletedEventDto = typeof backupCompletedEventSchema.infer; -export type RestoreStartedEventDto = typeof restoreStartedEventSchema.infer; -export type RestoreProgressEventDto = typeof restoreProgressEventSchema.infer; -export type RestoreCompletedEventDto = typeof restoreCompletedEventSchema.infer; -export type DumpStartedEventDto = typeof dumpStartedEventSchema.infer; -export type ServerBackupStartedEventDto = typeof serverBackupStartedEventSchema.infer; -export type ServerBackupProgressEventDto = typeof serverBackupProgressEventSchema.infer; -export type ServerBackupCompletedEventDto = typeof serverBackupCompletedEventSchema.infer; -export type ServerRestoreStartedEventDto = typeof serverRestoreStartedEventSchema.infer; -export type ServerRestoreProgressEventDto = typeof serverRestoreProgressEventSchema.infer; -export type ServerRestoreCompletedEventDto = typeof serverRestoreCompletedEventSchema.infer; -export type ServerDumpStartedEventDto = typeof serverDumpStartedEventSchema.infer; +export type BackupEventStatusDto = z.infer; +export type BackupStartedEventDto = z.infer; +export type BackupProgressEventDto = z.infer; +export type BackupCompletedEventDto = z.infer; +export type RestoreStartedEventDto = z.infer; +export type RestoreProgressEventDto = z.infer; +export type RestoreCompletedEventDto = z.infer; +export type DumpStartedEventDto = z.infer; +export type ServerBackupStartedEventDto = z.infer; +export type ServerBackupProgressEventDto = z.infer; +export type ServerBackupCompletedEventDto = z.infer; +export type ServerRestoreStartedEventDto = z.infer; +export type ServerRestoreProgressEventDto = z.infer; +export type ServerRestoreCompletedEventDto = z.infer; +export type ServerDumpStartedEventDto = z.infer; diff --git a/app/schemas/notifications.ts b/app/schemas/notifications.ts index d8909f6d..d93db511 100644 --- a/app/schemas/notifications.ts +++ b/app/schemas/notifications.ts @@ -1,4 +1,4 @@ -import { type } from "arktype"; +import { z } from "zod"; export const NOTIFICATION_TYPES = { email: "email", @@ -14,96 +14,98 @@ export const NOTIFICATION_TYPES = { export type NotificationType = keyof typeof NOTIFICATION_TYPES; -export const emailNotificationConfigSchema = type({ - type: "'email'", - smtpHost: "string", - smtpPort: "1 <= number <= 65535", - username: "string?", - password: "string?", - from: "string", - fromName: "string?", - to: "string[]", - useTLS: "boolean", +export const emailNotificationConfigSchema = z.object({ + type: z.literal("email"), + smtpHost: z.string().min(1), + smtpPort: z.number().int().min(1).max(65535), + username: z.string().optional(), + password: z.string().optional(), + from: z.string().min(1), + fromName: z.string().optional(), + to: z.array(z.string()), + useTLS: z.boolean(), }); -export const slackNotificationConfigSchema = type({ - type: "'slack'", - webhookUrl: "string", - channel: "string?", - username: "string?", - iconEmoji: "string?", +export const slackNotificationConfigSchema = z.object({ + type: z.literal("slack"), + webhookUrl: z.string().min(1), + channel: z.string().optional(), + username: z.string().optional(), + iconEmoji: z.string().optional(), }); -export const discordNotificationConfigSchema = type({ - type: "'discord'", - webhookUrl: "string", - username: "string?", - avatarUrl: "string?", - threadId: "string?", +export const discordNotificationConfigSchema = z.object({ + type: z.literal("discord"), + webhookUrl: z.string().min(1), + username: z.string().optional(), + avatarUrl: z.string().optional(), + threadId: z.string().optional(), }); -export const gotifyNotificationConfigSchema = type({ - type: "'gotify'", - serverUrl: "string", - token: "string", - path: "string?", - priority: "0 <= number <= 10", +export const gotifyNotificationConfigSchema = z.object({ + type: z.literal("gotify"), + serverUrl: z.string().min(1), + token: z.string().min(1), + path: z.string().optional(), + priority: z.number().min(0).max(10), }); -export const ntfyNotificationConfigSchema = type({ - type: "'ntfy'", - serverUrl: "string?", - topic: "string", - priority: "'max' | 'high' | 'default' | 'low' | 'min'", - username: "string?", - password: "string?", - accessToken: "string?", +export const ntfyNotificationConfigSchema = z.object({ + type: z.literal("ntfy"), + serverUrl: z.string().optional(), + topic: z.string().min(1), + priority: z.enum(["max", "high", "default", "low", "min"]), + username: z.string().optional(), + password: z.string().optional(), + accessToken: z.string().optional(), }); -export const pushoverNotificationConfigSchema = type({ - type: "'pushover'", - userKey: "string", - apiToken: "string", - devices: "string?", - priority: "-1 | 0 | 1", +export const pushoverNotificationConfigSchema = z.object({ + type: z.literal("pushover"), + userKey: z.string().min(1), + apiToken: z.string().min(1), + devices: z.string().optional(), + priority: z.union([z.literal(-1), z.literal(0), z.literal(1)]), }); -export const telegramNotificationConfigSchema = type({ - type: "'telegram'", - botToken: "string", - chatId: "string", - threadId: "string?", +export const telegramNotificationConfigSchema = z.object({ + type: z.literal("telegram"), + botToken: z.string().min(1), + chatId: z.string().min(1), + threadId: z.string().optional(), }); -export const genericNotificationConfigSchema = type({ - type: "'generic'", - url: "string", - method: "'GET' | 'POST'", - contentType: "string?", - headers: "string[]?", - useJson: "boolean?", - titleKey: "string?", - messageKey: "string?", +export const genericNotificationConfigSchema = z.object({ + type: z.literal("generic"), + url: z.string().min(1), + method: z.enum(["GET", "POST"]), + contentType: z.string().optional(), + headers: z.array(z.string()).optional(), + useJson: z.boolean().optional(), + titleKey: z.string().optional(), + messageKey: z.string().optional(), }); -export const customNotificationConfigSchema = type({ - type: "'custom'", - shoutrrrUrl: "string", +export const customNotificationConfigSchema = z.object({ + type: z.literal("custom"), + shoutrrrUrl: z.string().min(1), }); -export const notificationConfigSchemaBase = emailNotificationConfigSchema - .or(slackNotificationConfigSchema) - .or(discordNotificationConfigSchema) - .or(gotifyNotificationConfigSchema) - .or(ntfyNotificationConfigSchema) - .or(pushoverNotificationConfigSchema) - .or(telegramNotificationConfigSchema) - .or(genericNotificationConfigSchema) - .or(customNotificationConfigSchema); +export const notificationConfigSchemaBase = z.discriminatedUnion("type", [ + emailNotificationConfigSchema, + slackNotificationConfigSchema, + discordNotificationConfigSchema, + gotifyNotificationConfigSchema, + ntfyNotificationConfigSchema, + pushoverNotificationConfigSchema, + telegramNotificationConfigSchema, + genericNotificationConfigSchema, + customNotificationConfigSchema, +]); -export const notificationConfigSchema = notificationConfigSchemaBase.onUndeclaredKey("delete"); +export const notificationConfigSchema = notificationConfigSchemaBase; -export type NotificationConfig = typeof notificationConfigSchema.infer; +export type NotificationConfig = z.infer; export const NOTIFICATION_EVENTS = { start: "start", diff --git a/app/schemas/restic-dto.ts b/app/schemas/restic-dto.ts index 390cf265..e7185c7b 100644 --- a/app/schemas/restic-dto.ts +++ b/app/schemas/restic-dto.ts @@ -1,83 +1,72 @@ -import { type } from "arktype"; +import { z } from "zod"; -export const resticSummaryBaseSchema = type({ - files_new: "number", - files_changed: "number", - files_unmodified: "number", - dirs_new: "number", - dirs_changed: "number", - dirs_unmodified: "number", - data_blobs: "number", - tree_blobs: "number", - data_added: "number", - data_added_packed: "number?", - total_files_processed: "number", - total_bytes_processed: "number", +export const resticSummaryBaseSchema = z.object({ + files_new: z.number(), + files_changed: z.number(), + files_unmodified: z.number(), + dirs_new: z.number(), + dirs_changed: z.number(), + dirs_unmodified: z.number(), + data_blobs: z.number(), + tree_blobs: z.number(), + data_added: z.number(), + data_added_packed: z.number().optional(), + total_files_processed: z.number(), + total_bytes_processed: z.number(), }); -export const resticSnapshotSummarySchema = resticSummaryBaseSchema.and( - type({ - backup_start: "string", - backup_end: "string", - }), -); - -export const resticBackupRunSummarySchema = resticSummaryBaseSchema.and( - type({ - total_duration: "number", - snapshot_id: "string", - }), -); - -export const resticBackupOutputSchema = resticBackupRunSummarySchema.and( - type({ - message_type: "'summary'", - }), -); - -export const resticBackupProgressMetricsSchema = type({ - seconds_elapsed: "number", - seconds_remaining: "number = 0", - percent_done: "number", - total_files: "number", - files_done: "number", - total_bytes: "number", - bytes_done: "number", - current_files: type("string") - .array() - .default(() => []), +export const resticSnapshotSummarySchema = resticSummaryBaseSchema.extend({ + backup_start: z.string(), + backup_end: z.string(), }); -export const resticBackupProgressSchema = resticBackupProgressMetricsSchema.and( - type({ - message_type: "'status'", - }), -); - -export const resticRestoreOutputSchema = type({ - message_type: "'summary'", - total_files: "number?", - files_restored: "number", - files_skipped: "number", - total_bytes: "number?", - bytes_restored: "number?", - bytes_skipped: "number", +export const resticBackupRunSummarySchema = resticSummaryBaseSchema.extend({ + total_duration: z.number(), + snapshot_id: z.string(), }); -export const resticStatsSchema = type({ - total_size: "number = 0", - total_uncompressed_size: "number = 0", - compression_ratio: "number = 0", - compression_progress: "number = 0", - compression_space_saving: "number = 0", - snapshots_count: "number = 0", +export const resticBackupOutputSchema = resticBackupRunSummarySchema.extend({ + message_type: z.literal("summary"), }); -export type ResticSnapshotSummaryDto = typeof resticSnapshotSummarySchema.infer; -export type ResticBackupRunSummaryDto = typeof resticBackupRunSummarySchema.infer; -export type ResticBackupOutputDto = typeof resticBackupOutputSchema.infer; -export type ResticBackupProgressMetricsDto = typeof resticBackupProgressMetricsSchema.infer; -export type ResticBackupProgressDto = typeof resticBackupProgressSchema.infer; +export const resticBackupProgressMetricsSchema = z.object({ + seconds_elapsed: z.number(), + percent_done: z.number(), + total_files: z.number(), + files_done: z.number(), + total_bytes: z.number(), + bytes_done: z.number(), + current_files: z.array(z.string()).default([]), +}); -export type ResticRestoreOutputDto = typeof resticRestoreOutputSchema.infer; -export type ResticStatsDto = typeof resticStatsSchema.infer; +export const resticBackupProgressSchema = resticBackupProgressMetricsSchema.extend({ + message_type: z.literal("status"), +}); + +export const resticRestoreOutputSchema = z.object({ + message_type: z.literal("summary"), + total_files: z.number().optional(), + files_restored: z.number(), + files_skipped: z.number(), + total_bytes: z.number().optional(), + bytes_restored: z.number().optional(), + bytes_skipped: z.number(), +}); + +export const resticStatsSchema = z.object({ + total_size: z.number().default(0), + total_uncompressed_size: z.number().default(0), + compression_ratio: z.number().default(0), + compression_progress: z.number().default(0), + compression_space_saving: z.number().default(0), + snapshots_count: z.number().default(0), +}); + +export type ResticSnapshotSummaryDto = z.infer; +export type ResticBackupRunSummaryDto = z.infer; +export type ResticBackupOutputDto = z.infer; +export type ResticBackupProgressMetricsDto = z.infer; +export type ResticBackupProgressDto = z.infer; + +export type ResticRestoreOutputDto = z.infer; +export type ResticStatsDto = z.infer; diff --git a/app/schemas/restic.ts b/app/schemas/restic.ts index c92969c5..ee73f05a 100644 --- a/app/schemas/restic.ts +++ b/app/schemas/restic.ts @@ -1,4 +1,4 @@ -import { type } from "arktype"; +import { z } from "zod"; export const REPOSITORY_BACKENDS = { local: "local", @@ -21,98 +21,120 @@ export const BANDWIDTH_UNITS = { export type BandwidthUnit = keyof typeof BANDWIDTH_UNITS; -export const bandwidthLimitSchema = type({ - enabled: "boolean = false", - value: "number > 0 = 1", - unit: type.valueOf(BANDWIDTH_UNITS).default("Mbps"), +const bandwidthUnitSchema = z.enum(["Kbps", "Mbps", "Gbps"]); + +export const bandwidthLimitSchema = z.object({ + enabled: z.boolean().default(false), + value: z.number().positive().default(1), + unit: bandwidthUnitSchema.default("Mbps"), }); -export type BandwidthLimit = typeof bandwidthLimitSchema.infer; +export type BandwidthLimit = z.infer; -// Common fields for all repository configs -const baseRepositoryConfigSchema = type({ - isExistingRepository: "boolean?", - customPassword: "string?", - cacert: "string?", - insecureTls: "boolean?", - // Bandwidth controls +const baseRepositoryConfigSchema = z.object({ + isExistingRepository: z.boolean().optional(), + customPassword: z.string().optional(), + cacert: z.string().optional(), + insecureTls: z.boolean().optional(), uploadLimit: bandwidthLimitSchema.optional(), downloadLimit: bandwidthLimitSchema.optional(), }); -export const s3RepositoryConfigSchema = type({ - backend: "'s3'", - endpoint: "string", - bucket: "string", - accessKeyId: "string", - secretAccessKey: "string", -}).and(baseRepositoryConfigSchema); +export const s3RepositoryConfigSchema = z + .object({ + backend: z.literal("s3"), + endpoint: z.string().min(1), + bucket: z.string().min(1), + accessKeyId: z.string().min(1), + secretAccessKey: z.string().min(1), + }) + .extend(baseRepositoryConfigSchema.shape); -export const r2RepositoryConfigSchema = type({ - backend: "'r2'", - endpoint: "string", - bucket: "string", - accessKeyId: "string", - secretAccessKey: "string", -}).and(baseRepositoryConfigSchema); +export const r2RepositoryConfigSchema = z + .object({ + backend: z.literal("r2"), + endpoint: z.string().min(1), + bucket: z.string().min(1), + accessKeyId: z.string().min(1), + secretAccessKey: z.string().min(1), + }) + .extend(baseRepositoryConfigSchema.shape); -export const localRepositoryConfigSchema = type({ - backend: "'local'", - path: "string", -}).and(baseRepositoryConfigSchema); +export const localRepositoryConfigSchema = z + .object({ + backend: z.literal("local"), + path: z.string().min(1), + }) + .extend(baseRepositoryConfigSchema.shape); -export const gcsRepositoryConfigSchema = type({ - backend: "'gcs'", - bucket: "string", - projectId: "string", - credentialsJson: "string", -}).and(baseRepositoryConfigSchema); +export const gcsRepositoryConfigSchema = z + .object({ + backend: z.literal("gcs"), + bucket: z.string().min(1), + projectId: z.string().min(1), + credentialsJson: z.string().min(1), + }) + .extend(baseRepositoryConfigSchema.shape); -export const azureRepositoryConfigSchema = type({ - backend: "'azure'", - container: "string", - accountName: "string", - accountKey: "string", - endpointSuffix: "string?", -}).and(baseRepositoryConfigSchema); +export const azureRepositoryConfigSchema = z + .object({ + backend: z.literal("azure"), + container: z.string().min(1), + accountName: z.string().min(1), + accountKey: z.string().min(1), + endpointSuffix: z.string().optional(), + }) + .extend(baseRepositoryConfigSchema.shape); -export const rcloneRepositoryConfigSchema = type({ - backend: "'rclone'", - remote: "string", - path: "string", -}).and(baseRepositoryConfigSchema); +export const rcloneRepositoryConfigSchema = z + .object({ + backend: z.literal("rclone"), + remote: z.string().min(1), + path: z.string().min(1), + }) + .extend(baseRepositoryConfigSchema.shape); -export const restRepositoryConfigSchema = type({ - backend: "'rest'", - url: "string", - username: "string?", - password: "string?", - path: "string?", -}).and(baseRepositoryConfigSchema); +export const restRepositoryConfigSchema = z + .object({ + backend: z.literal("rest"), + url: z.string().min(1), + username: z.string().optional(), + password: z.string().optional(), + path: z.string().optional(), + }) + .extend(baseRepositoryConfigSchema.shape); -export const sftpRepositoryConfigSchema = type({ - backend: "'sftp'", - host: "string", - port: type("string.integer").or(type("number")).to("1 <= number <= 65535").default(22), - user: "string", - path: "string", - privateKey: "string", - skipHostKeyCheck: "boolean = true", - knownHosts: "string?", -}).and(baseRepositoryConfigSchema); +export const sftpRepositoryConfigSchema = z + .object({ + backend: z.literal("sftp"), + host: z.string().min(1), + port: z + .union([z.string(), z.number()]) + .transform((value) => (typeof value === "string" ? Number.parseInt(value, 10) : value)) + .pipe(z.number().int().min(1).max(65535)) + .default(22), + user: z.string().min(1), + path: z.string().min(1), + privateKey: z.string().min(1), + skipHostKeyCheck: z.boolean().default(true), + knownHosts: z.string().optional(), + }) + .extend(baseRepositoryConfigSchema.shape); -export const repositoryConfigSchemaBase = s3RepositoryConfigSchema - .or(r2RepositoryConfigSchema) - .or(localRepositoryConfigSchema) - .or(gcsRepositoryConfigSchema) - .or(azureRepositoryConfigSchema) - .or(rcloneRepositoryConfigSchema) - .or(restRepositoryConfigSchema) - .or(sftpRepositoryConfigSchema); +export const repositoryConfigSchemaBase = z.discriminatedUnion("backend", [ + s3RepositoryConfigSchema, + r2RepositoryConfigSchema, + localRepositoryConfigSchema, + gcsRepositoryConfigSchema, + azureRepositoryConfigSchema, + rcloneRepositoryConfigSchema, + restRepositoryConfigSchema, + sftpRepositoryConfigSchema, +]); -export const repositoryConfigSchema = repositoryConfigSchemaBase.onUndeclaredKey("delete"); +export const repositoryConfigSchema = repositoryConfigSchemaBase; -export type RepositoryConfig = typeof repositoryConfigSchema.infer; +export type RepositoryConfig = z.infer; export const COMPRESSION_MODES = { off: "off", @@ -132,22 +154,22 @@ export const REPOSITORY_STATUS = { export type RepositoryStatus = keyof typeof REPOSITORY_STATUS; -export const doctorStepSchema = type({ - step: "string", - success: "boolean", - output: "string | null", - error: "string | null", +export const doctorStepSchema = z.object({ + step: z.string(), + success: z.boolean(), + output: z.string().nullable(), + error: z.string().nullable(), }); -export type DoctorStep = typeof doctorStepSchema.infer; +export type DoctorStep = z.infer; -export const doctorResultSchema = type({ - success: "boolean", +export const doctorResultSchema = z.object({ + success: z.boolean(), steps: doctorStepSchema.array(), - completedAt: "number", + completedAt: z.number(), }); -export type DoctorResult = typeof doctorResultSchema.infer; +export type DoctorResult = z.infer; export const OVERWRITE_MODES = { always: "always", diff --git a/app/schemas/volumes.ts b/app/schemas/volumes.ts index dd57e713..cec43538 100644 --- a/app/schemas/volumes.ts +++ b/app/schemas/volumes.ts @@ -1,4 +1,4 @@ -import { type } from "arktype"; +import { z } from "zod"; export const BACKEND_TYPES = { nfs: "nfs", @@ -11,75 +11,93 @@ export const BACKEND_TYPES = { export type BackendType = keyof typeof BACKEND_TYPES; -export const nfsConfigSchema = type({ - backend: "'nfs'", - server: "string", - exportPath: "string", - port: type("string.integer").or(type("number")).to("1 <= number <= 65536").default(2049), - version: "'3' | '4' | '4.1'", - readOnly: "boolean?", +export const nfsConfigSchema = z.object({ + backend: z.literal("nfs"), + server: z.string().min(1), + exportPath: z.string().min(1), + port: z + .union([z.string(), z.number()]) + .transform((value) => (typeof value === "string" ? Number.parseInt(value, 10) : value)) + .pipe(z.number().int().min(1).max(65535)) + .default(2049), + version: z.enum(["3", "4", "4.1"]), + readOnly: z.boolean().optional(), }); -export const smbConfigSchema = type({ - backend: "'smb'", - server: "string", - share: "string", - username: "string?", - password: "string?", - guest: "boolean?", - vers: type("'1.0' | '2.0' | '2.1' | '3.0' | 'auto'").default("auto"), - domain: "string?", - port: type("string.integer").or(type("number")).to("1 <= number <= 65535").default(445), - readOnly: "boolean?", +export const smbConfigSchema = z.object({ + backend: z.literal("smb"), + server: z.string().min(1), + share: z.string().min(1), + username: z.string().optional(), + password: z.string().optional(), + guest: z.boolean().optional(), + vers: z.enum(["1.0", "2.0", "2.1", "3.0", "auto"]).default("auto"), + domain: z.string().optional(), + port: z + .union([z.string(), z.number()]) + .transform((value) => (typeof value === "string" ? Number.parseInt(value, 10) : value)) + .pipe(z.number().int().min(1).max(65535)) + .default(445), + readOnly: z.boolean().optional(), }); -export const directoryConfigSchema = type({ - backend: "'directory'", - path: "string", - readOnly: "false?", +export const directoryConfigSchema = z.object({ + backend: z.literal("directory"), + path: z.string().min(1), + readOnly: z.literal(false).optional(), }); -export const webdavConfigSchema = type({ - backend: "'webdav'", - server: "string", - path: "string", - username: "string?", - password: "string?", - port: type("string.integer").or(type("number")).to("1 <= number <= 65536").default(80), - readOnly: "boolean?", - ssl: "boolean?", +export const webdavConfigSchema = z.object({ + backend: z.literal("webdav"), + server: z.string().min(1), + path: z.string().min(1), + username: z.string().optional(), + password: z.string().optional(), + port: z + .union([z.string(), z.number()]) + .transform((value) => (typeof value === "string" ? Number.parseInt(value, 10) : value)) + .pipe(z.number().int().min(1).max(65535)) + .default(80), + readOnly: z.boolean().optional(), + ssl: z.boolean().optional(), }); -export const rcloneConfigSchema = type({ - backend: "'rclone'", - remote: "string", - path: "string", - readOnly: "boolean?", +export const rcloneConfigSchema = z.object({ + backend: z.literal("rclone"), + remote: z.string().min(1), + path: z.string().min(1), + readOnly: z.boolean().optional(), }); -export const sftpConfigSchema = type({ - backend: "'sftp'", - host: "string", - port: type("string.integer").or(type("number")).to("1 <= number <= 65535").default(22), - username: "string", - password: "string?", - privateKey: "string?", - path: "string", - readOnly: "boolean?", - skipHostKeyCheck: "boolean = true", - knownHosts: "string?", +export const sftpConfigSchema = z.object({ + backend: z.literal("sftp"), + host: z.string().min(1), + port: z + .union([z.string(), z.number()]) + .transform((value) => (typeof value === "string" ? Number.parseInt(value, 10) : value)) + .pipe(z.number().int().min(1).max(65535)) + .default(22), + username: z.string().min(1), + password: z.string().optional(), + privateKey: z.string().optional(), + path: z.string().min(1), + readOnly: z.boolean().optional(), + skipHostKeyCheck: z.boolean().default(true), + knownHosts: z.string().optional(), }); -export const volumeConfigSchemaBase = nfsConfigSchema - .or(smbConfigSchema) - .or(webdavConfigSchema) - .or(directoryConfigSchema) - .or(rcloneConfigSchema) - .or(sftpConfigSchema); +export const volumeConfigSchemaBase = z.discriminatedUnion("backend", [ + nfsConfigSchema, + smbConfigSchema, + webdavConfigSchema, + directoryConfigSchema, + rcloneConfigSchema, + sftpConfigSchema, +]); -export const volumeConfigSchema = volumeConfigSchemaBase.onUndeclaredKey("delete"); +export const volumeConfigSchema = volumeConfigSchemaBase; -export type BackendConfig = typeof volumeConfigSchema.infer; +export type BackendConfig = z.infer; export const BACKEND_STATUS = { mounted: "mounted", diff --git a/app/server/core/config.ts b/app/server/core/config.ts index a4392dc4..18284b95 100644 --- a/app/server/core/config.ts +++ b/app/server/core/config.ts @@ -1,6 +1,6 @@ import { readFileSync } from "node:fs"; import os from "node:os"; -import { type } from "arktype"; +import { z } from "zod"; import "dotenv/config"; const getResticHostname = () => { @@ -24,43 +24,45 @@ const getResticHostname = () => { return "zerobyte"; }; -const envSchema = type({ - NODE_ENV: type.enumerated("development", "production", "test").default("production"), - SERVER_IP: 'string = "localhost"', - SERVER_IDLE_TIMEOUT: 'string.integer.parse = "60"', - RESTIC_HOSTNAME: "string?", - PORT: 'string.integer.parse = "4096"', - MIGRATIONS_PATH: "string?", - APP_VERSION: "string = 'dev'", - TRUSTED_ORIGINS: "string?", - DISABLE_RATE_LIMITING: 'string = "false"', - APP_SECRET: "32 <= string <= 256", - BASE_URL: "string", - ENABLE_DEV_PANEL: 'string = "false"', -}).pipe((s) => ({ - __prod__: s.NODE_ENV === "production", - environment: s.NODE_ENV, - serverIp: s.SERVER_IP, - serverIdleTimeout: s.SERVER_IDLE_TIMEOUT, - resticHostname: s.RESTIC_HOSTNAME || getResticHostname(), - port: s.PORT, - migrationsPath: s.MIGRATIONS_PATH, - appVersion: s.APP_VERSION, - trustedOrigins: s.TRUSTED_ORIGINS?.split(",") - .map((origin) => origin.trim()) - .filter(Boolean) - .concat(s.BASE_URL) ?? [s.BASE_URL], - disableRateLimiting: s.DISABLE_RATE_LIMITING === "true", - appSecret: s.APP_SECRET, - baseUrl: s.BASE_URL, - isSecure: s.BASE_URL?.startsWith("https://") ?? false, - enableDevPanel: s.ENABLE_DEV_PANEL === "true", -})); +const envSchema = z + .object({ + NODE_ENV: z.enum(["development", "production", "test"]).default("production"), + SERVER_IP: z.string().default("localhost"), + SERVER_IDLE_TIMEOUT: z.coerce.number().int().default(60), + RESTIC_HOSTNAME: z.string().optional(), + PORT: z.coerce.number().int().default(4096), + MIGRATIONS_PATH: z.string().optional(), + APP_VERSION: z.string().default("dev"), + TRUSTED_ORIGINS: z.string().optional(), + DISABLE_RATE_LIMITING: z.string().default("false"), + APP_SECRET: z.string().min(32).max(256), + BASE_URL: z.string(), + ENABLE_DEV_PANEL: z.string().default("false"), + }) + .transform((s) => ({ + __prod__: s.NODE_ENV === "production", + environment: s.NODE_ENV, + serverIp: s.SERVER_IP, + serverIdleTimeout: s.SERVER_IDLE_TIMEOUT, + resticHostname: s.RESTIC_HOSTNAME || getResticHostname(), + port: s.PORT, + migrationsPath: s.MIGRATIONS_PATH, + appVersion: s.APP_VERSION, + trustedOrigins: s.TRUSTED_ORIGINS?.split(",") + .map((origin) => origin.trim()) + .filter(Boolean) + .concat(s.BASE_URL) ?? [s.BASE_URL], + disableRateLimiting: s.DISABLE_RATE_LIMITING === "true", + appSecret: s.APP_SECRET, + baseUrl: s.BASE_URL, + isSecure: s.BASE_URL?.startsWith("https://") ?? false, + enableDevPanel: s.ENABLE_DEV_PANEL === "true", + })); const parseConfig = (env: unknown) => { - const result = envSchema(env); + const result = envSchema.safeParse(env); - if (result instanceof type.errors) { + if (!result.success) { if (!process.env.APP_SECRET) { const errorMessage = [ "", @@ -82,11 +84,12 @@ const parseConfig = (env: unknown) => { console.error(errorMessage); } - console.error(`Environment variable validation failed: ${result.summary}`); + + console.error(`Environment variable validation failed: ${result.error.message}`); throw new Error("Invalid environment variables"); } - return result; + return result.data; }; export const config = parseConfig(process.env); diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts index 0d5e0f39..dc9e719a 100644 --- a/app/server/db/schema.ts +++ b/app/server/db/schema.ts @@ -3,14 +3,14 @@ import { index, int, integer, sqliteTable, text, real, primaryKey, unique, uniqu import type { CompressionMode, RepositoryBackend, - repositoryConfigSchema, + RepositoryConfig, RepositoryStatus, BandwidthUnit, DoctorResult, } from "~/schemas/restic"; import type { ResticStatsDto } from "~/schemas/restic-dto"; -import type { BackendStatus, BackendType, volumeConfigSchema } from "~/schemas/volumes"; -import type { NotificationType, notificationConfigSchema } from "~/schemas/notifications"; +import type { BackendConfig, BackendStatus, BackendType } from "~/schemas/volumes"; +import type { NotificationConfig, NotificationType } from "~/schemas/notifications"; import type { ShortId } from "~/server/utils/branded"; /** @@ -217,7 +217,7 @@ export const volumesTable = sqliteTable( .notNull() .$onUpdate(() => Date.now()) .default(sql`(unixepoch() * 1000)`), - config: text("config", { mode: "json" }).$type().notNull(), + config: text("config", { mode: "json" }).$type().notNull(), autoRemount: int("auto_remount", { mode: "boolean" }).notNull().default(true), organizationId: text("organization_id") .notNull() @@ -236,7 +236,7 @@ export const repositoriesTable = sqliteTable("repositories_table", { shortId: text("short_id").$type().notNull().unique(), name: text().notNull(), type: text().$type().notNull(), - config: text("config", { mode: "json" }).$type().notNull(), + config: text("config", { mode: "json" }).$type().notNull(), compressionMode: text("compression_mode").$type().default("auto"), status: text().$type().default("unknown"), lastChecked: int("last_checked", { mode: "number" }), @@ -319,7 +319,7 @@ export const notificationDestinationsTable = sqliteTable("notification_destinati name: text().notNull(), enabled: int("enabled", { mode: "boolean" }).notNull().default(true), type: text().$type().notNull(), - config: text("config", { mode: "json" }).$type().notNull(), + config: text("config", { mode: "json" }).$type().notNull(), createdAt: int("created_at", { mode: "number" }) .notNull() .default(sql`(unixepoch() * 1000)`), diff --git a/app/server/modules/auth/auth.dto.ts b/app/server/modules/auth/auth.dto.ts index eee61222..942d1ffb 100644 --- a/app/server/modules/auth/auth.dto.ts +++ b/app/server/modules/auth/auth.dto.ts @@ -1,26 +1,30 @@ -import { type } from "arktype"; +import { z } from "zod"; import { describeRoute, resolver } from "hono-openapi"; -const statusResponseSchema = type({ - hasUsers: "boolean", +const statusResponseSchema = z.object({ + hasUsers: z.boolean(), }); -export const adminUsersResponse = type({ - users: type({ - id: "string", - name: "string | null", - email: "string", - role: "string", - banned: "boolean", - accounts: type({ - id: "string", - providerId: "string", - }).array(), - }).array(), - total: "number", +export const adminUsersResponse = z.object({ + users: z + .object({ + id: z.string(), + name: z.string().nullable(), + email: z.string(), + role: z.string(), + banned: z.boolean(), + accounts: z + .object({ + id: z.string(), + providerId: z.string(), + }) + .array(), + }) + .array(), + total: z.number(), }); -export type AdminUsersDto = typeof adminUsersResponse.infer; +export type AdminUsersDto = z.infer; export const getAdminUsersDto = describeRoute({ description: "List admin users for settings management", @@ -54,21 +58,23 @@ export const getStatusDto = describeRoute({ }, }); -export type GetStatusDto = typeof statusResponseSchema.infer; +export type GetStatusDto = z.infer; -export const userDeletionImpactDto = type({ - organizations: type({ - id: "string", - name: "string", - resources: { - volumesCount: "number", - repositoriesCount: "number", - backupSchedulesCount: "number", - }, - }).array(), +export const userDeletionImpactDto = z.object({ + organizations: z + .object({ + id: z.string(), + name: z.string(), + resources: z.object({ + volumesCount: z.number(), + repositoriesCount: z.number(), + backupSchedulesCount: z.number(), + }), + }) + .array(), }); -export type UserDeletionImpactDto = typeof userDeletionImpactDto.infer; +export type UserDeletionImpactDto = z.infer; export const getUserDeletionImpactDto = describeRoute({ description: "Get impact of deleting a user", @@ -103,20 +109,22 @@ export const deleteUserAccountDto = describeRoute({ }, }); -export const orgMembersResponse = type({ - members: type({ - id: "string", - userId: "string", - role: "string", - createdAt: "string", - user: { - name: "string | null", - email: "string", - }, - }).array(), +export const orgMembersResponse = z.object({ + members: z + .object({ + id: z.string(), + userId: z.string(), + role: z.string(), + createdAt: z.string(), + user: z.object({ + name: z.string().nullable(), + email: z.string(), + }), + }) + .array(), }); -export type OrgMembersDto = typeof orgMembersResponse.infer; +export type OrgMembersDto = z.infer; export const getOrgMembersDto = describeRoute({ description: "Get members of the active organization", @@ -134,8 +142,8 @@ export const getOrgMembersDto = describeRoute({ }, }); -export const updateMemberRoleBody = type({ - role: "'member' | 'admin'", +export const updateMemberRoleBody = z.object({ + role: z.enum(["member", "admin"]), }); export const updateMemberRoleDto = describeRoute({ diff --git a/app/server/modules/backends/backend.ts b/app/server/modules/backends/backend.ts index 25f51411..25811167 100644 --- a/app/server/modules/backends/backend.ts +++ b/app/server/modules/backends/backend.ts @@ -41,5 +41,8 @@ export const createVolumeBackend = (volume: Volume): VolumeBackend => { case "sftp": { return makeSftpBackend(volume.config, path); } + default: { + throw new Error("Unsupported backend"); + } } }; diff --git a/app/server/modules/backups/backups.dto.ts b/app/server/modules/backups/backups.dto.ts index 560bb147..5cd08fa6 100644 --- a/app/server/modules/backups/backups.dto.ts +++ b/app/server/modules/backups/backups.dto.ts @@ -1,67 +1,61 @@ -import { type } from "arktype"; +import { z } from "zod"; import { describeRoute, resolver } from "hono-openapi"; import { volumeSchema } from "../volumes/volume.dto"; import { repositorySchema } from "../repositories/repositories.dto"; import { backupProgressEventSchema } from "~/schemas/events-dto"; -const retentionPolicySchema = type({ - keepLast: "number?", - keepHourly: "number?", - keepDaily: "number?", - keepWeekly: "number?", - keepMonthly: "number?", - keepYearly: "number?", - keepWithinDuration: "string?", +const retentionPolicySchema = z.object({ + keepLast: z.number().optional(), + keepHourly: z.number().optional(), + keepDaily: z.number().optional(), + keepWeekly: z.number().optional(), + keepMonthly: z.number().optional(), + keepYearly: z.number().optional(), + keepWithinDuration: z.string().optional(), }); -export type RetentionPolicy = typeof retentionPolicySchema.infer; +export type RetentionPolicy = z.infer; -const backupScheduleSchema = type({ - id: "number", - shortId: "string", - name: "string", - volumeId: "number", - repositoryId: "string", - enabled: "boolean", - cronExpression: "string", - retentionPolicy: retentionPolicySchema.or("null"), - excludePatterns: "string[] | null", - excludeIfPresent: "string[] | null", - includePatterns: "string[] | null", - oneFileSystem: "boolean", - customResticParams: "string[] | null", - lastBackupAt: "number | null", - lastBackupStatus: "'success' | 'error' | 'in_progress' | 'warning' | null", - lastBackupError: "string | null", - nextBackupAt: "number | null", - createdAt: "number", - updatedAt: "number", -}).and( - type({ - volume: volumeSchema, - repository: repositorySchema, - }), -); - -const scheduleMirrorSchema = type({ - scheduleId: "string", - repositoryId: "string", - enabled: "boolean", - lastCopyAt: "number | null", - lastCopyStatus: "'success' | 'error' | 'in_progress' | null", - lastCopyError: "string | null", - createdAt: "number", +const backupScheduleSchema = z.object({ + id: z.number(), + shortId: z.string(), + name: z.string(), + volumeId: z.number(), + repositoryId: z.string(), + enabled: z.boolean(), + cronExpression: z.string(), + retentionPolicy: retentionPolicySchema.nullable(), + excludePatterns: z.array(z.string()).nullable(), + excludeIfPresent: z.array(z.string()).nullable(), + includePatterns: z.array(z.string()).nullable(), + oneFileSystem: z.boolean(), + customResticParams: z.array(z.string()).nullable(), + lastBackupAt: z.number().nullable(), + lastBackupStatus: z.enum(["success", "error", "in_progress", "warning"]).nullable(), + lastBackupError: z.string().nullable(), + nextBackupAt: z.number().nullable(), + createdAt: z.number(), + updatedAt: z.number(), + volume: volumeSchema, repository: repositorySchema, }); -export type ScheduleMirrorDto = typeof scheduleMirrorSchema.infer; +const scheduleMirrorSchema = z.object({ + scheduleId: z.string(), + repositoryId: z.string(), + enabled: z.boolean(), + lastCopyAt: z.number().nullable(), + lastCopyStatus: z.enum(["success", "error", "in_progress"]).nullable(), + lastCopyError: z.string().nullable(), + createdAt: z.number(), + repository: repositorySchema, +}); + +export type ScheduleMirrorDto = z.infer; -/** - * List all backup schedules - */ export const listBackupSchedulesResponse = backupScheduleSchema.array(); -export type ListBackupSchedulesResponseDto = typeof listBackupSchedulesResponse.infer; +export type ListBackupSchedulesResponseDto = z.infer; export const listBackupSchedulesDto = describeRoute({ description: "List all backup schedules", @@ -79,12 +73,9 @@ export const listBackupSchedulesDto = describeRoute({ }, }); -/** - * Get a single backup schedule - */ export const getBackupScheduleResponse = backupScheduleSchema; -export type GetBackupScheduleDto = typeof getBackupScheduleResponse.infer; +export type GetBackupScheduleDto = z.infer; export const getBackupScheduleDto = describeRoute({ description: "Get a backup schedule by ID", @@ -102,9 +93,9 @@ export const getBackupScheduleDto = describeRoute({ }, }); -export const getBackupScheduleForVolumeResponse = backupScheduleSchema.or("null"); +export const getBackupScheduleForVolumeResponse = backupScheduleSchema.nullable(); -export type GetBackupScheduleForVolumeResponseDto = typeof getBackupScheduleForVolumeResponse.infer; +export type GetBackupScheduleForVolumeResponseDto = z.infer; export const getBackupScheduleForVolumeDto = describeRoute({ description: "Get a backup schedule for a specific volume", @@ -122,29 +113,26 @@ export const getBackupScheduleForVolumeDto = describeRoute({ }, }); -/** - * Create a new backup schedule - */ -export const createBackupScheduleBody = type({ - name: "1 <= string <= 128", - volumeId: "string | number", - repositoryId: "string", - enabled: "boolean", - cronExpression: "string", +export const createBackupScheduleBody = z.object({ + name: z.string().min(1).max(128), + volumeId: z.union([z.string(), z.number()]), + repositoryId: z.string(), + enabled: z.boolean(), + cronExpression: z.string(), retentionPolicy: retentionPolicySchema.optional(), - excludePatterns: "string[]?", - excludeIfPresent: "string[]?", - includePatterns: "string[]?", - oneFileSystem: "boolean?", - tags: "string[]?", - customResticParams: "string[]?", + excludePatterns: z.array(z.string()).optional(), + excludeIfPresent: z.array(z.string()).optional(), + includePatterns: z.array(z.string()).optional(), + oneFileSystem: z.boolean().optional(), + tags: z.array(z.string()).optional(), + customResticParams: z.array(z.string()).optional(), }); -export type CreateBackupScheduleBody = typeof createBackupScheduleBody.infer; +export type CreateBackupScheduleBody = z.infer; -export const createBackupScheduleResponse = backupScheduleSchema.omit("volume", "repository"); +export const createBackupScheduleResponse = backupScheduleSchema.omit({ volume: true, repository: true }); -export type CreateBackupScheduleDto = typeof createBackupScheduleResponse.infer; +export type CreateBackupScheduleDto = z.infer; export const createBackupScheduleDto = describeRoute({ description: "Create a new backup schedule for a volume", @@ -162,28 +150,25 @@ export const createBackupScheduleDto = describeRoute({ }, }); -/** - * Update a backup schedule - */ -export const updateBackupScheduleBody = type({ - name: "(1 <= string <= 128)?", - repositoryId: "string", - enabled: "boolean?", - cronExpression: "string", +export const updateBackupScheduleBody = z.object({ + name: z.string().min(1).max(128).optional(), + repositoryId: z.string(), + enabled: z.boolean().optional(), + cronExpression: z.string(), retentionPolicy: retentionPolicySchema.optional(), - excludePatterns: "string[]?", - excludeIfPresent: "string[]?", - includePatterns: "string[]?", - oneFileSystem: "boolean?", - tags: "string[]?", - customResticParams: "string[]?", + excludePatterns: z.array(z.string()).optional(), + excludeIfPresent: z.array(z.string()).optional(), + includePatterns: z.array(z.string()).optional(), + oneFileSystem: z.boolean().optional(), + tags: z.array(z.string()).optional(), + customResticParams: z.array(z.string()).optional(), }); -export type UpdateBackupScheduleBody = typeof updateBackupScheduleBody.infer; +export type UpdateBackupScheduleBody = z.infer; -export const updateBackupScheduleResponse = backupScheduleSchema.omit("volume", "repository"); +export const updateBackupScheduleResponse = backupScheduleSchema.omit({ volume: true, repository: true }); -export type UpdateBackupScheduleDto = typeof updateBackupScheduleResponse.infer; +export type UpdateBackupScheduleDto = z.infer; export const updateBackupScheduleDto = describeRoute({ description: "Update a backup schedule", @@ -201,14 +186,11 @@ export const updateBackupScheduleDto = describeRoute({ }, }); -/** - * Delete a backup schedule - */ -export const deleteBackupScheduleResponse = type({ - success: "boolean", +export const deleteBackupScheduleResponse = z.object({ + success: z.boolean(), }); -export type DeleteBackupScheduleDto = typeof deleteBackupScheduleResponse.infer; +export type DeleteBackupScheduleDto = z.infer; export const deleteBackupScheduleDto = describeRoute({ description: "Delete a backup schedule", @@ -226,14 +208,11 @@ export const deleteBackupScheduleDto = describeRoute({ }, }); -/** - * Run a backup immediately - */ -export const runBackupNowResponse = type({ - success: "boolean", +export const runBackupNowResponse = z.object({ + success: z.boolean(), }); -export type RunBackupNowDto = typeof runBackupNowResponse.infer; +export type RunBackupNowDto = z.infer; export const runBackupNowDto = describeRoute({ description: "Trigger a backup immediately for a schedule", @@ -251,14 +230,11 @@ export const runBackupNowDto = describeRoute({ }, }); -/** - * Stop a running backup - */ -export const stopBackupResponse = type({ - success: "boolean", +export const stopBackupResponse = z.object({ + success: z.boolean(), }); -export type StopBackupDto = typeof stopBackupResponse.infer; +export type StopBackupDto = z.infer; export const stopBackupDto = describeRoute({ description: "Stop a backup that is currently in progress", @@ -279,14 +255,11 @@ export const stopBackupDto = describeRoute({ }, }); -/** - * Run retention policy (forget) manually - */ -export const runForgetResponse = type({ - success: "boolean", +export const runForgetResponse = z.object({ + success: z.boolean(), }); -export type RunForgetDto = typeof runForgetResponse.infer; +export type RunForgetDto = z.infer; export const runForgetDto = describeRoute({ description: "Manually apply retention policy to clean up old snapshots", @@ -305,7 +278,7 @@ export const runForgetDto = describeRoute({ }); export const getScheduleMirrorsResponse = scheduleMirrorSchema.array(); -export type GetScheduleMirrorsDto = typeof getScheduleMirrorsResponse.infer; +export type GetScheduleMirrorsDto = z.infer; export const getScheduleMirrorsDto = describeRoute({ description: "Get mirror repository assignments for a backup schedule", @@ -323,17 +296,19 @@ export const getScheduleMirrorsDto = describeRoute({ }, }); -export const updateScheduleMirrorsBody = type({ - mirrors: type({ - repositoryId: "string", - enabled: "boolean", - }).array(), +export const updateScheduleMirrorsBody = z.object({ + mirrors: z + .object({ + repositoryId: z.string(), + enabled: z.boolean(), + }) + .array(), }); -export type UpdateScheduleMirrorsBody = typeof updateScheduleMirrorsBody.infer; +export type UpdateScheduleMirrorsBody = z.infer; export const updateScheduleMirrorsResponse = scheduleMirrorSchema.array(); -export type UpdateScheduleMirrorsDto = typeof updateScheduleMirrorsResponse.infer; +export type UpdateScheduleMirrorsDto = z.infer; export const updateScheduleMirrorsDto = describeRoute({ description: "Update mirror repository assignments for a backup schedule", @@ -351,14 +326,14 @@ export const updateScheduleMirrorsDto = describeRoute({ }, }); -const mirrorCompatibilitySchema = type({ - repositoryId: "string", - compatible: "boolean", - reason: "string | null", +const mirrorCompatibilitySchema = z.object({ + repositoryId: z.string(), + compatible: z.boolean(), + reason: z.string().nullable(), }); export const getMirrorCompatibilityResponse = mirrorCompatibilitySchema.array(); -export type GetMirrorCompatibilityDto = typeof getMirrorCompatibilityResponse.infer; +export type GetMirrorCompatibilityDto = z.infer; export const getMirrorCompatibilityDto = describeRoute({ description: "Get mirror compatibility info for all repositories relative to a backup schedule's primary repository", @@ -376,20 +351,17 @@ export const getMirrorCompatibilityDto = describeRoute({ }, }); -/** - * Reorder backup schedules - */ -export const reorderBackupSchedulesBody = type({ - scheduleShortIds: "string[]", +export const reorderBackupSchedulesBody = z.object({ + scheduleShortIds: z.array(z.string()), }); -export type ReorderBackupSchedulesBody = typeof reorderBackupSchedulesBody.infer; +export type ReorderBackupSchedulesBody = z.infer; -export const reorderBackupSchedulesResponse = type({ - success: "boolean", +export const reorderBackupSchedulesResponse = z.object({ + success: z.boolean(), }); -export type ReorderBackupSchedulesDto = typeof reorderBackupSchedulesResponse.infer; +export type ReorderBackupSchedulesDto = z.infer; export const reorderBackupSchedulesDto = describeRoute({ description: "Reorder backup schedules by providing an array of schedule short IDs in the desired order", @@ -407,11 +379,8 @@ export const reorderBackupSchedulesDto = describeRoute({ }, }); -/** - * Get current backup progress for a running backup - */ -const getBackupProgressResponse = backupProgressEventSchema.or("null"); -export type GetBackupProgressDto = typeof getBackupProgressResponse.infer; +const getBackupProgressResponse = backupProgressEventSchema.nullable(); +export type GetBackupProgressDto = z.infer; export const getBackupProgressDto = describeRoute({ description: diff --git a/app/server/modules/lifecycle/migrations/00004-concat-path-name.ts b/app/server/modules/lifecycle/migrations/00004-concat-path-name.ts index 77bec904..e9ec39af 100644 --- a/app/server/modules/lifecycle/migrations/00004-concat-path-name.ts +++ b/app/server/modules/lifecycle/migrations/00004-concat-path-name.ts @@ -5,7 +5,6 @@ import { logger } from "../../../utils/logger"; import { toMessage } from "~/server/utils/errors"; import { REPOSITORY_BASE } from "~/server/core/constants"; import { repositoryConfigSchema } from "~/schemas/restic"; -import { type } from "arktype"; type MigrationError = { name: string; error: string }; @@ -68,14 +67,15 @@ const execute = async () => { config.path = buildPath(currentPath, localRepositoryName); - const newConfig = repositoryConfigSchema(config); - if (newConfig instanceof type.errors) { + const newConfigResult = repositoryConfigSchema.safeParse(config); + if (!newConfigResult.success) { errors.push({ name: `repository:${repository.id}`, - error: `Validation failed for updated repository config: ${newConfig.summary}`, + error: `Validation failed for updated repository config: ${newConfigResult.error.message}`, }); continue; } + const newConfig = newConfigResult.data; await db .update(repositoriesTable) diff --git a/app/server/modules/notifications/notifications.dto.ts b/app/server/modules/notifications/notifications.dto.ts index 887a93ae..6acac0ed 100644 --- a/app/server/modules/notifications/notifications.dto.ts +++ b/app/server/modules/notifications/notifications.dto.ts @@ -1,27 +1,21 @@ -import { type } from "arktype"; +import { z } from "zod"; import { describeRoute, resolver } from "hono-openapi"; import { NOTIFICATION_TYPES, notificationConfigSchema } from "~/schemas/notifications"; -/** - * Notification Destination Schema - */ -export const notificationDestinationSchema = type({ - id: "number", - name: "string", - enabled: "boolean", - type: type.valueOf(NOTIFICATION_TYPES), +export const notificationDestinationSchema = z.object({ + id: z.number(), + name: z.string(), + enabled: z.boolean(), + type: z.enum(NOTIFICATION_TYPES), config: notificationConfigSchema, - createdAt: "number", - updatedAt: "number", + createdAt: z.number(), + updatedAt: z.number(), }); -export type NotificationDestinationDto = typeof notificationDestinationSchema.infer; +export type NotificationDestinationDto = z.infer; -/** - * List all notification destinations - */ export const listDestinationsResponse = notificationDestinationSchema.array(); -export type ListDestinationsDto = typeof listDestinationsResponse.infer; +export type ListDestinationsDto = z.infer; export const listDestinationsDto = describeRoute({ description: "List all notification destinations", @@ -39,16 +33,13 @@ export const listDestinationsDto = describeRoute({ }, }); -/** - * Create a new notification destination - */ -export const createDestinationBody = type({ - name: "string", +export const createDestinationBody = z.object({ + name: z.string(), config: notificationConfigSchema, }); export const createDestinationResponse = notificationDestinationSchema; -export type CreateDestinationDto = typeof createDestinationResponse.infer; +export type CreateDestinationDto = z.infer; export const createDestinationDto = describeRoute({ description: "Create a new notification destination", @@ -66,11 +57,8 @@ export const createDestinationDto = describeRoute({ }, }); -/** - * Get a single notification destination - */ export const getDestinationResponse = notificationDestinationSchema; -export type GetDestinationDto = typeof getDestinationResponse.infer; +export type GetDestinationDto = z.infer; export const getDestinationDto = describeRoute({ description: "Get a notification destination by ID", @@ -91,17 +79,14 @@ export const getDestinationDto = describeRoute({ }, }); -/** - * Update a notification destination - */ -export const updateDestinationBody = type({ - "name?": "string", - "enabled?": "boolean", - "config?": notificationConfigSchema, +export const updateDestinationBody = z.object({ + name: z.string().optional(), + enabled: z.boolean().optional(), + config: notificationConfigSchema.optional(), }); export const updateDestinationResponse = notificationDestinationSchema; -export type UpdateDestinationDto = typeof updateDestinationResponse.infer; +export type UpdateDestinationDto = z.infer; export const updateDestinationDto = describeRoute({ description: "Update a notification destination", @@ -122,13 +107,10 @@ export const updateDestinationDto = describeRoute({ }, }); -/** - * Delete a notification destination - */ -export const deleteDestinationResponse = type({ - message: "string", +export const deleteDestinationResponse = z.object({ + message: z.string(), }); -export type DeleteDestinationDto = typeof deleteDestinationResponse.infer; +export type DeleteDestinationDto = z.infer; export const deleteDestinationDto = describeRoute({ description: "Delete a notification destination", @@ -149,13 +131,10 @@ export const deleteDestinationDto = describeRoute({ }, }); -/** - * Test a notification destination - */ -export const testDestinationResponse = type({ - success: "boolean", +export const testDestinationResponse = z.object({ + success: z.boolean(), }); -export type TestDestinationDto = typeof testDestinationResponse.infer; +export type TestDestinationDto = z.infer; export const testDestinationDto = describeRoute({ description: "Test a notification destination by sending a test message", @@ -182,27 +161,21 @@ export const testDestinationDto = describeRoute({ }, }); -/** - * Backup Schedule Notification Assignment Schema - */ -export const scheduleNotificationAssignmentSchema = type({ - scheduleId: "number", - destinationId: "number", - notifyOnStart: "boolean", - notifyOnSuccess: "boolean", - notifyOnWarning: "boolean", - notifyOnFailure: "boolean", - createdAt: "number", +export const scheduleNotificationAssignmentSchema = z.object({ + scheduleId: z.number(), + destinationId: z.number(), + notifyOnStart: z.boolean(), + notifyOnSuccess: z.boolean(), + notifyOnWarning: z.boolean(), + notifyOnFailure: z.boolean(), + createdAt: z.number(), destination: notificationDestinationSchema, }); -export type ScheduleNotificationAssignmentDto = typeof scheduleNotificationAssignmentSchema.infer; +export type ScheduleNotificationAssignmentDto = z.infer; -/** - * Get notifications for a backup schedule - */ export const getScheduleNotificationsResponse = scheduleNotificationAssignmentSchema.array(); -export type GetScheduleNotificationsDto = typeof getScheduleNotificationsResponse.infer; +export type GetScheduleNotificationsDto = z.infer; export const getScheduleNotificationsDto = describeRoute({ description: "Get notification assignments for a backup schedule", @@ -220,21 +193,20 @@ export const getScheduleNotificationsDto = describeRoute({ }, }); -/** - * Update notifications for a backup schedule - */ -export const updateScheduleNotificationsBody = type({ - assignments: type({ - destinationId: "number", - notifyOnStart: "boolean", - notifyOnSuccess: "boolean", - notifyOnWarning: "boolean", - notifyOnFailure: "boolean", - }).array(), +export const updateScheduleNotificationsBody = z.object({ + assignments: z + .object({ + destinationId: z.number(), + notifyOnStart: z.boolean(), + notifyOnSuccess: z.boolean(), + notifyOnWarning: z.boolean(), + notifyOnFailure: z.boolean(), + }) + .array(), }); export const updateScheduleNotificationsResponse = scheduleNotificationAssignmentSchema.array(); -export type UpdateScheduleNotificationsDto = typeof updateScheduleNotificationsResponse.infer; +export type UpdateScheduleNotificationsDto = z.infer; export const updateScheduleNotificationsDto = describeRoute({ description: "Update notification assignments for a backup schedule", diff --git a/app/server/modules/notifications/notifications.service.ts b/app/server/modules/notifications/notifications.service.ts index 4f4222d6..31adb90f 100644 --- a/app/server/modules/notifications/notifications.service.ts +++ b/app/server/modules/notifications/notifications.service.ts @@ -14,7 +14,6 @@ import { buildShoutrrrUrl } from "./builders"; import { notificationConfigSchema, type NotificationConfig, type NotificationEvent } from "~/schemas/notifications"; import type { ResticBackupRunSummaryDto } from "~/schemas/restic-dto"; import { toMessage } from "../../utils/errors"; -import { type } from "arktype"; import { getOrganizationId } from "~/server/core/request-context"; import { formatBytes } from "~/utils/format-bytes"; @@ -192,10 +191,11 @@ const updateDestination = async ( updateData.enabled = updates.enabled; } - const newConfig = notificationConfigSchema(updates.config || existing.config); - if (newConfig instanceof type.errors) { + const newConfigResult = notificationConfigSchema.safeParse(updates.config || existing.config); + if (!newConfigResult.success) { throw new BadRequestError("Invalid notification configuration"); } + const newConfig = newConfigResult.data; const encryptedConfig = await encryptSensitiveFields(newConfig); updateData.config = encryptedConfig; diff --git a/app/server/modules/repositories/helpers/doctor.ts b/app/server/modules/repositories/helpers/doctor.ts index f4ad3a78..70ceb3f5 100644 --- a/app/server/modules/repositories/helpers/doctor.ts +++ b/app/server/modules/repositories/helpers/doctor.ts @@ -1,6 +1,6 @@ import { eq } from "drizzle-orm"; import { type DoctorStep, type DoctorResult, type RepositoryConfig } from "~/schemas/restic"; -import { type } from "arktype"; +import { z } from "zod"; import { getOrganizationId } from "~/server/core/request-context"; import { restic } from "~/server/utils/restic"; import { toMessage } from "~/server/utils/errors"; @@ -61,21 +61,24 @@ const runRepairIndexStep = async (config: RepositoryConfig, signal: AbortSignal) }; const parseCheckOutput = (checkOutput: string | null) => { - const schema = type({ suggest_repair_index: "boolean", suggest_prune: "boolean" }); + const schema = z.object({ + suggest_repair_index: z.boolean(), + suggest_prune: z.boolean(), + }); const parsedJson = safeJsonParse(checkOutput); if (parsedJson === null) { return null; } - const parsed = schema(parsedJson); + const parsed = schema.safeParse(parsedJson); - if (parsed instanceof type.errors) { - logger.error(`Invalid check output format: ${parsed.summary}`); + if (!parsed.success) { + logger.error(`Invalid check output format: ${parsed.error.message}`); return null; } - return parsed; + return parsed.data; }; const checkAbortSignal = (signal: AbortSignal | undefined): void => { diff --git a/app/server/modules/repositories/repositories.controller.ts b/app/server/modules/repositories/repositories.controller.ts index 5d7f3919..5e0515f7 100644 --- a/app/server/modules/repositories/repositories.controller.ts +++ b/app/server/modules/repositories/repositories.controller.ts @@ -179,8 +179,8 @@ export const repositoriesController = new Hono() const decodedPath = path ? decodeURIComponent(path) : undefined; - const offset = Math.max(0, Number.parseInt(query.offset ?? "0", 10) || 0); - const limit = Math.min(1000, Math.max(1, Number.parseInt(query.limit ?? "500", 10) || 500)); + const offset = Math.max(0, query.offset ?? 0); + const limit = Math.min(1000, Math.max(1, query.limit ?? 500)); const result = await repositoriesService.listSnapshotFiles(shortId, snapshotId, decodedPath, { offset, limit }); diff --git a/app/server/modules/repositories/repositories.dto.ts b/app/server/modules/repositories/repositories.dto.ts index 7d3a2d9b..db2e563f 100644 --- a/app/server/modules/repositories/repositories.dto.ts +++ b/app/server/modules/repositories/repositories.dto.ts @@ -1,4 +1,4 @@ -import { type } from "arktype"; +import { z } from "zod"; import { describeRoute, resolver } from "hono-openapi"; import { COMPRESSION_MODES, @@ -10,28 +10,25 @@ import { } from "~/schemas/restic"; import { resticSnapshotSummarySchema, resticStatsSchema } from "~/schemas/restic-dto"; -export const repositorySchema = type({ - id: "string", - shortId: "string", - name: "string", - type: type.valueOf(REPOSITORY_BACKENDS), +export const repositorySchema = z.object({ + id: z.string(), + shortId: z.string(), + name: z.string(), + type: z.enum(REPOSITORY_BACKENDS), config: repositoryConfigSchema, - compressionMode: type.valueOf(COMPRESSION_MODES).or("null"), - status: type.valueOf(REPOSITORY_STATUS).or("null"), - lastChecked: "number | null", - lastError: "string | null", - doctorResult: doctorResultSchema.or("null"), - createdAt: "number", - updatedAt: "number", + compressionMode: z.enum(COMPRESSION_MODES).nullable(), + status: z.enum(REPOSITORY_STATUS).nullable(), + lastChecked: z.number().nullable(), + lastError: z.string().nullable(), + doctorResult: doctorResultSchema.nullable(), + createdAt: z.number(), + updatedAt: z.number(), }); -export type RepositoryDto = typeof repositorySchema.infer; +export type RepositoryDto = z.infer; -/** - * List all repositories - */ export const listRepositoriesResponse = repositorySchema.array(); -export type ListRepositoriesDto = typeof listRepositoriesResponse.infer; +export type ListRepositoriesDto = z.infer; export const listRepositoriesDto = describeRoute({ description: "List all repositories", @@ -49,27 +46,24 @@ export const listRepositoriesDto = describeRoute({ }, }); -/** - * Create a new repository - */ -export const createRepositoryBody = type({ - name: "string", - compressionMode: type.valueOf(COMPRESSION_MODES).optional(), +export const createRepositoryBody = z.object({ + name: z.string(), + compressionMode: z.enum(COMPRESSION_MODES).optional(), config: repositoryConfigSchema, }); -export type CreateRepositoryBody = typeof createRepositoryBody.infer; +export type CreateRepositoryBody = z.infer; -export const createRepositoryResponse = type({ - message: "string", - repository: type({ - id: "string", - shortId: "string", - name: "string", +export const createRepositoryResponse = z.object({ + message: z.string(), + repository: z.object({ + id: z.string(), + shortId: z.string(), + name: z.string(), }), }); -export type CreateRepositoryDto = typeof createRepositoryResponse.infer; +export type CreateRepositoryDto = z.infer; export const createRepositoryDto = describeRoute({ description: "Create a new restic repository", @@ -87,11 +81,8 @@ export const createRepositoryDto = describeRoute({ }, }); -/** - * Get a single repository - */ export const getRepositoryResponse = repositorySchema; -export type GetRepositoryDto = typeof getRepositoryResponse.infer; +export type GetRepositoryDto = z.infer; export const getRepositoryDto = describeRoute({ description: "Get a single repository by ID", @@ -109,12 +100,9 @@ export const getRepositoryDto = describeRoute({ }, }); -/** - * Get repository stats - */ export const repositoryStatsSchema = resticStatsSchema; export const getRepositoryStatsResponse = repositoryStatsSchema; -export type GetRepositoryStatsDto = typeof getRepositoryStatsResponse.infer; +export type GetRepositoryStatsDto = z.infer; export const getRepositoryStatsDto = describeRoute({ description: "Get repository storage and compression statistics", @@ -133,7 +121,7 @@ export const getRepositoryStatsDto = describeRoute({ }); export const refreshRepositoryStatsResponse = repositoryStatsSchema; -export type RefreshRepositoryStatsDto = typeof refreshRepositoryStatsResponse.infer; +export type RefreshRepositoryStatsDto = z.infer; export const refreshRepositoryStatsDto = describeRoute({ description: "Refresh repository storage and compression statistics", @@ -151,14 +139,11 @@ export const refreshRepositoryStatsDto = describeRoute({ }, }); -/** - * Delete a repository - */ -export const deleteRepositoryResponse = type({ - message: "string", +export const deleteRepositoryResponse = z.object({ + message: z.string(), }); -export type DeleteRepositoryDto = typeof deleteRepositoryResponse.infer; +export type DeleteRepositoryDto = z.infer; export const deleteRepositoryDto = describeRoute({ description: "Delete a repository", @@ -176,19 +161,16 @@ export const deleteRepositoryDto = describeRoute({ }, }); -/** - * Update a repository - */ -export const updateRepositoryBody = type({ - name: "string?", - compressionMode: type.valueOf(COMPRESSION_MODES).optional(), +export const updateRepositoryBody = z.object({ + name: z.string().optional(), + compressionMode: z.enum(COMPRESSION_MODES).optional(), config: repositoryConfigSchema.optional(), }); -export type UpdateRepositoryBody = typeof updateRepositoryBody.infer; +export type UpdateRepositoryBody = z.infer; export const updateRepositoryResponse = repositorySchema; -export type UpdateRepositoryDto = typeof updateRepositoryResponse.infer; +export type UpdateRepositoryDto = z.infer; export const updateRepositoryDto = describeRoute({ description: "Update a repository's name or settings", @@ -215,27 +197,24 @@ export const updateRepositoryDto = describeRoute({ }, }); -/** - * List snapshots in a repository - */ -export const snapshotSchema = type({ - short_id: "string", - time: "number", - paths: "string[]", - size: "number", - duration: "number", - tags: "string[]", - retentionCategories: "string[]", - hostname: "string?", +export const snapshotSchema = z.object({ + short_id: z.string(), + time: z.number(), + paths: z.array(z.string()), + size: z.number(), + duration: z.number(), + tags: z.array(z.string()), + retentionCategories: z.array(z.string()), + hostname: z.string().optional(), summary: resticSnapshotSummarySchema.optional(), }); const listSnapshotsResponse = snapshotSchema.array(); -export type ListSnapshotsDto = typeof listSnapshotsResponse.infer; +export type ListSnapshotsDto = z.infer; -export const listSnapshotsFilters = type({ - backupId: "string?", +export const listSnapshotsFilters = z.object({ + backupId: z.string().optional(), }); export const listSnapshotsDto = describeRoute({ @@ -254,12 +233,9 @@ export const listSnapshotsDto = describeRoute({ }, }); -/** - * Get snapshot details - */ export const getSnapshotDetailsResponse = snapshotSchema; -export type GetSnapshotDetailsDto = typeof getSnapshotDetailsResponse.infer; +export type GetSnapshotDetailsDto = z.infer; export const getSnapshotDetailsDto = describeRoute({ description: "Get details of a specific snapshot", @@ -277,43 +253,40 @@ export const getSnapshotDetailsDto = describeRoute({ }, }); -/** - * List files in a snapshot - */ -export const snapshotFileNodeSchema = type({ - name: "string", - type: "string", - path: "string", - uid: "number?", - gid: "number?", - size: "number?", - mode: "number?", - mtime: "string?", - atime: "string?", - ctime: "string?", +export const snapshotFileNodeSchema = z.object({ + name: z.string(), + type: z.string(), + path: z.string(), + uid: z.number().optional(), + gid: z.number().optional(), + size: z.number().optional(), + mode: z.number().optional(), + mtime: z.string().optional(), + atime: z.string().optional(), + ctime: z.string().optional(), }); -export const listSnapshotFilesResponse = type({ - snapshot: type({ - id: "string", - short_id: "string", - time: "string", - hostname: "string", - paths: "string[]", +export const listSnapshotFilesResponse = z.object({ + snapshot: z.object({ + id: z.string(), + short_id: z.string(), + time: z.string(), + hostname: z.string().optional(), + paths: z.array(z.string()), }), files: snapshotFileNodeSchema.array(), - offset: "number", - limit: "number", - total: "number", - hasMore: "boolean", + offset: z.number(), + limit: z.number(), + total: z.number(), + hasMore: z.boolean(), }); -export type ListSnapshotFilesDto = typeof listSnapshotFilesResponse.infer; +export type ListSnapshotFilesDto = z.infer; -export const listSnapshotFilesQuery = type({ - path: "string?", - offset: "string.integer?", - limit: "string.integer?", +export const listSnapshotFilesQuery = z.object({ + path: z.string().optional(), + offset: z.coerce.number().int().optional(), + limit: z.coerce.number().int().optional(), }); export const listSnapshotFilesDto = describeRoute({ @@ -337,14 +310,11 @@ const DUMP_PATH_KINDS = { dir: "dir", } as const; -export const dumpPathKindSchema = type.valueOf(DUMP_PATH_KINDS); -export type DumpPathKind = typeof dumpPathKindSchema.infer; +export const dumpPathKindSchema = z.enum(DUMP_PATH_KINDS); +export type DumpPathKind = z.infer; -/** - * Download snapshot paths as tar archives (folders) or raw file streams (single files) - */ -export const dumpSnapshotQuery = type({ - path: "string?", +export const dumpSnapshotQuery = z.object({ + path: z.string().optional(), kind: dumpPathKindSchema.optional(), }); @@ -367,32 +337,29 @@ export const dumpSnapshotDto = describeRoute({ }, }); -/** - * Restore a snapshot - */ -export const overwriteModeSchema = type.valueOf(OVERWRITE_MODES); +export const overwriteModeSchema = z.enum(OVERWRITE_MODES); -export const restoreSnapshotBody = type({ - snapshotId: "string", - include: "string[]?", +export const restoreSnapshotBody = z.object({ + snapshotId: z.string(), + include: z.array(z.string()).optional(), selectedItemKind: dumpPathKindSchema.optional(), - exclude: "string[]?", - excludeXattr: "string[]?", - delete: "boolean?", - targetPath: "string?", + exclude: z.array(z.string()).optional(), + excludeXattr: z.array(z.string()).optional(), + delete: z.boolean().optional(), + targetPath: z.string().optional(), overwrite: overwriteModeSchema.optional(), }); -export type RestoreSnapshotBody = typeof restoreSnapshotBody.infer; +export type RestoreSnapshotBody = z.infer; -export const restoreSnapshotResponse = type({ - success: "boolean", - message: "string", - filesRestored: "number", - filesSkipped: "number", +export const restoreSnapshotResponse = z.object({ + success: z.boolean(), + message: z.string(), + filesRestored: z.number(), + filesSkipped: z.number(), }); -export type RestoreSnapshotDto = typeof restoreSnapshotResponse.infer; +export type RestoreSnapshotDto = z.infer; export const restoreSnapshotDto = describeRoute({ description: "Restore a snapshot to a target path on the filesystem", @@ -410,15 +377,12 @@ export const restoreSnapshotDto = describeRoute({ }, }); -/** - * Start doctor operation - */ -export const startDoctorResponse = type({ - message: "string", - repositoryId: "string", +export const startDoctorResponse = z.object({ + message: z.string(), + repositoryId: z.string(), }); -export type StartDoctorDto = typeof startDoctorResponse.infer; +export type StartDoctorDto = z.infer; export const startDoctorDto = describeRoute({ description: @@ -440,14 +404,11 @@ export const startDoctorDto = describeRoute({ }, }); -/** - * Cancel running doctor operation - */ -export const cancelDoctorResponse = type({ - message: "string", +export const cancelDoctorResponse = z.object({ + message: z.string(), }); -export type CancelDoctorDto = typeof cancelDoctorResponse.infer; +export type CancelDoctorDto = z.infer; export const cancelDoctorDto = describeRoute({ description: "Cancel a running doctor operation on a repository", @@ -468,12 +429,9 @@ export const cancelDoctorDto = describeRoute({ }, }); -/** - * List rclone available remotes - */ -const rcloneRemoteSchema = type({ - name: "string", - type: "string", +const rcloneRemoteSchema = z.object({ + name: z.string(), + type: z.string(), }); const listRcloneRemotesResponse = rcloneRemoteSchema.array(); @@ -494,14 +452,11 @@ export const listRcloneRemotesDto = describeRoute({ }, }); -/** - * Delete a snapshot - */ -export const deleteSnapshotResponse = type({ - message: "string", +export const deleteSnapshotResponse = z.object({ + message: z.string(), }); -export type DeleteSnapshotDto = typeof deleteSnapshotResponse.infer; +export type DeleteSnapshotDto = z.infer; export const deleteSnapshotDto = describeRoute({ description: "Delete a specific snapshot from a repository", @@ -519,18 +474,15 @@ export const deleteSnapshotDto = describeRoute({ }, }); -/** - * Delete multiple snapshots - */ -export const deleteSnapshotsBody = type({ - snapshotIds: "string[]>=1", +export const deleteSnapshotsBody = z.object({ + snapshotIds: z.array(z.string()).min(1), }); -export const deleteSnapshotsResponse = type({ - message: "string", +export const deleteSnapshotsResponse = z.object({ + message: z.string(), }); -export type DeleteSnapshotsResponseDto = typeof deleteSnapshotsResponse.infer; +export type DeleteSnapshotsResponseDto = z.infer; export const deleteSnapshotsDto = describeRoute({ description: "Delete multiple snapshots from a repository", @@ -548,21 +500,18 @@ export const deleteSnapshotsDto = describeRoute({ }, }); -/** - * Tag multiple snapshots - */ -export const tagSnapshotsBody = type({ - snapshotIds: "string[]>=1", - add: "string[]?", - remove: "string[]?", - set: "string[]?", +export const tagSnapshotsBody = z.object({ + snapshotIds: z.array(z.string()).min(1), + add: z.array(z.string()).optional(), + remove: z.array(z.string()).optional(), + set: z.array(z.string()).optional(), }); -export const tagSnapshotsResponse = type({ - message: "string", +export const tagSnapshotsResponse = z.object({ + message: z.string(), }); -export type TagSnapshotsResponseDto = typeof tagSnapshotsResponse.infer; +export type TagSnapshotsResponseDto = z.infer; export const tagSnapshotsDto = describeRoute({ description: "Tag multiple snapshots in a repository", @@ -580,15 +529,12 @@ export const tagSnapshotsDto = describeRoute({ }, }); -/** - * Refresh snapshots cache - */ -export const refreshSnapshotsResponse = type({ - message: "string", - count: "number", +export const refreshSnapshotsResponse = z.object({ + message: z.string(), + count: z.number(), }); -export type RefreshSnapshotsDto = typeof refreshSnapshotsResponse.infer; +export type RefreshSnapshotsDto = z.infer; export const refreshSnapshotsDto = describeRoute({ description: "Clear snapshot cache and force refresh from repository", @@ -606,12 +552,12 @@ export const refreshSnapshotsDto = describeRoute({ }, }); -export const devPanelExecBody = type({ - command: "string", - args: "string[]?", +export const devPanelExecBody = z.object({ + command: z.string(), + args: z.array(z.string()).optional(), }); -export type DevPanelExecBody = typeof devPanelExecBody.infer; +export type DevPanelExecBody = z.infer; export const devPanelExecDto = describeRoute({ description: "Execute a restic command against a repository (dev panel only)", @@ -632,15 +578,12 @@ export const devPanelExecDto = describeRoute({ }, }); -/** - * Unlock repository - */ -export const unlockRepositoryResponse = type({ - success: "boolean", - message: "string", +export const unlockRepositoryResponse = z.object({ + success: z.boolean(), + message: z.string(), }); -export type UnlockRepositoryDto = typeof unlockRepositoryResponse.infer; +export type UnlockRepositoryDto = z.infer; export const unlockRepositoryDto = describeRoute({ description: "Unlock a repository by removing all stale locks", diff --git a/app/server/modules/repositories/repositories.service.ts b/app/server/modules/repositories/repositories.service.ts index b5eacb80..d79a916f 100644 --- a/app/server/modules/repositories/repositories.service.ts +++ b/app/server/modules/repositories/repositories.service.ts @@ -1,6 +1,5 @@ import crypto from "node:crypto"; import nodePath from "node:path"; -import { type } from "arktype"; import { and, eq } from "drizzle-orm"; import { BadRequestError, ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced"; import { @@ -732,10 +731,11 @@ const updateRepository = async (shortId: ShortId, updates: UpdateRepositoryBody) throw new NotFoundError("Repository not found"); } - const existingConfig = repositoryConfigSchema(existing.config); - if (existingConfig instanceof type.errors) { + const existingConfigResult = repositoryConfigSchema.safeParse(existing.config); + if (!existingConfigResult.success) { throw new InternalServerError("Invalid repository configuration"); } + const existingConfig = existingConfigResult.data; let newName = existing.name; if (updates.name) { @@ -747,10 +747,11 @@ const updateRepository = async (shortId: ShortId, updates: UpdateRepositoryBody) let parsedConfig = existingConfig; if (updates.config) { - const nextConfig = repositoryConfigSchema(updates.config); - if (nextConfig instanceof type.errors) { + const nextConfigResult = repositoryConfigSchema.safeParse(updates.config); + if (!nextConfigResult.success) { throw new BadRequestError("Invalid repository configuration"); } + const nextConfig = nextConfigResult.data; if (nextConfig.backend !== existing.type) { throw new BadRequestError("Repository backend cannot be changed"); @@ -759,7 +760,7 @@ const updateRepository = async (shortId: ShortId, updates: UpdateRepositoryBody) parsedConfig = nextConfig; } - const decryptedExisting = existingConfig ? await decryptConfig(existingConfig) : null; + const decryptedExisting = await decryptConfig(existingConfig); const configChanged = updates.config && JSON.stringify(decryptedExisting) !== JSON.stringify(parsedConfig); const encryptedConfig = updates.config ? await encryptConfig(parsedConfig) : existingConfig; diff --git a/app/server/modules/sso/sso.dto.ts b/app/server/modules/sso/sso.dto.ts index 11da94a0..5b764d07 100644 --- a/app/server/modules/sso/sso.dto.ts +++ b/app/server/modules/sso/sso.dto.ts @@ -1,16 +1,16 @@ -import { type } from "arktype"; +import { z } from "zod"; import { describeRoute, resolver } from "hono-openapi"; -export const publicSsoProvidersDto = type({ - providers: type({ - providerId: "string", - organizationSlug: "string", - }) - .onUndeclaredKey("delete") +export const publicSsoProvidersDto = z.object({ + providers: z + .object({ + providerId: z.string(), + organizationSlug: z.string(), + }) .array(), }); -export type PublicSsoProvidersDto = typeof publicSsoProvidersDto.infer; +export type PublicSsoProvidersDto = z.infer; export const getPublicSsoProvidersDto = describeRoute({ description: "Get public SSO providers for the instance", @@ -28,25 +28,29 @@ export const getPublicSsoProvidersDto = describeRoute({ }, }); -export const ssoSettingsResponse = type({ - providers: type({ - providerId: "string", - type: "string", - issuer: "string", - domain: "string", - autoLinkMatchingEmails: "boolean", - organizationId: "string | null", - }).array(), - invitations: type({ - id: "string", - email: "string", - role: "string", - status: "string", - expiresAt: "string", - }).array(), +export const ssoSettingsResponse = z.object({ + providers: z + .object({ + providerId: z.string(), + type: z.string(), + issuer: z.string(), + domain: z.string(), + autoLinkMatchingEmails: z.boolean(), + organizationId: z.string().nullable(), + }) + .array(), + invitations: z + .object({ + id: z.string(), + email: z.string(), + role: z.string(), + status: z.string(), + expiresAt: z.string(), + }) + .array(), }); -export type SsoSettingsDto = typeof ssoSettingsResponse.infer; +export type SsoSettingsDto = z.infer; export const getSsoSettingsDto = describeRoute({ description: "Get SSO providers and invitations for the active organization", @@ -95,8 +99,8 @@ export const deleteSsoInvitationDto = describeRoute({ }, }); -export const updateSsoProviderAutoLinkingBody = type({ - enabled: "boolean", +export const updateSsoProviderAutoLinkingBody = z.object({ + enabled: z.boolean(), }); export const updateSsoProviderAutoLinkingDto = describeRoute({ diff --git a/app/server/modules/system/system.dto.ts b/app/server/modules/system/system.dto.ts index 354304bf..705ada28 100644 --- a/app/server/modules/system/system.dto.ts +++ b/app/server/modules/system/system.dto.ts @@ -1,32 +1,32 @@ -import { type } from "arktype"; +import { z } from "zod"; import { describeRoute, resolver } from "hono-openapi"; -export const capabilitiesSchema = type({ - rclone: "boolean", - sysAdmin: "boolean", +export const capabilitiesSchema = z.object({ + rclone: z.boolean(), + sysAdmin: z.boolean(), }); -export const systemInfoResponse = type({ +export const systemInfoResponse = z.object({ capabilities: capabilitiesSchema, }); -export type SystemInfoDto = typeof systemInfoResponse.infer; +export type SystemInfoDto = z.infer; -export const releaseInfoSchema = type({ - version: "string", - url: "string", - publishedAt: "string", - body: "string", +export const releaseInfoSchema = z.object({ + version: z.string(), + url: z.string(), + publishedAt: z.string(), + body: z.string(), }); -export const updateInfoResponse = type({ - currentVersion: "string", - latestVersion: "string", - hasUpdate: "boolean", +export const updateInfoResponse = z.object({ + currentVersion: z.string(), + latestVersion: z.string(), + hasUpdate: z.boolean(), missedReleases: releaseInfoSchema.array(), }); -export type UpdateInfoDto = typeof updateInfoResponse.infer; +export type UpdateInfoDto = z.infer; export const systemInfoDto = describeRoute({ description: "Get system information including available capabilities", @@ -60,8 +60,8 @@ export const getUpdatesDto = describeRoute({ }, }); -export const downloadResticPasswordBodySchema = type({ - password: "string", +export const downloadResticPasswordBodySchema = z.object({ + password: z.string(), }); export const downloadResticPasswordDto = describeRoute({ @@ -81,14 +81,14 @@ export const downloadResticPasswordDto = describeRoute({ }, }); -export const registrationStatusResponse = type({ - enabled: "boolean", +export const registrationStatusResponse = z.object({ + enabled: z.boolean(), }); -export type RegistrationStatusDto = typeof registrationStatusResponse.infer; +export type RegistrationStatusDto = z.infer; -export const registrationStatusBody = type({ - enabled: "boolean", +export const registrationStatusBody = z.object({ + enabled: z.boolean(), }); export const getRegistrationStatusDto = describeRoute({ @@ -123,11 +123,11 @@ export const setRegistrationStatusDto = describeRoute({ }, }); -export const devPanelResponse = type({ - enabled: "boolean", +export const devPanelResponse = z.object({ + enabled: z.boolean(), }); -export type DevPanelDto = typeof devPanelResponse.infer; +export type DevPanelDto = z.infer; export const getDevPanelDto = describeRoute({ description: "Get the dev panel status", diff --git a/app/server/modules/volumes/volume.controller.ts b/app/server/modules/volumes/volume.controller.ts index 6128de7b..2223f12f 100644 --- a/app/server/modules/volumes/volume.controller.ts +++ b/app/server/modules/volumes/volume.controller.ts @@ -110,8 +110,8 @@ export const volumeController = new Hono() const shortId = asShortId(c.req.param("shortId")); const { path, ...query } = c.req.valid("query"); - const offset = Math.max(0, Number.parseInt(query.offset ?? "0", 10) || 0); - const limit = Math.min(1000, Math.max(1, Number.parseInt(query.limit ?? "500", 10) || 500)); + const offset = Math.max(0, query.offset ?? 0); + const limit = Math.min(1000, Math.max(1, query.limit ?? 500)); const result = await volumeService.listFiles(shortId, path, offset, limit); diff --git a/app/server/modules/volumes/volume.dto.ts b/app/server/modules/volumes/volume.dto.ts index 01214f95..e407e535 100644 --- a/app/server/modules/volumes/volume.dto.ts +++ b/app/server/modules/volumes/volume.dto.ts @@ -1,28 +1,25 @@ -import { type } from "arktype"; +import { z } from "zod"; import { describeRoute, resolver } from "hono-openapi"; import { BACKEND_STATUS, BACKEND_TYPES, volumeConfigSchema } from "~/schemas/volumes"; -export const volumeSchema = type({ - id: "number", - shortId: "string", - name: "string", - type: type.valueOf(BACKEND_TYPES), - status: type.valueOf(BACKEND_STATUS), - lastError: "string | null", - createdAt: "number", - updatedAt: "number", - lastHealthCheck: "number", +export const volumeSchema = z.object({ + id: z.number(), + shortId: z.string(), + name: z.string(), + type: z.enum(BACKEND_TYPES), + status: z.enum(BACKEND_STATUS), + lastError: z.string().nullable(), + createdAt: z.number(), + updatedAt: z.number(), + lastHealthCheck: z.number(), config: volumeConfigSchema, - autoRemount: "boolean", + autoRemount: z.boolean(), }); -export type VolumeDto = typeof volumeSchema.infer; +export type VolumeDto = z.infer; -/** - * List all volumes - */ export const listVolumesResponse = volumeSchema.array(); -export type ListVolumesDto = typeof listVolumesResponse.infer; +export type ListVolumesDto = z.infer; export const listVolumesDto = describeRoute({ description: "List all volumes", @@ -40,16 +37,13 @@ export const listVolumesDto = describeRoute({ }, }); -/** - * Create a new volume - */ -export const createVolumeBody = type({ - name: "string", +export const createVolumeBody = z.object({ + name: z.string(), config: volumeConfigSchema, }); export const createVolumeResponse = volumeSchema; -export type CreateVolumeDto = typeof createVolumeResponse.infer; +export type CreateVolumeDto = z.infer; export const createVolumeDto = describeRoute({ description: "Create a new volume", @@ -67,13 +61,10 @@ export const createVolumeDto = describeRoute({ }, }); -/** - * Delete a volume - */ -export const deleteVolumeResponse = type({ - message: "string", +export const deleteVolumeResponse = z.object({ + message: z.string(), }); -export type DeleteVolumeDto = typeof deleteVolumeResponse.infer; +export type DeleteVolumeDto = z.infer; export const deleteVolumeDto = describeRoute({ description: "Delete a volume", @@ -91,21 +82,19 @@ export const deleteVolumeDto = describeRoute({ }, }); -const statfsSchema = type({ - total: "number", - used: "number", - free: "number", +const statfsSchema = z.object({ + total: z.number(), + used: z.number(), + free: z.number(), }); -const getVolumeResponse = type({ +const getVolumeResponse = z.object({ volume: volumeSchema, statfs: statfsSchema, }); -export type GetVolumeDto = typeof getVolumeResponse.infer; -/** - * Get a volume - */ +export type GetVolumeDto = z.infer; + export const getVolumeDto = describeRoute({ description: "Get a volume by name", operationId: "getVolume", @@ -125,19 +114,16 @@ export const getVolumeDto = describeRoute({ }, }); -/** - * Update a volume - */ -export const updateVolumeBody = type({ - name: "string?", - autoRemount: "boolean?", +export const updateVolumeBody = z.object({ + name: z.string().optional(), + autoRemount: z.boolean().optional(), config: volumeConfigSchema.optional(), }); -export type UpdateVolumeBody = typeof updateVolumeBody.infer; +export type UpdateVolumeBody = z.infer; export const updateVolumeResponse = volumeSchema; -export type UpdateVolumeDto = typeof updateVolumeResponse.infer; +export type UpdateVolumeDto = z.infer; export const updateVolumeDto = describeRoute({ description: "Update a volume's configuration", @@ -158,18 +144,15 @@ export const updateVolumeDto = describeRoute({ }, }); -/** - * Test connection - */ -export const testConnectionBody = type({ +export const testConnectionBody = z.object({ config: volumeConfigSchema, }); -export const testConnectionResponse = type({ - success: "boolean", - message: "string", +export const testConnectionResponse = z.object({ + success: z.boolean(), + message: z.string(), }); -export type TestConnectionDto = typeof testConnectionResponse.infer; +export type TestConnectionDto = z.infer; export const testConnectionDto = describeRoute({ description: "Test connection to backend", @@ -187,14 +170,11 @@ export const testConnectionDto = describeRoute({ }, }); -/** - * Mount volume - */ -export const mountVolumeResponse = type({ - error: "string?", - status: type.valueOf(BACKEND_STATUS), +export const mountVolumeResponse = z.object({ + error: z.string().optional(), + status: z.enum(BACKEND_STATUS), }); -export type MountVolumeDto = typeof mountVolumeResponse.infer; +export type MountVolumeDto = z.infer; export const mountVolumeDto = describeRoute({ description: "Mount a volume", @@ -212,14 +192,11 @@ export const mountVolumeDto = describeRoute({ }, }); -/** - * Unmount volume - */ -export const unmountVolumeResponse = type({ - error: "string?", - status: type.valueOf(BACKEND_STATUS), +export const unmountVolumeResponse = z.object({ + error: z.string().optional(), + status: z.enum(BACKEND_STATUS), }); -export type UnmountVolumeDto = typeof unmountVolumeResponse.infer; +export type UnmountVolumeDto = z.infer; export const unmountVolumeDto = describeRoute({ description: "Unmount a volume", @@ -237,11 +214,11 @@ export const unmountVolumeDto = describeRoute({ }, }); -export const healthCheckResponse = type({ - error: "string?", - status: type.valueOf(BACKEND_STATUS), +export const healthCheckResponse = z.object({ + error: z.string().optional(), + status: z.enum(BACKEND_STATUS), }); -export type HealthCheckDto = typeof healthCheckResponse.infer; +export type HealthCheckDto = z.infer; export const healthCheckDto = describeRoute({ description: "Perform a health check on a volume", @@ -262,31 +239,28 @@ export const healthCheckDto = describeRoute({ }, }); -/** - * List files in a volume - */ -const fileEntrySchema = type({ - name: "string", - path: "string", - type: type.enumerated("file", "directory"), - size: "number?", - modifiedAt: "number?", +const fileEntrySchema = z.object({ + name: z.string(), + path: z.string(), + type: z.enum(["file", "directory"]), + size: z.number().optional(), + modifiedAt: z.number().optional(), }); -export const listFilesResponse = type({ +export const listFilesResponse = z.object({ files: fileEntrySchema.array(), - path: "string", - offset: "number", - limit: "number", - total: "number", - hasMore: "boolean", + path: z.string(), + offset: z.number(), + limit: z.number(), + total: z.number(), + hasMore: z.boolean(), }); -export type ListFilesDto = typeof listFilesResponse.infer; +export type ListFilesDto = z.infer; -export const listFilesQuery = type({ - path: "string?", - offset: "string.integer?", - limit: "string.integer?", +export const listFilesQuery = z.object({ + path: z.string().optional(), + offset: z.coerce.number().int().optional(), + limit: z.coerce.number().int().optional(), }); export const listFilesDto = describeRoute({ @@ -305,14 +279,11 @@ export const listFilesDto = describeRoute({ }, }); -/** - * Browse filesystem directories - */ -export const browseFilesystemResponse = type({ +export const browseFilesystemResponse = z.object({ directories: fileEntrySchema.array(), - path: "string", + path: z.string(), }); -export type BrowseFilesystemDto = typeof browseFilesystemResponse.infer; +export type BrowseFilesystemDto = z.infer; export const browseFilesystemDto = describeRoute({ description: "Browse directories on the host filesystem", diff --git a/app/server/modules/volumes/volume.service.ts b/app/server/modules/volumes/volume.service.ts index ac122c35..76b2443c 100644 --- a/app/server/modules/volumes/volume.service.ts +++ b/app/server/modules/volumes/volume.service.ts @@ -16,7 +16,6 @@ import { getVolumePath } from "./helpers"; import { logger } from "../../utils/logger"; import { serverEvents } from "../../core/events"; import { volumeConfigSchema, type BackendConfig } from "~/schemas/volumes"; -import { type } from "arktype"; import { getOrganizationId } from "~/server/core/request-context"; import { isNodeJSErrnoException } from "~/server/utils/fs"; import { asShortId, type ShortId } from "~/server/utils/branded"; @@ -201,10 +200,11 @@ const updateVolume = async (shortId: ShortId, volumeData: UpdateVolumeBody) => { await backend.unmount(); } - const newConfig = volumeConfigSchema(volumeData.config || existing.config); - if (newConfig instanceof type.errors) { + const newConfigResult = volumeConfigSchema.safeParse(volumeData.config || existing.config); + if (!newConfigResult.success) { throw new BadRequestError("Invalid volume configuration"); } + const newConfig = newConfigResult.data; const encryptedConfig = await encryptSensitiveFields(newConfig); diff --git a/app/server/utils/restic/commands/backup.ts b/app/server/utils/restic/commands/backup.ts index 5b66222b..56094bde 100644 --- a/app/server/utils/restic/commands/backup.ts +++ b/app/server/utils/restic/commands/backup.ts @@ -1,7 +1,6 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { type } from "arktype"; import { throttle } from "es-toolkit"; import type { CompressionMode, RepositoryConfig } from "~/schemas/restic"; import { @@ -108,11 +107,11 @@ export const backup = async ( if (options.onProgress) { try { const jsonData = JSON.parse(data); - const progress = resticBackupProgressSchema(jsonData); - if (!(progress instanceof type.errors)) { - options.onProgress(progress); + const progressResult = resticBackupProgressSchema.safeParse(jsonData); + if (progressResult.success) { + options.onProgress(progressResult.data); } else { - logger.error(progress.summary); + logger.error(progressResult.error.message); } } catch { // Ignore JSON parse errors for non-JSON lines @@ -168,12 +167,12 @@ export const backup = async ( } logger.debug(`Restic backup output last line: ${JSON.stringify(summaryLine)}`); - const result = resticBackupOutputSchema(summaryLine); + const result = resticBackupOutputSchema.safeParse(summaryLine); - if (result instanceof type.errors) { - logger.error(`Restic backup output validation failed: ${result.summary}`); + if (!result.success) { + logger.error(`Restic backup output validation failed: ${result.error.message}`); return { result: null, exitCode: res.exitCode }; } - return { result, exitCode: res.exitCode }; + return { result: result.data, exitCode: res.exitCode }; }; diff --git a/app/server/utils/restic/commands/ls.ts b/app/server/utils/restic/commands/ls.ts index bff352f7..3493b802 100644 --- a/app/server/utils/restic/commands/ls.ts +++ b/app/server/utils/restic/commands/ls.ts @@ -1,4 +1,4 @@ -import { type } from "arktype"; +import { z } from "zod"; import type { RepositoryConfig } from "~/schemas/restic"; import { ResticError } from "~/server/utils/errors"; import { logger } from "~/server/utils/logger"; @@ -8,40 +8,54 @@ import { buildEnv } from "../helpers/build-env"; import { buildRepoUrl } from "../helpers/build-repo-url"; import { cleanupTemporaryKeys } from "../helpers/cleanup-temporary-keys"; -const lsNodeSchema = type({ - name: "string", - type: "string", - path: "string", - uid: "number?", - gid: "number?", - size: "number?", - mode: "number?", - mtime: "string?", - atime: "string?", - ctime: "string?", - struct_type: "'node'", +const lsNodeSchema = z.object({ + name: z.string(), + type: z.string(), + path: z.string(), + uid: z.number().optional(), + gid: z.number().optional(), + size: z.number().optional(), + mode: z.number().optional(), + mtime: z.string().optional(), + atime: z.string().optional(), + ctime: z.string().optional(), + struct_type: z.literal("node"), }); -const lsSnapshotInfoSchema = type({ - time: "string", - parent: "string?", - tree: "string", - paths: "string[]", - hostname: "string", - username: "string?", - id: "string", - short_id: "string", - struct_type: "'snapshot'", - message_type: "'snapshot'", +const lsSnapshotInfoSchema = z.object({ + time: z.string(), + parent: z.string().optional(), + tree: z.string(), + paths: z.array(z.string()), + hostname: z.string(), + username: z.string().optional(), + id: z.string(), + short_id: z.string(), + struct_type: z.literal("snapshot"), + message_type: z.literal("snapshot"), }); +type LsNode = z.infer; +type LsSnapshotInfo = z.infer; + +export type ResticLsResult = { + snapshot: LsSnapshotInfo | null; + nodes: LsNode[]; + pagination: { + offset: number; + limit: number; + total: number; + hasMore: boolean; + }; +}; + export const ls = async ( config: RepositoryConfig, snapshotId: string, organizationId: string, path?: string, options?: { offset?: number; limit?: number }, -) => { +): Promise => { const repoUrl = buildRepoUrl(config); const env = await buildEnv(config, organizationId); @@ -53,8 +67,8 @@ export const ls = async ( addCommonArgs(args, env, config); - let snapshot: typeof lsSnapshotInfoSchema.infer | null = null; - const nodes: Array = []; + let snapshot: LsSnapshotInfo | null = null; + const nodes: LsNode[] = []; let totalNodes = 0; let isFirstLine = true; let hasMore = false; @@ -77,21 +91,21 @@ export const ls = async ( if (isFirstLine) { isFirstLine = false; - const snapshotValidation = lsSnapshotInfoSchema(data); - if (!(snapshotValidation instanceof type.errors)) { - snapshot = snapshotValidation; + const snapshotValidation = lsSnapshotInfoSchema.safeParse(data); + if (snapshotValidation.success) { + snapshot = snapshotValidation.data; } return; } - const nodeValidation = lsNodeSchema(data); - if (nodeValidation instanceof type.errors) { - logger.warn(`Skipping invalid node: ${nodeValidation.summary}`); + const nodeValidation = lsNodeSchema.safeParse(data); + if (!nodeValidation.success) { + logger.warn(`Skipping invalid node: ${nodeValidation.error.message}`); return; } if (totalNodes >= offset && totalNodes < offset + limit) { - nodes.push(nodeValidation); + nodes.push(nodeValidation.data); } totalNodes++; @@ -116,7 +130,7 @@ export const ls = async ( } return { - snapshot: snapshot as typeof lsSnapshotInfoSchema.infer | null, + snapshot, nodes, pagination: { offset, diff --git a/app/server/utils/restic/commands/restore.ts b/app/server/utils/restic/commands/restore.ts index 29f1da9e..da7ff169 100644 --- a/app/server/utils/restic/commands/restore.ts +++ b/app/server/utils/restic/commands/restore.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import { type } from "arktype"; +import { z } from "zod"; import { throttle } from "es-toolkit"; import type { OverwriteMode, RepositoryConfig } from "~/schemas/restic"; import type { ResticRestoreOutputDto } from "~/schemas/restic-dto"; @@ -13,17 +13,17 @@ import { buildEnv } from "../helpers/build-env"; import { buildRepoUrl } from "../helpers/build-repo-url"; import { cleanupTemporaryKeys } from "../helpers/cleanup-temporary-keys"; -const restoreProgressSchema = type({ - message_type: "'status' | 'summary'", - seconds_elapsed: "number", - percent_done: "number = 0", - total_files: "number", - files_restored: "number = 0", - total_bytes: "number = 0", - bytes_restored: "number = 0", +const restoreProgressSchema = z.object({ + message_type: z.enum(["status", "summary"]), + seconds_elapsed: z.number(), + percent_done: z.number().default(0), + total_files: z.number(), + files_restored: z.number().default(0), + total_bytes: z.number().default(0), + bytes_restored: z.number().default(0), }); -export type RestoreProgress = typeof restoreProgressSchema.infer; +export type RestoreProgress = z.infer; export const restore = async ( config: RepositoryConfig, @@ -100,11 +100,11 @@ export const restore = async ( if (options.onProgress) { try { const jsonData = JSON.parse(data); - const progress = restoreProgressSchema(jsonData); - if (!(progress instanceof type.errors)) { - options.onProgress(progress); + const progress = restoreProgressSchema.safeParse(jsonData); + if (progress.success) { + options.onProgress(progress.data); } else { - logger.error(progress.summary); + logger.error(progress.error.message); } } catch { // Ignore JSON parse errors for non-JSON lines @@ -142,10 +142,10 @@ export const restore = async ( } logger.debug(`Restic restore output last line: ${JSON.stringify(summaryLine)}`); - const result = resticRestoreOutputSchema(summaryLine); + const result = resticRestoreOutputSchema.safeParse(summaryLine); - if (result instanceof type.errors) { - logger.warn(`Restic restore output validation failed: ${result.summary}`); + if (!result.success) { + logger.warn(`Restic restore output validation failed: ${result.error.message}`); logger.info(`Restic restore completed for snapshot ${snapshotId} to target ${target}`); const fallback: ResticRestoreOutputDto = { message_type: "summary" as const, @@ -159,8 +159,8 @@ export const restore = async ( } logger.info( - `Restic restore completed for snapshot ${snapshotId} to target ${target}: ${result.files_restored} restored, ${result.files_skipped} skipped`, + `Restic restore completed for snapshot ${snapshotId} to target ${target}: ${result.data.files_restored} restored, ${result.data.files_skipped} skipped`, ); - return result; + return result.data; }; diff --git a/app/server/utils/restic/commands/snapshots.ts b/app/server/utils/restic/commands/snapshots.ts index a628cafc..f5b43d15 100644 --- a/app/server/utils/restic/commands/snapshots.ts +++ b/app/server/utils/restic/commands/snapshots.ts @@ -1,4 +1,4 @@ -import { type } from "arktype"; +import { z } from "zod"; import type { RepositoryConfig } from "~/schemas/restic"; import { resticSnapshotSummarySchema } from "~/schemas/restic-dto"; import { logger } from "~/server/utils/logger"; @@ -8,18 +8,18 @@ import { buildEnv } from "../helpers/build-env"; import { buildRepoUrl } from "../helpers/build-repo-url"; import { cleanupTemporaryKeys } from "../helpers/cleanup-temporary-keys"; -const snapshotInfoSchema = type({ - gid: "number?", - hostname: "string", - id: "string", - parent: "string?", - paths: "string[]", - program_version: "string?", - short_id: "string", - time: "string", - uid: "number?", - username: "string?", - tags: "string[]?", +const snapshotInfoSchema = z.object({ + gid: z.number().optional(), + hostname: z.string(), + id: z.string(), + parent: z.string().optional(), + paths: z.array(z.string()), + program_version: z.string().optional(), + short_id: z.string(), + time: z.string(), + uid: z.number().optional(), + username: z.string().optional(), + tags: z.array(z.string()).optional(), summary: resticSnapshotSummarySchema.optional(), }); @@ -47,12 +47,12 @@ export const snapshots = async (config: RepositoryConfig, options: { tags?: stri throw new Error(`Restic snapshots retrieval failed: ${res.stderr}`); } - const result = snapshotInfoSchema.array()(JSON.parse(res.stdout)); + const result = snapshotInfoSchema.array().safeParse(JSON.parse(res.stdout)); - if (result instanceof type.errors) { - logger.error(`Restic snapshots output validation failed: ${result.summary}`); - throw new Error(`Restic snapshots output validation failed: ${result.summary}`); + if (!result.success) { + logger.error(`Restic snapshots output validation failed: ${result.error.message}`); + throw new Error(`Restic snapshots output validation failed: ${result.error.message}`); } - return result; + return result.data; }; diff --git a/app/server/utils/restic/commands/stats.ts b/app/server/utils/restic/commands/stats.ts index 112947d0..02acab3e 100644 --- a/app/server/utils/restic/commands/stats.ts +++ b/app/server/utils/restic/commands/stats.ts @@ -1,4 +1,3 @@ -import { type } from "arktype"; import type { RepositoryConfig } from "~/schemas/restic"; import { resticStatsSchema } from "~/schemas/restic-dto"; import { safeJsonParse } from "~/server/utils/json"; @@ -26,12 +25,12 @@ export const stats = async (config: RepositoryConfig, options: { organizationId: } const parsedJson = safeJsonParse(res.stdout); - const result = resticStatsSchema(parsedJson); + const result = resticStatsSchema.safeParse(parsedJson); - if (result instanceof type.errors) { - logger.error(`Restic stats output validation failed: ${result.summary}`); - throw new Error(`Restic stats output validation failed: ${result.summary}`); + if (!result.success) { + logger.error(`Restic stats output validation failed: ${result.error.message}`); + throw new Error(`Restic stats output validation failed: ${result.error.message}`); } - return result; + return result.data; }; diff --git a/bun.lock b/bun.lock index 4a5d9b81..9a62ee42 100644 --- a/bun.lock +++ b/bun.lock @@ -34,7 +34,6 @@ "@tanstack/react-router": "^1.166.2", "@tanstack/react-router-ssr-query": "^1.166.2", "@tanstack/react-start": "^1.166.2", - "arktype": "^2.2.0", "better-auth": "^1.5.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -69,6 +68,7 @@ "tiny-typed-emitter": "^2.1.0", "web-haptics": "^0.0.6", "yaml": "^2.8.2", + "zod": "^4.0.1", }, "devDependencies": { "@babel/preset-typescript": "^7.28.5", diff --git a/package.json b/package.json index b0600f0a..5c29473d 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "@tanstack/react-router": "^1.166.2", "@tanstack/react-router-ssr-query": "^1.166.2", "@tanstack/react-start": "^1.166.2", - "arktype": "^2.2.0", "better-auth": "^1.5.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -89,7 +88,8 @@ "tailwind-merge": "^3.5.0", "tiny-typed-emitter": "^2.1.0", "web-haptics": "^0.0.6", - "yaml": "^2.8.2" + "yaml": "^2.8.2", + "zod": "^4.0.1" }, "devDependencies": { "@babel/preset-typescript": "^7.28.5",