diff --git a/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx b/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx
index 2fa68b6e..788407d5 100644
--- a/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx
+++ b/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx
@@ -36,6 +36,9 @@ export default async function RoutePage(props: PageParams<{
const dbItem = await db.query.database.findFirst({
where: and(inArray(drizzleDb.schemas.backup.id, databasesProject.ids ?? []), eq(drizzleDb.schemas.database.id, databaseId), eq(drizzleDb.schemas.database.projectId, projectId)),
+ with: {
+ project: true
+ }
});
if (!dbItem) {
@@ -71,6 +74,12 @@ export default async function RoutePage(props: PageParams<{
.then((rows) => rows.length),
]);
+
+ const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
+ if (!settings) {
+ notFound();
+ }
+
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
return (
@@ -88,7 +97,8 @@ export default async function RoutePage(props: PageParams<{
{dbItem.description}
-
diff --git a/app/api/agent/[agentId]/backup/route.ts b/app/api/agent/[agentId]/backup/route.ts
index 74df5e0b..90ca43c2 100644
--- a/app/api/agent/[agentId]/backup/route.ts
+++ b/app/api/agent/[agentId]/backup/route.ts
@@ -1,12 +1,13 @@
import {NextResponse} from "next/server";
import {isUuidv4} from "@/utils/verify-uuid";
-import {uploadLocalPrivate} from "@/features/upload/private/upload.action";
+import {uploadLocalPrivate, uploadS3Private} from "@/features/upload/private/upload.action";
import {v4 as uuidv4} from "uuid";
import {eventEmitter} from "../../../events/route";
import {db} from "@/db";
import {Backup} from "@/db/schema/06_database";
import {and, eq} from "drizzle-orm";
import * as drizzleDb from "@/db";
+import {env} from "@/env.mjs";
export async function POST(
request: Request,
@@ -48,6 +49,9 @@ export async function POST(
const database = await db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
+ with: {
+ project: true
+ }
});
if (!database) {
@@ -108,7 +112,19 @@ export async function POST(
const fileName = `${uuid}.dump`;
const buffer = Buffer.from(await file.arrayBuffer());
- const {success, message, filePath} = await uploadLocalPrivate(fileName, buffer);
+ const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
+ if (!settings) {
+ throw new Error("System settings not found.");
+ }
+
+ let success: boolean, message: string, filePath: string;
+
+ const result =
+ settings.storage === "local"
+ ? await uploadLocalPrivate(fileName, buffer)
+ : await uploadS3Private(`${database.project?.slug}/${fileName}`, buffer, env.S3_BUCKET_NAME!);
+
+ ({success, message, filePath} = result);
if (!success) {
return NextResponse.json(
diff --git a/app/api/agent/[agentId]/status/route.ts b/app/api/agent/[agentId]/status/route.ts
index c2217968..09e559a0 100644
--- a/app/api/agent/[agentId]/status/route.ts
+++ b/app/api/agent/[agentId]/status/route.ts
@@ -18,7 +18,7 @@ export type Body = {
databases: databaseAgent[]
}
-
+// Function to test the get file url presigned local
export async function GET(request: Request) {
const url = await getFileUrlPresignedLocal("d4a7fa35-2506-4d01-a612-a8ef2e2cc1c5.dump")
return Response.json({
diff --git a/portabase.config.ts b/portabase.config.ts
index 2e5e6d27..28d833a1 100644
--- a/portabase.config.ts
+++ b/portabase.config.ts
@@ -13,6 +13,7 @@ export const PORTABASE_DEFAULT_SETTINGS = {
"https://code.iconify.com",
"https://cdn.iconify.design",
"https://api.iconify.design",
+
],
STYLE_SRC: [
"'self'",
@@ -31,6 +32,7 @@ export const PORTABASE_DEFAULT_SETTINGS = {
"https://cdn.iconify.design",
"https://code.iconify.com",
"https://api.iconify.design",
+ "http://localhost:9000",
],
FONT_SRC: [
"'self'",
diff --git a/src/components/wrappers/dashboard/profile/avatar/avatar-with-upload.tsx b/src/components/wrappers/dashboard/profile/avatar/avatar-with-upload.tsx
index 680f51ad..60f10264 100644
--- a/src/components/wrappers/dashboard/profile/avatar/avatar-with-upload.tsx
+++ b/src/components/wrappers/dashboard/profile/avatar/avatar-with-upload.tsx
@@ -7,6 +7,7 @@ import { useMutation } from "@tanstack/react-query";
import { updateImageUserAction } from "@/components/wrappers/dashboard/profile/avatar/avatar.action";
import { useRouter } from "next/navigation";
import { User } from "@/db/schema/01_user";
+import {ChangeEvent} from "react";
export type AvatarWithUploadProps = {
user: User;
@@ -43,7 +44,7 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
},
});
- const handleImageUpload = async (event: React.ChangeEvent) => {
+ const handleImageUpload = async (event: ChangeEvent) => {
const file = event.target.files?.[0];
if (!file) return;
if (!file.type.includes("image")) {
diff --git a/src/components/wrappers/dashboard/projects/database/database-tabs.tsx b/src/components/wrappers/dashboard/projects/database/database-tabs.tsx
index 8da6a7e4..000c1397 100644
--- a/src/components/wrappers/dashboard/projects/database/database-tabs.tsx
+++ b/src/components/wrappers/dashboard/projects/database/database-tabs.tsx
@@ -7,13 +7,15 @@ 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, Restoration} from "@/db/schema/06_database";
+import {Backup, Database, DatabaseWith, Restoration} from "@/db/schema/06_database";
+import {Setting} from "@/db/schema/00_setting";
export type DatabaseTabsProps = {
+ settings: Setting
backups: Backup[];
restorations: Restoration[];
isAlreadyRestore: boolean;
- database: Database;
+ database: DatabaseWith;
};
export const DatabaseTabs = (props: DatabaseTabsProps) => {
@@ -41,7 +43,7 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
-
+
diff --git a/src/db/schema/06_database.ts b/src/db/schema/06_database.ts
index aaba0889..066ad824 100644
--- a/src/db/schema/06_database.ts
+++ b/src/db/schema/06_database.ts
@@ -1,10 +1,10 @@
-import { pgTable, text, boolean, timestamp, uuid, integer, pgEnum, uniqueIndex } from "drizzle-orm/pg-core";
-import { Agent, agent } from "./07_agent";
-import { Project, project } from "./05_project";
-import { relations } from "drizzle-orm";
-import { dbmsEnum, statusEnum } from "./types";
-import { createSelectSchema } from "drizzle-zod";
-import { z } from "zod";
+import {pgTable, text, boolean, timestamp, uuid, integer, pgEnum, uniqueIndex} from "drizzle-orm/pg-core";
+import {Agent, agent} from "./07_agent";
+import {Project, project} from "./05_project";
+import {relations} from "drizzle-orm";
+import {dbmsEnum, statusEnum} from "./types";
+import {createSelectSchema} from "drizzle-zod";
+import {z} from "zod";
export const database = pgTable("databases", {
id: uuid("id").primaryKey().defaultRandom(),
@@ -19,7 +19,7 @@ export const database = pgTable("databases", {
updatedAt: timestamp("updated_at"),
agentId: uuid("agent_id")
.notNull()
- .references(() => agent.id, { onDelete: "cascade" }),
+ .references(() => agent.id, {onDelete: "cascade"}),
lastContact: timestamp("last_contact"),
projectId: uuid("project_id")
@@ -36,7 +36,7 @@ export const backup = pgTable(
updatedAt: timestamp("updated_at"),
databaseId: uuid("database_id")
.notNull()
- .references(() => database.id, { onDelete: "cascade" }),
+ .references(() => database.id, {onDelete: "cascade"}),
},
// (table) => [uniqueIndex("database_id_status_unique").on(table.databaseId, table.status)]
);
@@ -48,25 +48,25 @@ export const restoration = pgTable("restorations", {
updatedAt: timestamp("updated_at"),
backupId: uuid("backup_id")
.notNull()
- .references(() => backup.id, { onDelete: "cascade" }),
- databaseId: uuid("database_id").references(() => database.id, { onDelete: "cascade" }),
+ .references(() => backup.id, {onDelete: "cascade"}),
+ databaseId: uuid("database_id").references(() => database.id, {onDelete: "cascade"}),
});
-export const databaseRelations = relations(database, ({ one, many }) => ({
- agent: one(agent, { fields: [database.agentId], references: [agent.id] }),
- project: one(project, { fields: [database.projectId], references: [project.id] }),
+export const databaseRelations = relations(database, ({one, many}) => ({
+ agent: one(agent, {fields: [database.agentId], references: [agent.id]}),
+ project: one(project, {fields: [database.projectId], references: [project.id]}),
backups: many(backup),
restorations: many(restoration),
}));
-export const backupRelations = relations(backup, ({ one, many }) => ({
- database: one(database, { fields: [backup.databaseId], references: [database.id] }),
+export const backupRelations = relations(backup, ({one, many}) => ({
+ database: one(database, {fields: [backup.databaseId], references: [database.id]}),
restorations: many(restoration),
}));
-export const restorationRelations = relations(restoration, ({ one }) => ({
- backup: one(backup, { fields: [restoration.backupId], references: [backup.id] }),
- database: one(database, { fields: [restoration.databaseId], references: [database.id] }),
+export const restorationRelations = relations(restoration, ({one}) => ({
+ backup: one(backup, {fields: [restoration.backupId], references: [backup.id]}),
+ database: one(database, {fields: [restoration.databaseId], references: [database.id]}),
}));
export const databaseSchema = createSelectSchema(database);
@@ -79,8 +79,9 @@ export const restorationSchema = createSelectSchema(restoration);
export type Restoration = z.infer;
export type DatabaseWith = Database & {
- agent: Agent;
- project: Project;
- backups: Backup[];
- restorations: Restoration[];
+ agent?: Agent | null;
+ project?: Project | null;
+ backups?: Backup[] | null;
+ restorations?: Restoration[] | null;
};
+
diff --git a/src/features/dashboard/backup/columns.tsx b/src/features/dashboard/backup/columns.tsx
index d1723495..f871bc8b 100644
--- a/src/features/dashboard/backup/columns.tsx
+++ b/src/features/dashboard/backup/columns.tsx
@@ -12,18 +12,23 @@ import {
import {Button} from "@/components/ui/button";
import {Download, MoreHorizontal, Trash2} from "lucide-react";
import {ReloadIcon} from "@radix-ui/react-icons";
-import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action";
+import {
+ getFileUrlPresignedLocal,
+ getFileUrlPresignedS3,
+ getFileUrlPreSignedS3Action
+} from "@/features/upload/private/upload.action";
import {useMutation} from "@tanstack/react-query";
import {createRestorationAction, deleteBackupAction} from "@/features/dashboard/restore/restore.action";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
import {StatusBadge} from "@/components/wrappers/common/status-badge";
-import {Backup} from "@/db/schema/06_database";
+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";
-export function backupColumns(isAlreadyRestore: boolean): ColumnDef[] {
+export function backupColumns(isAlreadyRestore: boolean, settings: Setting, database: DatabaseWith): ColumnDef[] {
return [
{
@@ -98,7 +103,19 @@ export function backupColumns(isAlreadyRestore: boolean): ColumnDef[] {
};
const handleDownload = async (fileName: string) => {
- const url = await getFileUrlPresignedLocal(fileName);
+ let url: string = "";
+ if (settings.storage == "local") {
+ url = 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);
+ }
+ }
window.open(url, "_self");
};
diff --git a/src/features/upload/private/upload.action.ts b/src/features/upload/private/upload.action.ts
index e69d8d02..63d12f03 100644
--- a/src/features/upload/private/upload.action.ts
+++ b/src/features/upload/private/upload.action.ts
@@ -1,16 +1,25 @@
"use server";
-import { mkdir, writeFile } from "fs/promises";
+import {mkdir, writeFile} from "fs/promises";
import path from "path";
import * as fs from "node:fs";
-import { getServerUrl } from "@/utils/get-server-url";
+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 {z} from "zod";
+import {ServerActionResult} from "@/types/action-type";
+import {Backup} from "@/db/schema/06_database";
+import {db} from "@/db";
+import * as drizzleDb from "@/db";
const privateLocalDir = "private/uploads/files/";
+const privateS3Dir = "backups/";
export async function uploadLocalPrivate(fileName: string, buffer: any) {
try {
- await mkdir(path.join(process.cwd(), privateLocalDir), { recursive: true });
-
+ await mkdir(path.join(process.cwd(), privateLocalDir), {recursive: true});
await writeFile(path.join(process.cwd(), privateLocalDir, fileName), buffer);
return {
@@ -24,10 +33,31 @@ export async function uploadLocalPrivate(fileName: string, buffer: any) {
}
}
+export async function uploadS3Private(fileName: string, buffer: any, bucketName: string) {
+ try {
+
+ await saveFileInBucket({
+ bucketName,
+ fileName: `${privateS3Dir}${fileName}`,
+ file: buffer,
+ });
+
+ return {
+ success: true,
+ filePath: `${privateS3Dir}${fileName}`,
+ message: "File uploaded successfully",
+ };
+ } catch (error) {
+ console.error("Error occurred:", error);
+ throw new Error("An error occurred while importing the private file");
+ }
+}
+
+
export async function getFileUrlPresignedLocal(fileName: string) {
try {
const filePath = path.join(privateLocalDir, fileName);
- await mkdir(path.join(process.cwd(), privateLocalDir), { recursive: true });
+ await mkdir(path.join(process.cwd(), privateLocalDir), {recursive: true});
if (!fs.existsSync(filePath)) {
console.error("File not found at:", filePath);
@@ -43,3 +73,46 @@ export async function getFileUrlPresignedLocal(fileName: string) {
throw error;
}
}
+
+export async function getFileUrlPresignedS3(fileName: string) {
+ try {
+ return await createPresignedUrlToDownload({
+ bucketName: env.S3_BUCKET_NAME!,
+ fileName: fileName,
+ });
+ } catch (error) {
+ throw error;
+ }
+}
+
+
+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},
+ },
+ };
+ }
+});
+
+
+
diff --git a/src/features/upload/public/upload.action.ts b/src/features/upload/public/upload.action.ts
index 5b186c4f..8d2150a8 100644
--- a/src/features/upload/public/upload.action.ts
+++ b/src/features/upload/public/upload.action.ts
@@ -30,11 +30,13 @@ export const uploadImageAction = userAction.schema(z.instanceof(FormData)).actio
let result: void | UploadedObjectInfo;
const bucketName = "public-image-bucket";
- if (settings.storage === "local") {
- result = await uploadLocal(fileName, buffer);
- } else if (settings.storage === "s3") {
- result = await uploadS3Compatible(bucketName, fileName, buffer);
- }
+ // TODO : Do not delete
+ // if (settings.storage === "local") {
+ // result = await uploadLocal(fileName, buffer);
+ // } else if (settings.storage === "s3") {
+ // result = await uploadS3Compatible(bucketName, fileName, buffer);
+ // }
+ result = await uploadLocal(fileName, buffer);
const url = getUrl(fileName, settings, bucketName);
console.log(url);
diff --git a/src/utils/s3-file-management.ts b/src/utils/s3-file-management.ts
index e4472c4a..e47fb95f 100644
--- a/src/utils/s3-file-management.ts
+++ b/src/utils/s3-file-management.ts
@@ -23,16 +23,23 @@ async function getS3Client() {
secretKey: settings.s3SecretAccessKey ?? "",
};
- const s3Client =
- env.NODE_ENV === "production"
- ? new Minio.Client({
- ...baseConfig,
- })
- : new Minio.Client({
- ...baseConfig,
- port: Number(env.S3_PORT ?? 0),
- useSSL: env.S3_USE_SSL === "true",
- });
+ // const s3Client =
+ // env.NODE_ENV === "production"
+ // ? new Minio.Client({
+ // ...baseConfig,
+ // useSSL: env.S3_USE_SSL === "true",
+ // })
+ // : new Minio.Client({
+ // ...baseConfig,
+ // port: Number(env.S3_PORT ?? 0),
+ // useSSL: env.S3_USE_SSL === "true",
+ // });
+
+ const s3Client = new Minio.Client({
+ ...baseConfig,
+ port: Number(env.S3_PORT ?? 0),
+ useSSL: env.S3_USE_SSL === "true",
+ })
return s3Client;
}
@@ -168,3 +175,30 @@ export async function createPublicBucket({bucketName}: { bucketName: string }) {
console.error("Error creating bucket:", error);
}
}
+
+/**
+ * Generate a presigned URL for downloading a file from a private S3 bucket
+ * @param bucketName name of the bucket
+ * @param fileName name of the file
+ * @param expiry expiry time in seconds (default 1 hour)
+ * @returns presigned download URL
+ */
+export async function createPresignedUrlToDownload({
+ bucketName,
+ fileName,
+ expiry = 60 * 60, // 1 hour
+ }: {
+ bucketName: string;
+ fileName: string;
+ expiry?: number;
+}) {
+ 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.");
+ }
+
+ return await s3Client.presignedGetObject(bucketName, fileName, expiry);
+}
\ No newline at end of file