diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 9ac44460..8e135a75 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -841,7 +841,7 @@ export type ListRepositoriesResponses = { } | { backend: "local"; - name: string; + path: string; cacert?: string; customPassword?: string; downloadLimit?: { @@ -851,7 +851,6 @@ export type ListRepositoriesResponses = { }; insecureTls?: boolean; isExistingRepository?: boolean; - path?: string; uploadLimit?: { unit?: "Gbps" | "Kbps" | "Mbps"; value?: number; @@ -1034,7 +1033,7 @@ export type CreateRepositoryData = { } | { backend: "local"; - name: string; + path: string; cacert?: string; customPassword?: string; downloadLimit?: { @@ -1044,7 +1043,6 @@ export type CreateRepositoryData = { }; insecureTls?: boolean; isExistingRepository?: boolean; - path?: string; uploadLimit?: { unit?: "Gbps" | "Kbps" | "Mbps"; value?: number; @@ -1279,7 +1277,7 @@ export type GetRepositoryResponses = { } | { backend: "local"; - name: string; + path: string; cacert?: string; customPassword?: string; downloadLimit?: { @@ -1289,7 +1287,6 @@ export type GetRepositoryResponses = { }; insecureTls?: boolean; isExistingRepository?: boolean; - path?: string; uploadLimit?: { unit?: "Gbps" | "Kbps" | "Mbps"; value?: number; @@ -1387,6 +1384,172 @@ export type GetRepositoryResponse = GetRepositoryResponses[keyof GetRepositoryRe export type UpdateRepositoryData = { body?: { compressionMode?: "auto" | "max" | "off"; + 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; + }; + }; name?: string; }; path: { @@ -1397,6 +1560,10 @@ export type UpdateRepositoryData = { }; export type UpdateRepositoryErrors = { + /** + * Invalid repository update payload + */ + 400: unknown; /** * Repository not found */ @@ -1499,7 +1666,7 @@ export type UpdateRepositoryResponses = { } | { backend: "local"; - name: string; + path: string; cacert?: string; customPassword?: string; downloadLimit?: { @@ -1509,7 +1676,6 @@ export type UpdateRepositoryResponses = { }; insecureTls?: boolean; isExistingRepository?: boolean; - path?: string; uploadLimit?: { unit?: "Gbps" | "Kbps" | "Mbps"; value?: number; @@ -2077,7 +2243,7 @@ export type ListBackupSchedulesResponses = { } | { backend: "local"; - name: string; + path: string; cacert?: string; customPassword?: string; downloadLimit?: { @@ -2087,7 +2253,6 @@ export type ListBackupSchedulesResponses = { }; insecureTls?: boolean; isExistingRepository?: boolean; - path?: string; uploadLimit?: { unit?: "Gbps" | "Kbps" | "Mbps"; value?: number; @@ -2460,7 +2625,7 @@ export type GetBackupScheduleResponses = { } | { backend: "local"; - name: string; + path: string; cacert?: string; customPassword?: string; downloadLimit?: { @@ -2470,7 +2635,6 @@ export type GetBackupScheduleResponses = { }; insecureTls?: boolean; isExistingRepository?: boolean; - path?: string; uploadLimit?: { unit?: "Gbps" | "Kbps" | "Mbps"; value?: number; @@ -2824,7 +2988,7 @@ export type GetBackupScheduleForVolumeResponses = { } | { backend: "local"; - name: string; + path: string; cacert?: string; customPassword?: string; downloadLimit?: { @@ -2834,7 +2998,6 @@ export type GetBackupScheduleForVolumeResponses = { }; insecureTls?: boolean; isExistingRepository?: boolean; - path?: string; uploadLimit?: { unit?: "Gbps" | "Kbps" | "Mbps"; value?: number; @@ -3400,7 +3563,7 @@ export type GetScheduleMirrorsResponses = { } | { backend: "local"; - name: string; + path: string; cacert?: string; customPassword?: string; downloadLimit?: { @@ -3410,7 +3573,6 @@ export type GetScheduleMirrorsResponses = { }; insecureTls?: boolean; isExistingRepository?: boolean; - path?: string; uploadLimit?: { unit?: "Gbps" | "Kbps" | "Mbps"; value?: number; @@ -3620,7 +3782,7 @@ export type UpdateScheduleMirrorsResponses = { } | { backend: "local"; - name: string; + path: string; cacert?: string; customPassword?: string; downloadLimit?: { @@ -3630,7 +3792,6 @@ export type UpdateScheduleMirrorsResponses = { }; insecureTls?: boolean; isExistingRepository?: boolean; - path?: string; uploadLimit?: { unit?: "Gbps" | "Kbps" | "Mbps"; value?: number; diff --git a/app/client/components/snapshots-table.tsx b/app/client/components/snapshots-table.tsx index f87d7110..bcd96847 100644 --- a/app/client/components/snapshots-table.tsx +++ b/app/client/components/snapshots-table.tsx @@ -172,8 +172,8 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
e.stopPropagation()} className="hover:underline" > diff --git a/app/client/modules/backups/components/backup-card.tsx b/app/client/modules/backups/components/backup-card.tsx index c8a98bff..99b1adc2 100644 --- a/app/client/modules/backups/components/backup-card.tsx +++ b/app/client/modules/backups/components/backup-card.tsx @@ -7,7 +7,7 @@ import { Link } from "@tanstack/react-router"; export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => { return ( - +
diff --git a/app/client/modules/backups/components/snapshot-file-browser.tsx b/app/client/modules/backups/components/snapshot-file-browser.tsx index 0a672c80..a12942bf 100644 --- a/app/client/modules/backups/components/snapshot-file-browser.tsx +++ b/app/client/modules/backups/components/snapshot-file-browser.tsx @@ -95,11 +95,11 @@ export const SnapshotFileBrowser = (props: Props) => {
diff --git a/app/client/modules/repositories/components/create-repository-form.tsx b/app/client/modules/repositories/components/create-repository-form.tsx index a811b149..7561423b 100644 --- a/app/client/modules/repositories/components/create-repository-form.tsx +++ b/app/client/modules/repositories/components/create-repository-form.tsx @@ -33,6 +33,7 @@ import { SftpRepositoryForm, AdvancedForm, } from "./repository-forms"; +import { REPOSITORY_BASE } from "~/client/lib/constants"; export const formSchema = type({ name: "2<=string<=32", @@ -52,7 +53,7 @@ type Props = { }; const defaultValuesForType = { - local: { backend: "local" as const, compressionMode: "auto" as const }, + local: { backend: "local" as const, compressionMode: "auto" as const, path: REPOSITORY_BASE }, s3: { backend: "s3" as const, compressionMode: "auto" as const }, r2: { backend: "r2" as const, compressionMode: "auto" as const }, gcs: { backend: "gcs" as const, compressionMode: "auto" as const }, @@ -128,6 +129,7 @@ export const CreateRepositoryForm = ({ }); }} value={field.value} + disabled={mode === "update"} > @@ -192,6 +194,7 @@ export const CreateRepositoryForm = ({ { field.onChange(checked); if (!checked) { diff --git a/app/client/modules/repositories/routes/edit-repository.tsx b/app/client/modules/repositories/routes/edit-repository.tsx new file mode 100644 index 00000000..0f875b83 --- /dev/null +++ b/app/client/modules/repositories/routes/edit-repository.tsx @@ -0,0 +1,176 @@ +import { useMutation, useSuspenseQuery } from "@tanstack/react-query"; +import { Database } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { getRepositoryOptions, updateRepositoryMutation } from "~/client/api-client/@tanstack/react-query.gen"; +import { + CreateRepositoryForm, + type RepositoryFormValues, +} from "~/client/modules/repositories/components/create-repository-form"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "~/client/components/ui/alert-dialog"; +import { Button } from "~/client/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card"; +import { parseError } from "~/client/lib/errors"; +import { Alert, AlertDescription } from "~/client/components/ui/alert"; +import type { RepositoryConfig } from "~/schemas/restic"; +import { useNavigate } from "@tanstack/react-router"; + +const riskyLocationFieldsByBackend = { + local: ["path"], + s3: ["endpoint", "bucket"], + r2: ["endpoint", "bucket"], + gcs: ["bucket", "projectId"], + azure: ["container", "accountName", "endpointSuffix"], + rest: ["url", "path"], + sftp: ["host", "port", "path", "user"], + rclone: ["remote", "path"], +} as const; + +const hasRiskyLocationChange = (initialConfig: RepositoryConfig, nextConfig: RepositoryFormValues): boolean => { + const fields = riskyLocationFieldsByBackend[initialConfig.backend] ?? []; + + return fields.some( + (field) => initialConfig[field as keyof RepositoryConfig] !== nextConfig[field as keyof RepositoryFormValues], + ); +}; + +export function EditRepositoryPage({ repositoryId }: { repositoryId: string }) { + const navigate = useNavigate(); + const [showRiskConfirm, setShowRiskConfirm] = useState(false); + const [pendingValues, setPendingValues] = useState(null); + + const { data: repository } = useSuspenseQuery({ + ...getRepositoryOptions({ path: { id: repositoryId } }), + }); + + const updateRepository = useMutation({ + ...updateRepositoryMutation(), + onSuccess: async (data) => { + toast.success("Repository updated successfully"); + setShowRiskConfirm(false); + setPendingValues(null); + void navigate({ to: `/repositories/${data.shortId}` }); + }, + onError: (error) => { + toast.error("Failed to update repository", { + description: parseError(error)?.message, + }); + }, + }); + + const initialConfig = repository.config as RepositoryConfig; + const initialValues: RepositoryFormValues = { + ...initialConfig, + name: repository.name, + compressionMode: repository.compressionMode ?? "auto", + }; + + const submitUpdate = (values: RepositoryFormValues) => { + updateRepository.mutate({ + path: { id: repositoryId }, + body: { + name: values.name, + compressionMode: values.compressionMode, + config: values, + }, + }); + }; + + const handleSubmit = (values: RepositoryFormValues) => { + const nextConfig = values; + + if (hasRiskyLocationChange(initialConfig, nextConfig)) { + setPendingValues(values); + setShowRiskConfirm(true); + return; + } + + submitUpdate(values); + }; + + const handleSaveAnyway = () => { + if (!pendingValues) { + return; + } + + submitUpdate(pendingValues); + }; + + const handleRiskConfirmOpenChange = (open: boolean) => { + setShowRiskConfirm(open); + if (!open) { + setPendingValues(null); + } + }; + + return ( + <> +
+ + +
+
+ +
+ Edit Repository +
+
+ + {updateRepository.isError && ( + + + Failed to update repository: +
+ {parseError(updateRepository.error)?.message} +
+
+ )} + + +
+ +
+
+
+
+ + + + + Repository location changed + + Changing endpoint, bucket, host, or path fields may point to a different repository location. Before + saving, ensure the repository already exists at the new target. + + + + Cancel + + Save anyway + + + + + + ); +} diff --git a/app/client/modules/repositories/routes/repository-details.tsx b/app/client/modules/repositories/routes/repository-details.tsx index 3460db62..3fd8dc36 100644 --- a/app/client/modules/repositories/routes/repository-details.tsx +++ b/app/client/modules/repositories/routes/repository-details.tsx @@ -8,7 +8,7 @@ import { useNavigate, useSearch } from "@tanstack/react-router"; export default function RepositoryDetailsPage({ repositoryId }: { repositoryId: string }) { const navigate = useNavigate(); - const { tab } = useSearch({ from: "/(dashboard)/repositories/$repositoryId" }); + const { tab } = useSearch({ from: "/(dashboard)/repositories/$repositoryId/" }); const activeTab = tab || "info"; const { data } = useSuspenseQuery({ diff --git a/app/client/modules/repositories/routes/snapshot-details.tsx b/app/client/modules/repositories/routes/snapshot-details.tsx index a305164b..96298539 100644 --- a/app/client/modules/repositories/routes/snapshot-details.tsx +++ b/app/client/modules/repositories/routes/snapshot-details.tsx @@ -14,7 +14,7 @@ import { Database } from "lucide-react"; import { Link, useParams } from "@tanstack/react-router"; export const SnapshotError = () => { - const { repoId } = useParams({ from: "/(dashboard)/repositories/$repoId/$snapshotId" }); + const { repositoryId } = useParams({ from: "/(dashboard)/repositories/$repositoryId/$snapshotId/" }); return ( @@ -27,7 +27,7 @@ export const SnapshotError = () => { ({ tab: "snapshots" })} - params={{ repositoryId: repoId }} + params={{ repositoryId }} > @@ -125,9 +125,9 @@ export function SnapshotDetailsPage({ repositoryId, snapshotId }: { repositoryId Backup Schedule:

{backupSchedule?.name} diff --git a/app/client/modules/repositories/tabs/info.tsx b/app/client/modules/repositories/tabs/info.tsx index 41c1c66a..26eefee9 100644 --- a/app/client/modules/repositories/tabs/info.tsx +++ b/app/client/modules/repositories/tabs/info.tsx @@ -1,19 +1,15 @@ import { useMutation } from "@tanstack/react-query"; import { useState } from "react"; import { toast } from "sonner"; -import { Check, Save, Square, Stethoscope, Trash2, Unlock } from "lucide-react"; +import { Pencil, Square, Stethoscope, Trash2, Unlock } from "lucide-react"; import { Card } from "~/client/components/ui/card"; import { Button } from "~/client/components/ui/button"; -import { Input } from "~/client/components/ui/input"; -import { Label } from "~/client/components/ui/label"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, - AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "~/client/components/ui/alert-dialog"; @@ -25,9 +21,8 @@ import { deleteRepositoryMutation, startDoctorMutation, unlockRepositoryMutation, - updateRepositoryMutation, } from "~/client/api-client/@tanstack/react-query.gen"; -import type { CompressionMode, RepositoryConfig } from "~/schemas/restic"; +import type { RepositoryConfig } from "~/schemas/restic"; import { DoctorReport } from "../components/doctor-report"; import { parseError } from "~/client/lib/errors"; import { useNavigate } from "@tanstack/react-router"; @@ -37,38 +32,16 @@ type Props = { }; const getEffectiveLocalPath = (repository: Repository): string | null => { - if (repository.type !== "local") return null; - const config = repository.config as { name: string; path?: string; isExistingRepository?: boolean }; - - if (config.isExistingRepository) { - return config.path ?? null; - } - - const basePath = config.path || REPOSITORY_BASE; - return `${basePath}/${config.name}`; + if (repository.config.backend !== "local") return null; + return repository.config.path; }; export const RepositoryInfoTabContent = ({ repository }: Props) => { - const [name, setName] = useState(repository.name); - const [compressionMode, setCompressionMode] = useState(repository.compressionMode || "off"); - const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const navigate = useNavigate(); const effectiveLocalPath = getEffectiveLocalPath(repository); - const updateMutation = useMutation({ - ...updateRepositoryMutation(), - onSuccess: () => { - toast.success("Repository updated successfully"); - setShowConfirmDialog(false); - }, - onError: (error) => { - toast.error("Failed to update repository", { description: error.message, richColors: true }); - setShowConfirmDialog(false); - }, - }); - const deleteRepo = useMutation({ ...deleteRepositoryMutation(), onSuccess: () => { @@ -115,48 +88,40 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => { }, }); - const handleSubmit = (e: React.SubmitEvent) => { - e.preventDefault(); - setShowConfirmDialog(true); - }; - - const confirmUpdate = () => { - updateMutation.mutate({ - path: { id: repository.id }, - body: { name, compressionMode }, - }); - }; - const handleConfirmDelete = () => { setShowDeleteConfirm(false); deleteRepo.mutate({ path: { id: repository.id } }); }; - const hasChanges = - name !== repository.name || compressionMode !== ((repository.compressionMode as CompressionMode) || "off"); - const config = repository.config as RepositoryConfig; return ( <> - -

-
-
- Repository Settings -
-
- {repository.status === "doctor" ? ( - - ) : ( + +
+
+ Repository Settings +
+
+ + {repository.status === "doctor" ? ( + + ) : ( -
-
-
-
- - setName(e.target.value)} - placeholder="Repository name" - maxLength={32} - minLength={2} - /> -

Unique identifier for the repository.

-
-
- - -

Compression level for new data.

-
-
- -
-

Repository Information

-
-
-
Backend
-

{repository.type}

-
-
-
Status
-

{repository.status || "unknown"}

-
- {effectiveLocalPath && ( -
-
Local path
-

{effectiveLocalPath}

-
- )} -
-
Created at
-

{formatDateTime(repository.createdAt)}

-
-
-
Last checked
-

{formatTimeAgo(repository.lastChecked)}

-
- {config.cacert && ( -
-
CA Certificate
-

- configured -

-
- )} - {"insecureTls" in config && ( -
-
TLS Certificate Validation
-

- {config.insecureTls ? ( - disabled - ) : ( - enabled - )} -

-
- )} -
-
- - {repository.lastError && ( -
-
-

Last Error

-
-
-

{repository.lastError}

-
-
- )} - -
-

Configuration

-
-
{JSON.stringify(repository.config, null, 2)}
-
-
- - - -
-
- -
+
- - - - Update repository - Are you sure you want to update the repository settings? - - - Cancel - - - Update - - - - +
+

Current Configuration

+
+
+
Name
+

{repository.name}

+
+
+
Compression mode
+

{repository.compressionMode || "off"}

+
+
+
+ +
+

Repository Information

+
+
+
Backend
+

{repository.type}

+
+
+
Status
+

{repository.status || "unknown"}

+
+ {effectiveLocalPath && ( +
+
Local path
+

{effectiveLocalPath}

+
+ )} +
+
Created at
+

{formatDateTime(repository.createdAt)}

+
+
+
Last checked
+

{formatTimeAgo(repository.lastChecked)}

+
+ {config.cacert && ( +
+
CA Certificate
+

+ configured +

+
+ )} + {"insecureTls" in config && ( +
+
TLS Certificate Validation
+

+ {config.insecureTls ? ( + disabled + ) : ( + enabled + )} +

+
+ )} +
+
+ + {repository.lastError && ( +
+
+

Last Error

+
+
+

{repository.lastError}

+
+
+ )} + +
+

Configuration

+
+
{JSON.stringify(repository.config, null, 2)}
+
+
+ + + diff --git a/app/drizzle/20260212184359_concat-path-name/migration.sql b/app/drizzle/20260212184359_concat-path-name/migration.sql new file mode 100644 index 00000000..2227caf7 --- /dev/null +++ b/app/drizzle/20260212184359_concat-path-name/migration.sql @@ -0,0 +1,23 @@ +UPDATE `repositories_table` +SET + `config` = json_set( + `config`, + '$.path', + CASE + WHEN json_extract(`config`, '$.path') IS NULL OR trim(json_extract(`config`, '$.path')) = '' + THEN '/var/lib/zerobyte/repositories/' || json_extract(`config`, '$.name') + ELSE rtrim(json_extract(`config`, '$.path'), '/') || '/' || json_extract(`config`, '$.name') + END + ), + `updated_at` = (unixepoch() * 1000) +WHERE `type` = 'local' + AND json_extract(`config`, '$.name') IS NOT NULL + AND trim(json_extract(`config`, '$.name')) <> '' + AND ( + json_extract(`config`, '$.path') IS NULL + OR trim(json_extract(`config`, '$.path')) = '' + OR substr( + rtrim(json_extract(`config`, '$.path'), '/'), + -length('/' || json_extract(`config`, '$.name')) + ) != '/' || json_extract(`config`, '$.name') + ); diff --git a/app/drizzle/20260212184359_concat-path-name/snapshot.json b/app/drizzle/20260212184359_concat-path-name/snapshot.json new file mode 100644 index 00000000..0db364c3 --- /dev/null +++ b/app/drizzle/20260212184359_concat-path-name/snapshot.json @@ -0,0 +1,2154 @@ +{ + "id": "5201fdb9-eceb-4cc5-9ebe-bf1e2bd9b6dd", + "prevIds": [ + "3a308c54-d950-464f-9490-fee06985fbeb" + ], + "version": "7", + "dialect": "sqlite", + "ddl": [ + { + "name": "account", + "entityType": "tables" + }, + { + "name": "app_metadata", + "entityType": "tables" + }, + { + "name": "backup_schedule_mirrors_table", + "entityType": "tables" + }, + { + "name": "backup_schedule_notifications_table", + "entityType": "tables" + }, + { + "name": "backup_schedules_table", + "entityType": "tables" + }, + { + "name": "invitation", + "entityType": "tables" + }, + { + "name": "member", + "entityType": "tables" + }, + { + "name": "notification_destinations_table", + "entityType": "tables" + }, + { + "name": "organization", + "entityType": "tables" + }, + { + "name": "repositories_table", + "entityType": "tables" + }, + { + "name": "sessions_table", + "entityType": "tables" + }, + { + "name": "two_factor", + "entityType": "tables" + }, + { + "name": "users_table", + "entityType": "tables" + }, + { + "name": "verification", + "entityType": "tables" + }, + { + "name": "volumes_table", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "account_id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "provider_id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token_expires_at", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token_expires_at", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "scope", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "password", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "key", + "entityType": "columns", + "table": "app_metadata" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "value", + "entityType": "columns", + "table": "app_metadata" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "app_metadata" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "app_metadata" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "backup_schedule_mirrors_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "schedule_id", + "entityType": "columns", + "table": "backup_schedule_mirrors_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "repository_id", + "entityType": "columns", + "table": "backup_schedule_mirrors_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "true", + "generated": null, + "name": "enabled", + "entityType": "columns", + "table": "backup_schedule_mirrors_table" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_copy_at", + "entityType": "columns", + "table": "backup_schedule_mirrors_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_copy_status", + "entityType": "columns", + "table": "backup_schedule_mirrors_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_copy_error", + "entityType": "columns", + "table": "backup_schedule_mirrors_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "backup_schedule_mirrors_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "schedule_id", + "entityType": "columns", + "table": "backup_schedule_notifications_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "destination_id", + "entityType": "columns", + "table": "backup_schedule_notifications_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "notify_on_start", + "entityType": "columns", + "table": "backup_schedule_notifications_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "notify_on_success", + "entityType": "columns", + "table": "backup_schedule_notifications_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "true", + "generated": null, + "name": "notify_on_warning", + "entityType": "columns", + "table": "backup_schedule_notifications_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "true", + "generated": null, + "name": "notify_on_failure", + "entityType": "columns", + "table": "backup_schedule_notifications_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "backup_schedule_notifications_table" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "short_id", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "volume_id", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "repository_id", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "true", + "generated": null, + "name": "enabled", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "cron_expression", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "retention_policy", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": "'[]'", + "generated": null, + "name": "exclude_patterns", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": "'[]'", + "generated": null, + "name": "exclude_if_present", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": "'[]'", + "generated": null, + "name": "include_patterns", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_backup_at", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_backup_status", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_backup_error", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "next_backup_at", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "one_file_system", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "sort_order", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "organization_id", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "invitation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "organization_id", + "entityType": "columns", + "table": "invitation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "invitation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "role", + "entityType": "columns", + "table": "invitation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'pending'", + "generated": null, + "name": "status", + "entityType": "columns", + "table": "invitation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expires_at", + "entityType": "columns", + "table": "invitation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "invitation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "inviter_id", + "entityType": "columns", + "table": "invitation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "member" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "organization_id", + "entityType": "columns", + "table": "member" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "member" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'member'", + "generated": null, + "name": "role", + "entityType": "columns", + "table": "member" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "member" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "notification_destinations_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "notification_destinations_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "true", + "generated": null, + "name": "enabled", + "entityType": "columns", + "table": "notification_destinations_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "notification_destinations_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "config", + "entityType": "columns", + "table": "notification_destinations_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "notification_destinations_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "notification_destinations_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "organization_id", + "entityType": "columns", + "table": "notification_destinations_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "organization" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "organization" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "organization" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "logo", + "entityType": "columns", + "table": "organization" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "organization" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "metadata", + "entityType": "columns", + "table": "organization" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "short_id", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "config", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": "'auto'", + "generated": null, + "name": "compression_mode", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": "'unknown'", + "generated": null, + "name": "status", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_checked", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_error", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "doctor_result", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "upload_limit_enabled", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "real", + "notNull": true, + "autoincrement": false, + "default": "1", + "generated": null, + "name": "upload_limit_value", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'Mbps'", + "generated": null, + "name": "upload_limit_unit", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "download_limit_enabled", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "real", + "notNull": true, + "autoincrement": false, + "default": "1", + "generated": null, + "name": "download_limit_value", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'Mbps'", + "generated": null, + "name": "download_limit_unit", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "organization_id", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expires_at", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ip_address", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_agent", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "impersonated_by", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_organization_id", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "two_factor" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "two_factor" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "backup_codes", + "entityType": "columns", + "table": "two_factor" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "two_factor" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "username", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "password_hash", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "has_downloaded_restic_password", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "email_verified", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "image", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "display_username", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "two_factor_enabled", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'user'", + "generated": null, + "name": "role", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "banned", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ban_reason", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ban_expires", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "verification" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "identifier", + "entityType": "columns", + "table": "verification" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "value", + "entityType": "columns", + "table": "verification" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expires_at", + "entityType": "columns", + "table": "verification" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "verification" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "verification" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "short_id", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'unmounted'", + "generated": null, + "name": "status", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_error", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "last_health_check", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "config", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "true", + "generated": null, + "name": "auto_remount", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "organization_id", + "entityType": "columns", + "table": "volumes_table" + }, + { + "columns": [ + "user_id" + ], + "tableTo": "users_table", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "account_user_id_users_table_id_fk", + "entityType": "fks", + "table": "account" + }, + { + "columns": [ + "schedule_id" + ], + "tableTo": "backup_schedules_table", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk", + "entityType": "fks", + "table": "backup_schedule_mirrors_table" + }, + { + "columns": [ + "repository_id" + ], + "tableTo": "repositories_table", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk", + "entityType": "fks", + "table": "backup_schedule_mirrors_table" + }, + { + "columns": [ + "schedule_id" + ], + "tableTo": "backup_schedules_table", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk", + "entityType": "fks", + "table": "backup_schedule_notifications_table" + }, + { + "columns": [ + "destination_id" + ], + "tableTo": "notification_destinations_table", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk", + "entityType": "fks", + "table": "backup_schedule_notifications_table" + }, + { + "columns": [ + "volume_id" + ], + "tableTo": "volumes_table", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "backup_schedules_table_volume_id_volumes_table_id_fk", + "entityType": "fks", + "table": "backup_schedules_table" + }, + { + "columns": [ + "repository_id" + ], + "tableTo": "repositories_table", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "backup_schedules_table_repository_id_repositories_table_id_fk", + "entityType": "fks", + "table": "backup_schedules_table" + }, + { + "columns": [ + "organization_id" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "backup_schedules_table_organization_id_organization_id_fk", + "entityType": "fks", + "table": "backup_schedules_table" + }, + { + "columns": [ + "organization_id" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "invitation_organization_id_organization_id_fk", + "entityType": "fks", + "table": "invitation" + }, + { + "columns": [ + "inviter_id" + ], + "tableTo": "users_table", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "invitation_inviter_id_users_table_id_fk", + "entityType": "fks", + "table": "invitation" + }, + { + "columns": [ + "organization_id" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "member_organization_id_organization_id_fk", + "entityType": "fks", + "table": "member" + }, + { + "columns": [ + "user_id" + ], + "tableTo": "users_table", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "member_user_id_users_table_id_fk", + "entityType": "fks", + "table": "member" + }, + { + "columns": [ + "organization_id" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "notification_destinations_table_organization_id_organization_id_fk", + "entityType": "fks", + "table": "notification_destinations_table" + }, + { + "columns": [ + "organization_id" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "repositories_table_organization_id_organization_id_fk", + "entityType": "fks", + "table": "repositories_table" + }, + { + "columns": [ + "user_id" + ], + "tableTo": "users_table", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "sessions_table_user_id_users_table_id_fk", + "entityType": "fks", + "table": "sessions_table" + }, + { + "columns": [ + "user_id" + ], + "tableTo": "users_table", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "two_factor_user_id_users_table_id_fk", + "entityType": "fks", + "table": "two_factor" + }, + { + "columns": [ + "organization_id" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "volumes_table_organization_id_organization_id_fk", + "entityType": "fks", + "table": "volumes_table" + }, + { + "columns": [ + "schedule_id", + "destination_id" + ], + "nameExplicit": false, + "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk", + "entityType": "pks", + "table": "backup_schedule_notifications_table" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "key" + ], + "nameExplicit": false, + "name": "app_metadata_pk", + "table": "app_metadata", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "backup_schedule_mirrors_table_pk", + "table": "backup_schedule_mirrors_table", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "backup_schedules_table_pk", + "table": "backup_schedules_table", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "invitation_pk", + "table": "invitation", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "member_pk", + "table": "member", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "notification_destinations_table_pk", + "table": "notification_destinations_table", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "organization_pk", + "table": "organization", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "repositories_table_pk", + "table": "repositories_table", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "sessions_table_pk", + "table": "sessions_table", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "two_factor_pk", + "table": "two_factor", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "users_table_pk", + "table": "users_table", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "verification_pk", + "table": "verification", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "volumes_table_pk", + "table": "volumes_table", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "account_userId_idx", + "entityType": "indexes", + "table": "account" + }, + { + "columns": [ + { + "value": "organization_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "invitation_organizationId_idx", + "entityType": "indexes", + "table": "invitation" + }, + { + "columns": [ + { + "value": "email", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "invitation_email_idx", + "entityType": "indexes", + "table": "invitation" + }, + { + "columns": [ + { + "value": "organization_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "member_organizationId_idx", + "entityType": "indexes", + "table": "member" + }, + { + "columns": [ + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "member_userId_idx", + "entityType": "indexes", + "table": "member" + }, + { + "columns": [ + { + "value": "organization_id", + "isExpression": false + }, + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "member_org_user_uidx", + "entityType": "indexes", + "table": "member" + }, + { + "columns": [ + { + "value": "slug", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "organization_slug_uidx", + "entityType": "indexes", + "table": "organization" + }, + { + "columns": [ + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "sessionsTable_userId_idx", + "entityType": "indexes", + "table": "sessions_table" + }, + { + "columns": [ + { + "value": "secret", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "twoFactor_secret_idx", + "entityType": "indexes", + "table": "two_factor" + }, + { + "columns": [ + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "twoFactor_userId_idx", + "entityType": "indexes", + "table": "two_factor" + }, + { + "columns": [ + { + "value": "identifier", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "verification_identifier_idx", + "entityType": "indexes", + "table": "verification" + }, + { + "columns": [ + "schedule_id", + "repository_id" + ], + "nameExplicit": false, + "name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique", + "entityType": "uniques", + "table": "backup_schedule_mirrors_table" + }, + { + "columns": [ + "name", + "organization_id" + ], + "nameExplicit": false, + "name": "volumes_table_name_organization_id_unique", + "entityType": "uniques", + "table": "volumes_table" + }, + { + "columns": [ + "short_id" + ], + "nameExplicit": false, + "name": "backup_schedules_table_short_id_unique", + "entityType": "uniques", + "table": "backup_schedules_table" + }, + { + "columns": [ + "short_id" + ], + "nameExplicit": false, + "name": "repositories_table_short_id_unique", + "entityType": "uniques", + "table": "repositories_table" + }, + { + "columns": [ + "token" + ], + "nameExplicit": false, + "name": "sessions_table_token_unique", + "entityType": "uniques", + "table": "sessions_table" + }, + { + "columns": [ + "username" + ], + "nameExplicit": false, + "name": "users_table_username_unique", + "entityType": "uniques", + "table": "users_table" + }, + { + "columns": [ + "email" + ], + "nameExplicit": false, + "name": "users_table_email_unique", + "entityType": "uniques", + "table": "users_table" + }, + { + "columns": [ + "short_id" + ], + "nameExplicit": false, + "name": "volumes_table_short_id_unique", + "entityType": "uniques", + "table": "volumes_table" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/app/routeTree.gen.ts b/app/routeTree.gen.ts index 83a8e813..c9067284 100644 --- a/app/routeTree.gen.ts +++ b/app/routeTree.gen.ts @@ -23,14 +23,15 @@ import { Route as dashboardBackupsIndexRouteImport } from './routes/(dashboard)/ import { Route as dashboardVolumesCreateRouteImport } from './routes/(dashboard)/volumes/create' import { Route as dashboardVolumesVolumeIdRouteImport } from './routes/(dashboard)/volumes/$volumeId' import { Route as dashboardRepositoriesCreateRouteImport } from './routes/(dashboard)/repositories/create' -import { Route as dashboardRepositoriesRepositoryIdRouteImport } from './routes/(dashboard)/repositories/$repositoryId' import { Route as dashboardNotificationsCreateRouteImport } from './routes/(dashboard)/notifications/create' import { Route as dashboardNotificationsNotificationIdRouteImport } from './routes/(dashboard)/notifications/$notificationId' import { Route as dashboardBackupsCreateRouteImport } from './routes/(dashboard)/backups/create' -import { Route as dashboardBackupsScheduleIdRouteImport } from './routes/(dashboard)/backups/$scheduleId' -import { Route as dashboardRepositoriesRepoIdSnapshotIdRouteImport } from './routes/(dashboard)/repositories/$repoId.$snapshotId' -import { Route as dashboardRepositoriesRepoIdSnapIdRestoreRouteImport } from './routes/(dashboard)/repositories/$repoId.$snapId.restore' -import { Route as dashboardBackupsBackupIdSnapshotIdRestoreRouteImport } from './routes/(dashboard)/backups/$backupId.$snapshotId.restore' +import { Route as dashboardRepositoriesRepositoryIdIndexRouteImport } from './routes/(dashboard)/repositories/$repositoryId/index' +import { Route as dashboardBackupsBackupIdIndexRouteImport } from './routes/(dashboard)/backups/$backupId/index' +import { Route as dashboardRepositoriesRepositoryIdEditRouteImport } from './routes/(dashboard)/repositories/$repositoryId/edit' +import { Route as dashboardRepositoriesRepositoryIdSnapshotIdIndexRouteImport } from './routes/(dashboard)/repositories/$repositoryId/$snapshotId/index' +import { Route as dashboardRepositoriesRepositoryIdSnapshotIdRestoreRouteImport } from './routes/(dashboard)/repositories/$repositoryId/$snapshotId/restore' +import { Route as dashboardBackupsBackupIdSnapshotIdRestoreRouteImport } from './routes/(dashboard)/backups/$backupId/$snapshotId.restore' const dashboardRouteRoute = dashboardRouteRouteImport.update({ id: '/(dashboard)', @@ -105,12 +106,6 @@ const dashboardRepositoriesCreateRoute = path: '/repositories/create', getParentRoute: () => dashboardRouteRoute, } as any) -const dashboardRepositoriesRepositoryIdRoute = - dashboardRepositoriesRepositoryIdRouteImport.update({ - id: '/repositories/$repositoryId', - path: '/repositories/$repositoryId', - getParentRoute: () => dashboardRouteRoute, - } as any) const dashboardNotificationsCreateRoute = dashboardNotificationsCreateRouteImport.update({ id: '/notifications/create', @@ -128,22 +123,34 @@ const dashboardBackupsCreateRoute = dashboardBackupsCreateRouteImport.update({ path: '/backups/create', getParentRoute: () => dashboardRouteRoute, } as any) -const dashboardBackupsScheduleIdRoute = - dashboardBackupsScheduleIdRouteImport.update({ - id: '/backups/$scheduleId', - path: '/backups/$scheduleId', +const dashboardRepositoriesRepositoryIdIndexRoute = + dashboardRepositoriesRepositoryIdIndexRouteImport.update({ + id: '/repositories/$repositoryId/', + path: '/repositories/$repositoryId/', getParentRoute: () => dashboardRouteRoute, } as any) -const dashboardRepositoriesRepoIdSnapshotIdRoute = - dashboardRepositoriesRepoIdSnapshotIdRouteImport.update({ - id: '/repositories/$repoId/$snapshotId', - path: '/repositories/$repoId/$snapshotId', +const dashboardBackupsBackupIdIndexRoute = + dashboardBackupsBackupIdIndexRouteImport.update({ + id: '/backups/$backupId/', + path: '/backups/$backupId/', getParentRoute: () => dashboardRouteRoute, } as any) -const dashboardRepositoriesRepoIdSnapIdRestoreRoute = - dashboardRepositoriesRepoIdSnapIdRestoreRouteImport.update({ - id: '/repositories/$repoId/$snapId/restore', - path: '/repositories/$repoId/$snapId/restore', +const dashboardRepositoriesRepositoryIdEditRoute = + dashboardRepositoriesRepositoryIdEditRouteImport.update({ + id: '/repositories/$repositoryId/edit', + path: '/repositories/$repositoryId/edit', + getParentRoute: () => dashboardRouteRoute, + } as any) +const dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute = + dashboardRepositoriesRepositoryIdSnapshotIdIndexRouteImport.update({ + id: '/repositories/$repositoryId/$snapshotId/', + path: '/repositories/$repositoryId/$snapshotId/', + getParentRoute: () => dashboardRouteRoute, + } as any) +const dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute = + dashboardRepositoriesRepositoryIdSnapshotIdRestoreRouteImport.update({ + id: '/repositories/$repositoryId/$snapshotId/restore', + path: '/repositories/$repositoryId/$snapshotId/restore', getParentRoute: () => dashboardRouteRoute, } as any) const dashboardBackupsBackupIdSnapshotIdRestoreRoute = @@ -159,11 +166,9 @@ export interface FileRoutesByFullPath { '/login': typeof authLoginRoute '/onboarding': typeof authOnboardingRoute '/api/$': typeof ApiSplatRoute - '/backups/$scheduleId': typeof dashboardBackupsScheduleIdRoute '/backups/create': typeof dashboardBackupsCreateRoute '/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute '/notifications/create': typeof dashboardNotificationsCreateRoute - '/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute '/repositories/create': typeof dashboardRepositoriesCreateRoute '/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute '/volumes/create': typeof dashboardVolumesCreateRoute @@ -172,9 +177,12 @@ export interface FileRoutesByFullPath { '/repositories/': typeof dashboardRepositoriesIndexRoute '/settings/': typeof dashboardSettingsIndexRoute '/volumes/': typeof dashboardVolumesIndexRoute - '/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute + '/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute + '/backups/$backupId/': typeof dashboardBackupsBackupIdIndexRoute + '/repositories/$repositoryId/': typeof dashboardRepositoriesRepositoryIdIndexRoute '/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute - '/repositories/$repoId/$snapId/restore': typeof dashboardRepositoriesRepoIdSnapIdRestoreRoute + '/repositories/$repositoryId/$snapshotId/restore': typeof dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute + '/repositories/$repositoryId/$snapshotId/': typeof dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute @@ -182,11 +190,9 @@ export interface FileRoutesByTo { '/login': typeof authLoginRoute '/onboarding': typeof authOnboardingRoute '/api/$': typeof ApiSplatRoute - '/backups/$scheduleId': typeof dashboardBackupsScheduleIdRoute '/backups/create': typeof dashboardBackupsCreateRoute '/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute '/notifications/create': typeof dashboardNotificationsCreateRoute - '/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute '/repositories/create': typeof dashboardRepositoriesCreateRoute '/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute '/volumes/create': typeof dashboardVolumesCreateRoute @@ -195,9 +201,12 @@ export interface FileRoutesByTo { '/repositories': typeof dashboardRepositoriesIndexRoute '/settings': typeof dashboardSettingsIndexRoute '/volumes': typeof dashboardVolumesIndexRoute - '/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute + '/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute + '/backups/$backupId': typeof dashboardBackupsBackupIdIndexRoute + '/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdIndexRoute '/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute - '/repositories/$repoId/$snapId/restore': typeof dashboardRepositoriesRepoIdSnapIdRestoreRoute + '/repositories/$repositoryId/$snapshotId/restore': typeof dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute + '/repositories/$repositoryId/$snapshotId': typeof dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -207,11 +216,9 @@ export interface FileRoutesById { '/(auth)/login': typeof authLoginRoute '/(auth)/onboarding': typeof authOnboardingRoute '/api/$': typeof ApiSplatRoute - '/(dashboard)/backups/$scheduleId': typeof dashboardBackupsScheduleIdRoute '/(dashboard)/backups/create': typeof dashboardBackupsCreateRoute '/(dashboard)/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute '/(dashboard)/notifications/create': typeof dashboardNotificationsCreateRoute - '/(dashboard)/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute '/(dashboard)/repositories/create': typeof dashboardRepositoriesCreateRoute '/(dashboard)/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute '/(dashboard)/volumes/create': typeof dashboardVolumesCreateRoute @@ -220,9 +227,12 @@ export interface FileRoutesById { '/(dashboard)/repositories/': typeof dashboardRepositoriesIndexRoute '/(dashboard)/settings/': typeof dashboardSettingsIndexRoute '/(dashboard)/volumes/': typeof dashboardVolumesIndexRoute - '/(dashboard)/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute + '/(dashboard)/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute + '/(dashboard)/backups/$backupId/': typeof dashboardBackupsBackupIdIndexRoute + '/(dashboard)/repositories/$repositoryId/': typeof dashboardRepositoriesRepositoryIdIndexRoute '/(dashboard)/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute - '/(dashboard)/repositories/$repoId/$snapId/restore': typeof dashboardRepositoriesRepoIdSnapIdRestoreRoute + '/(dashboard)/repositories/$repositoryId/$snapshotId/restore': typeof dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute + '/(dashboard)/repositories/$repositoryId/$snapshotId/': typeof dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -232,11 +242,9 @@ export interface FileRouteTypes { | '/login' | '/onboarding' | '/api/$' - | '/backups/$scheduleId' | '/backups/create' | '/notifications/$notificationId' | '/notifications/create' - | '/repositories/$repositoryId' | '/repositories/create' | '/volumes/$volumeId' | '/volumes/create' @@ -245,9 +253,12 @@ export interface FileRouteTypes { | '/repositories/' | '/settings/' | '/volumes/' - | '/repositories/$repoId/$snapshotId' + | '/repositories/$repositoryId/edit' + | '/backups/$backupId/' + | '/repositories/$repositoryId/' | '/backups/$backupId/$snapshotId/restore' - | '/repositories/$repoId/$snapId/restore' + | '/repositories/$repositoryId/$snapshotId/restore' + | '/repositories/$repositoryId/$snapshotId/' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -255,11 +266,9 @@ export interface FileRouteTypes { | '/login' | '/onboarding' | '/api/$' - | '/backups/$scheduleId' | '/backups/create' | '/notifications/$notificationId' | '/notifications/create' - | '/repositories/$repositoryId' | '/repositories/create' | '/volumes/$volumeId' | '/volumes/create' @@ -268,9 +277,12 @@ export interface FileRouteTypes { | '/repositories' | '/settings' | '/volumes' - | '/repositories/$repoId/$snapshotId' + | '/repositories/$repositoryId/edit' + | '/backups/$backupId' + | '/repositories/$repositoryId' | '/backups/$backupId/$snapshotId/restore' - | '/repositories/$repoId/$snapId/restore' + | '/repositories/$repositoryId/$snapshotId/restore' + | '/repositories/$repositoryId/$snapshotId' id: | '__root__' | '/' @@ -279,11 +291,9 @@ export interface FileRouteTypes { | '/(auth)/login' | '/(auth)/onboarding' | '/api/$' - | '/(dashboard)/backups/$scheduleId' | '/(dashboard)/backups/create' | '/(dashboard)/notifications/$notificationId' | '/(dashboard)/notifications/create' - | '/(dashboard)/repositories/$repositoryId' | '/(dashboard)/repositories/create' | '/(dashboard)/volumes/$volumeId' | '/(dashboard)/volumes/create' @@ -292,9 +302,12 @@ export interface FileRouteTypes { | '/(dashboard)/repositories/' | '/(dashboard)/settings/' | '/(dashboard)/volumes/' - | '/(dashboard)/repositories/$repoId/$snapshotId' + | '/(dashboard)/repositories/$repositoryId/edit' + | '/(dashboard)/backups/$backupId/' + | '/(dashboard)/repositories/$repositoryId/' | '/(dashboard)/backups/$backupId/$snapshotId/restore' - | '/(dashboard)/repositories/$repoId/$snapId/restore' + | '/(dashboard)/repositories/$repositoryId/$snapshotId/restore' + | '/(dashboard)/repositories/$repositoryId/$snapshotId/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -406,13 +419,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof dashboardRepositoriesCreateRouteImport parentRoute: typeof dashboardRouteRoute } - '/(dashboard)/repositories/$repositoryId': { - id: '/(dashboard)/repositories/$repositoryId' - path: '/repositories/$repositoryId' - fullPath: '/repositories/$repositoryId' - preLoaderRoute: typeof dashboardRepositoriesRepositoryIdRouteImport - parentRoute: typeof dashboardRouteRoute - } '/(dashboard)/notifications/create': { id: '/(dashboard)/notifications/create' path: '/notifications/create' @@ -434,25 +440,39 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof dashboardBackupsCreateRouteImport parentRoute: typeof dashboardRouteRoute } - '/(dashboard)/backups/$scheduleId': { - id: '/(dashboard)/backups/$scheduleId' - path: '/backups/$scheduleId' - fullPath: '/backups/$scheduleId' - preLoaderRoute: typeof dashboardBackupsScheduleIdRouteImport + '/(dashboard)/repositories/$repositoryId/': { + id: '/(dashboard)/repositories/$repositoryId/' + path: '/repositories/$repositoryId' + fullPath: '/repositories/$repositoryId/' + preLoaderRoute: typeof dashboardRepositoriesRepositoryIdIndexRouteImport parentRoute: typeof dashboardRouteRoute } - '/(dashboard)/repositories/$repoId/$snapshotId': { - id: '/(dashboard)/repositories/$repoId/$snapshotId' - path: '/repositories/$repoId/$snapshotId' - fullPath: '/repositories/$repoId/$snapshotId' - preLoaderRoute: typeof dashboardRepositoriesRepoIdSnapshotIdRouteImport + '/(dashboard)/backups/$backupId/': { + id: '/(dashboard)/backups/$backupId/' + path: '/backups/$backupId' + fullPath: '/backups/$backupId/' + preLoaderRoute: typeof dashboardBackupsBackupIdIndexRouteImport parentRoute: typeof dashboardRouteRoute } - '/(dashboard)/repositories/$repoId/$snapId/restore': { - id: '/(dashboard)/repositories/$repoId/$snapId/restore' - path: '/repositories/$repoId/$snapId/restore' - fullPath: '/repositories/$repoId/$snapId/restore' - preLoaderRoute: typeof dashboardRepositoriesRepoIdSnapIdRestoreRouteImport + '/(dashboard)/repositories/$repositoryId/edit': { + id: '/(dashboard)/repositories/$repositoryId/edit' + path: '/repositories/$repositoryId/edit' + fullPath: '/repositories/$repositoryId/edit' + preLoaderRoute: typeof dashboardRepositoriesRepositoryIdEditRouteImport + parentRoute: typeof dashboardRouteRoute + } + '/(dashboard)/repositories/$repositoryId/$snapshotId/': { + id: '/(dashboard)/repositories/$repositoryId/$snapshotId/' + path: '/repositories/$repositoryId/$snapshotId' + fullPath: '/repositories/$repositoryId/$snapshotId/' + preLoaderRoute: typeof dashboardRepositoriesRepositoryIdSnapshotIdIndexRouteImport + parentRoute: typeof dashboardRouteRoute + } + '/(dashboard)/repositories/$repositoryId/$snapshotId/restore': { + id: '/(dashboard)/repositories/$repositoryId/$snapshotId/restore' + path: '/repositories/$repositoryId/$snapshotId/restore' + fullPath: '/repositories/$repositoryId/$snapshotId/restore' + preLoaderRoute: typeof dashboardRepositoriesRepositoryIdSnapshotIdRestoreRouteImport parentRoute: typeof dashboardRouteRoute } '/(dashboard)/backups/$backupId/$snapshotId/restore': { @@ -466,11 +486,9 @@ declare module '@tanstack/react-router' { } interface dashboardRouteRouteChildren { - dashboardBackupsScheduleIdRoute: typeof dashboardBackupsScheduleIdRoute dashboardBackupsCreateRoute: typeof dashboardBackupsCreateRoute dashboardNotificationsNotificationIdRoute: typeof dashboardNotificationsNotificationIdRoute dashboardNotificationsCreateRoute: typeof dashboardNotificationsCreateRoute - dashboardRepositoriesRepositoryIdRoute: typeof dashboardRepositoriesRepositoryIdRoute dashboardRepositoriesCreateRoute: typeof dashboardRepositoriesCreateRoute dashboardVolumesVolumeIdRoute: typeof dashboardVolumesVolumeIdRoute dashboardVolumesCreateRoute: typeof dashboardVolumesCreateRoute @@ -479,19 +497,19 @@ interface dashboardRouteRouteChildren { dashboardRepositoriesIndexRoute: typeof dashboardRepositoriesIndexRoute dashboardSettingsIndexRoute: typeof dashboardSettingsIndexRoute dashboardVolumesIndexRoute: typeof dashboardVolumesIndexRoute - dashboardRepositoriesRepoIdSnapshotIdRoute: typeof dashboardRepositoriesRepoIdSnapshotIdRoute + dashboardRepositoriesRepositoryIdEditRoute: typeof dashboardRepositoriesRepositoryIdEditRoute + dashboardBackupsBackupIdIndexRoute: typeof dashboardBackupsBackupIdIndexRoute + dashboardRepositoriesRepositoryIdIndexRoute: typeof dashboardRepositoriesRepositoryIdIndexRoute dashboardBackupsBackupIdSnapshotIdRestoreRoute: typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute - dashboardRepositoriesRepoIdSnapIdRestoreRoute: typeof dashboardRepositoriesRepoIdSnapIdRestoreRoute + dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute: typeof dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute + dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute: typeof dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute } const dashboardRouteRouteChildren: dashboardRouteRouteChildren = { - dashboardBackupsScheduleIdRoute: dashboardBackupsScheduleIdRoute, dashboardBackupsCreateRoute: dashboardBackupsCreateRoute, dashboardNotificationsNotificationIdRoute: dashboardNotificationsNotificationIdRoute, dashboardNotificationsCreateRoute: dashboardNotificationsCreateRoute, - dashboardRepositoriesRepositoryIdRoute: - dashboardRepositoriesRepositoryIdRoute, dashboardRepositoriesCreateRoute: dashboardRepositoriesCreateRoute, dashboardVolumesVolumeIdRoute: dashboardVolumesVolumeIdRoute, dashboardVolumesCreateRoute: dashboardVolumesCreateRoute, @@ -500,12 +518,17 @@ const dashboardRouteRouteChildren: dashboardRouteRouteChildren = { dashboardRepositoriesIndexRoute: dashboardRepositoriesIndexRoute, dashboardSettingsIndexRoute: dashboardSettingsIndexRoute, dashboardVolumesIndexRoute: dashboardVolumesIndexRoute, - dashboardRepositoriesRepoIdSnapshotIdRoute: - dashboardRepositoriesRepoIdSnapshotIdRoute, + dashboardRepositoriesRepositoryIdEditRoute: + dashboardRepositoriesRepositoryIdEditRoute, + dashboardBackupsBackupIdIndexRoute: dashboardBackupsBackupIdIndexRoute, + dashboardRepositoriesRepositoryIdIndexRoute: + dashboardRepositoriesRepositoryIdIndexRoute, dashboardBackupsBackupIdSnapshotIdRestoreRoute: dashboardBackupsBackupIdSnapshotIdRestoreRoute, - dashboardRepositoriesRepoIdSnapIdRestoreRoute: - dashboardRepositoriesRepoIdSnapIdRestoreRoute, + dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute: + dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute, + dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute: + dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute, } const dashboardRouteRouteWithChildren = dashboardRouteRoute._addFileChildren( diff --git a/app/routes/(dashboard)/backups/$backupId.$snapshotId.restore.tsx b/app/routes/(dashboard)/backups/$backupId/$snapshotId.restore.tsx similarity index 99% rename from app/routes/(dashboard)/backups/$backupId.$snapshotId.restore.tsx rename to app/routes/(dashboard)/backups/$backupId/$snapshotId.restore.tsx index fb15bb24..0103e57b 100644 --- a/app/routes/(dashboard)/backups/$backupId.$snapshotId.restore.tsx +++ b/app/routes/(dashboard)/backups/$backupId/$snapshotId.restore.tsx @@ -17,7 +17,7 @@ export const Route = createFileRoute("/(dashboard)/backups/$backupId/$snapshotId ...getSnapshotDetailsOptions({ path: { id: schedule.data?.repositoryId, snapshotId: params.snapshotId } }), }), context.queryClient.ensureQueryData({ ...getRepositoryOptions({ path: { id: schedule.data?.repositoryId } }) }), - ]); + ]) return { snapshot, repository, schedule: schedule.data }; }, @@ -51,5 +51,5 @@ function RouteComponent() { snapshot={snapshot} repository={repository} /> - ); + ) } diff --git a/app/routes/(dashboard)/backups/$scheduleId.tsx b/app/routes/(dashboard)/backups/$backupId/index.tsx similarity index 84% rename from app/routes/(dashboard)/backups/$scheduleId.tsx rename to app/routes/(dashboard)/backups/$backupId/index.tsx index 7c25a3cc..3e74f9a9 100644 --- a/app/routes/(dashboard)/backups/$scheduleId.tsx +++ b/app/routes/(dashboard)/backups/$backupId/index.tsx @@ -9,22 +9,22 @@ import { } from "~/client/api-client/@tanstack/react-query.gen"; import { ScheduleDetailsPage } from "~/client/modules/backups/routes/backup-details"; -export const Route = createFileRoute("/(dashboard)/backups/$scheduleId")({ +export const Route = createFileRoute("/(dashboard)/backups/$backupId/")({ component: RouteComponent, loader: async ({ params, context }) => { - const { scheduleId } = params; + const { backupId } = params; const [schedule, notifs, repos, scheduleNotifs, mirrors] = await Promise.all([ - context.queryClient.ensureQueryData({ ...getBackupScheduleOptions({ path: { scheduleId } }) }), + context.queryClient.ensureQueryData({ ...getBackupScheduleOptions({ path: { scheduleId: backupId } }) }), context.queryClient.ensureQueryData({ ...listNotificationDestinationsOptions() }), context.queryClient.ensureQueryData({ ...listRepositoriesOptions() }), - context.queryClient.ensureQueryData({ ...getScheduleNotificationsOptions({ path: { scheduleId } }) }), - context.queryClient.ensureQueryData({ ...getScheduleMirrorsOptions({ path: { scheduleId } }) }), - ]); + context.queryClient.ensureQueryData({ ...getScheduleNotificationsOptions({ path: { scheduleId: backupId } }) }), + context.queryClient.ensureQueryData({ ...getScheduleMirrorsOptions({ path: { scheduleId: backupId } }) }), + ]) void context.queryClient.prefetchQuery({ ...listSnapshotsOptions({ path: { id: schedule.repository.id }, query: { backupId: schedule.shortId } }), - }); + }) return { schedule, notifs, repos, scheduleNotifs, mirrors }; }, @@ -47,7 +47,7 @@ export const Route = createFileRoute("/(dashboard)/backups/$scheduleId")({ function RouteComponent() { const loaderData = Route.useLoaderData(); - const { scheduleId } = Route.useParams(); + const { backupId } = Route.useParams(); - return ; + return ; } diff --git a/app/routes/(dashboard)/repositories/$repoId.$snapshotId.tsx b/app/routes/(dashboard)/repositories/$repositoryId/$snapshotId/index.tsx similarity index 73% rename from app/routes/(dashboard)/repositories/$repoId.$snapshotId.tsx rename to app/routes/(dashboard)/repositories/$repositoryId/$snapshotId/index.tsx index 4b1c470e..be3a7130 100644 --- a/app/routes/(dashboard)/repositories/$repoId.$snapshotId.tsx +++ b/app/routes/(dashboard)/repositories/$repositoryId/$snapshotId/index.tsx @@ -6,32 +6,32 @@ import { } from "~/client/api-client/@tanstack/react-query.gen"; import { SnapshotDetailsPage } from "~/client/modules/repositories/routes/snapshot-details"; -export const Route = createFileRoute("/(dashboard)/repositories/$repoId/$snapshotId")({ +export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/$snapshotId/")({ component: RouteComponent, errorComponent: (e) =>
{e.error.message}
, loader: async ({ params, context }) => { const res = await context.queryClient.ensureQueryData({ - ...getRepositoryOptions({ path: { id: params.repoId } }), - }); + ...getRepositoryOptions({ path: { id: params.repositoryId } }), + }) void context.queryClient.prefetchQuery({ ...getSnapshotDetailsOptions({ - path: { id: params.repoId, snapshotId: params.snapshotId }, + path: { id: params.repositoryId, snapshotId: params.snapshotId }, }), - }); + }) void context.queryClient.prefetchQuery({ ...listSnapshotFilesOptions({ - path: { id: params.repoId, snapshotId: params.snapshotId }, + path: { id: params.repositoryId, snapshotId: params.snapshotId }, query: { path: "/" }, }), - }); + }) return res; }, staticData: { breadcrumb: (match) => [ { label: "Repositories", href: "/repositories" }, - { label: match.loaderData?.name || "Repository", href: `/repositories/${match.params.repoId}` }, + { label: match.loaderData?.name || "Repository", href: `/repositories/${match.params.repositoryId}` }, { label: match.params.snapshotId }, ], }, @@ -47,7 +47,7 @@ export const Route = createFileRoute("/(dashboard)/repositories/$repoId/$snapsho }); function RouteComponent() { - const { repoId, snapshotId } = Route.useParams(); + const { repositoryId, snapshotId } = Route.useParams(); - return ; + return ; } diff --git a/app/routes/(dashboard)/repositories/$repoId.$snapId.restore.tsx b/app/routes/(dashboard)/repositories/$repositoryId/$snapshotId/restore.tsx similarity index 66% rename from app/routes/(dashboard)/repositories/$repoId.$snapId.restore.tsx rename to app/routes/(dashboard)/repositories/$repositoryId/$snapshotId/restore.tsx index 702c7267..9c1ff381 100644 --- a/app/routes/(dashboard)/repositories/$repoId.$snapId.restore.tsx +++ b/app/routes/(dashboard)/repositories/$repositoryId/$snapshotId/restore.tsx @@ -2,30 +2,30 @@ import { createFileRoute } from "@tanstack/react-router"; import { getRepositoryOptions, getSnapshotDetailsOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { RestoreSnapshotPage } from "~/client/modules/repositories/routes/restore-snapshot"; -export const Route = createFileRoute("/(dashboard)/repositories/$repoId/$snapId/restore")({ +export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/$snapshotId/restore")({ component: RouteComponent, errorComponent: (e) =>
{e.error.message}
, loader: async ({ params, context }) => { const [snapshot, repository] = await Promise.all([ context.queryClient.ensureQueryData({ - ...getSnapshotDetailsOptions({ path: { id: params.repoId, snapshotId: params.snapId } }), + ...getSnapshotDetailsOptions({ path: { id: params.repositoryId, snapshotId: params.snapshotId } }), }), - context.queryClient.ensureQueryData({ ...getRepositoryOptions({ path: { id: params.repoId } }) }), - ]); + context.queryClient.ensureQueryData({ ...getRepositoryOptions({ path: { id: params.repositoryId } }) }), + ]) return { snapshot, repository }; }, staticData: { breadcrumb: (match) => [ { label: "Repositories", href: "/repositories" }, - { label: match.loaderData?.repository?.name || "Repository", href: `/repositories/${match.params.repoId}` }, - { label: match.params.snapId, href: `/repositories/${match.params.repoId}/${match.params.snapId}` }, + { label: match.loaderData?.repository?.name || "Repository", href: `/repositories/${match.params.repositoryId}` }, + { label: match.params.snapshotId, href: `/repositories/${match.params.repositoryId}/${match.params.snapshotId}` }, { label: "Restore" }, ], }, head: ({ params }) => ({ meta: [ - { title: `Zerobyte - Restore Snapshot ${params.snapId}` }, + { title: `Zerobyte - Restore Snapshot ${params.snapshotId}` }, { name: "description", content: "Restore files from a backup snapshot.", @@ -35,15 +35,15 @@ export const Route = createFileRoute("/(dashboard)/repositories/$repoId/$snapId/ }); function RouteComponent() { - const { repoId, snapId } = Route.useParams(); + const { repositoryId, snapshotId } = Route.useParams(); const { snapshot, repository } = Route.useLoaderData(); return ( - ); + ) } diff --git a/app/routes/(dashboard)/repositories/$repositoryId/edit.tsx b/app/routes/(dashboard)/repositories/$repositoryId/edit.tsx new file mode 100644 index 00000000..b9e7a345 --- /dev/null +++ b/app/routes/(dashboard)/repositories/$repositoryId/edit.tsx @@ -0,0 +1,37 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { getRepositoryOptions } from "~/client/api-client/@tanstack/react-query.gen"; +import { EditRepositoryPage } from "~/client/modules/repositories/routes/edit-repository"; + +export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/edit")({ + component: RouteComponent, + errorComponent: (e) =>
{e.error.message}
, + loader: async ({ params, context }) => { + const repository = await context.queryClient.ensureQueryData({ + ...getRepositoryOptions({ path: { id: params.repositoryId } }), + }) + + return repository; + }, + staticData: { + breadcrumb: (match) => [ + { label: "Repositories", href: "/repositories" }, + { label: match.loaderData?.name || "Repository", href: `/repositories/${match.params.repositoryId}` }, + { label: "Edit" }, + ], + }, + head: ({ loaderData }) => ({ + meta: [ + { title: `Zerobyte - Edit ${loaderData?.name ?? "Repository"}` }, + { + name: "description", + content: "Edit repository configuration.", + }, + ], + }), +}); + +function RouteComponent() { + const { repositoryId } = Route.useParams(); + + return ; +} diff --git a/app/routes/(dashboard)/repositories/$repositoryId.tsx b/app/routes/(dashboard)/repositories/$repositoryId/index.tsx similarity index 97% rename from app/routes/(dashboard)/repositories/$repositoryId.tsx rename to app/routes/(dashboard)/repositories/$repositoryId/index.tsx index d9c88fd7..6c10fa95 100644 --- a/app/routes/(dashboard)/repositories/$repositoryId.tsx +++ b/app/routes/(dashboard)/repositories/$repositoryId/index.tsx @@ -7,20 +7,20 @@ import { } from "~/client/api-client/@tanstack/react-query.gen"; import RepositoryDetailsPage from "~/client/modules/repositories/routes/repository-details"; -export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId")({ +export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/")({ component: RouteComponent, errorComponent: (e) =>
{e.error.message}
, loader: async ({ params, context }) => { void context.queryClient.prefetchQuery({ ...listSnapshotsOptions({ path: { id: params.repositoryId } }), - }); + }) void context.queryClient.prefetchQuery({ ...listBackupSchedulesOptions(), - }); + }) const res = await context.queryClient.ensureQueryData({ ...getRepositoryOptions({ path: { id: params.repositoryId } }), - }); + }) return res; }, diff --git a/app/schemas/restic.ts b/app/schemas/restic.ts index 4c3f7aee..c92969c5 100644 --- a/app/schemas/restic.ts +++ b/app/schemas/restic.ts @@ -58,8 +58,7 @@ export const r2RepositoryConfigSchema = type({ export const localRepositoryConfigSchema = type({ backend: "'local'", - name: "string", - path: "string?", + path: "string", }).and(baseRepositoryConfigSchema); export const gcsRepositoryConfigSchema = type({ diff --git a/app/server/modules/repositories/__tests__/repositories.controller.test.ts b/app/server/modules/repositories/__tests__/repositories.controller.test.ts index 3b506ed9..da5445c1 100644 --- a/app/server/modules/repositories/__tests__/repositories.controller.test.ts +++ b/app/server/modules/repositories/__tests__/repositories.controller.test.ts @@ -1,9 +1,43 @@ import { test, describe, expect } from "bun:test"; +import crypto from "node:crypto"; import { createApp } from "~/server/app"; +import { db } from "~/server/db/db"; +import { repositoriesTable } from "~/server/db/schema"; +import { generateShortId } from "~/server/utils/id"; import { createTestSession, getAuthHeaders } from "~/test/helpers/auth"; const app = createApp(); +const createRepositoryRecord = async (organizationId: string) => { + const [repository] = await db + .insert(repositoriesTable) + .values({ + id: crypto.randomUUID(), + shortId: generateShortId(), + name: `Repository-${crypto.randomUUID()}`, + type: "local", + config: { + backend: "local", + name: generateShortId(), + path: `/tmp/repository-${crypto.randomUUID()}`, + isExistingRepository: true, + }, + compressionMode: "off", + status: "error", + lastChecked: Date.now(), + lastError: "old error", + doctorResult: { + success: false, + steps: [], + completedAt: Date.now(), + }, + organizationId, + }) + .returning(); + + return repository; +}; + describe("repositories security", () => { test("should return 401 if no session cookie is provided", async () => { const res = await app.request("/api/v1/repositories"); @@ -96,3 +130,97 @@ describe("repositories security", () => { }); }); }); + +describe("repositories updates", () => { + test("PATCH updates full config and metadata using shortId", async () => { + const { token, organizationId } = await createTestSession(); + const repository = await createRepositoryRecord(organizationId); + const nextPath = `/tmp/updated-${crypto.randomUUID()}`; + + const res = await app.request(`/api/v1/repositories/${repository.shortId}`, { + method: "PATCH", + headers: { + ...getAuthHeaders(token), + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: "Updated repository", + compressionMode: "max", + config: { + backend: "local", + path: nextPath, + isExistingRepository: true, + }, + }), + }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.name).toBe("Updated repository"); + expect(body.compressionMode).toBe("max"); + expect(body.config.backend).toBe("local"); + expect(body.config.path).toBe(nextPath); + expect(body.status).toBe("unknown"); + expect(body.lastChecked).toBeNull(); + expect(body.lastError).toBeNull(); + expect(body.doctorResult).toBeNull(); + + const updated = await db.query.repositoriesTable.findFirst({ + where: { id: repository.id }, + }); + expect(updated).toBeTruthy(); + expect(updated?.name).toBe("Updated repository"); + expect(updated?.compressionMode).toBe("max"); + expect((updated?.config as { path?: string }).path).toBe(nextPath); + expect(updated?.status).toBe("unknown"); + expect(updated?.lastChecked).toBeNull(); + expect(updated?.lastError).toBeNull(); + expect(updated?.doctorResult).toBeNull(); + }); + + test("PATCH rejects backend changes", async () => { + const { token, organizationId } = await createTestSession(); + const repository = await createRepositoryRecord(organizationId); + + const res = await app.request(`/api/v1/repositories/${repository.id}`, { + method: "PATCH", + headers: { + ...getAuthHeaders(token), + "Content-Type": "application/json", + }, + body: JSON.stringify({ + config: { + backend: "s3", + endpoint: "s3.amazonaws.com", + bucket: "bucket-name", + accessKeyId: "access-key", + secretAccessKey: "secret-key", + }, + }), + }); + + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.message).toBe("Repository backend cannot be changed"); + }); + + test("PATCH rejects invalid config payload", async () => { + const { token, organizationId } = await createTestSession(); + const repository = await createRepositoryRecord(organizationId); + + const res = await app.request(`/api/v1/repositories/${repository.id}`, { + method: "PATCH", + headers: { + ...getAuthHeaders(token), + "Content-Type": "application/json", + }, + body: JSON.stringify({ + config: { + backend: "local", + }, + }), + }); + + expect(res.status).toBe(400); + }); +}); diff --git a/app/server/modules/repositories/repositories.dto.ts b/app/server/modules/repositories/repositories.dto.ts index dda6dbc7..d550deef 100644 --- a/app/server/modules/repositories/repositories.dto.ts +++ b/app/server/modules/repositories/repositories.dto.ts @@ -140,6 +140,7 @@ export const deleteRepositoryDto = describeRoute({ export const updateRepositoryBody = type({ name: "string?", compressionMode: type.valueOf(COMPRESSION_MODES).optional(), + config: repositoryConfigSchema.optional(), }); export type UpdateRepositoryBody = typeof updateRepositoryBody.infer; @@ -160,6 +161,9 @@ export const updateRepositoryDto = describeRoute({ }, }, }, + 400: { + description: "Invalid repository update payload", + }, 404: { description: "Repository not found", }, diff --git a/app/server/modules/repositories/repositories.service.ts b/app/server/modules/repositories/repositories.service.ts index ae49493b..20e735c7 100644 --- a/app/server/modules/repositories/repositories.service.ts +++ b/app/server/modules/repositories/repositories.service.ts @@ -1,6 +1,6 @@ import crypto from "node:crypto"; import { and, eq } from "drizzle-orm"; -import { ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced"; +import { BadRequestError, ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced"; import { db } from "../../db/db"; import { repositoriesTable } from "../../db/schema"; import { toMessage } from "../../utils/errors"; @@ -23,6 +23,7 @@ import { executeDoctor } from "./doctor"; import { logger } from "~/server/utils/logger"; import { parseRetentionCategories, type RetentionCategory } from "~/server/utils/retention-categories"; import { backupsService } from "../backups/backups.service"; +import type { UpdateRepositoryBody } from "./repositories.dto"; const runningDoctors = new Map(); @@ -80,16 +81,51 @@ const encryptConfig = async (config: RepositoryConfig): Promise => { + const decryptedConfig: Record = { ...config }; + + if (config.customPassword) { + decryptedConfig.customPassword = await cryptoUtils.resolveSecret(config.customPassword); + } + + if (config.cacert) { + decryptedConfig.cacert = await cryptoUtils.resolveSecret(config.cacert); + } + + switch (config.backend) { + case "s3": + case "r2": + decryptedConfig.accessKeyId = await cryptoUtils.resolveSecret(config.accessKeyId); + decryptedConfig.secretAccessKey = await cryptoUtils.resolveSecret(config.secretAccessKey); + break; + case "gcs": + decryptedConfig.credentialsJson = await cryptoUtils.resolveSecret(config.credentialsJson); + break; + case "azure": + decryptedConfig.accountKey = await cryptoUtils.resolveSecret(config.accountKey); + break; + case "rest": + if (config.username) { + decryptedConfig.username = await cryptoUtils.resolveSecret(config.username); + } + if (config.password) { + decryptedConfig.password = await cryptoUtils.resolveSecret(config.password); + } + break; + case "sftp": + decryptedConfig.privateKey = await cryptoUtils.resolveSecret(config.privateKey); + break; + } + + return decryptedConfig as RepositoryConfig; +}; + const createRepository = async (name: string, config: RepositoryConfig, compressionMode?: CompressionMode) => { const organizationId = getOrganizationId(); const id = crypto.randomUUID(); const shortId = generateShortId(); let processedConfig = config; - if (config.backend === "local" && !config.isExistingRepository) { - processedConfig = { ...config, name: shortId }; - } - const encryptedConfig = await encryptConfig(processedConfig); const [created] = await db @@ -526,40 +562,76 @@ const refreshSnapshots = async (id: string) => { } }; -const updateRepository = async (id: string, updates: { name?: string; compressionMode?: CompressionMode }) => { +const updateRepository = async (id: string, updates: UpdateRepositoryBody) => { const existing = await findRepository(id); if (!existing) { throw new NotFoundError("Repository not found"); } - const newConfig = repositoryConfigSchema(existing.config); - if (newConfig instanceof type.errors) { + const existingConfig = repositoryConfigSchema(existing.config); + if (existingConfig instanceof type.errors) { throw new InternalServerError("Invalid repository configuration"); } - const encryptedConfig = await encryptConfig(newConfig); - let newName = existing.name; - if (updates.name !== undefined && updates.name !== existing.name) { + if (updates.name) { newName = updates.name.trim(); + if (newName.length === 0) { + throw new BadRequestError("Repository name cannot be empty"); + } } + let parsedConfig = existingConfig; + if (updates.config) { + const nextConfig = repositoryConfigSchema(updates.config); + if (nextConfig instanceof type.errors) { + throw new BadRequestError("Invalid repository configuration"); + } + + if (nextConfig.backend !== existing.type) { + throw new BadRequestError("Repository backend cannot be changed"); + } + + parsedConfig = nextConfig; + } + + const decryptedExisting = existingConfig ? await decryptConfig(existingConfig) : null; + const configChanged = + updates.config && JSON.stringify(decryptedExisting) !== JSON.stringify(parsedConfig); + const encryptedConfig = updates.config ? await encryptConfig(parsedConfig) : existingConfig; + + const updatedAt = Date.now(); + const updatePayload = { + name: newName, + compressionMode: updates.compressionMode ?? existing.compressionMode, + updatedAt, + config: encryptedConfig, + ...(configChanged + ? { + status: "unknown" as const, + lastChecked: null, + lastError: null, + doctorResult: null, + } + : {}), + }; + const [updated] = await db .update(repositoriesTable) - .set({ - name: newName, - compressionMode: updates.compressionMode ?? existing.compressionMode, - updatedAt: Date.now(), - config: encryptedConfig, - }) - .where(eq(repositoriesTable.id, existing.id)) + .set(updatePayload) + .where(and(eq(repositoriesTable.id, existing.id), eq(repositoriesTable.organizationId, existing.organizationId))) .returning(); if (!updated) { throw new InternalServerError("Failed to update repository"); } + if (configChanged) { + cache.delByPrefix(`snapshots:${existing.id}:`); + cache.delByPrefix(`ls:${existing.id}:`); + } + return { repository: updated }; }; diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts index 8a3f9092..f51569ea 100644 --- a/app/server/utils/restic.ts +++ b/app/server/utils/restic.ts @@ -4,7 +4,7 @@ import path from "node:path"; import os from "node:os"; import { throttle } from "es-toolkit"; import { type } from "arktype"; -import { REPOSITORY_BASE, RESTIC_PASS_FILE, DEFAULT_EXCLUDES, RESTIC_CACHE_DIR } from "../core/constants"; +import { RESTIC_PASS_FILE, DEFAULT_EXCLUDES, RESTIC_CACHE_DIR } from "../core/constants"; import { config as appConfig } from "../core/config"; import { logger } from "./logger"; import { cryptoUtils } from "./crypto"; @@ -42,12 +42,7 @@ const snapshotInfoSchema = type({ export const buildRepoUrl = (config: RepositoryConfig): string => { switch (config.backend) { case "local": - if (config.isExistingRepository) { - if (!config.path) throw new Error("Path is required for existing local repositories"); - return config.path; - } - - return config.path ? `${config.path}/${config.name}` : `${REPOSITORY_BASE}/${config.name}`; + return config.path; case "s3": return `s3:${config.endpoint}/${config.bucket}`; case "r2": { diff --git a/app/test/helpers/repository.ts b/app/test/helpers/repository.ts index dd8592cd..a8466cbe 100644 --- a/app/test/helpers/repository.ts +++ b/app/test/helpers/repository.ts @@ -11,7 +11,7 @@ export const createTestRepository = async (overrides: Partial name: faker.string.alphanumeric(10), shortId: faker.string.alphanumeric(6), config: { - name: "test-repo", + path: `/var/lib/zerobyte/repositories/${faker.string.alphanumeric(8)}`, backend: "local", }, type: "local",