mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Adding methods to delete backups on remote storage s3 and local. Some refactoring on models db and adding common.ts model with createdAt, updatedAt, and deletedAt.
This commit is contained in:
@@ -21,10 +21,10 @@ import {createRestorationAction, deleteBackupAction} from "@/features/dashboard/
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {StatusBadge} from "@/components/wrappers/common/status-badge";
|
||||
import {Backup, DatabaseWith} from "@/db/schema/06_database";
|
||||
import {Backup, DatabaseWith} from "@/db/schema/07_database";
|
||||
import {formatFrenchDate} from "@/utils/date-formatting";
|
||||
import {TooltipCustom} from "@/components/wrappers/common/tooltip-custom";
|
||||
import {Setting} from "@/db/schema/00_setting";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {SafeActionResult} from "next-safe-action";
|
||||
import {ZodString} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
@@ -80,18 +80,22 @@ export function backupColumns(isAlreadyRestore: boolean, settings: Setting, data
|
||||
|
||||
const mutationDeleteBackup = useMutation({
|
||||
mutationFn: async () => {
|
||||
const restoration = await deleteBackupAction({
|
||||
|
||||
|
||||
const deletion = await deleteBackupAction({
|
||||
backupId: rowData.id,
|
||||
databaseId: rowData.databaseId,
|
||||
file: rowData.file!,
|
||||
projectSlug: database.project?.slug!
|
||||
});
|
||||
// @ts-ignore
|
||||
if (restoration.data.success) {
|
||||
if (deletion.data.success) {
|
||||
// @ts-ignore
|
||||
toast.success(restoration.data.actionSuccess.message);
|
||||
toast.success(deletion.data.actionSuccess.message);
|
||||
router.refresh();
|
||||
} else {
|
||||
// @ts-ignore
|
||||
toast.error(restoration.data.actionError.message);
|
||||
toast.error(deletion.data.actionError.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Button } from "@/components/ui/button";
|
||||
import {MoreHorizontal, Trash2} from "lucide-react";
|
||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||
import { StatusBadge } from "@/components/wrappers/common/status-badge";
|
||||
import {Backup, Restoration} from "@/db/schema/06_database";
|
||||
import {Backup, Restoration} from "@/db/schema/07_database";
|
||||
import {formatFrenchDate} from "@/utils/date-formatting";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
"use server"
|
||||
import { userAction } from "@/safe-actions";
|
||||
import { z } from "zod";
|
||||
import { ServerActionResult } from "@/types/action-type";
|
||||
import {userAction} from "@/safe-actions";
|
||||
import {z} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import * as drizzleDb from "@/db";
|
||||
|
||||
import { db } from "@/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {Backup, Restoration} from "@/db/schema/06_database";
|
||||
import {db} from "@/db";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {Backup, Restoration} from "@/db/schema/07_database";
|
||||
import {NextResponse} from "next/server";
|
||||
import {
|
||||
deleteFileS3Private,
|
||||
deleteLocalPrivate,
|
||||
uploadLocalPrivate,
|
||||
uploadS3Private
|
||||
} from "@/features/upload/private/upload.action";
|
||||
import {env} from "@/env.mjs";
|
||||
|
||||
export const deleteRestoreAction = userAction
|
||||
.schema(
|
||||
@@ -14,7 +22,7 @@ export const deleteRestoreAction = userAction
|
||||
restorationId: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({ parsedInput }): Promise<ServerActionResult<Backup>> => {
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<Backup>> => {
|
||||
try {
|
||||
await db
|
||||
.delete(drizzleDb.schemas.restoration)
|
||||
@@ -36,24 +44,60 @@ export const deleteRestoreAction = userAction
|
||||
message: "Failed to delete restoration.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: { message: "Error deleting the restoration" },
|
||||
messageParams: {message: "Error deleting the restoration"},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
export const deleteBackupAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
backupId: z.string(),
|
||||
databaseId: z.string(),
|
||||
projectSlug: z.string(),
|
||||
file: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({ parsedInput }): Promise<ServerActionResult<Backup>> => {
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<Backup>> => {
|
||||
try {
|
||||
|
||||
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||
if (!settings) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "No settings found.",
|
||||
status: 404,
|
||||
cause: "No settings found.",
|
||||
messageParams: {message: "Error deleting the backup"},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
let success: boolean, message: string;
|
||||
|
||||
const result =
|
||||
settings.storage === "local"
|
||||
? await deleteLocalPrivate(parsedInput.file)
|
||||
: await deleteFileS3Private(`${parsedInput.projectSlug}/${parsedInput.file}`, env.S3_BUCKET_NAME!);
|
||||
|
||||
({success, message} = result);
|
||||
|
||||
if (!success) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: message,
|
||||
status: 404,
|
||||
cause: "Unable to delete backup from storage",
|
||||
messageParams: {message: "Error deleting the backup"},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(drizzleDb.schemas.backup)
|
||||
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
|
||||
@@ -65,6 +109,7 @@ export const deleteBackupAction = userAction
|
||||
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
|
||||
.execute();
|
||||
|
||||
|
||||
if (backupExists.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
@@ -78,8 +123,8 @@ export const deleteBackupAction = userAction
|
||||
actionError: {
|
||||
message: "Backup not found or already deleted.",
|
||||
status: 404,
|
||||
cause: "Backup could not be deleted.",
|
||||
messageParams: { message: "Error deleting the backup" },
|
||||
cause: "Backup could not be deleted (from database or remote storage).",
|
||||
messageParams: {message: "Error deleting the backup"},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -90,25 +135,24 @@ export const deleteBackupAction = userAction
|
||||
message: "Failed to delete backup.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: { message: "Error deleting the backup" },
|
||||
messageParams: {message: "Error deleting the backup"},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
export const rerunRestorationAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
restorationId: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({ parsedInput }): Promise<ServerActionResult<Restoration>> => {
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<Restoration>> => {
|
||||
try {
|
||||
const updateResult = await db
|
||||
.update(drizzleDb.schemas.restoration)
|
||||
.set({ status: "waiting" })
|
||||
.set({status: "waiting"})
|
||||
.where(eq(drizzleDb.schemas.restoration.id, parsedInput.restorationId))
|
||||
.returning()
|
||||
.execute();
|
||||
@@ -122,7 +166,7 @@ export const rerunRestorationAction = userAction
|
||||
message: "Restoration not found.",
|
||||
status: 404,
|
||||
cause: "No restoration with the given ID exists.",
|
||||
messageParams: { message: "Restoration not found" },
|
||||
messageParams: {message: "Restoration not found"},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -132,7 +176,7 @@ export const rerunRestorationAction = userAction
|
||||
value: updatedRestoration,
|
||||
actionSuccess: {
|
||||
message: "Restoration has been requeued.",
|
||||
messageParams: { restorationId: updatedRestoration.id },
|
||||
messageParams: {restorationId: updatedRestoration.id},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -142,7 +186,7 @@ export const rerunRestorationAction = userAction
|
||||
message: "Failed to rerun restoration.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: { message: "Error updating the restoration" },
|
||||
messageParams: {message: "Error updating the restoration"},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -157,7 +201,7 @@ export const createRestorationAction = userAction
|
||||
databaseId: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({ parsedInput }): Promise<ServerActionResult<Restoration>> => {
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<Restoration>> => {
|
||||
try {
|
||||
// Insert new restoration into the database
|
||||
const restorationData = await db
|
||||
@@ -177,7 +221,7 @@ export const createRestorationAction = userAction
|
||||
value: createdRestoration,
|
||||
actionSuccess: {
|
||||
message: "Restoration has been successfully created.",
|
||||
messageParams: { restorationId: createdRestoration.id },
|
||||
messageParams: {restorationId: createdRestoration.id},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -187,7 +231,7 @@ export const createRestorationAction = userAction
|
||||
message: "Failed to create restoration.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: { message: "Error creating the restoration" },
|
||||
messageParams: {message: "Error creating the restoration"},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,15 +4,12 @@ import {mkdir, writeFile} from "fs/promises";
|
||||
import path from "path";
|
||||
import * as fs from "node:fs";
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
import {createPresignedUrlToDownload, saveFileInBucket} from "@/utils/s3-file-management";
|
||||
import crypto from "crypto";
|
||||
import {createPresignedUrlToDownload, deleteFileFromBucket, saveFileInBucket} from "@/utils/s3-file-management";
|
||||
import {env} from "@/env.mjs";
|
||||
import {action, userAction} from "@/safe-actions";
|
||||
import {action} 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";
|
||||
import {unlink} from "fs/promises";
|
||||
|
||||
const privateLocalDir = "private/uploads/files/";
|
||||
const privateS3Dir = "backups/";
|
||||
@@ -54,6 +51,53 @@ export async function uploadS3Private(fileName: string, buffer: any, bucketName:
|
||||
}
|
||||
|
||||
|
||||
export async function deleteFileS3Private(fileName: string, bucketName: string) {
|
||||
try {
|
||||
|
||||
await deleteFileFromBucket({
|
||||
bucketName,
|
||||
fileName: `${privateS3Dir}${fileName}`,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "File deleted successfully",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error occurred:", error);
|
||||
throw new Error("An error occurred while deleting the private file");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete a file from local private storage
|
||||
*/
|
||||
export async function deleteLocalPrivate(fileName: string) {
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), privateLocalDir, fileName);
|
||||
|
||||
// Delete locally
|
||||
await unlink(filePath);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `File '${fileName}' deleted successfully`,
|
||||
};
|
||||
} catch (error: any) {
|
||||
if (error.code === "ENOENT") {
|
||||
return {
|
||||
success: false,
|
||||
message: `File '${fileName}' not found`,
|
||||
};
|
||||
}
|
||||
|
||||
console.error("Error occurred while deleting file:", error);
|
||||
throw new Error("An error occurred while deleting the file");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// export async function getFileUrlPresignedLocal(fileName: string) {
|
||||
// try {
|
||||
// const filePath = path.join(privateLocalDir, fileName);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { UploadedObjectInfo } from "minio/src/internal/type";
|
||||
import { getServerUrl } from "@/utils/get-server-url";
|
||||
import { db } from "@/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {Setting} from "@/db/schema/00_setting";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import * as drizzleDb from "@/db";
|
||||
|
||||
export const uploadImageAction = userAction.schema(z.instanceof(FormData)).action(async ({ parsedInput: formData, ctx }) => {
|
||||
|
||||
Reference in New Issue
Block a user