From 7d818206b0d01283154fa47863cdbde54bbb168c Mon Sep 17 00:00:00 2001 From: charlesgauthereau Date: Sat, 17 Jan 2026 17:27:33 +0100 Subject: [PATCH] refactor: API route for backup/restore --- app/api/agent/[agentId]/status/helpers.ts | 112 ++++++--- app/api/agent/[agentId]/status/route.ts | 10 - app/api/files/[fileName]/route.ts | 60 ----- app/api/files/route.ts | 91 +++++++ proxy.ts | 2 +- .../auth/login/login-form/login-form.tsx | 1 - .../register/register-form/register-form.tsx | 22 +- .../backup/actions/backup-actions-form.tsx | 48 +++- .../backup/actions/backup-actions.action.ts | 53 +++- .../migrations/0026_demonic_santa_claus.sql | 2 + src/db/migrations/meta/0026_snapshot.json | 231 ++++++++++-------- src/db/migrations/meta/_journal.json | 7 + src/db/schema/07_database.ts | 4 + src/features/storages/providers/local.ts | 9 +- 14 files changed, 415 insertions(+), 237 deletions(-) delete mode 100644 app/api/files/[fileName]/route.ts create mode 100644 app/api/files/route.ts create mode 100644 src/db/migrations/0026_demonic_santa_claus.sql diff --git a/app/api/agent/[agentId]/status/helpers.ts b/app/api/agent/[agentId]/status/helpers.ts index 7db9bb10..1fe3f186 100644 --- a/app/api/agent/[agentId]/status/helpers.ts +++ b/app/api/agent/[agentId]/status/helpers.ts @@ -12,6 +12,8 @@ import {ServerActionResult} from "@/types/action-type"; import {SafeActionResult} from "next-safe-action"; import {ZodString} from "zod"; import {withUpdatedAt} from "@/db/utils"; +import type {StorageInput} from "@/features/storages/types"; +import {dispatchStorage} from "@/features/storages/dispatch"; export async function handleDatabases(body: Body, agent: Agent, lastContact: Date) { const databasesResponse = []; @@ -88,7 +90,10 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat const restoration = await dbClient.query.restoration.findFirst({ - where: and(eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.restoration.status, "waiting")) + where: and(eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.restoration.status, "waiting")), + with: { + backupStorage: true + } }) @@ -105,53 +110,53 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat restoreAction = true - const backupToRestore = await dbClient.query.backup.findFirst({ - where: eq(drizzleDb.schemas.backup.id, restoration.backupId), - with: { - database: { - with: { - project: true - } - } - } - }) + // const backupToRestore = await dbClient.query.backup.findFirst({ + // where: eq(drizzleDb.schemas.backup.id, restoration.backupId), + // with: { + // database: { + // with: { + // project: true + // } + // } + // } + // }) - const [settings] = await dbClient.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1); - if (!settings) { - return NextResponse.json( - {error: "Unable to find settings"}, - {status: 500} - ); + // const [settings] = await dbClient.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1); + // if (!settings) { + // return NextResponse.json( + // {error: "Unable to find settings"}, + // {status: 500} + // ); + // } + + if (!restoration.backupStorage || restoration.backupStorage.status != "success" || !restoration.backupStorage.path) { + restoreAction = false + return; } - const fileName = backupToRestore?.file + const input: StorageInput = { + action: "get", + data: { + path: restoration.backupStorage.path, + signedUrl: true, + }, + }; - let data: SafeActionResult, object> | undefined try { + const result = await dispatchStorage(input, undefined, restoration.backupStorage.storageChannelId); - if (settings.storage == "local") { - data = await getFileUrlPresignedLocal({fileName: fileName!}) - } else if (settings.storage == "s3") { - - data = await getFileUrlPreSignedS3Action(`backups/${backupToRestore?.database.project?.slug}/${fileName}`); - } - - if (data?.data?.success) { - urlBackup = data.data.value ?? ""; + if (result.success) { + urlBackup = result.url ?? ""; } else { await dbClient .update(drizzleDb.schemas.restoration) .set({status: "failed"}) .where(eq(drizzleDb.schemas.restoration.id, restoration.id)); - // @ts-ignore - const errorMessage = data?.data?.actionError?.message || "Failed to get presigned URL"; + const errorMessage = "Failed to get backup URL"; console.error("Restoration failed: ", errorMessage); - continue; } } catch (err) { @@ -160,9 +165,48 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat .update(drizzleDb.schemas.restoration) .set({status: "failed"}) .where(eq(drizzleDb.schemas.restoration.id, restoration.id)); - continue; } + + + // const fileName = backupToRestore?.file + // + // let data: SafeActionResult, object> | undefined + // + // try { + // + // if (settings.storage == "local") { + // data = await getFileUrlPresignedLocal({fileName: fileName!}) + // } else if (settings.storage == "s3") { + // + // data = await getFileUrlPreSignedS3Action(`backups/${backupToRestore?.database.project?.slug}/${fileName}`); + // } + // + // if (data?.data?.success) { + // urlBackup = data.data.value ?? ""; + // } else { + // await dbClient + // .update(drizzleDb.schemas.restoration) + // .set({status: "failed"}) + // .where(eq(drizzleDb.schemas.restoration.id, restoration.id)); + // + // // @ts-ignore + // const errorMessage = data?.data?.actionError?.message || "Failed to get presigned URL"; + // console.error("Restoration failed: ", errorMessage); + // + // continue; + // } + // } catch (err) { + // console.error("Restoration crashed unexpectedly:", err); + // await dbClient + // .update(drizzleDb.schemas.restoration) + // .set({status: "failed"}) + // .where(eq(drizzleDb.schemas.restoration.id, restoration.id)); + // + // continue; + // } await dbClient .update(drizzleDb.schemas.restoration) .set({status: "ongoing"}) diff --git a/app/api/agent/[agentId]/status/route.ts b/app/api/agent/[agentId]/status/route.ts index d3b31d13..a0749a0b 100644 --- a/app/api/agent/[agentId]/status/route.ts +++ b/app/api/agent/[agentId]/status/route.ts @@ -1,5 +1,4 @@ import {NextResponse} from "next/server"; -import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action"; import {handleDatabases} from "./helpers"; import {eventEmitter} from "../../../events/route"; import * as drizzleDb from "@/db"; @@ -20,13 +19,6 @@ export type Body = { databases: databaseAgent[] } -// Function to test the get file url presigned local -export async function GET(request: Request) { - const url = await getFileUrlPresignedLocal({fileName: "d4a7fa35-2506-4d01-a612-a8ef2e2cc1c5.dump"}) - return Response.json({ - message: url - }) -} export async function POST( request: Request, @@ -38,7 +30,6 @@ export async function POST( const lastContact = new Date(); let message: string - if (!isUuidv4(agentId)) { message = "agentId is not a valid uuid" return NextResponse.json( @@ -57,7 +48,6 @@ export async function POST( } const databasesResponse = await handleDatabases(body, agent, lastContact) - await db .update(drizzleDb.schemas.agent) .set(withUpdatedAt({ diff --git a/app/api/files/[fileName]/route.ts b/app/api/files/[fileName]/route.ts deleted file mode 100644 index f70347e0..00000000 --- a/app/api/files/[fileName]/route.ts +++ /dev/null @@ -1,60 +0,0 @@ -import * as fs from "node:fs"; -import {NextResponse} from "next/server"; -import path from "path"; - -export async function GET( - request: Request, - {params}: { params: Promise<{ fileName: string }> } -) { - - const {searchParams} = new URL(request.url); - const token = searchParams.get('token'); - const expires = searchParams.get('expires'); - const fileName = (await params).fileName - - const uploadsDir = "private/uploads/files/"; - const uploadPath = path.join(uploadsDir, fileName); - - const crypto = require('crypto'); - - let filePath = null; - if (fs.existsSync(uploadPath)) { - filePath = uploadPath; - } else { - return NextResponse.json({error: "File not found"}, {status: 404}) - } - - const expectedToken = crypto.createHash('sha256').update(`${fileName}${expires}`).digest('hex'); - if (token !== expectedToken) { - return NextResponse.json( - {error: 'Invalid signed token'}, - {status: 403} - ); - } - - const expiresAt = parseInt(expires!, 10); - if (Date.now() > expiresAt) { - return NextResponse.json( - {error: 'Signed token expired'}, - {status: 403} - ); - } - - const fileStream = fs.createReadStream(filePath); - const stream = new ReadableStream({ - start(controller) { - fileStream.on('data', (chunk) => controller.enqueue(chunk)); - fileStream.on('end', () => controller.close()); - fileStream.on('error', (err) => controller.error(err)); - }, - }); - - return new NextResponse(stream, { - headers: { - 'Content-Disposition': `attachment; filename="${fileName}"`, - 'Content-Type': 'application/octet-stream', - }, - }); -} - - diff --git a/app/api/files/route.ts b/app/api/files/route.ts new file mode 100644 index 00000000..b4dc04b4 --- /dev/null +++ b/app/api/files/route.ts @@ -0,0 +1,91 @@ +import {NextResponse} from "next/server"; +import path from "path"; +import {db} from "@/db"; +import {eq} from "drizzle-orm"; +import * as drizzleDb from "@/db"; +import type {StorageInput} from "@/features/storages/types"; +import {dispatchStorage} from "@/features/storages/dispatch"; +import {Readable} from "node:stream"; + +export async function GET( + request: Request, + {params}: { params: Promise<{ fileName: string }> } +) { + + const {searchParams} = new URL(request.url); + const token = searchParams.get('token'); + const expires = searchParams.get('expires'); + const pathFromUrl = searchParams.get('path'); + + if (!pathFromUrl) { + return NextResponse.json({error: "Missing file path in search params"}, {status: 404}) + } + + const localStorageChannel = await db.query.storageChannel.findFirst({ + where: eq(drizzleDb.schemas.storageChannel.provider, "local"), + }) + + if (!localStorageChannel) { + return NextResponse.json({error: "No local storage channel found"}) + } + + const input: StorageInput = { + action: "get", + data: { + path: pathFromUrl, + signedUrl: true, + }, + }; + + const result = await dispatchStorage(input, undefined, localStorageChannel.id); + + if (!result.success) { + return NextResponse.json({error: "Enable to get file from local storage channel, an error occurred !"}) + } + + + const fileName = path.basename(pathFromUrl); + + const crypto = require('crypto'); + const expectedToken = crypto.createHash('sha256').update(`${fileName}${expires}`).digest('hex'); + if (token !== expectedToken) { + return NextResponse.json( + {error: 'Invalid signed token'}, + {status: 403} + ); + } + + const expiresAt = parseInt(expires!, 10); + if (Date.now() > expiresAt) { + return NextResponse.json( + {error: 'Signed token expired'}, + {status: 403} + ); + } + + if (!result.file || !Buffer.isBuffer(result.file)) { + return NextResponse.json( + {error: "Invalid file payload"}, + {status: 500} + ); + } + + const fileStream = Readable.from(result.file); + + const stream = new ReadableStream({ + start(controller) { + fileStream.on('data', (chunk) => controller.enqueue(chunk)); + fileStream.on('end', () => controller.close()); + fileStream.on('error', (err) => controller.error(err)); + }, + }); + + return new NextResponse(stream, { + headers: { + 'Content-Disposition': `attachment; filename="${fileName}"`, + 'Content-Type': 'application/octet-stream', + }, + }); +} + + diff --git a/proxy.ts b/proxy.ts index 55fd06e4..ac68b7eb 100644 --- a/proxy.ts +++ b/proxy.ts @@ -54,7 +54,7 @@ function checkRouteExists(pathname: string) { /^\/api\/agent\/[^/]+\/status\/?$/, /^\/api\/agent\/[^/]+\/backup\/?$/, /^\/api\/agent\/[^/]+\/restore\/?$/, - /^\/api\/files\/[^/]+\/?$/, + /^\/api\/files\/?$/, /^\/api\/images\/[^/]+\/?$/, /^\/api\/events\/?$/, /^\/api\/init\/?$/, diff --git a/src/components/wrappers/auth/login/login-form/login-form.tsx b/src/components/wrappers/auth/login/login-form/login-form.tsx index cb94fb21..bed6ad47 100644 --- a/src/components/wrappers/auth/login/login-form/login-form.tsx +++ b/src/components/wrappers/auth/login/login-form/login-form.tsx @@ -3,7 +3,6 @@ import {useEffect, useState} from "react"; import {useMutation} from "@tanstack/react-query"; import {toast} from "sonner"; - import {FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form"; import {Input} from "@/components/ui/input"; import {Form} from "@/components/ui/form"; diff --git a/src/components/wrappers/auth/register/register-form/register-form.tsx b/src/components/wrappers/auth/register/register-form/register-form.tsx index 197c2998..4ef63a63 100644 --- a/src/components/wrappers/auth/register/register-form/register-form.tsx +++ b/src/components/wrappers/auth/register/register-form/register-form.tsx @@ -1,21 +1,25 @@ "use client"; import {useRouter} from "next/navigation"; -import {Info} from "lucide-react"; import {useMutation} from "@tanstack/react-query"; import {toast} from "sonner"; - -import {Card, CardContent, CardHeader} from "@/components/ui/card"; -import {FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form"; -import {Input} from "@/components/ui/input"; +import {CardContent, CardHeader} from "@/components/ui/card"; import {Form} from "@/components/ui/form"; -import {Button} from "@/components/ui/button"; import {TooltipProvider, TooltipTrigger, Tooltip, TooltipContent} from "@/components/ui/tooltip"; import {RegisterSchema, RegisterType} from "@/components/wrappers/auth/register/register-form/register-form.schema"; -import {PasswordInput} from "@/components/ui/password-input"; import {signUp} from "@/lib/auth/auth-client"; -import Link from "next/link"; +import {useZodForm} from "@/components/ui/form"; import {CardAuth} from "@/features/layout/card-auth"; +import {FormField} from "@/components/ui/form"; +import {FormItem} from "@/components/ui/form"; +import {FormLabel} from "@/components/ui/form"; +import {FormControl} from "@/components/ui/form"; +import {FormMessage} from "@/components/ui/form"; +import {Input} from "@/components/ui/input"; +import {PasswordInput} from "@/components/ui/password-input"; +import {Button} from "@/components/ui/button"; +import Link from "next/link"; +import {Info} from "lucide-react"; export type registerFormProps = { defaultValues?: RegisterType; @@ -26,9 +30,11 @@ export const RegisterForm = (props: registerFormProps) => { const form = useZodForm({ schema: RegisterSchema, }); + const router = useRouter(); const mutation = useMutation({ mutationFn: async (values: RegisterType) => { + // @ts-ignore await signUp.email(values, { onSuccess: () => { toast.success(`Account successfully created`); diff --git a/src/components/wrappers/dashboard/database/backup/actions/backup-actions-form.tsx b/src/components/wrappers/dashboard/database/backup/actions/backup-actions-form.tsx index 98756abd..b3abec7c 100644 --- a/src/components/wrappers/dashboard/database/backup/actions/backup-actions-form.tsx +++ b/src/components/wrappers/dashboard/database/backup/actions/backup-actions-form.tsx @@ -1,5 +1,5 @@ "use client" -import {BackupWith} from "@/db/schema/07_database"; +import {BackupWith, Restoration} from "@/db/schema/07_database"; import React, {useState} from "react"; import {Swiper, SwiperSlide} from "swiper/react"; @@ -7,7 +7,7 @@ import "swiper/css"; import "swiper/css/pagination"; import {Pagination, Mousewheel} from "swiper/modules"; -import {DatabaseActionKind} from "@/components/wrappers/dashboard/database/backup/backup-modal-context"; +import {DatabaseActionKind, useBackupModal} from "@/components/wrappers/dashboard/database/backup/backup-modal-context"; import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form"; import { BackupActionsSchema, @@ -22,8 +22,14 @@ import {truncateWords} from "@/utils/text"; import {useIsMobile} from "@/hooks/use-mobile"; import {Badge} from "@/components/ui/badge"; import {getStatusColor, getStatusIcon} from "@/components/wrappers/dashboard/admin/notifications/logs/columns"; -import {downloadBackupAction} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions.action"; +import { + createRestorationBackupAction, + downloadBackupAction +} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions.action"; import {toast} from "sonner"; +import {SafeActionResult} from "next-safe-action"; +import {ServerActionResult} from "@/types/action-type"; +import {ZodString} from "zod"; type BackupActionsFormProps = { backup: BackupWith; @@ -32,6 +38,7 @@ type BackupActionsFormProps = { export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => { const isMobile = useIsMobile(); + const {closeModal} = useBackupModal(); const form = useZodForm({ schema: BackupActionsSchema, @@ -39,18 +46,38 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => { const mutation = useMutation({ mutationFn: async (values: BackupActionsType) => { - // implement your mutation logic here - console.log(values); - const result = await downloadBackupAction({backupStorageId: values.backupStorageId}) + let result: SafeActionResult, object> | undefined + + + if (action === "download") { + result = await downloadBackupAction({backupStorageId: values.backupStorageId}) + } else if (action === "restore") { + result = await createRestorationBackupAction({ + databaseId: backup.databaseId, + backupStorageId: values.backupStorageId, + backupId: backup.id + }) + } const inner = result?.data; - console.log(inner); - - if (inner?.success) { toast.success(inner.actionSuccess?.message); + + if (action === "download") { + console.log(inner.value) + const url = inner.value + if (typeof url === "string") { + window.open(url, "_self"); + } + closeModal() + } else if (action === "restore") { + closeModal() + } + } else { toast.error(inner?.actionError?.message); } @@ -146,7 +173,8 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => { )} />
- + Confirm
diff --git a/src/components/wrappers/dashboard/database/backup/actions/backup-actions.action.ts b/src/components/wrappers/dashboard/database/backup/actions/backup-actions.action.ts index 18f638ae..f20e80a3 100644 --- a/src/components/wrappers/dashboard/database/backup/actions/backup-actions.action.ts +++ b/src/components/wrappers/dashboard/database/backup/actions/backup-actions.action.ts @@ -7,6 +7,7 @@ import {dispatchStorage} from "@/features/storages/dispatch"; import {db} from "@/db"; import {eq} from "drizzle-orm"; import * as drizzleDb from "@/db"; +import {Restoration} from "@/db/schema/07_database"; export const downloadBackupAction = userAction.schema( @@ -32,9 +33,6 @@ export const downloadBackupAction = userAction.schema( }, }; } - - console.log(backupStorage); - if (backupStorage.status != "success" || !backupStorage.path) { return { success: false, @@ -56,8 +54,6 @@ export const downloadBackupAction = userAction.schema( const result = await dispatchStorage(input, undefined, backupStorage.storageChannelId); - console.log(result); - return { success: true, value: result.url, @@ -78,4 +74,49 @@ export const downloadBackupAction = userAction.schema( }, }; } -}); \ No newline at end of file +}); + + +export const createRestorationBackupAction = userAction + .schema( + z.object({ + backupId: z.string(), + databaseId: z.string(), + backupStorageId: z.string(), + }) + ) + .action(async ({parsedInput}): Promise> => { + try { + const restorationData = await db + .insert(drizzleDb.schemas.restoration) + .values({ + databaseId: parsedInput.databaseId, + backupId: parsedInput.backupId, + backupStorageId: parsedInput.backupStorageId, + status: "waiting", + }) + .returning() + .execute(); + + const createdRestoration = restorationData[0]; + + return { + success: true, + value: createdRestoration, + actionSuccess: { + message: "Restoration has been successfully created.", + messageParams: {restorationId: createdRestoration.id}, + }, + }; + } catch (error) { + return { + success: false, + actionError: { + message: "Failed to create restoration.", + status: 500, + cause: error instanceof Error ? error.message : "Unknown error", + messageParams: {message: "Error creating the restoration"}, + }, + }; + } + }); diff --git a/src/db/migrations/0026_demonic_santa_claus.sql b/src/db/migrations/0026_demonic_santa_claus.sql new file mode 100644 index 00000000..ec77e081 --- /dev/null +++ b/src/db/migrations/0026_demonic_santa_claus.sql @@ -0,0 +1,2 @@ +ALTER TABLE "restorations" ADD COLUMN "backup_storage_id" uuid;--> statement-breakpoint +ALTER TABLE "restorations" ADD CONSTRAINT "restorations_backup_storage_id_backup_storage_id_fk" FOREIGN KEY ("backup_storage_id") REFERENCES "public"."backup_storage"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/src/db/migrations/meta/0026_snapshot.json b/src/db/migrations/meta/0026_snapshot.json index ba8b1fc6..8110e6cc 100644 --- a/src/db/migrations/meta/0026_snapshot.json +++ b/src/db/migrations/meta/0026_snapshot.json @@ -1,6 +1,6 @@ { - "id": "2767f202-1e4b-42b1-8be0-1fa9dcd48cbb", - "prevId": "a9aa4c81-9ca9-413f-89cb-37361d26927c", + "id": "5d244170-557d-433b-8acd-742ead8246c8", + "prevId": "2767f202-1e4b-42b1-8be0-1fa9dcd48cbb", "version": "7", "dialect": "postgresql", "tables": { @@ -114,25 +114,25 @@ "settings_default_storage_channel_id_storage_channel_id_fk": { "name": "settings_default_storage_channel_id_storage_channel_id_fk", "tableFrom": "settings", + "tableTo": "storage_channel", "columnsFrom": [ "default_storage_channel_id" ], - "tableTo": "storage_channel", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "set null" + "onDelete": "set null", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": { "settings_name_unique": { "name": "settings_name_unique", + "nullsNotDistinct": false, "columns": [ "name" - ], - "nullsNotDistinct": false + ] } }, "policies": {}, @@ -235,15 +235,15 @@ "account_user_id_user_id_fk": { "name": "account_user_id_user_id_fk", "tableFrom": "account", + "tableTo": "user", "columnsFrom": [ "user_id" ], - "tableTo": "user", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -336,15 +336,15 @@ "passkey_userId_user_id_fk": { "name": "passkey_userId_user_id_fk", "tableFrom": "passkey", + "tableTo": "user", "columnsFrom": [ "userId" ], - "tableTo": "user", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -431,25 +431,25 @@ "session_user_id_user_id_fk": { "name": "session_user_id_user_id_fk", "tableFrom": "session", + "tableTo": "user", "columnsFrom": [ "user_id" ], - "tableTo": "user", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": { "session_token_unique": { "name": "session_token_unique", + "nullsNotDistinct": false, "columns": [ "token" - ], - "nullsNotDistinct": false + ] } }, "policies": {}, @@ -491,15 +491,15 @@ "two_factor_user_id_user_id_fk": { "name": "two_factor_user_id_user_id_fk", "tableFrom": "two_factor", + "tableTo": "user", "columnsFrom": [ "user_id" ], - "tableTo": "user", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -614,10 +614,10 @@ "uniqueConstraints": { "user_email_unique": { "name": "user_email_unique", + "nullsNotDistinct": false, "columns": [ "email" - ], - "nullsNotDistinct": false + ] } }, "policies": {}, @@ -742,10 +742,10 @@ "uniqueConstraints": { "organization_slug_unique": { "name": "organization_slug_unique", + "nullsNotDistinct": false, "columns": [ "slug" - ], - "nullsNotDistinct": false + ] } }, "policies": {}, @@ -806,28 +806,28 @@ "member_organization_id_organization_id_fk": { "name": "member_organization_id_organization_id_fk", "tableFrom": "member", + "tableTo": "organization", "columnsFrom": [ "organization_id" ], - "tableTo": "organization", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" }, "member_user_id_user_id_fk": { "name": "member_user_id_user_id_fk", "tableFrom": "member", + "tableTo": "user", "columnsFrom": [ "user_id" ], - "tableTo": "user", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -908,28 +908,28 @@ "invitation_organization_id_organization_id_fk": { "name": "invitation_organization_id_organization_id_fk", "tableFrom": "invitation", + "tableTo": "organization", "columnsFrom": [ "organization_id" ], - "tableTo": "organization", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" }, "invitation_inviter_id_user_id_fk": { "name": "invitation_inviter_id_user_id_fk", "tableFrom": "invitation", + "tableTo": "user", "columnsFrom": [ "inviter_id" ], - "tableTo": "user", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -999,25 +999,25 @@ "projects_organization_id_organization_id_fk": { "name": "projects_organization_id_organization_id_fk", "tableFrom": "projects", + "tableTo": "organization", "columnsFrom": [ "organization_id" ], - "tableTo": "organization", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": { "projects_slug_unique": { "name": "projects_slug_unique", + "nullsNotDistinct": false, "columns": [ "slug" - ], - "nullsNotDistinct": false + ] } }, "policies": {}, @@ -1086,15 +1086,15 @@ "backups_database_id_databases_id_fk": { "name": "backups_database_id_databases_id_fk", "tableFrom": "backups", + "tableTo": "databases", "columnsFrom": [ "database_id" ], - "tableTo": "databases", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -1202,28 +1202,28 @@ "databases_agent_id_agents_id_fk": { "name": "databases_agent_id_agents_id_fk", "tableFrom": "databases", + "tableTo": "agents", "columnsFrom": [ "agent_id" ], - "tableTo": "agents", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" }, "databases_project_id_projects_id_fk": { "name": "databases_project_id_projects_id_fk", "tableFrom": "databases", + "tableTo": "projects", "columnsFrom": [ "project_id" ], - "tableTo": "projects", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "no action" + "onDelete": "no action", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -1251,6 +1251,12 @@ "notNull": true, "default": "'waiting'" }, + "backup_storage_id": { + "name": "backup_storage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "backup_id": { "name": "backup_id", "type": "uuid", @@ -1285,31 +1291,44 @@ }, "indexes": {}, "foreignKeys": { - "restorations_backup_id_backups_id_fk": { - "name": "restorations_backup_id_backups_id_fk", + "restorations_backup_storage_id_backup_storage_id_fk": { + "name": "restorations_backup_storage_id_backup_storage_id_fk", "tableFrom": "restorations", + "tableTo": "backup_storage", "columnsFrom": [ - "backup_id" + "backup_storage_id" ], - "tableTo": "backups", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" + }, + "restorations_backup_id_backups_id_fk": { + "name": "restorations_backup_id_backups_id_fk", + "tableFrom": "restorations", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" }, "restorations_database_id_databases_id_fk": { "name": "restorations_database_id_databases_id_fk", "tableFrom": "restorations", + "tableTo": "databases", "columnsFrom": [ "database_id" ], - "tableTo": "databases", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -1409,15 +1428,15 @@ "retention_policies_database_id_databases_id_fk": { "name": "retention_policies_database_id_databases_id_fk", "tableFrom": "retention_policies", + "tableTo": "databases", "columnsFrom": [ "database_id" ], - "tableTo": "databases", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -1500,10 +1519,10 @@ "uniqueConstraints": { "agents_slug_unique": { "name": "agents_slug_unique", + "nullsNotDistinct": false, "columns": [ "slug" - ], - "nullsNotDistinct": false + ] } }, "policies": {}, @@ -1578,15 +1597,15 @@ "notification_channel_organization_id_organization_id_fk": { "name": "notification_channel_organization_id_organization_id_fk", "tableFrom": "notification_channel", + "tableTo": "organization", "columnsFrom": [ "organization_id" ], - "tableTo": "organization", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -1617,39 +1636,39 @@ "organization_notification_channels_organization_id_organization_id_fk": { "name": "organization_notification_channels_organization_id_organization_id_fk", "tableFrom": "organization_notification_channels", + "tableTo": "organization", "columnsFrom": [ "organization_id" ], - "tableTo": "organization", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" }, "organization_notification_channels_notification_channel_id_notification_channel_id_fk": { "name": "organization_notification_channels_notification_channel_id_notification_channel_id_fk", "tableFrom": "organization_notification_channels", + "tableTo": "notification_channel", "columnsFrom": [ "notification_channel_id" ], - "tableTo": "notification_channel", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": { "organization_notification_channels_organization_id_notification_channel_id_unique": { "name": "organization_notification_channels_organization_id_notification_channel_id_unique", + "nullsNotDistinct": false, "columns": [ "organization_id", "notification_channel_id" - ], - "nullsNotDistinct": false + ] } }, "policies": {}, @@ -1718,28 +1737,28 @@ "alert_policy_notification_channel_id_notification_channel_id_fk": { "name": "alert_policy_notification_channel_id_notification_channel_id_fk", "tableFrom": "alert_policy", + "tableTo": "notification_channel", "columnsFrom": [ "notification_channel_id" ], - "tableTo": "notification_channel", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" }, "alert_policy_database_id_databases_id_fk": { "name": "alert_policy_database_id_databases_id_fk", "tableFrom": "alert_policy", + "tableTo": "databases", "columnsFrom": [ "database_id" ], - "tableTo": "databases", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -1895,39 +1914,39 @@ "organization_storage_channels_organization_id_organization_id_fk": { "name": "organization_storage_channels_organization_id_organization_id_fk", "tableFrom": "organization_storage_channels", + "tableTo": "organization", "columnsFrom": [ "organization_id" ], - "tableTo": "organization", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" }, "organization_storage_channels_storage_channel_id_storage_channel_id_fk": { "name": "organization_storage_channels_storage_channel_id_storage_channel_id_fk", "tableFrom": "organization_storage_channels", + "tableTo": "storage_channel", "columnsFrom": [ "storage_channel_id" ], - "tableTo": "storage_channel", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": { "organization_storage_channels_organization_id_storage_channel_id_unique": { "name": "organization_storage_channels_organization_id_storage_channel_id_unique", + "nullsNotDistinct": false, "columns": [ "organization_id", "storage_channel_id" - ], - "nullsNotDistinct": false + ] } }, "policies": {}, @@ -2002,15 +2021,15 @@ "storage_channel_organization_id_organization_id_fk": { "name": "storage_channel_organization_id_organization_id_fk", "tableFrom": "storage_channel", + "tableTo": "organization", "columnsFrom": [ "organization_id" ], - "tableTo": "organization", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -2074,28 +2093,28 @@ "storage_policy_storage_channel_id_storage_channel_id_fk": { "name": "storage_policy_storage_channel_id_storage_channel_id_fk", "tableFrom": "storage_policy", + "tableTo": "storage_channel", "columnsFrom": [ "storage_channel_id" ], - "tableTo": "storage_channel", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" }, "storage_policy_database_id_databases_id_fk": { "name": "storage_policy_database_id_databases_id_fk", "tableFrom": "storage_policy", + "tableTo": "databases", "columnsFrom": [ "database_id" ], - "tableTo": "databases", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -2178,28 +2197,28 @@ "backup_storage_backup_id_backups_id_fk": { "name": "backup_storage_backup_id_backups_id_fk", "tableFrom": "backup_storage", + "tableTo": "backups", "columnsFrom": [ "backup_id" ], - "tableTo": "backups", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" }, "backup_storage_storage_channel_id_storage_channel_id_fk": { "name": "backup_storage_storage_channel_id_storage_channel_id_fk", "tableFrom": "backup_storage", + "tableTo": "storage_channel", "columnsFrom": [ "storage_channel_id" ], - "tableTo": "storage_channel", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -2306,10 +2325,10 @@ } }, "schemas": {}, - "views": {}, "sequences": {}, "roles": {}, "policies": {}, + "views": {}, "_meta": { "columns": {}, "schemas": {}, diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 20f2f7cc..35fe3740 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -183,6 +183,13 @@ "when": 1768424936753, "tag": "0025_past_franklin_richards", "breakpoints": true + }, + { + "idx": 26, + "version": "7", + "when": 1768665081232, + "tag": "0026_demonic_santa_claus", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema/07_database.ts b/src/db/schema/07_database.ts index 4e14e1dd..73966b5a 100644 --- a/src/db/schema/07_database.ts +++ b/src/db/schema/07_database.ts @@ -9,6 +9,7 @@ import {timestamps} from "@/db/schema/00_common"; import {AlertPolicy, alertPolicy} from "@/db/schema/10_alert-policy"; import {StoragePolicy, storagePolicy} from "@/db/schema/13_storage-policy"; import {BackupStorage, backupStorage} from "@/db/schema/14_storage-backup"; +import {storageChannel} from "@/db/schema/12_storage-channel"; export const database = pgTable("databases", { id: uuid("id").primaryKey().defaultRandom(), @@ -63,6 +64,8 @@ export const retentionPolicy = pgTable("retention_policies", { export const restoration = pgTable("restorations", { id: uuid("id").primaryKey().defaultRandom(), status: statusEnum("status").default("waiting").notNull(), + backupStorageId: uuid("backup_storage_id") + .references(() => backupStorage.id, {onDelete: "cascade"}), backupId: uuid("backup_id") .notNull() .references(() => backup.id, {onDelete: "cascade"}), @@ -93,6 +96,7 @@ export const backupRelations = relations(backup, ({one, many}) => ({ export const restorationRelations = relations(restoration, ({one}) => ({ backup: one(backup, {fields: [restoration.backupId], references: [backup.id]}), database: one(database, {fields: [restoration.databaseId], references: [database.id]}), + backupStorage: one(backupStorage, {fields: [restoration.backupStorageId], references: [backupStorage.id]}), })); diff --git a/src/features/storages/providers/local.ts b/src/features/storages/providers/local.ts index 6114597b..e6924f9d 100644 --- a/src/features/storages/providers/local.ts +++ b/src/features/storages/providers/local.ts @@ -49,11 +49,18 @@ export async function getLocal( const expiresAt = Date.now() + 60 * 1000; const token = crypto.createHash("sha256").update(`${fileName}${expiresAt}`).digest("hex"); + const params = new URLSearchParams({ + path: input.data.path, + token, + expires: expiresAt.toString(), + }); + + return { success: true, provider: 'local', file: file, - url: `${baseUrl}/api/files/${fileName}?token=${token}&expires=${expiresAt}`, + url: `${baseUrl}/api/files/?${params.toString()}`, }; }