From 63e83032f197373754f6096f14228830e97ddccc Mon Sep 17 00:00:00 2001 From: charlesgauthereau Date: Sun, 15 Feb 2026 10:19:28 +0100 Subject: [PATCH] fix: working on adding internal postgres database. --- docker-compose.prod.yml | 10 +- docker/entrypoints/app-prod-entrypoint.sh | 35 ++- .../dashboard/restore/restore.action.ts | 127 --------- .../projects/components/project.dialog.tsx | 7 +- .../projects/components/project.form.tsx | 5 - src/features/storages/dispatch.ts | 5 +- src/features/upload/private/upload.action.ts | 198 -------------- src/utils/s3-file-management.ts | 243 ------------------ 8 files changed, 42 insertions(+), 588 deletions(-) delete mode 100644 src/features/upload/private/upload.action.ts delete mode 100644 src/utils/s3-file-management.ts diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 9a7660b7..5134f3dd 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,10 +1,10 @@ services: app: - build: - context: . - dockerfile: docker/dockerfile/Dockerfile - target: prod -# image: portabase/portabase:latest +# build: +# context: . +# dockerfile: docker/dockerfile/Dockerfile +# target: prod + image: portabase/portabase:latest ports: - '8887:80' environment: diff --git a/docker/entrypoints/app-prod-entrypoint.sh b/docker/entrypoints/app-prod-entrypoint.sh index 38f07f06..2dd72312 100644 --- a/docker/entrypoints/app-prod-entrypoint.sh +++ b/docker/entrypoints/app-prod-entrypoint.sh @@ -24,33 +24,52 @@ if [ -z "$DATABASE_URL" ]; then if [ ! -f "$PGDATA/PG_VERSION" ]; then echo "[INFO] Initializing database cluster..." - su postgres -c "initdb -D '$PGDATA'" + if ! su postgres -c "initdb -D '$PGDATA'" > /dev/null 2>&1; then + echo "[ERROR] initdb failed" + exit 1 + fi fi - su postgres -c "pg_ctl -D '$PGDATA' -o \"-c listen_addresses='localhost'\" -w start" + if ! su postgres -c "pg_ctl -D '$PGDATA' \ + -o \"-c listen_addresses='localhost' -c logging_collector=on\" \ + -l $PGDATA/postgres.log -w start" > /dev/null 2>&1; then + echo "[ERROR] PostgreSQL failed to start" + exit 1 + fi - until pg_isready -h 127.0.0.1 -p 5432; do - echo "Waiting for Postgres..." + until su postgres -c "pg_isready -h 127.0.0.1 -p 5432" > /dev/null 2>&1; do sleep 1 done + echo "[INFO] PostgreSQL server is up and accepting connections" + DB_USER="${POSTGRES_USER:-portabase_user}" DB_PASS="${POSTGRES_PASSWORD:-JaB6b1SUtIWYvt7srnOt}" DB_NAME="${POSTGRES_DB:-portabase_db}" - USER_EXISTS=$(su postgres -c "psql -tAc \"SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'\"") + USER_EXISTS=$(su postgres -c "psql -tAc \"SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'\"" 2>/dev/null) if [ "$USER_EXISTS" != "1" ]; then - su postgres -c "psql -c \"CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';\"" + if ! su postgres -c "psql -c \"CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';\"" > /dev/null 2>&1; then + echo "[ERROR] Failed creating user" + exit 1 + fi fi - DB_EXISTS=$(su - postgres -c "psql -tAc \"SELECT 1 FROM pg_database WHERE datname='$DB_NAME'\"") + DB_EXISTS=$(su postgres -c "psql -tAc \"SELECT 1 FROM pg_database WHERE datname='$DB_NAME'\"" 2>/dev/null) if [ "$DB_EXISTS" != "1" ]; then - su postgres -c "psql -c \"CREATE DATABASE $DB_NAME OWNER $DB_USER;\"" + if ! su postgres -c "psql -c \"CREATE DATABASE $DB_NAME OWNER $DB_USER;\"" > /dev/null 2>&1; then + echo "[ERROR] Failed creating database" + exit 1 + fi fi export DATABASE_URL="postgres://$DB_USER:$DB_PASS@127.0.0.1:5432/$DB_NAME" + + echo "[SUCCESS] Internal PostgreSQL started successfully" + echo "[SUCCESS] Database: $DB_NAME | User: $DB_USER | Host: 127.0.0.1:5432" fi + mkdir -p /data/private/uploads/tmp echo "▶ Starting tusd server..." tusd --base-path /tus/files/ --upload-dir /data/private/uploads/tmp --hooks-http http://127.0.0.1:3000/api/tus/hooks --port 1080 --max-size 21474836480 & diff --git a/src/features/dashboard/restore/restore.action.ts b/src/features/dashboard/restore/restore.action.ts index 6ae72a74..794b8e58 100644 --- a/src/features/dashboard/restore/restore.action.ts +++ b/src/features/dashboard/restore/restore.action.ts @@ -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> => { - 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> => { - 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"}, - }, - }; - } - }); diff --git a/src/features/projects/components/project.dialog.tsx b/src/features/projects/components/project.dialog.tsx index 5076e96c..9d8ee6d1 100644 --- a/src/features/projects/components/project.dialog.tsx +++ b/src/features/projects/components/project.dialog.tsx @@ -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 ( @@ -60,7 +62,10 @@ export const ProjectDialog = ({ {isEdit ? `Edit ${project?.name}` : "Create new project"} setOpen(false)} + onSuccess={() => { + setOpen(false) + router.refresh() + }} databases={databases} organization={organization} defaultValues={project ? { diff --git a/src/features/projects/components/project.form.tsx b/src/features/projects/components/project.form.tsx index 67b8726a..e1cad22b 100644 --- a/src/features/projects/components/project.form.tsx +++ b/src/features/projects/components/project.form.tsx @@ -22,11 +22,6 @@ export type projectFormProps = { }; - - - - - export const ProjectForm = (props: projectFormProps) => { const router = useRouter(); const queryClient = useQueryClient(); diff --git a/src/features/storages/dispatch.ts b/src/features/storages/dispatch.ts index 5dba383f..32b87fc8 100644 --- a/src/features/storages/dispatch.ts +++ b/src/features/storages/dispatch.ts @@ -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 { diff --git a/src/features/upload/private/upload.action.ts b/src/features/upload/private/upload.action.ts deleted file mode 100644 index bf9a56c9..00000000 --- a/src/features/upload/private/upload.action.ts +++ /dev/null @@ -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> => { - 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> => { - 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}, - }, - }; - } - }); - - diff --git a/src/utils/s3-file-management.ts b/src/utils/s3-file-management.ts deleted file mode 100644 index 1a07aeba..00000000 --- a/src/utils/s3-file-management.ts +++ /dev/null @@ -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 { - 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 { - const s3 = await getS3Client(); - return await s3.getObject(bucketName, fileName); -} - - - -export async function checkFileExistsInBucket({ - bucketName, - fileName, - }: { - bucketName: string; - fileName: string; -}): Promise { - 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"}; - } -}