mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
fix: working on adding internal postgres database.
This commit is contained in:
@@ -7,12 +7,6 @@ import * as drizzleDb from "@/db";
|
||||
import {db} from "@/db";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {Backup, Restoration} from "@/db/schema/07_database";
|
||||
import {
|
||||
deleteFileS3Private,
|
||||
deleteLocalPrivate,
|
||||
} from "@/features/upload/private/upload.action";
|
||||
import {env} from "@/env.mjs";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
|
||||
export const deleteRestoreAction = userAction
|
||||
.schema(
|
||||
@@ -48,84 +42,6 @@ export const deleteRestoreAction = userAction
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const deleteBackupAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
backupId: z.string(),
|
||||
databaseId: z.string(),
|
||||
projectSlug: z.string(),
|
||||
status: z.enum(["ongoing", "failed", "success", "waiting"]),
|
||||
file: z.string(),
|
||||
})
|
||||
)
|
||||
.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"},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await db
|
||||
.update(drizzleDb.schemas.backup)
|
||||
.set(withUpdatedAt({
|
||||
deletedAt: new Date(),
|
||||
status: parsedInput.status == "ongoing" ? "failed" : parsedInput.status
|
||||
}))
|
||||
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
|
||||
|
||||
let success: boolean, message: string;
|
||||
|
||||
if (parsedInput.file) {
|
||||
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"},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
actionSuccess: {
|
||||
message: `Backup deleted successfully (ref: ${parsedInput.backupId}).`,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to delete backup.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {message: "Error deleting the backup"},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const rerunRestorationAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
@@ -175,46 +91,3 @@ export const rerunRestorationAction = userAction
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const createRestorationAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
backupId: z.string(),
|
||||
databaseId: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<Restoration>> => {
|
||||
try {
|
||||
const restorationData = await db
|
||||
.insert(drizzleDb.schemas.restoration)
|
||||
.values({
|
||||
databaseId: parsedInput.databaseId,
|
||||
backupId: parsedInput.backupId,
|
||||
status: "waiting",
|
||||
})
|
||||
.returning()
|
||||
.execute();
|
||||
|
||||
const createdRestoration = restorationData[0];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: createdRestoration,
|
||||
actionSuccess: {
|
||||
message: "Restoration has been successfully created.",
|
||||
messageParams: {restorationId: createdRestoration.id},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to create restoration.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {message: "Error creating the restoration"},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import {Organization} from "@/db/schema/03_organization";
|
||||
import {ProjectWith} from "@/db/schema/06_project";
|
||||
import {GearIcon} from "@radix-ui/react-icons";
|
||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||
import {useRouter} from "next/navigation";
|
||||
|
||||
type ProjectDialogProps = {
|
||||
databases: DatabaseWith[];
|
||||
@@ -33,6 +34,7 @@ export const ProjectDialog = ({
|
||||
isEmpty = false
|
||||
}: ProjectDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
@@ -60,7 +62,10 @@ export const ProjectDialog = ({
|
||||
<DialogTitle>{isEdit ? `Edit ${project?.name}` : "Create new project"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<ProjectForm
|
||||
onSuccess={() => setOpen(false)}
|
||||
onSuccess={() => {
|
||||
setOpen(false)
|
||||
router.refresh()
|
||||
}}
|
||||
databases={databases}
|
||||
organization={organization}
|
||||
defaultValues={project ? {
|
||||
|
||||
@@ -22,11 +22,6 @@ export type projectFormProps = {
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const ProjectForm = (props: projectFormProps) => {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -74,7 +74,10 @@ export async function dispatchStorage(
|
||||
};
|
||||
}
|
||||
|
||||
if (channelData) channel = {...channelData, config: channelData.config as Json};
|
||||
if (channelData) {
|
||||
// @ts-ignore
|
||||
channel = {...channelData, config: channelData.config as Json};
|
||||
}
|
||||
|
||||
if (!channel) {
|
||||
return {
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
"use server";
|
||||
|
||||
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, deleteFileFromBucket, saveFileInBucket} from "@/utils/s3-file-management";
|
||||
import {env} from "@/env.mjs";
|
||||
import {z} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {unlink} from "fs/promises";
|
||||
import {action} from "@/lib/safe-actions/actions";
|
||||
|
||||
const privateLocalDir = "private/uploads/files/";
|
||||
const privateS3Dir = "backups/";
|
||||
|
||||
export async function uploadLocalPrivate(fileName: string, buffer: any) {
|
||||
try {
|
||||
|
||||
const privatePath = path.join(env.PRIVATE_PATH!, '/keys/master_key.bin')
|
||||
|
||||
await mkdir(path.join(process.cwd(), privateLocalDir), {recursive: true});
|
||||
await writeFile(path.join(process.cwd(), privateLocalDir, fileName), buffer);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "File uploaded successfully",
|
||||
filePath: path.join(privateLocalDir, fileName),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error occurred:", error);
|
||||
throw new Error("An error occurred while importing the private file");
|
||||
}
|
||||
}
|
||||
|
||||
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 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);
|
||||
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 getFileUrlPresignedS3(fileName: string) {
|
||||
try {
|
||||
return await createPresignedUrlToDownload({
|
||||
bucketName: env.S3_BUCKET_NAME!,
|
||||
fileName: fileName,
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const getFileUrlPresignedLocal = action
|
||||
.schema(z.object({
|
||||
dir: z.string().optional(),
|
||||
fileName: z.string()
|
||||
}))
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<string>> => {
|
||||
try {
|
||||
const filePath = path.join(parsedInput.dir ? parsedInput.dir : privateLocalDir, parsedInput.fileName);
|
||||
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.fileName}${expiresAt}`).digest("hex");
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: `${baseUrl}/api/files/${parsedInput.fileName}?token=${token}&expires=${expiresAt}`,
|
||||
actionSuccess: {
|
||||
message: "Successfully retrieved presigned URL Local",
|
||||
messageParams: {fileName: parsedInput.fileName},
|
||||
},
|
||||
};
|
||||
} 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.fileName},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
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},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
import * as Minio from "minio";
|
||||
import {env} from "@/env.mjs";
|
||||
import internal from "node:stream";
|
||||
import stream from "node:stream";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {db} from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
|
||||
|
||||
async function getS3Client() {
|
||||
const settings = await db
|
||||
.select()
|
||||
.from(drizzleDb.schemas.setting)
|
||||
.where(eq(drizzleDb.schemas.setting.name, "system"))
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (!settings) {
|
||||
throw new Error("S3 settings not found in database.");
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
endPoint: settings?.s3EndPointUrl ?? "",
|
||||
accessKey: settings?.s3AccessKeyId ?? "",
|
||||
secretKey: settings?.s3SecretAccessKey ?? "",
|
||||
};
|
||||
|
||||
return new Minio.Client({
|
||||
...baseConfig,
|
||||
port: Number(env.S3_PORT ?? 0),
|
||||
useSSL: env.S3_USE_SSL === "true",
|
||||
});
|
||||
}
|
||||
|
||||
export async function checkMinioAlive() {
|
||||
try {
|
||||
console.log("Check MinioAlive");
|
||||
const s3Client = await getS3Client();
|
||||
const buckets = await s3Client.listBuckets();
|
||||
console.log("MinIO is up and running. Buckets:", buckets);
|
||||
return {message: true};
|
||||
} catch (error) {
|
||||
console.error("Error connecting to MinIO:", error);
|
||||
return {error: error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function createBucketIfNotExists(bucketName: string) {
|
||||
const s3Client = await getS3Client();
|
||||
|
||||
const bucketExists = await s3Client.bucketExists(bucketName);
|
||||
if (!bucketExists) {
|
||||
console.log(`Creating bucket ${bucketName}`);
|
||||
await s3Client.makeBucket(bucketName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file from a bucket
|
||||
* @param bucketName name of the bucket
|
||||
* @param fileName name of the file
|
||||
* @returns true if deleted, false if not
|
||||
*/
|
||||
export async function deleteFileFromBucket({
|
||||
bucketName,
|
||||
fileName,
|
||||
}: {
|
||||
bucketName: string;
|
||||
fileName: string;
|
||||
}): Promise<boolean> {
|
||||
const s3Client = await getS3Client();
|
||||
|
||||
try {
|
||||
const fileExists = await checkFileExistsInBucket({ bucketName, fileName });
|
||||
|
||||
if (!fileExists) {
|
||||
console.warn(`File not found: ${bucketName}/${fileName}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
await s3Client.removeObject(bucketName, fileName);
|
||||
console.log(`Deleted file: ${bucketName}/${fileName}`);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error("Error deleting file from bucket:", {
|
||||
bucketName,
|
||||
fileName,
|
||||
error: error.message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save file in S3 bucket
|
||||
* @param bucketName name of the bucket
|
||||
* @param fileName name of the file
|
||||
* @param file file to save
|
||||
*/
|
||||
export async function saveFileInBucket({bucketName, fileName, file}: {
|
||||
bucketName: string;
|
||||
fileName: string;
|
||||
file: Buffer | internal.Readable
|
||||
}) {
|
||||
await checkMinioAlive();
|
||||
await createBucketIfNotExists(bucketName);
|
||||
const fileExists = await checkFileExistsInBucket({
|
||||
bucketName,
|
||||
fileName,
|
||||
});
|
||||
console.log("File exists:", fileExists);
|
||||
if (fileExists) {
|
||||
throw new Error("File already exists");
|
||||
}
|
||||
const s3Client = await getS3Client();
|
||||
|
||||
return await s3Client.putObject(bucketName, fileName, file);
|
||||
}
|
||||
|
||||
|
||||
export async function getObjectFromClient({
|
||||
bucketName,
|
||||
fileName,
|
||||
}: {
|
||||
bucketName: string;
|
||||
fileName: string;
|
||||
}): Promise<stream.Readable> {
|
||||
const s3 = await getS3Client();
|
||||
return await s3.getObject(bucketName, fileName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function checkFileExistsInBucket({
|
||||
bucketName,
|
||||
fileName,
|
||||
}: {
|
||||
bucketName: string;
|
||||
fileName: string;
|
||||
}): Promise<boolean> {
|
||||
const s3 = await getS3Client();
|
||||
try {
|
||||
const stat = await s3.statObject(bucketName, fileName);
|
||||
return !!stat;
|
||||
} catch (error: any) {
|
||||
if (error.code === 'NoSuchKey' || error.message?.includes('not found')) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate presigned urls for uploading files to S3
|
||||
* @param files files to upload
|
||||
* @returns promise with array of presigned urls
|
||||
*/
|
||||
export async function createPresignedUrlToUpload({
|
||||
bucketName,
|
||||
fileName,
|
||||
expiry = 60 * 60, // 1 hour
|
||||
}: {
|
||||
bucketName: string;
|
||||
fileName: string;
|
||||
expiry?: number;
|
||||
}) {
|
||||
// Create bucket if it doesn't exist
|
||||
await createBucketIfNotExists(bucketName);
|
||||
const s3Client = await getS3Client();
|
||||
|
||||
return await s3Client.presignedPutObject(bucketName, fileName, expiry);
|
||||
}
|
||||
|
||||
export async function createPublicBucket({bucketName}: { bucketName: string }) {
|
||||
const s3Client = await getS3Client();
|
||||
|
||||
try {
|
||||
const exists = await s3Client.bucketExists(bucketName);
|
||||
if (!exists) {
|
||||
await s3Client.makeBucket(bucketName);
|
||||
console.log(`Bucket ${bucketName} created successfully.`);
|
||||
} else {
|
||||
console.log(`Bucket ${bucketName} already exists.`);
|
||||
}
|
||||
|
||||
const policy = {
|
||||
Version: "2012-10-17",
|
||||
Statement: [
|
||||
{
|
||||
Effect: "Allow",
|
||||
Principal: "*",
|
||||
Action: "s3:GetObject",
|
||||
Resource: `arn:aws:s3:::${bucketName}/*`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await s3Client.setBucketPolicy(bucketName, JSON.stringify(policy));
|
||||
console.log(`Bucket ${bucketName} is now public.`);
|
||||
} catch (error) {
|
||||
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,
|
||||
}: {
|
||||
bucketName: string;
|
||||
fileName: string;
|
||||
expiry?: number;
|
||||
}) {
|
||||
try {
|
||||
const s3Client = await getS3Client();
|
||||
|
||||
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,
|
||||
});
|
||||
throw {error: err.message ?? "Unknown error"};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user