From 0287bca4bb7da0fb23e8ec81932b4a787da5f29f Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Sun, 30 Nov 2025 16:43:34 +0100 Subject: [PATCH] restore as a page (#87) * feat: add custom restore target directory Adds the ability to restore snapshots to a custom directory instead of only the original path. Closes #12. Changes: - Add target parameter to restore API endpoint - Add directory picker UI in file browser restore dialog - Add target input field in snapshot restore form - Create reusable PathSelector component Note: Run `bun run gen:api-client` after merging to regenerate types. * refactor: path selector design * refactor: unify restore snapshot dialogs * refactor: restore snapshot as a page * chore: fix liniting issues * chore(create-notification): remove un-used prop --------- Co-authored-by: Deepseek1 --- app/client/components/path-selector.tsx | 39 + app/client/components/restore-form.tsx | 325 ++++ .../components/snapshot-file-browser.tsx | 264 +--- .../modules/backups/routes/backup-details.tsx | 2 +- .../backups/routes/restore-snapshot.tsx | 54 + .../components/create-notification-form.tsx | 1 - .../routes/create-notification.tsx | 7 +- .../routes/notification-details.tsx | 26 +- .../components/restore-snapshot-dialog.tsx | 100 -- .../components/restore-snapshot-form.tsx | 138 -- .../repositories/routes/restore-snapshot.tsx | 45 + .../repositories/routes/snapshot-details.tsx | 2 - app/drizzle/meta/0016_snapshot.json | 1339 ++++++++--------- app/drizzle/meta/0017_snapshot.json | 1339 ++++++++--------- app/drizzle/meta/_journal.json | 262 ++-- app/routes.ts | 2 + app/server/modules/auth/auth.controller.ts | 1 - app/server/modules/lifecycle/checkpoint.ts | 1 - 18 files changed, 1933 insertions(+), 2014 deletions(-) create mode 100644 app/client/components/path-selector.tsx create mode 100644 app/client/components/restore-form.tsx create mode 100644 app/client/modules/backups/routes/restore-snapshot.tsx delete mode 100644 app/client/modules/repositories/components/restore-snapshot-dialog.tsx delete mode 100644 app/client/modules/repositories/components/restore-snapshot-form.tsx create mode 100644 app/client/modules/repositories/routes/restore-snapshot.tsx diff --git a/app/client/components/path-selector.tsx b/app/client/components/path-selector.tsx new file mode 100644 index 0000000..5d8132b --- /dev/null +++ b/app/client/components/path-selector.tsx @@ -0,0 +1,39 @@ +import { useState } from "react"; +import { DirectoryBrowser } from "./directory-browser"; +import { Button } from "./ui/button"; + +type Props = { + value: string; + onChange: (path: string) => void; + label?: string; +}; + +export const PathSelector = ({ value, onChange }: Props) => { + const [showBrowser, setShowBrowser] = useState(false); + + if (showBrowser) { + return ( +
+ { + onChange(path); + setShowBrowser(false); + }} + selectedPath={value} + /> + +
+ ); + } + + return ( +
+
{value}
+ +
+ ); +}; diff --git a/app/client/components/restore-form.tsx b/app/client/components/restore-form.tsx new file mode 100644 index 0000000..4e4f779 --- /dev/null +++ b/app/client/components/restore-form.tsx @@ -0,0 +1,325 @@ +import { useCallback, useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useNavigate } from "react-router"; +import { toast } from "sonner"; +import { ChevronDown, FileIcon, FolderOpen, RotateCcw } from "lucide-react"; +import { Button } from "~/client/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; +import { Checkbox } from "~/client/components/ui/checkbox"; +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 { PathSelector } from "~/client/components/path-selector"; +import { FileTree } from "~/client/components/file-tree"; +import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen"; +import { useFileBrowser } from "~/client/hooks/use-file-browser"; +import { OVERWRITE_MODES, type OverwriteMode } from "~/schemas/restic"; +import type { Snapshot } from "~/client/lib/types"; + +type RestoreLocation = "original" | "custom"; + +interface RestoreFormProps { + snapshot: Snapshot; + repositoryName: string; + snapshotId: string; + returnPath: string; +} + +export function RestoreForm({ snapshot, repositoryName, snapshotId, returnPath }: RestoreFormProps) { + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + const volumeBasePath = snapshot.paths[0]?.match(/^(.*?_data)(\/|$)/)?.[1] || "/"; + + const [restoreLocation, setRestoreLocation] = useState("original"); + const [customTargetPath, setCustomTargetPath] = useState(""); + const [overwriteMode, setOverwriteMode] = useState("always"); + const [showAdvanced, setShowAdvanced] = useState(false); + const [excludeXattr, setExcludeXattr] = useState(""); + const [deleteExtraFiles, setDeleteExtraFiles] = useState(false); + + const [selectedPaths, setSelectedPaths] = useState>(new Set()); + + const { data: filesData, isLoading: filesLoading } = useQuery({ + ...listSnapshotFilesOptions({ + path: { name: repositoryName, snapshotId }, + query: { path: volumeBasePath }, + }), + enabled: !!repositoryName && !!snapshotId, + }); + + const stripBasePath = useCallback( + (path: string): string => { + if (!volumeBasePath) return path; + if (path === volumeBasePath) return "/"; + if (path.startsWith(`${volumeBasePath}/`)) { + const stripped = path.slice(volumeBasePath.length); + return stripped; + } + return path; + }, + [volumeBasePath], + ); + + const addBasePath = useCallback( + (displayPath: string): string => { + const vbp = volumeBasePath === "/" ? "" : volumeBasePath; + + if (!vbp) return displayPath; + if (displayPath === "/") return vbp; + return `${vbp}${displayPath}`; + }, + [volumeBasePath], + ); + + const fileBrowser = useFileBrowser({ + initialData: filesData, + isLoading: filesLoading, + fetchFolder: async (path) => { + return await queryClient.ensureQueryData( + listSnapshotFilesOptions({ + path: { name: repositoryName, snapshotId }, + query: { path }, + }), + ); + }, + prefetchFolder: (path) => { + queryClient.prefetchQuery( + listSnapshotFilesOptions({ + path: { name: repositoryName, snapshotId }, + query: { path }, + }), + ); + }, + pathTransform: { + strip: stripBasePath, + add: addBasePath, + }, + }); + + const { mutate: restoreSnapshot, isPending: isRestoring } = useMutation({ + ...restoreSnapshotMutation(), + onSuccess: (data) => { + toast.success("Restore completed", { + description: `Successfully restored ${data.filesRestored} file(s). ${data.filesSkipped} file(s) skipped.`, + }); + navigate(returnPath); + }, + onError: (error) => { + toast.error("Restore failed", { description: error.message || "Failed to restore snapshot" }); + }, + }); + + const handleRestore = useCallback(() => { + if (!repositoryName || !snapshotId) return; + + const excludeXattrArray = excludeXattr + ?.split(",") + .map((s) => s.trim()) + .filter(Boolean); + + const isCustomLocation = restoreLocation === "custom"; + const targetPath = isCustomLocation && customTargetPath.trim() ? customTargetPath.trim() : undefined; + + const pathsArray = Array.from(selectedPaths); + const includePaths = pathsArray.map((path) => addBasePath(path)); + + restoreSnapshot({ + path: { name: repositoryName }, + body: { + snapshotId, + include: includePaths.length > 0 ? includePaths : undefined, + delete: deleteExtraFiles, + excludeXattr: excludeXattrArray && excludeXattrArray.length > 0 ? excludeXattrArray : undefined, + targetPath, + overwrite: overwriteMode, + }, + }); + }, [ + repositoryName, + snapshotId, + excludeXattr, + restoreLocation, + customTargetPath, + selectedPaths, + addBasePath, + deleteExtraFiles, + overwriteMode, + restoreSnapshot, + ]); + + const canRestore = restoreLocation === "original" || customTargetPath.trim(); + + return ( +
+
+
+

Restore Snapshot

+

+ {repositoryName} / {snapshotId} +

+
+
+ + +
+
+ +
+
+ + + Restore Location + Choose where to restore the files + + +
+ + +
+ {restoreLocation === "custom" && ( +
+ +

Files will be restored directly to this path

+
+ )} +
+
+ + + + Overwrite Mode + How to handle existing files + + + +

+ {overwriteMode === OVERWRITE_MODES.always && + "Existing files will always be replaced with the snapshot version."} + {overwriteMode === OVERWRITE_MODES.ifChanged && + "Files are only replaced if their content differs from the snapshot."} + {overwriteMode === OVERWRITE_MODES.ifNewer && + "Files are only replaced if the snapshot version has a newer modification time."} + {overwriteMode === OVERWRITE_MODES.never && + "Existing files will never be replaced, only missing files are restored."} +

+
+
+ + + setShowAdvanced(!showAdvanced)}> +
+ Advanced options + +
+
+ {showAdvanced && ( + +
+ + setExcludeXattr(e.target.value)} + /> +

+ Exclude specific extended attributes during restore (comma-separated) +

+
+
+ setDeleteExtraFiles(checked === true)} + /> + +
+
+ )} +
+
+ + + Select Files to Restore + + {selectedPaths.size > 0 + ? `${selectedPaths.size} ${selectedPaths.size === 1 ? "item" : "items"} selected` + : "Select specific files or folders, or leave empty to restore everything"} + + + + {fileBrowser.isLoading && ( +
+

Loading files...

+
+ )} + + {fileBrowser.isEmpty && ( +
+ +

No files in this snapshot

+
+ )} + + {!fileBrowser.isLoading && !fileBrowser.isEmpty && ( +
+ +
+ )} +
+
+
+
+ ); +} diff --git a/app/client/modules/backups/components/snapshot-file-browser.tsx b/app/client/modules/backups/components/snapshot-file-browser.tsx index 90382ae..8ace50b 100644 --- a/app/client/modules/backups/components/snapshot-file-browser.tsx +++ b/app/client/modules/backups/components/snapshot-file-browser.tsx @@ -1,54 +1,26 @@ -import { useCallback, useState } from "react"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ChevronDown, FileIcon, FolderOpen, RotateCcw } from "lucide-react"; +import { useCallback } from "react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { FileIcon } from "lucide-react"; +import { Link } from "react-router"; import { FileTree } from "~/client/components/file-tree"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; -import { Button } from "~/client/components/ui/button"; -import { Checkbox } from "~/client/components/ui/checkbox"; -import { Label } from "~/client/components/ui/label"; -import { Input } from "~/client/components/ui/input"; -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"; -import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip"; -import type { Snapshot, Volume } from "~/client/lib/types"; -import { toast } from "sonner"; -import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen"; +import { Button, buttonVariants } from "~/client/components/ui/button"; +import type { Snapshot } from "~/client/lib/types"; +import { listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { useFileBrowser } from "~/client/hooks/use-file-browser"; -import { OVERWRITE_MODES, type OverwriteMode } from "~/schemas/restic"; - -type RestoreLocation = "original" | "custom"; interface Props { snapshot: Snapshot; repositoryName: string; - volume?: Volume; + backupId?: string; onDeleteSnapshot?: (snapshotId: string) => void; isDeletingSnapshot?: boolean; } export const SnapshotFileBrowser = (props: Props) => { - const { snapshot, repositoryName, volume, onDeleteSnapshot, isDeletingSnapshot } = props; - - const isReadOnly = volume?.config && "readOnly" in volume.config && volume.config.readOnly === true; + const { snapshot, repositoryName, backupId, onDeleteSnapshot, isDeletingSnapshot } = props; const queryClient = useQueryClient(); - const [selectedPaths, setSelectedPaths] = useState>(new Set()); - const [showRestoreDialog, setShowRestoreDialog] = useState(false); - const [deleteExtraFiles, setDeleteExtraFiles] = useState(false); - const [showAdvanced, setShowAdvanced] = useState(false); - const [excludeXattr, setExcludeXattr] = useState(""); - const [restoreLocation, setRestoreLocation] = useState("original"); - const [customTargetPath, setCustomTargetPath] = useState(""); - const [overwriteMode, setOverwriteMode] = useState("always"); const volumeBasePath = snapshot.paths[0]?.match(/^(.*?_data)(\/|$)/)?.[1] || "/"; @@ -108,61 +80,6 @@ export const SnapshotFileBrowser = (props: Props) => { }, }); - const { mutate: restoreSnapshot, isPending: isRestoring } = useMutation({ - ...restoreSnapshotMutation(), - onSuccess: (data) => { - toast.success("Restore completed", { - description: `Successfully restored ${data.filesRestored} file(s). ${data.filesSkipped} file(s) skipped.`, - }); - setSelectedPaths(new Set()); - }, - onError: (error) => { - toast.error("Restore failed", { description: error.message || "Failed to restore snapshot" }); - }, - }); - - const handleRestoreClick = useCallback(() => { - setShowRestoreDialog(true); - }, []); - - const handleConfirmRestore = useCallback(() => { - const pathsArray = Array.from(selectedPaths); - const includePaths = pathsArray.map((path) => addBasePath(path)); - - const excludeXattrArray = excludeXattr - ?.split(",") - .map((s) => s.trim()) - .filter(Boolean); - - const isCustomLocation = restoreLocation === "custom"; - const targetPath = isCustomLocation && customTargetPath.trim() ? customTargetPath.trim() : undefined; - - restoreSnapshot({ - path: { name: repositoryName }, - body: { - snapshotId: snapshot.short_id, - include: includePaths, - delete: deleteExtraFiles, - excludeXattr: excludeXattrArray && excludeXattrArray.length > 0 ? excludeXattrArray : undefined, - targetPath, - overwrite: overwriteMode, - }, - }); - - setShowRestoreDialog(false); - }, [ - selectedPaths, - addBasePath, - repositoryName, - snapshot.short_id, - restoreSnapshot, - deleteExtraFiles, - excludeXattr, - restoreLocation, - customTargetPath, - overwriteMode, - ]); - return (
@@ -173,30 +90,16 @@ export const SnapshotFileBrowser = (props: Props) => { {`Viewing snapshot from ${new Date(snapshot?.time ?? 0).toLocaleString()}`}
- {selectedPaths.size > 0 && ( - - - - - - - {isReadOnly && ( - -

Volume is mounted as read-only.

-

Please remount with read-only disabled to restore files.

-
- )} -
- )} + + Restore + {onDeleteSnapshot && (
)} - - - - - Confirm Restore - - {selectedPaths.size > 0 - ? `This will restore ${selectedPaths.size} selected ${selectedPaths.size === 1 ? "item" : "items"} from the snapshot.` - : "This will restore everything from the snapshot."} - - -
-
- -
- - -
- {restoreLocation === "custom" && ( -
- setCustomTargetPath(e.target.value)} - /> -

Files will be restored directly to this path

-
- )} -
- -
- - -

- {overwriteMode === OVERWRITE_MODES.always && - "Existing files will always be replaced with the snapshot version."} - {overwriteMode === OVERWRITE_MODES.ifChanged && - "Files are only replaced if their content differs from the snapshot."} - {overwriteMode === OVERWRITE_MODES.ifNewer && - "Files are only replaced if the snapshot version has a newer modification time."} - {overwriteMode === OVERWRITE_MODES.never && - "Existing files will never be replaced, only missing files are restored."} -

-
- -
- - - {showAdvanced && ( -
-
- - setExcludeXattr(e.target.value)} - /> -

- Exclude specific extended attributes during restore (comma-separated) -

-
-
- setDeleteExtraFiles(checked === true)} - /> - -
-
- )} -
-
- - Cancel - - Confirm Restore - - -
-
); }; diff --git a/app/client/modules/backups/routes/backup-details.tsx b/app/client/modules/backups/routes/backup-details.tsx index c7c8288..09433c1 100644 --- a/app/client/modules/backups/routes/backup-details.tsx +++ b/app/client/modules/backups/routes/backup-details.tsx @@ -240,7 +240,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon key={selectedSnapshot?.short_id} snapshot={selectedSnapshot} repositoryName={schedule.repository.name} - volume={schedule.volume} + backupId={schedule.id.toString()} onDeleteSnapshot={handleDeleteSnapshot} isDeletingSnapshot={deleteSnapshot.isPending} /> diff --git a/app/client/modules/backups/routes/restore-snapshot.tsx b/app/client/modules/backups/routes/restore-snapshot.tsx new file mode 100644 index 0000000..0a8ce52 --- /dev/null +++ b/app/client/modules/backups/routes/restore-snapshot.tsx @@ -0,0 +1,54 @@ +import { redirect } from "react-router"; +import { getBackupSchedule, getSnapshotDetails } from "~/client/api-client"; +import { RestoreForm } from "~/client/components/restore-form"; +import type { Route } from "./+types/restore-snapshot"; + +export const handle = { + breadcrumb: (match: Route.MetaArgs) => [ + { label: "Backups", href: "/backups" }, + { label: `Schedule #${match.params.id}`, href: `/backups/${match.params.id}` }, + { label: match.params.snapshotId }, + { label: "Restore" }, + ], +}; + +export function meta({ params }: Route.MetaArgs) { + return [ + { title: `Zerobyte - Restore Snapshot ${params.snapshotId}` }, + { + name: "description", + content: "Restore files from a backup snapshot.", + }, + ]; +} + +export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => { + const schedule = await getBackupSchedule({ path: { scheduleId: params.id } }); + if (!schedule.data) return redirect("/backups"); + + const repositoryName = schedule.data.repository.name; + const snapshot = await getSnapshotDetails({ + path: { name: repositoryName, snapshotId: params.snapshotId }, + }); + if (!snapshot.data) return redirect(`/backups/${params.id}`); + + return { + snapshot: snapshot.data, + repositoryName, + snapshotId: params.snapshotId, + backupId: params.id, + }; +}; + +export default function RestoreSnapshotFromBackupPage({ loaderData }: Route.ComponentProps) { + const { snapshot, repositoryName, snapshotId, backupId } = loaderData; + + return ( + + ); +} diff --git a/app/client/modules/notifications/components/create-notification-form.tsx b/app/client/modules/notifications/components/create-notification-form.tsx index fcf31bd..4c809df 100644 --- a/app/client/modules/notifications/components/create-notification-form.tsx +++ b/app/client/modules/notifications/components/create-notification-form.tsx @@ -30,7 +30,6 @@ type Props = { mode?: "create" | "update"; initialValues?: Partial; formId?: string; - loading?: boolean; className?: string; }; diff --git a/app/client/modules/notifications/routes/create-notification.tsx b/app/client/modules/notifications/routes/create-notification.tsx index 6f6f0dd..7990d65 100644 --- a/app/client/modules/notifications/routes/create-notification.tsx +++ b/app/client/modules/notifications/routes/create-notification.tsx @@ -62,12 +62,7 @@ export default function CreateNotification() { )} - +
-
- + +
+ +
diff --git a/app/client/modules/repositories/components/restore-snapshot-dialog.tsx b/app/client/modules/repositories/components/restore-snapshot-dialog.tsx deleted file mode 100644 index f7e64d5..0000000 --- a/app/client/modules/repositories/components/restore-snapshot-dialog.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { RotateCcw } from "lucide-react"; -import { useId, useState } from "react"; -import { toast } from "sonner"; -import { restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen"; -import { parseError } from "~/client/lib/errors"; -import { Button } from "~/client/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "~/client/components/ui/dialog"; -import { ScrollArea } from "~/client/components/ui/scroll-area"; -import { RestoreSnapshotForm, type RestoreSnapshotFormValues } from "./restore-snapshot-form"; - -type Props = { - name: string; - snapshotId: string; -}; - -export const RestoreSnapshotDialog = ({ name, snapshotId }: Props) => { - const [open, setOpen] = useState(false); - const formId = useId(); - - const restore = useMutation({ - ...restoreSnapshotMutation(), - onSuccess: (data) => { - toast.success("Snapshot restored successfully", { - description: `${data.filesRestored} files restored, ${data.filesSkipped} files skipped`, - }); - setOpen(false); - }, - onError: (error) => { - toast.error("Failed to restore snapshot", { - description: parseError(error)?.message, - }); - }, - }); - - const handleSubmit = (values: RestoreSnapshotFormValues) => { - const include = values.include - ?.split(",") - .map((s) => s.trim()) - .filter(Boolean); - - const exclude = values.exclude - ?.split(",") - .map((s) => s.trim()) - .filter(Boolean); - - const excludeXattr = values.excludeXattr - ?.split(",") - .map((s) => s.trim()) - .filter(Boolean); - - restore.mutate({ - path: { name }, - body: { - snapshotId, - include: include && include.length > 0 ? include : undefined, - exclude: exclude && exclude.length > 0 ? exclude : undefined, - excludeXattr: excludeXattr && excludeXattr.length > 0 ? excludeXattr : undefined, - }, - }); - }; - - return ( - - - - - - - - Restore Snapshot - - Restore snapshot {snapshotId.substring(0, 8)} to a local filesystem path - - - - - - - - - - - ); -}; diff --git a/app/client/modules/repositories/components/restore-snapshot-form.tsx b/app/client/modules/repositories/components/restore-snapshot-form.tsx deleted file mode 100644 index e39fa80..0000000 --- a/app/client/modules/repositories/components/restore-snapshot-form.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { arktypeResolver } from "@hookform/resolvers/arktype"; -import { type } from "arktype"; -import { ChevronDown } from "lucide-react"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "~/client/components/ui/form"; -import { Input } from "~/client/components/ui/input"; -import { Button } from "~/client/components/ui/button"; - -const restoreSnapshotFormSchema = type({ - path: "string?", - include: "string?", - exclude: "string?", - excludeXattr: "string?", -}); - -export type RestoreSnapshotFormValues = typeof restoreSnapshotFormSchema.inferIn; - -type Props = { - formId: string; - onSubmit: (values: RestoreSnapshotFormValues) => void; - className?: string; -}; - -export const RestoreSnapshotForm = ({ formId, onSubmit, className }: Props) => { - const [showAdvanced, setShowAdvanced] = useState(false); - - const form = useForm({ - resolver: arktypeResolver(restoreSnapshotFormSchema), - defaultValues: { - path: "", - include: "", - exclude: "", - excludeXattr: "", - }, - }); - - const handleSubmit = (values: RestoreSnapshotFormValues) => { - onSubmit(values); - }; - - return ( -
- -
- ( - - Path (Optional) - - - - - Restore only a specific path from the snapshot (leave empty to restore all) - - - - )} - /> - - ( - - Include Patterns (Optional) - - - - Include only files matching these patterns (comma-separated) - - - )} - /> - - ( - - Exclude Patterns (Optional) - - - - Exclude files matching these patterns (comma-separated) - - - )} - /> - -
- - - {showAdvanced && ( -
- ( - - Exclude Extended Attributes (Optional) - - - - - Exclude specific extended attributes during restore (comma-separated) - - - - )} - /> -
- )} -
-
-
- - ); -}; diff --git a/app/client/modules/repositories/routes/restore-snapshot.tsx b/app/client/modules/repositories/routes/restore-snapshot.tsx new file mode 100644 index 0000000..a1d7075 --- /dev/null +++ b/app/client/modules/repositories/routes/restore-snapshot.tsx @@ -0,0 +1,45 @@ +import { redirect } from "react-router"; +import { getSnapshotDetails } from "~/client/api-client"; +import { RestoreForm } from "~/client/components/restore-form"; +import type { Route } from "./+types/restore-snapshot"; + +export const handle = { + breadcrumb: (match: Route.MetaArgs) => [ + { label: "Repositories", href: "/repositories" }, + { label: match.params.name, href: `/repositories/${match.params.name}` }, + { label: match.params.snapshotId, href: `/repositories/${match.params.name}/${match.params.snapshotId}` }, + { label: "Restore" }, + ], +}; + +export function meta({ params }: Route.MetaArgs) { + return [ + { title: `Zerobyte - Restore Snapshot ${params.snapshotId}` }, + { + name: "description", + content: "Restore files from a backup snapshot.", + }, + ]; +} + +export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => { + const snapshot = await getSnapshotDetails({ + path: { name: params.name, snapshotId: params.snapshotId }, + }); + if (snapshot.data) return { snapshot: snapshot.data, name: params.name, snapshotId: params.snapshotId }; + + return redirect("/repositories"); +}; + +export default function RestoreSnapshotPage({ loaderData }: Route.ComponentProps) { + const { snapshot, name, snapshotId } = loaderData; + + return ( + + ); +} diff --git a/app/client/modules/repositories/routes/snapshot-details.tsx b/app/client/modules/repositories/routes/snapshot-details.tsx index 1697574..cc9ed36 100644 --- a/app/client/modules/repositories/routes/snapshot-details.tsx +++ b/app/client/modules/repositories/routes/snapshot-details.tsx @@ -2,7 +2,6 @@ import { useQuery } from "@tanstack/react-query"; import { redirect, useParams } from "react-router"; import { listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card"; -import { RestoreSnapshotDialog } from "../components/restore-snapshot-dialog"; import { SnapshotFileBrowser } from "~/client/modules/backups/components/snapshot-file-browser"; import { getSnapshotDetails } from "~/client/api-client"; import type { Route } from "./+types/snapshot-details"; @@ -63,7 +62,6 @@ export default function SnapshotDetailsPage({ loaderData }: Route.ComponentProps

{name}

Snapshot: {snapshotId}

- diff --git a/app/drizzle/meta/0016_snapshot.json b/app/drizzle/meta/0016_snapshot.json index 411b2cb..5d2f85e 100644 --- a/app/drizzle/meta/0016_snapshot.json +++ b/app/drizzle/meta/0016_snapshot.json @@ -1,688 +1,653 @@ { - "id": "e50ff0fb-4111-4d20-b550-9407ee397517", - "prevId": "e52fe10a-3f36-4b21-abef-c15990d28363", - "version": "6", - "dialect": "sqlite", - "tables": { - "app_metadata": { - "name": "app_metadata", - "columns": { - "key": { - "name": "key", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "backup_schedule_notifications_table": { - "name": "backup_schedule_notifications_table", - "columns": { - "schedule_id": { - "name": "schedule_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "destination_id": { - "name": "destination_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "notify_on_start": { - "name": "notify_on_start", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "notify_on_success": { - "name": "notify_on_success", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "notify_on_failure": { - "name": "notify_on_failure", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": {}, - "foreignKeys": { - "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": { - "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk", - "tableFrom": "backup_schedule_notifications_table", - "columnsFrom": [ - "schedule_id" - ], - "tableTo": "backup_schedules_table", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": { - "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk", - "tableFrom": "backup_schedule_notifications_table", - "columnsFrom": [ - "destination_id" - ], - "tableTo": "notification_destinations_table", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": { - "backup_schedule_notifications_table_schedule_id_destination_id_pk": { - "columns": [ - "schedule_id", - "destination_id" - ], - "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk" - } - }, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "backup_schedules_table": { - "name": "backup_schedules_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "volume_id": { - "name": "volume_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "repository_id": { - "name": "repository_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "enabled": { - "name": "enabled", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "cron_expression": { - "name": "cron_expression", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "retention_policy": { - "name": "retention_policy", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "exclude_patterns": { - "name": "exclude_patterns", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "'[]'" - }, - "include_patterns": { - "name": "include_patterns", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "'[]'" - }, - "last_backup_at": { - "name": "last_backup_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_backup_status": { - "name": "last_backup_status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_backup_error": { - "name": "last_backup_error", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "next_backup_at": { - "name": "next_backup_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": {}, - "foreignKeys": { - "backup_schedules_table_volume_id_volumes_table_id_fk": { - "name": "backup_schedules_table_volume_id_volumes_table_id_fk", - "tableFrom": "backup_schedules_table", - "columnsFrom": [ - "volume_id" - ], - "tableTo": "volumes_table", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "backup_schedules_table_repository_id_repositories_table_id_fk": { - "name": "backup_schedules_table_repository_id_repositories_table_id_fk", - "tableFrom": "backup_schedules_table", - "columnsFrom": [ - "repository_id" - ], - "tableTo": "repositories_table", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "notification_destinations_table": { - "name": "notification_destinations_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "enabled": { - "name": "enabled", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "config": { - "name": "config", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": { - "notification_destinations_table_name_unique": { - "name": "notification_destinations_table_name_unique", - "columns": [ - "name" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "repositories_table": { - "name": "repositories_table", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "short_id": { - "name": "short_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "config": { - "name": "config", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "compression_mode": { - "name": "compression_mode", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "'auto'" - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "'unknown'" - }, - "last_checked": { - "name": "last_checked", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_error": { - "name": "last_error", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": { - "repositories_table_short_id_unique": { - "name": "repositories_table_short_id_unique", - "columns": [ - "short_id" - ], - "isUnique": true - }, - "repositories_table_name_unique": { - "name": "repositories_table_name_unique", - "columns": [ - "name" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "sessions_table": { - "name": "sessions_table", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "expires_at": { - "name": "expires_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": {}, - "foreignKeys": { - "sessions_table_user_id_users_table_id_fk": { - "name": "sessions_table_user_id_users_table_id_fk", - "tableFrom": "sessions_table", - "columnsFrom": [ - "user_id" - ], - "tableTo": "users_table", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "users_table": { - "name": "users_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "username": { - "name": "username", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "password_hash": { - "name": "password_hash", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "has_downloaded_restic_password": { - "name": "has_downloaded_restic_password", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": { - "users_table_username_unique": { - "name": "users_table_username_unique", - "columns": [ - "username" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "volumes_table": { - "name": "volumes_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "short_id": { - "name": "short_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'unmounted'" - }, - "last_error": { - "name": "last_error", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_health_check": { - "name": "last_health_check", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - }, - "config": { - "name": "config", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "auto_remount": { - "name": "auto_remount", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - } - }, - "indexes": { - "volumes_table_short_id_unique": { - "name": "volumes_table_short_id_unique", - "columns": [ - "short_id" - ], - "isUnique": true - }, - "volumes_table_name_unique": { - "name": "volumes_table_name_unique", - "columns": [ - "name" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file + "id": "e50ff0fb-4111-4d20-b550-9407ee397517", + "prevId": "e52fe10a-3f36-4b21-abef-c15990d28363", + "version": "6", + "dialect": "sqlite", + "tables": { + "app_metadata": { + "name": "app_metadata", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedule_notifications_table": { + "name": "backup_schedule_notifications_table", + "columns": { + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "destination_id": { + "name": "destination_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notify_on_start": { + "name": "notify_on_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notify_on_success": { + "name": "notify_on_success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notify_on_failure": { + "name": "notify_on_failure", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": { + "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk", + "tableFrom": "backup_schedule_notifications_table", + "columnsFrom": ["schedule_id"], + "tableTo": "backup_schedules_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": { + "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk", + "tableFrom": "backup_schedule_notifications_table", + "columnsFrom": ["destination_id"], + "tableTo": "notification_destinations_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "backup_schedule_notifications_table_schedule_id_destination_id_pk": { + "columns": ["schedule_id", "destination_id"], + "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedules_table": { + "name": "backup_schedules_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "volume_id": { + "name": "volume_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retention_policy": { + "name": "retention_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exclude_patterns": { + "name": "exclude_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "include_patterns": { + "name": "include_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "last_backup_at": { + "name": "last_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_status": { + "name": "last_backup_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_error": { + "name": "last_backup_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_backup_at": { + "name": "next_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedules_table_volume_id_volumes_table_id_fk": { + "name": "backup_schedules_table_volume_id_volumes_table_id_fk", + "tableFrom": "backup_schedules_table", + "columnsFrom": ["volume_id"], + "tableTo": "volumes_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "backup_schedules_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedules_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedules_table", + "columnsFrom": ["repository_id"], + "tableTo": "repositories_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_destinations_table": { + "name": "notification_destinations_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "notification_destinations_table_name_unique": { + "name": "notification_destinations_table_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repositories_table": { + "name": "repositories_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compression_mode": { + "name": "compression_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'auto'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'unknown'" + }, + "last_checked": { + "name": "last_checked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "repositories_table_short_id_unique": { + "name": "repositories_table_short_id_unique", + "columns": ["short_id"], + "isUnique": true + }, + "repositories_table_name_unique": { + "name": "repositories_table_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions_table": { + "name": "sessions_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_table_user_id_users_table_id_fk": { + "name": "sessions_table_user_id_users_table_id_fk", + "tableFrom": "sessions_table", + "columnsFrom": ["user_id"], + "tableTo": "users_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users_table": { + "name": "users_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "has_downloaded_restic_password": { + "name": "has_downloaded_restic_password", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "users_table_username_unique": { + "name": "users_table_username_unique", + "columns": ["username"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "volumes_table": { + "name": "volumes_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unmounted'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_health_check": { + "name": "last_health_check", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auto_remount": { + "name": "auto_remount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "volumes_table_short_id_unique": { + "name": "volumes_table_short_id_unique", + "columns": ["short_id"], + "isUnique": true + }, + "volumes_table_name_unique": { + "name": "volumes_table_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/app/drizzle/meta/0017_snapshot.json b/app/drizzle/meta/0017_snapshot.json index 6ca2138..c0dec40 100644 --- a/app/drizzle/meta/0017_snapshot.json +++ b/app/drizzle/meta/0017_snapshot.json @@ -1,688 +1,653 @@ { - "id": "d0bfd316-b8f5-459b-ab17-0ce679479321", - "prevId": "e50ff0fb-4111-4d20-b550-9407ee397517", - "version": "6", - "dialect": "sqlite", - "tables": { - "app_metadata": { - "name": "app_metadata", - "columns": { - "key": { - "name": "key", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "backup_schedule_notifications_table": { - "name": "backup_schedule_notifications_table", - "columns": { - "schedule_id": { - "name": "schedule_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "destination_id": { - "name": "destination_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "notify_on_start": { - "name": "notify_on_start", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "notify_on_success": { - "name": "notify_on_success", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "notify_on_failure": { - "name": "notify_on_failure", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": {}, - "foreignKeys": { - "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": { - "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk", - "tableFrom": "backup_schedule_notifications_table", - "columnsFrom": [ - "schedule_id" - ], - "tableTo": "backup_schedules_table", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": { - "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk", - "tableFrom": "backup_schedule_notifications_table", - "columnsFrom": [ - "destination_id" - ], - "tableTo": "notification_destinations_table", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": { - "backup_schedule_notifications_table_schedule_id_destination_id_pk": { - "columns": [ - "schedule_id", - "destination_id" - ], - "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk" - } - }, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "backup_schedules_table": { - "name": "backup_schedules_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "volume_id": { - "name": "volume_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "repository_id": { - "name": "repository_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "enabled": { - "name": "enabled", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "cron_expression": { - "name": "cron_expression", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "retention_policy": { - "name": "retention_policy", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "exclude_patterns": { - "name": "exclude_patterns", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "'[]'" - }, - "include_patterns": { - "name": "include_patterns", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "'[]'" - }, - "last_backup_at": { - "name": "last_backup_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_backup_status": { - "name": "last_backup_status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_backup_error": { - "name": "last_backup_error", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "next_backup_at": { - "name": "next_backup_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": {}, - "foreignKeys": { - "backup_schedules_table_volume_id_volumes_table_id_fk": { - "name": "backup_schedules_table_volume_id_volumes_table_id_fk", - "tableFrom": "backup_schedules_table", - "columnsFrom": [ - "volume_id" - ], - "tableTo": "volumes_table", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "backup_schedules_table_repository_id_repositories_table_id_fk": { - "name": "backup_schedules_table_repository_id_repositories_table_id_fk", - "tableFrom": "backup_schedules_table", - "columnsFrom": [ - "repository_id" - ], - "tableTo": "repositories_table", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "notification_destinations_table": { - "name": "notification_destinations_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "enabled": { - "name": "enabled", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "config": { - "name": "config", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": { - "notification_destinations_table_name_unique": { - "name": "notification_destinations_table_name_unique", - "columns": [ - "name" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "repositories_table": { - "name": "repositories_table", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "short_id": { - "name": "short_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "config": { - "name": "config", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "compression_mode": { - "name": "compression_mode", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "'auto'" - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "'unknown'" - }, - "last_checked": { - "name": "last_checked", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_error": { - "name": "last_error", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": { - "repositories_table_short_id_unique": { - "name": "repositories_table_short_id_unique", - "columns": [ - "short_id" - ], - "isUnique": true - }, - "repositories_table_name_unique": { - "name": "repositories_table_name_unique", - "columns": [ - "name" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "sessions_table": { - "name": "sessions_table", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "expires_at": { - "name": "expires_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": {}, - "foreignKeys": { - "sessions_table_user_id_users_table_id_fk": { - "name": "sessions_table_user_id_users_table_id_fk", - "tableFrom": "sessions_table", - "columnsFrom": [ - "user_id" - ], - "tableTo": "users_table", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "users_table": { - "name": "users_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "username": { - "name": "username", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "password_hash": { - "name": "password_hash", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "has_downloaded_restic_password": { - "name": "has_downloaded_restic_password", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": { - "users_table_username_unique": { - "name": "users_table_username_unique", - "columns": [ - "username" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "volumes_table": { - "name": "volumes_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "short_id": { - "name": "short_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'unmounted'" - }, - "last_error": { - "name": "last_error", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_health_check": { - "name": "last_health_check", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - }, - "config": { - "name": "config", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "auto_remount": { - "name": "auto_remount", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - } - }, - "indexes": { - "volumes_table_short_id_unique": { - "name": "volumes_table_short_id_unique", - "columns": [ - "short_id" - ], - "isUnique": true - }, - "volumes_table_name_unique": { - "name": "volumes_table_name_unique", - "columns": [ - "name" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file + "id": "d0bfd316-b8f5-459b-ab17-0ce679479321", + "prevId": "e50ff0fb-4111-4d20-b550-9407ee397517", + "version": "6", + "dialect": "sqlite", + "tables": { + "app_metadata": { + "name": "app_metadata", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedule_notifications_table": { + "name": "backup_schedule_notifications_table", + "columns": { + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "destination_id": { + "name": "destination_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notify_on_start": { + "name": "notify_on_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notify_on_success": { + "name": "notify_on_success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notify_on_failure": { + "name": "notify_on_failure", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": { + "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk", + "tableFrom": "backup_schedule_notifications_table", + "columnsFrom": ["schedule_id"], + "tableTo": "backup_schedules_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": { + "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk", + "tableFrom": "backup_schedule_notifications_table", + "columnsFrom": ["destination_id"], + "tableTo": "notification_destinations_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "backup_schedule_notifications_table_schedule_id_destination_id_pk": { + "columns": ["schedule_id", "destination_id"], + "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedules_table": { + "name": "backup_schedules_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "volume_id": { + "name": "volume_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retention_policy": { + "name": "retention_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exclude_patterns": { + "name": "exclude_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "include_patterns": { + "name": "include_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "last_backup_at": { + "name": "last_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_status": { + "name": "last_backup_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_error": { + "name": "last_backup_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_backup_at": { + "name": "next_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedules_table_volume_id_volumes_table_id_fk": { + "name": "backup_schedules_table_volume_id_volumes_table_id_fk", + "tableFrom": "backup_schedules_table", + "columnsFrom": ["volume_id"], + "tableTo": "volumes_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "backup_schedules_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedules_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedules_table", + "columnsFrom": ["repository_id"], + "tableTo": "repositories_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_destinations_table": { + "name": "notification_destinations_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "notification_destinations_table_name_unique": { + "name": "notification_destinations_table_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repositories_table": { + "name": "repositories_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compression_mode": { + "name": "compression_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'auto'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'unknown'" + }, + "last_checked": { + "name": "last_checked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "repositories_table_short_id_unique": { + "name": "repositories_table_short_id_unique", + "columns": ["short_id"], + "isUnique": true + }, + "repositories_table_name_unique": { + "name": "repositories_table_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions_table": { + "name": "sessions_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_table_user_id_users_table_id_fk": { + "name": "sessions_table_user_id_users_table_id_fk", + "tableFrom": "sessions_table", + "columnsFrom": ["user_id"], + "tableTo": "users_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users_table": { + "name": "users_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "has_downloaded_restic_password": { + "name": "has_downloaded_restic_password", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "users_table_username_unique": { + "name": "users_table_username_unique", + "columns": ["username"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "volumes_table": { + "name": "volumes_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unmounted'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_health_check": { + "name": "last_health_check", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auto_remount": { + "name": "auto_remount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "volumes_table_short_id_unique": { + "name": "volumes_table_short_id_unique", + "columns": ["short_id"], + "isUnique": true + }, + "volumes_table_name_unique": { + "name": "volumes_table_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/app/drizzle/meta/_journal.json b/app/drizzle/meta/_journal.json index e21e045..e11599d 100644 --- a/app/drizzle/meta/_journal.json +++ b/app/drizzle/meta/_journal.json @@ -1,132 +1,132 @@ { - "version": "7", - "dialect": "sqlite", - "entries": [ - { - "idx": 0, - "version": "6", - "when": 1755765658194, - "tag": "0000_known_madelyne_pryor", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1755775437391, - "tag": "0001_far_frank_castle", - "breakpoints": true - }, - { - "idx": 2, - "version": "6", - "when": 1756930554198, - "tag": "0002_cheerful_randall", - "breakpoints": true - }, - { - "idx": 3, - "version": "6", - "when": 1758653407064, - "tag": "0003_mature_hellcat", - "breakpoints": true - }, - { - "idx": 4, - "version": "6", - "when": 1758961535488, - "tag": "0004_wealthy_tomas", - "breakpoints": true - }, - { - "idx": 5, - "version": "6", - "when": 1759416698274, - "tag": "0005_simple_alice", - "breakpoints": true - }, - { - "idx": 6, - "version": "6", - "when": 1760734377440, - "tag": "0006_secret_micromacro", - "breakpoints": true - }, - { - "idx": 7, - "version": "6", - "when": 1761224911352, - "tag": "0007_watery_sersi", - "breakpoints": true - }, - { - "idx": 8, - "version": "6", - "when": 1761414054481, - "tag": "0008_silent_lady_bullseye", - "breakpoints": true - }, - { - "idx": 9, - "version": "6", - "when": 1762095226041, - "tag": "0009_little_adam_warlock", - "breakpoints": true - }, - { - "idx": 10, - "version": "6", - "when": 1762610065889, - "tag": "0010_perfect_proemial_gods", - "breakpoints": true - }, - { - "idx": 11, - "version": "6", - "when": 1763644043601, - "tag": "0011_familiar_stone_men", - "breakpoints": true - }, - { - "idx": 12, - "version": "6", - "when": 1764100562084, - "tag": "0012_add_short_ids", - "breakpoints": true - }, - { - "idx": 13, - "version": "6", - "when": 1764182159797, - "tag": "0013_elite_sprite", - "breakpoints": true - }, - { - "idx": 14, - "version": "6", - "when": 1764182405089, - "tag": "0014_wild_echo", - "breakpoints": true - }, - { - "idx": 15, - "version": "6", - "when": 1764182465287, - "tag": "0015_jazzy_sersi", - "breakpoints": true - }, - { - "idx": 16, - "version": "6", - "when": 1764194697035, - "tag": "0016_fix-timestamps-to-ms", - "breakpoints": true - }, - { - "idx": 17, - "version": "6", - "when": 1764357897219, - "tag": "0017_fix-compression-modes", - "breakpoints": true - } - ] -} \ No newline at end of file + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1755765658194, + "tag": "0000_known_madelyne_pryor", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1755775437391, + "tag": "0001_far_frank_castle", + "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1756930554198, + "tag": "0002_cheerful_randall", + "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1758653407064, + "tag": "0003_mature_hellcat", + "breakpoints": true + }, + { + "idx": 4, + "version": "6", + "when": 1758961535488, + "tag": "0004_wealthy_tomas", + "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1759416698274, + "tag": "0005_simple_alice", + "breakpoints": true + }, + { + "idx": 6, + "version": "6", + "when": 1760734377440, + "tag": "0006_secret_micromacro", + "breakpoints": true + }, + { + "idx": 7, + "version": "6", + "when": 1761224911352, + "tag": "0007_watery_sersi", + "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1761414054481, + "tag": "0008_silent_lady_bullseye", + "breakpoints": true + }, + { + "idx": 9, + "version": "6", + "when": 1762095226041, + "tag": "0009_little_adam_warlock", + "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1762610065889, + "tag": "0010_perfect_proemial_gods", + "breakpoints": true + }, + { + "idx": 11, + "version": "6", + "when": 1763644043601, + "tag": "0011_familiar_stone_men", + "breakpoints": true + }, + { + "idx": 12, + "version": "6", + "when": 1764100562084, + "tag": "0012_add_short_ids", + "breakpoints": true + }, + { + "idx": 13, + "version": "6", + "when": 1764182159797, + "tag": "0013_elite_sprite", + "breakpoints": true + }, + { + "idx": 14, + "version": "6", + "when": 1764182405089, + "tag": "0014_wild_echo", + "breakpoints": true + }, + { + "idx": 15, + "version": "6", + "when": 1764182465287, + "tag": "0015_jazzy_sersi", + "breakpoints": true + }, + { + "idx": 16, + "version": "6", + "when": 1764194697035, + "tag": "0016_fix-timestamps-to-ms", + "breakpoints": true + }, + { + "idx": 17, + "version": "6", + "when": 1764357897219, + "tag": "0017_fix-compression-modes", + "breakpoints": true + } + ] +} diff --git a/app/routes.ts b/app/routes.ts index a7a2eb3..6880e2d 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -12,10 +12,12 @@ export default [ route("backups", "./client/modules/backups/routes/backups.tsx"), route("backups/create", "./client/modules/backups/routes/create-backup.tsx"), route("backups/:id", "./client/modules/backups/routes/backup-details.tsx"), + route("backups/:id/:snapshotId/restore", "./client/modules/backups/routes/restore-snapshot.tsx"), route("repositories", "./client/modules/repositories/routes/repositories.tsx"), route("repositories/create", "./client/modules/repositories/routes/create-repository.tsx"), route("repositories/:name", "./client/modules/repositories/routes/repository-details.tsx"), route("repositories/:name/:snapshotId", "./client/modules/repositories/routes/snapshot-details.tsx"), + route("repositories/:name/:snapshotId/restore", "./client/modules/repositories/routes/restore-snapshot.tsx"), route("notifications", "./client/modules/notifications/routes/notifications.tsx"), route("notifications/create", "./client/modules/notifications/routes/create-notification.tsx"), route("notifications/:id", "./client/modules/notifications/routes/notification-details.tsx"), diff --git a/app/server/modules/auth/auth.controller.ts b/app/server/modules/auth/auth.controller.ts index d6252aa..c78d281 100644 --- a/app/server/modules/auth/auth.controller.ts +++ b/app/server/modules/auth/auth.controller.ts @@ -21,7 +21,6 @@ import { } from "./auth.dto"; import { authService } from "./auth.service"; import { toMessage } from "../../utils/errors"; -import { logger } from "~/server/utils/logger"; const COOKIE_NAME = "session_id"; const COOKIE_OPTIONS = { diff --git a/app/server/modules/lifecycle/checkpoint.ts b/app/server/modules/lifecycle/checkpoint.ts index 4fbde5d..d9f80a2 100644 --- a/app/server/modules/lifecycle/checkpoint.ts +++ b/app/server/modules/lifecycle/checkpoint.ts @@ -2,7 +2,6 @@ import { eq, sql } from "drizzle-orm"; import { db } from "../../db/db"; import { appMetadataTable, usersTable } from "../../db/schema"; import { logger } from "../../utils/logger"; -import { REQUIRED_MIGRATIONS } from "~/server/core/constants"; const MIGRATION_KEY_PREFIX = "migration:";