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:
charlesgauthereau
2025-08-29 15:31:23 +02:00
parent c8da2579e8
commit 45e3b31fda
48 changed files with 310 additions and 148 deletions
+10 -6
View File
@@ -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);
}
},
});
+1 -1
View File
@@ -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"},
},
};
}
+50 -6
View File
@@ -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);
+1 -1
View File
@@ -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 }) => {