From baeec9f7a0d805ce6aac1cc0aa0b02db5c33315a Mon Sep 17 00:00:00 2001 From: charlesgauthereau Date: Sun, 3 Aug 2025 12:17:10 +0200 Subject: [PATCH] Some fix on the s3 and local methods for the files saving, we have to improve testing. Working on table actions. --- app/api/agent/[agentId]/status/helpers.ts | 91 +++++++-- .../wrappers/common/table/data-table.tsx | 189 +++--------------- .../projects/database/database-tabs.tsx | 173 ++++++++++++++-- src/features/dashboard/backup/columns.tsx | 29 ++- src/features/upload/private/upload.action.ts | 146 +++++++++----- src/utils/s3-file-management.ts | 65 ++++-- 6 files changed, 422 insertions(+), 271 deletions(-) diff --git a/app/api/agent/[agentId]/status/helpers.ts b/app/api/agent/[agentId]/status/helpers.ts index 7a6a1938..6fefbf38 100644 --- a/app/api/agent/[agentId]/status/helpers.ts +++ b/app/api/agent/[agentId]/status/helpers.ts @@ -1,13 +1,16 @@ import {NextResponse} from "next/server"; import {Body} from "./route"; import {isUuidv4} from "@/utils/verify-uuid"; -import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action"; +import {getFileUrlPresignedLocal, getFileUrlPreSignedS3Action} from "@/features/upload/private/upload.action"; import {Agent} from "@/db/schema/07_agent"; import {Database} from "@/db/schema/06_database"; import * as drizzleDb from "@/db"; import {db as dbClient} from "@/db"; import {and, eq} from "drizzle-orm"; import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types"; +import {ServerActionResult} from "@/types/action-type"; +import {SafeActionResult} from "next-safe-action"; +import {ZodString} from "zod"; export async function handleDatabases(body: Body, agent: Agent, lastContact: Date) { const databasesResponse = []; @@ -35,13 +38,13 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat let backupAction: boolean = false let restoreAction: boolean = false - let UrlBackup: string = "" + let urlBackup: string = "" if (!existingDatabase) { if (!isUuidv4(db.generatedId)) { return NextResponse.json( - { error: "generatedId is not a valid uuid" }, - { status: 500 } + {error: "generatedId is not a valid uuid"}, + {status: 500} ); } console.log(db) @@ -58,62 +61,108 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat .returning(); if (databaseCreated) { - databasesResponse.push(formatDatabase(databaseCreated, backupAction,restoreAction, UrlBackup)); + databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup)); } } else { - const [databaseUpdated] = await dbClient .update(drizzleDb.schemas.database) - .set({ lastContact: lastContact }) + .set({lastContact: lastContact}) .where(eq(drizzleDb.schemas.database.id, existingDatabase.id)) .returning(); - const backup = await dbClient.query.backup.findFirst({ - where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status,"waiting")) + where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status, "waiting")) }) 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")) }) - if(backup){ + if (backup) { backupAction = true await dbClient .update(drizzleDb.schemas.backup) - .set({ status: "ongoing" }) + .set({status: "ongoing"}) .where(eq(drizzleDb.schemas.backup.id, backup.id)); } - if(restoration){ + if (restoration) { restoreAction = 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 fileName = backupToRestore?.file - UrlBackup = await getFileUrlPresignedLocal(fileName ?? "") + let data: SafeActionResult, object> | undefined + + try { + + if (settings.storage == "local") { + data = await getFileUrlPresignedLocal(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" }) + .set({status: "ongoing"}) .where(eq(drizzleDb.schemas.restoration.id, restoration.id)); - } - - - databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, UrlBackup)); + databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup)); } } - return databasesResponse; -} \ No newline at end of file +} + diff --git a/src/components/wrappers/common/table/data-table.tsx b/src/components/wrappers/common/table/data-table.tsx index 2446e9d5..c87568c1 100644 --- a/src/components/wrappers/common/table/data-table.tsx +++ b/src/components/wrappers/common/table/data-table.tsx @@ -1,157 +1,3 @@ -// "use client"; -// -// import { -// ColumnDef, -// flexRender, -// getCoreRowModel, -// useReactTable, -// getPaginationRowModel, -// getSortedRowModel, -// SortingState, -// ColumnFiltersState, -// getFilteredRowModel, -// } from "@tanstack/react-table"; -// -// import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -// import { Input } from "@/components/ui/input"; -// -// import { useState } from "react"; -// import { TablePagination } from "./table-pagination"; -// import { Checkbox } from "@/components/ui/checkbox"; -// -// interface DataTableProps { -// columns: ColumnDef[]; -// data: TData[]; -// enableFilter?: boolean; -// enableSelect?: boolean; -// enablePagination?: boolean; -// paginationOptions?: { -// pageSize: number[]; -// pageVisible: number; -// className?: string; -// }; -// filterOptions?: { -// title?: string; -// key: string; -// }; -// emptyText?: string; -// } -// -// export function DataTable({ -// columns, -// data, -// enableFilter = false, -// enablePagination = true, -// enableSelect = true, -// paginationOptions = { pageSize: [10, 20, 30, 40, 50, 100], pageVisible: 3 }, -// filterOptions = { key: "id", title: "Filter by ID" }, -// emptyText = "No data.", -// }: DataTableProps) { -// const [sorting, setSorting] = useState([]); -// const [columnFilters, setColumnFilters] = useState([]); -// const [rowSelection, setRowSelection] = useState({}); -// -// if (enableSelect && data.length > 0) { -// const selectColumnExists = columns.some((column) => column.id === "select"); -// -// if (!selectColumnExists) { -// columns.unshift({ -// id: "select", -// header: ({ table }) => ( -// table.toggleAllPageRowsSelected(!!value)} -// aria-label="Select all" -// /> -// ), -// cell: ({ row }) => row.toggleSelected(!!value)} aria-label="Select row" />, -// enableSorting: false, -// enableHiding: false, -// }); -// } -// } -// -// const table = useReactTable({ -// data, -// columns, -// getCoreRowModel: getCoreRowModel(), -// getPaginationRowModel: getPaginationRowModel(), -// onSortingChange: setSorting, -// getSortedRowModel: getSortedRowModel(), -// onColumnFiltersChange: setColumnFilters, -// getFilteredRowModel: getFilteredRowModel(), -// onRowSelectionChange: setRowSelection, -// state: { -// sorting, -// columnFilters, -// rowSelection, -// }, -// }); -// -// return ( -//
-// {enableFilter && ( -//
-// table.getColumn(filterOptions.key)?.setFilterValue(event.target.value)} -// className="max-w-sm" -// /> -//
-// )} -//
-// -// -// {table.getHeaderGroups().map((headerGroup) => ( -// -// {headerGroup.headers.map((header) => { -// return ( -// -// {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} -// -// ); -// })} -// -// ))} -// -// -// {table.getRowModel().rows?.length ? ( -// table.getRowModel().rows.map((row) => ( -// -// {row.getVisibleCells().map((cell) => ( -// {flexRender(cell.column.columnDef.cell, cell.getContext())} -// ))} -// -// )) -// ) : ( -// -// -// {emptyText} -// -// -// )} -// -//
-//
-//
-// {enableSelect && ( -//
-// {table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s) selected. -//
-// )} -// {enablePagination && ( -// -// )} -//
-//
-// ); -// } "use client" import { ColumnDef, @@ -168,7 +14,7 @@ import { import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table"; import {Input} from "@/components/ui/input"; -import {useState} from "react"; +import {ReactNode, useEffect, useState} from "react"; import {TablePagination} from "./table-pagination"; import {Checkbox} from "@/components/ui/checkbox"; import {Button} from "@/components/ui/button"; @@ -195,6 +41,7 @@ interface DataTableProps { path: string; }; highlightRow?: (row: TData) => boolean; + selectedActions?: (rows: TData[]) => ReactNode; } @@ -207,12 +54,18 @@ export function DataTable({ paginationOptions = {pageSize: [10, 20, 30, 40, 50, 100], pageVisible: 3}, filterOptions = {key: "id", title: "Filter by ID"}, emptyButton, - highlightRow + highlightRow, + selectedActions, + + }: DataTableProps) { const [sorting, setSorting] = useState([]); const [columnFilters, setColumnFilters] = useState([]); const [rowSelection, setRowSelection] = useState({}); + useEffect(() => { + setRowSelection({}); + }, [data]); if (enableSelect && data.length > 0) { const selectColumnExists = columns.some((column) => column.id === "select"); @@ -257,14 +110,20 @@ export function DataTable({
- {enableFilter && ( + {enableFilter || selectedActions && (
- table.getColumn(filterOptions.key)?.setFilterValue(event.target.value)} - className="max-w-sm" - /> + {enableFilter && ( + table.getColumn(filterOptions.key)?.setFilterValue(event.target.value)} + className="max-w-sm" + /> + )} + {/*{selectedActions && table.getSelectedRowModel().rows.length > 0 && (*/} + {selectedActions && ( + selectedActions(table.getSelectedRowModel().rows.map(row => row.original)) + )}
)}
@@ -277,6 +136,7 @@ export function DataTable({ return ( {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + ); })} @@ -342,10 +202,7 @@ export function DataTable({ /> )}
- -
- ); } diff --git a/src/components/wrappers/dashboard/projects/database/database-tabs.tsx b/src/components/wrappers/dashboard/projects/database/database-tabs.tsx index 000c1397..f82e7fa0 100644 --- a/src/components/wrappers/dashboard/projects/database/database-tabs.tsx +++ b/src/components/wrappers/dashboard/projects/database/database-tabs.tsx @@ -1,14 +1,20 @@ "use client"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { eventUpdate } from "@/types/events"; -import { backupColumns } from "@/features/dashboard/backup/columns"; -import { restoreColumns } from "@/features/dashboard/restore/columns"; -import { DataTable } from "@/components/wrappers/common/table/data-table"; +import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"; +import {useEffect, useState} from "react"; +import {useRouter, useSearchParams} from "next/navigation"; +import {eventUpdate} from "@/types/events"; +import {backupColumns} from "@/features/dashboard/backup/columns"; +import {restoreColumns} from "@/features/dashboard/restore/columns"; +import {DataTable} from "@/components/wrappers/common/table/data-table"; import {Backup, Database, DatabaseWith, Restoration} from "@/db/schema/06_database"; import {Setting} from "@/db/schema/00_setting"; +import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu"; +import {MoreHorizontal, Trash2} from "lucide-react"; +import {useMutation} from "@tanstack/react-query"; +import {deleteBackupAction, deleteRestoreAction} from "@/features/dashboard/restore/restore.action"; +import {toast} from "sonner"; +import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading"; export type DatabaseTabsProps = { settings: Setting @@ -20,9 +26,12 @@ export type DatabaseTabsProps = { export const DatabaseTabs = (props: DatabaseTabsProps) => { const router = useRouter(); + const searchParams = useSearchParams(); + + const [tab, setTab] = useState(() => searchParams.get("tab") ?? "backup"); + useEffect(() => { const eventSource = new EventSource("/api/events"); - eventSource.addEventListener("modification", (event) => { const data: eventUpdate = JSON.parse(event.data); if (data.update) { @@ -35,18 +44,158 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => { }; }, []); + + useEffect(() => { + const newTab = searchParams.get("tab") ?? "backup"; + setTab(newTab); + }, [searchParams]); + + const handleChangeTab = (value: string) => { + router.push(`?tab=${value}`); + }; + + + const mutationDeleteBackups = useMutation({ + mutationFn: async (backups: Backup[]) => { + const results = await Promise.all( + backups.map(async (backup) => { + const backupDeleted = await deleteBackupAction({ + backupId: backup.id, + databaseId: backup.databaseId, + }); + return { + success: backupDeleted?.data?.success, + message: backupDeleted?.data?.success + ? backupDeleted?.data?.actionSuccess?.message + // @ts-ignore + : restoration?.data?.actionError.message, + }; + }) + ); + results.forEach((result) => { + if (result.success) { + toast.success(result.message); + } else { + toast.error(result.message); + } + }); + router.refresh(); + }, + }); + + const mutationDeleteRestorations = useMutation({ + mutationFn: async (restorations: Restoration[]) => { + const results = await Promise.all( + restorations.map(async (restoration) => { + const restorationDeleted = await deleteRestoreAction({ + restorationId: restoration.id, + }); + return { + success: restorationDeleted?.data?.success, + message: restorationDeleted?.data?.success + ? restorationDeleted?.data?.actionSuccess?.message + // @ts-ignore + : restorationDeleted?.data?.actionError.message, + }; + }) + ); + results.forEach((result) => { + if (result.success) { + toast.success(result.message); + } else { + toast.error(result.message); + } + }); + router.refresh(); + }, + }); + + return ( - + Backup Restoration - - + ( + + + { + + }} + disabled={rows.length === 0 || mutationDeleteBackups.isPending} + icon={} + isPending={mutationDeleteBackups.isPending} + size="sm" + /> + + + {/* {*/} + {/* console.log("Deleting rows:", rows)*/} + {/* }}*/} + {/*>*/} + {/* */} + {/* Download Selected*/} + {/**/} + {/**/} + { + console.log("Deleting rows:", rows) + await mutationDeleteBackups.mutateAsync(rows) + }} + className="text-red-600 focus:text-red-700" + > + + Delete Selected + + + + )} + /> - + ( + + + { + }} + disabled={rows.length === 0 || mutationDeleteRestorations.isPending} + icon={} + isPending={mutationDeleteRestorations.isPending} + size="sm" + /> + + + { + await mutationDeleteRestorations.mutateAsync(rows) + }} + disabled={props.isAlreadyRestore} + className="text-red-600 focus:text-red-700" + > + + Delete Selected + + + + )} + /> ); diff --git a/src/features/dashboard/backup/columns.tsx b/src/features/dashboard/backup/columns.tsx index f871bc8b..ffc7f3b5 100644 --- a/src/features/dashboard/backup/columns.tsx +++ b/src/features/dashboard/backup/columns.tsx @@ -14,7 +14,6 @@ import {Download, MoreHorizontal, Trash2} from "lucide-react"; import {ReloadIcon} from "@radix-ui/react-icons"; import { getFileUrlPresignedLocal, - getFileUrlPresignedS3, getFileUrlPreSignedS3Action } from "@/features/upload/private/upload.action"; import {useMutation} from "@tanstack/react-query"; @@ -26,6 +25,9 @@ import {Backup, DatabaseWith} from "@/db/schema/06_database"; import {formatFrenchDate} from "@/utils/date-formatting"; import {TooltipCustom} from "@/components/wrappers/common/tooltip-custom"; import {Setting} from "@/db/schema/00_setting"; +import {SafeActionResult} from "next-safe-action"; +import {ZodString} from "zod"; +import {ServerActionResult} from "@/types/action-type"; export function backupColumns(isAlreadyRestore: boolean, settings: Setting, database: DatabaseWith): ColumnDef[] { @@ -103,19 +105,26 @@ export function backupColumns(isAlreadyRestore: boolean, settings: Setting, data }; const handleDownload = async (fileName: string) => { + let url: string = ""; + let data: SafeActionResult, object> | undefined + if (settings.storage == "local") { - url = await getFileUrlPresignedLocal(fileName); + data = await getFileUrlPresignedLocal(fileName!) } else if (settings.storage == "s3") { - const data = await getFileUrlPreSignedS3Action(`backups/${database.project?.slug}/${fileName}`) - if (data?.data?.success) { - url = data.data.value ?? ""; - } else { - // @ts-ignore - const errorMessage = data?.data?.actionError?.message || "Failed to get file!"; - toast.error(errorMessage); - } + data = await getFileUrlPreSignedS3Action(`backups/${database.project?.slug}/${fileName}`); } + console.log(data) + if (data?.data?.success) { + url = data.data.value ?? ""; + } else { + // @ts-ignore + const errorMessage = data?.data?.actionError?.message || "Failed to get file!"; + toast.error(errorMessage); + } + window.open(url, "_self"); }; diff --git a/src/features/upload/private/upload.action.ts b/src/features/upload/private/upload.action.ts index 63d12f03..70cefea5 100644 --- a/src/features/upload/private/upload.action.ts +++ b/src/features/upload/private/upload.action.ts @@ -7,7 +7,7 @@ import {getServerUrl} from "@/utils/get-server-url"; import {createPresignedUrlToDownload, saveFileInBucket} from "@/utils/s3-file-management"; import crypto from "crypto"; import {env} from "@/env.mjs"; -import {userAction} from "@/safe-actions"; +import {action, userAction} from "@/safe-actions"; import {z} from "zod"; import {ServerActionResult} from "@/types/action-type"; import {Backup} from "@/db/schema/06_database"; @@ -54,25 +54,25 @@ export async function uploadS3Private(fileName: string, buffer: any, bucketName: } -export async function getFileUrlPresignedLocal(fileName: string) { - try { - const filePath = path.join(privateLocalDir, fileName); - await mkdir(path.join(process.cwd(), privateLocalDir), {recursive: true}); - - if (!fs.existsSync(filePath)) { - console.error("File not found at:", filePath); - return `File not found at: ${filePath}`; - } - const crypto = require("crypto"); - const baseUrl = getServerUrl(); - - const expiresAt = Date.now() + 60 * 1000; // expires in 1 minute - const token = crypto.createHash("sha256").update(`${fileName}${expiresAt}`).digest("hex"); - return `${baseUrl}/api/files/${fileName}?token=${token}&expires=${expiresAt}`; - } catch (error) { - throw error; - } -} +// export async function getFileUrlPresignedLocal(fileName: string) { +// try { +// const filePath = path.join(privateLocalDir, fileName); +// await mkdir(path.join(process.cwd(), privateLocalDir), {recursive: true}); +// +// if (!fs.existsSync(filePath)) { +// console.error("File not found at:", filePath); +// return `File not found at: ${filePath}`; +// } +// const crypto = require("crypto"); +// const baseUrl = getServerUrl(); +// +// const expiresAt = Date.now() + 60 * 1000; // expires in 1 minute +// const token = crypto.createHash("sha256").update(`${fileName}${expiresAt}`).digest("hex"); +// return `${baseUrl}/api/files/${fileName}?token=${token}&expires=${expiresAt}`; +// } catch (error) { +// throw error; +// } +// } export async function getFileUrlPresignedS3(fileName: string) { try { @@ -86,33 +86,85 @@ export async function getFileUrlPresignedS3(fileName: string) { } -export const getFileUrlPreSignedS3Action = userAction.schema(z.string()).action(async ({parsedInput}): Promise> => { - try { - const url = await createPresignedUrlToDownload({ - bucketName: env.S3_BUCKET_NAME!, - fileName: parsedInput, - }); - return { - success: true, - value: url, - actionSuccess: { - message: "Successfully get url", - messageParams: {fileName: parsedInput}, - }, - }; - } catch (error) { - console.error("Error creating backup:", error); - return { - success: false, - actionError: { - message: "Failed to create url pre signed s3.", - status: 500, - cause: error instanceof Error ? error.message : "Unknown error", - messageParams: {fileName: parsedInput}, - }, - }; - } -}); +export const getFileUrlPresignedLocal = action + .schema(z.string()) + .action(async ({parsedInput}): Promise> => { + try { + const filePath = path.join(privateLocalDir, parsedInput); + await mkdir(path.join(process.cwd(), privateLocalDir), {recursive: true}); + + if (!fs.existsSync(filePath)) { + console.error("File not found at:", filePath); + throw new Error(`File not found at: ${filePath}`); + } + const crypto = require("crypto"); + const baseUrl = getServerUrl(); + + const expiresAt = Date.now() + 60 * 1000; // expires in 1 minute + const token = crypto.createHash("sha256").update(`${parsedInput}${expiresAt}`).digest("hex"); + + return { + success: true, + value: `${baseUrl}/api/files/${parsedInput}?token=${token}&expires=${expiresAt}`, + actionSuccess: { + message: "Successfully retrieved presigned URL Local", + messageParams: {fileName: parsedInput}, + }, + }; + } catch (error) { + + return { + success: false, + actionError: { + message: "Failed to generate presigned URL", + status: 500, + cause: error instanceof Error ? error.message : "Unknown error", + messageParams: {fileName: parsedInput}, + }, + }; + } + }); +export const getFileUrlPreSignedS3Action = action + .schema(z.string()) + .action(async ({parsedInput}): Promise> => { + try { + const data = await createPresignedUrlToDownload({ + bucketName: env.S3_BUCKET_NAME!, + fileName: parsedInput, + }); + + return { + success: true, + value: data.url, + actionSuccess: { + message: "Successfully retrieved presigned URL", + messageParams: {fileName: parsedInput}, + }, + }; + } catch (error) { + const isNotFound = error instanceof Error && error.message.includes("File does not exist"); + + const logContext = { + file: parsedInput, + reason: error instanceof Error ? error.message : "Unknown", + }; + + console.error("Presigned URL generation failed:", logContext); + + return { + success: false, + actionError: { + message: isNotFound + ? "File not found in S3 bucket" + : "Failed to generate presigned URL", + status: isNotFound ? 404 : 500, + cause: error instanceof Error ? error.message : "Unknown error", + messageParams: {fileName: parsedInput}, + }, + }; + } + }); + diff --git a/src/utils/s3-file-management.ts b/src/utils/s3-file-management.ts index e47fb95f..986fa559 100644 --- a/src/utils/s3-file-management.ts +++ b/src/utils/s3-file-management.ts @@ -108,15 +108,35 @@ export async function saveFileInBucket({bucketName, fileName, file}: { * @param fileName name of the file * @returns true if file exists, false if not */ -export async function checkFileExistsInBucket({bucketName, fileName}: { bucketName: string; fileName: string }) { - const s3Client = await getS3Client(); - +// export async function checkFileExistsInBucket({bucketName, fileName}: { bucketName: string; fileName: string }) { +// const s3Client = await getS3Client(); +// +// try { +// await s3Client.statObject(bucketName, fileName); +// } catch (error) { +// return false; +// } +// return true; +// } +export async function checkFileExistsInBucket({ + bucketName, + fileName, + }: { + bucketName: string; + fileName: string; +}): Promise { + const s3 = await getS3Client(); try { - await s3Client.statObject(bucketName, fileName); - } catch (error) { + const stat = await s3.statObject(bucketName, fileName); + return !!stat; + } catch (error: any) { + if (error.code === 'NoSuchKey' || error.message?.includes('not found')) { + return false; + } + // Instead of throwing, return false to prevent crashes + // console.error("Unexpected S3 statObject error:", error); return false; } - return true; } /** @@ -186,19 +206,34 @@ export async function createPublicBucket({bucketName}: { bucketName: string }) { export async function createPresignedUrlToDownload({ bucketName, fileName, - expiry = 60 * 60, // 1 hour + expiry = 60 * 60, }: { bucketName: string; fileName: string; expiry?: number; }) { - const s3Client = await getS3Client(); + try { + const s3Client = await getS3Client(); - // Optionally: ensure file exists - const fileExists = await checkFileExistsInBucket({ bucketName, fileName }); - if (!fileExists) { - throw new Error("File does not exist in the bucket."); + console.debug("Checking if file exists in bucket:", {bucketName, fileName}); + + const fileExists = await checkFileExistsInBucket({bucketName, fileName}); + + if (!fileExists) { + console.warn("File does not exist:", {bucketName, fileName}); + throw new Error("File does not exist in the bucket."); + } + const presignedUrl = await s3Client.presignedGetObject(bucketName, fileName, expiry); + console.debug("Generated pre signed URL:", presignedUrl); + + return {url: presignedUrl}; + } catch (err: any) { + console.error("Error in createPreSignedUrlToDownload:", { + bucketName, + fileName, + errorMessage: err?.message, + // stack: err?.stack, + }); + throw {error: err.message ?? "Unknown error"}; } - - return await s3Client.presignedGetObject(bucketName, fileName, expiry); -} \ No newline at end of file +}