mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Some fix on the s3 and local methods for the files saving, we have to improve testing. Working on table actions.
This commit is contained in:
@@ -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<TData, TValue> {
|
||||
// columns: ColumnDef<TData, TValue>[];
|
||||
// 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<TData, TValue>({
|
||||
// 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<TData, TValue>) {
|
||||
// const [sorting, setSorting] = useState<SortingState>([]);
|
||||
// const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
// 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 }) => (
|
||||
// <Checkbox
|
||||
// checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
|
||||
// onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
// aria-label="Select all"
|
||||
// />
|
||||
// ),
|
||||
// cell: ({ row }) => <Checkbox checked={row.getIsSelected()} onCheckedChange={(value) => 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 (
|
||||
// <div className="h-full">
|
||||
// {enableFilter && (
|
||||
// <div className="flex items-center py-4">
|
||||
// <Input
|
||||
// placeholder={`${filterOptions.title ?? `Filter by ${filterOptions.key}`}`}
|
||||
// value={(table.getColumn(filterOptions.key)?.getFilterValue() as string) ?? ""}
|
||||
// onChange={(event) => table.getColumn(filterOptions.key)?.setFilterValue(event.target.value)}
|
||||
// className="max-w-sm"
|
||||
// />
|
||||
// </div>
|
||||
// )}
|
||||
// <div className="rounded-md border w-full">
|
||||
// <Table className="w-full">
|
||||
// <TableHeader>
|
||||
// {table.getHeaderGroups().map((headerGroup) => (
|
||||
// <TableRow key={headerGroup.id}>
|
||||
// {headerGroup.headers.map((header) => {
|
||||
// return (
|
||||
// <TableHead key={header.id}>
|
||||
// {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
// </TableHead>
|
||||
// );
|
||||
// })}
|
||||
// </TableRow>
|
||||
// ))}
|
||||
// </TableHeader>
|
||||
// <TableBody>
|
||||
// {table.getRowModel().rows?.length ? (
|
||||
// table.getRowModel().rows.map((row) => (
|
||||
// <TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
// {row.getVisibleCells().map((cell) => (
|
||||
// <TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
|
||||
// ))}
|
||||
// </TableRow>
|
||||
// ))
|
||||
// ) : (
|
||||
// <TableRow>
|
||||
// <TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
// {emptyText}
|
||||
// </TableCell>
|
||||
// </TableRow>
|
||||
// )}
|
||||
// </TableBody>
|
||||
// </Table>
|
||||
// </div>
|
||||
// <div className="flex items-center justify-end space-x-2 py-4 mt-6">
|
||||
// {enableSelect && (
|
||||
// <div className="flex-1 text-sm text-muted-foreground">
|
||||
// {table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s) selected.
|
||||
// </div>
|
||||
// )}
|
||||
// {enablePagination && (
|
||||
// <TablePagination
|
||||
// table={table}
|
||||
// maxVisiblePages={paginationOptions?.pageVisible}
|
||||
// pageSizeOptions={paginationOptions.pageSize}
|
||||
// className={paginationOptions.className}
|
||||
// />
|
||||
// )}
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
"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<TData, TValue> {
|
||||
path: string;
|
||||
};
|
||||
highlightRow?: (row: TData) => boolean;
|
||||
selectedActions?: (rows: TData[]) => ReactNode;
|
||||
|
||||
}
|
||||
|
||||
@@ -207,12 +54,18 @@ export function DataTable<TData, TValue>({
|
||||
paginationOptions = {pageSize: [10, 20, 30, 40, 50, 100], pageVisible: 3},
|
||||
filterOptions = {key: "id", title: "Filter by ID"},
|
||||
emptyButton,
|
||||
highlightRow
|
||||
highlightRow,
|
||||
selectedActions,
|
||||
|
||||
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
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<TData, TValue>({
|
||||
<div
|
||||
className="flex flex-col h-full"
|
||||
>
|
||||
{enableFilter && (
|
||||
{enableFilter || selectedActions && (
|
||||
<div className="flex items-center py-4">
|
||||
<Input
|
||||
placeholder={`${filterOptions.title ?? `Filter by ${filterOptions.key}`}`}
|
||||
value={(table.getColumn(filterOptions.key)?.getFilterValue() as string) ?? ""}
|
||||
onChange={(event) => table.getColumn(filterOptions.key)?.setFilterValue(event.target.value)}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
{enableFilter && (
|
||||
<Input
|
||||
placeholder={`${filterOptions.title ?? `Filter by ${filterOptions.key}`}`}
|
||||
value={(table.getColumn(filterOptions.key)?.getFilterValue() as string) ?? ""}
|
||||
onChange={(event) => 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))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col justify-between h-full">
|
||||
@@ -277,6 +136,7 @@ export function DataTable<TData, TValue>({
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
@@ -342,10 +202,7 @@ export function DataTable<TData, TValue>({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string>(() => 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 (
|
||||
<Tabs className="flex flex-col flex-1" defaultValue="backup">
|
||||
<Tabs className="flex flex-col flex-1" value={tab} onValueChange={handleChangeTab}>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="backup">Backup</TabsTrigger>
|
||||
<TabsTrigger value="restore">Restoration</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent className="h-full justify-between" value="backup">
|
||||
<DataTable columns={backupColumns(props.isAlreadyRestore, props.settings, props.database)} data={props.backups} enablePagination />
|
||||
<DataTable
|
||||
columns={backupColumns(props.isAlreadyRestore, props.settings, props.database)}
|
||||
data={props.backups}
|
||||
enablePagination
|
||||
selectedActions={(rows) => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ButtonWithLoading
|
||||
variant="outline"
|
||||
text="Actions"
|
||||
onClick={() => {
|
||||
|
||||
}}
|
||||
disabled={rows.length === 0 || mutationDeleteBackups.isPending}
|
||||
icon={<MoreHorizontal/>}
|
||||
isPending={mutationDeleteBackups.isPending}
|
||||
size="sm"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
{/*<DropdownMenuItem*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* console.log("Deleting rows:", rows)*/}
|
||||
{/* }}*/}
|
||||
{/*>*/}
|
||||
{/* <Download className="w-4 h-4 mr-2"/>*/}
|
||||
{/* Download Selected*/}
|
||||
{/*</DropdownMenuItem>*/}
|
||||
{/*<Separator/>*/}
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
console.log("Deleting rows:", rows)
|
||||
await mutationDeleteBackups.mutateAsync(rows)
|
||||
}}
|
||||
className="text-red-600 focus:text-red-700"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2"/>
|
||||
Delete Selected
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent className="h-full justify-between" value="restore">
|
||||
<DataTable columns={restoreColumns(props.isAlreadyRestore)} data={props.restorations} enablePagination />
|
||||
<DataTable
|
||||
columns={restoreColumns(props.isAlreadyRestore)}
|
||||
data={props.restorations}
|
||||
enablePagination
|
||||
selectedActions={(rows) => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ButtonWithLoading
|
||||
variant="outline"
|
||||
text="Actions"
|
||||
onClick={() => {
|
||||
}}
|
||||
disabled={rows.length === 0 || mutationDeleteRestorations.isPending}
|
||||
icon={<MoreHorizontal/>}
|
||||
isPending={mutationDeleteRestorations.isPending}
|
||||
size="sm"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await mutationDeleteRestorations.mutateAsync(rows)
|
||||
}}
|
||||
disabled={props.isAlreadyRestore}
|
||||
className="text-red-600 focus:text-red-700"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2"/>
|
||||
Delete Selected
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
|
||||
@@ -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<Backup>[] {
|
||||
@@ -103,19 +105,26 @@ export function backupColumns(isAlreadyRestore: boolean, settings: Setting, data
|
||||
};
|
||||
|
||||
const handleDownload = async (fileName: string) => {
|
||||
|
||||
let url: string = "";
|
||||
let data: SafeActionResult<string, ZodString, readonly [], {
|
||||
_errors?: string[] | undefined;
|
||||
}, readonly [], ServerActionResult<string>, 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");
|
||||
};
|
||||
|
||||
|
||||
@@ -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<ServerActionResult<string>> => {
|
||||
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<ServerActionResult<string>> => {
|
||||
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<ServerActionResult<string>> => {
|
||||
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},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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<boolean> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user