From 297439a34857ca9f56b0a9b01bd0f9ad4734e411 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 29 Mar 2025 19:27:05 -0600 Subject: [PATCH] Update restore-backup component and backup router for web server support: set default database name based on type, disable input for web server, and streamline backup restoration process with improved logging and error handling. --- .../database/backups/restore-backup.tsx | 8 +- apps/dokploy/server/api/routers/backup.ts | 64 ++------- packages/server/src/index.ts | 1 + packages/server/src/utils/restore/index.ts | 1 + .../server/src/utils/restore/web-server.ts | 126 ++++++++++++++---- 5 files changed, 115 insertions(+), 85 deletions(-) diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx index 769f0b57..10ebbe08 100644 --- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx @@ -91,7 +91,7 @@ export const RestoreBackup = ({ defaultValues: { destinationId: "", backupFile: "", - databaseName: "", + databaseName: databaseType === "web-server" ? "dokploy" : "", }, resolver: zodResolver(RestoreBackupSchema), }); @@ -340,7 +340,11 @@ export const RestoreBackup = ({ Database Name - + diff --git a/apps/dokploy/server/api/routers/backup.ts b/apps/dokploy/server/api/routers/backup.ts index 21849716..c691a406 100644 --- a/apps/dokploy/server/api/routers/backup.ts +++ b/apps/dokploy/server/api/routers/backup.ts @@ -19,13 +19,13 @@ import { findPostgresByBackupId, findPostgresById, findServerById, - paths, removeBackupById, removeScheduleBackup, runMariadbBackup, runMongoBackup, runMySqlBackup, runPostgresBackup, + runWebServerBackup, scheduleBackup, updateBackupById, } from "@dokploy/server"; @@ -41,6 +41,7 @@ import { restoreMongoBackup, restoreMySqlBackup, restorePostgresBackup, + restoreWebServerBackup, } from "@dokploy/server/utils/restore"; import { TRPCError } from "@trpc/server"; import { observable } from "@trpc/server/observable"; @@ -235,50 +236,9 @@ export const backupRouter = createTRPCRouter({ manualBackupWebServer: protectedProcedure .input(apiFindOneBackup) .mutation(async ({ input }) => { - try { - const backup = await findBackupById(input.backupId); - const destination = await findDestinationById(backup.destinationId); - const rcloneFlags = getS3Credentials(destination); - const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); - const { BASE_PATH } = paths(); - const tempDir = `${BASE_PATH}/temp-backup-${timestamp}`; - const backupFileName = `webserver-backup-${timestamp}.zip`; - const s3Path = `:s3:${destination.bucket}/${backup.prefix}${backupFileName}`; - - try { - await execAsync(`mkdir -p ${tempDir}/filesystem`); - - const postgresCommand = `docker exec $(docker ps --filter "name=dokploy-postgres" -q) pg_dump -v -Fc -U dokploy -d dokploy > ${tempDir}/database.sql`; - await execAsync(postgresCommand); - - await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`); - - await execAsync( - `cd ${tempDir} && zip -r ${backupFileName} database.sql filesystem/`, - ); - - // // Show zip contents and size - // console.log("Zip file contents:"); - // await execAsync(`unzip -l ${tempDir}/${backupFileName}`); - // await execAsync(`du -sh ${tempDir}/${backupFileName}`); - - // Upload to S3 - const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`; - await execAsync(uploadCommand); - return true; - } finally { - // Cleanup temporary files - console.log("Cleaning up temporary files..."); - await execAsync(`rm -rf ${tempDir}`); - } - } catch (error) { - console.error("Backup error:", error); - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error running manual Web Server backup", - cause: error, - }); - } + const backup = await findBackupById(input.backupId); + await runWebServerBackup(backup); + return true; }), listBackupFiles: protectedProcedure .input( @@ -424,16 +384,12 @@ export const backupRouter = createTRPCRouter({ }, ); }); - } else if (input.databaseType === "web-server") { + } + if (input.databaseType === "web-server") { return observable((emit) => { - restoreWebServerBackup( - webServer, - destination, - input.backupFile, - (log) => { - emit.next(log); - }, - ); + restoreWebServerBackup(destination, input.backupFile, (log) => { + emit.next(log); + }); }); } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index ef3ab02b..711570d8 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -48,6 +48,7 @@ export * from "./utils/backups/mongo"; export * from "./utils/backups/mysql"; export * from "./utils/backups/postgres"; export * from "./utils/backups/utils"; +export * from "./utils/backups/web-server"; export * from "./templates/processors"; export * from "./utils/notifications/build-error"; diff --git a/packages/server/src/utils/restore/index.ts b/packages/server/src/utils/restore/index.ts index e8adbbe1..972e1d1d 100644 --- a/packages/server/src/utils/restore/index.ts +++ b/packages/server/src/utils/restore/index.ts @@ -2,3 +2,4 @@ export { restorePostgresBackup } from "./postgres"; export { restoreMySqlBackup } from "./mysql"; export { restoreMariadbBackup } from "./mariadb"; export { restoreMongoBackup } from "./mongo"; +export { restoreWebServerBackup } from "./web-server"; diff --git a/packages/server/src/utils/restore/web-server.ts b/packages/server/src/utils/restore/web-server.ts index 23834bd5..fa255fe3 100644 --- a/packages/server/src/utils/restore/web-server.ts +++ b/packages/server/src/utils/restore/web-server.ts @@ -1,10 +1,7 @@ import type { Destination } from "@dokploy/server/services/destination"; import { getS3Credentials } from "../backups/utils"; -import { - getRemoteServiceContainer, - getServiceContainer, -} from "../docker/utils"; -import { execAsync, execAsyncRemote } from "../process/execAsync"; +import { execAsync } from "../process/execAsync"; +import { paths } from "@dokploy/server"; export const restoreWebServerBackup = async ( destination: Destination, @@ -12,46 +9,117 @@ export const restoreWebServerBackup = async ( emit: (log: string) => void, ) => { try { - const { appName, databaseUser, serverId } = postgres; - const rcloneFlags = getS3Credentials(destination); const bucketPath = `:s3:${destination.bucket}`; - const backupPath = `${bucketPath}/${backupFile}`; + const { BASE_PATH } = paths(); + const tempDir = `${BASE_PATH}/temp-restore-${new Date().toISOString().replace(/[:.]/g, "-")}`; - const { Id: containerName } = serverId - ? await getRemoteServiceContainer(serverId, appName) - : await getServiceContainer(appName); + try { + emit("Starting restore..."); + emit(`Backup path: ${backupPath}`); + emit(`Temp directory: ${tempDir}`); - emit("Starting restore..."); - emit(`Backup path: ${backupPath}`); + // Create temp directory + emit("Creating temporary directory..."); + await execAsync(`mkdir -p ${tempDir}`); - const command = `\ -rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | docker exec -i ${containerName} pg_restore -U ${databaseUser} -d ${database} --clean --if-exists`; + // Download backup from S3 + emit("Downloading backup from S3..."); + await execAsync( + `rclone copyto ${rcloneFlags.join(" ")} "${backupPath}" "${tempDir}/${backupFile}"`, + ); - emit(`Executing command: ${command}`); + // List files before extraction + emit("Listing files before extraction..."); + const { stdout: beforeFiles } = await execAsync(`ls -la ${tempDir}`); + emit(`Files before extraction: ${beforeFiles}`); - if (serverId) { - const { stdout, stderr } = await execAsyncRemote(serverId, command); - emit(stdout); - emit(stderr); - } else { - const { stdout, stderr } = await execAsync(command); - console.log("stdout", stdout); - console.log("stderr", stderr); - emit(stdout); - emit(stderr); + // Extract backup + emit("Extracting backup..."); + await execAsync(`cd ${tempDir} && unzip ${backupFile}`); + + // Check if database.sql.gz exists and decompress it + const { stdout: hasGzFile } = await execAsync( + `ls ${tempDir}/database.sql.gz || true`, + ); + if (hasGzFile.includes("database.sql.gz")) { + emit("Found compressed database file, decompressing..."); + await execAsync(`cd ${tempDir} && gunzip database.sql.gz`); + } + + // Verify database file exists + const { stdout: hasSqlFile } = await execAsync( + `ls ${tempDir}/database.sql || true`, + ); + if (!hasSqlFile.includes("database.sql")) { + throw new Error("Database file not found after extraction"); + } + + // Restore database + emit("Restoring database..."); + + // Drop and recreate database + emit("Disconnecting all users from database..."); + await execAsync( + `docker exec $(docker ps --filter "name=dokploy-postgres" -q) psql -U dokploy postgres -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'dokploy' AND pid <> pg_backend_pid();"`, + ); + + emit("Dropping existing database..."); + await execAsync( + `docker exec $(docker ps --filter "name=dokploy-postgres" -q) psql -U dokploy postgres -c "DROP DATABASE IF EXISTS dokploy;"`, + ); + + emit("Creating fresh database..."); + await execAsync( + `docker exec $(docker ps --filter "name=dokploy-postgres" -q) psql -U dokploy postgres -c "CREATE DATABASE dokploy;"`, + ); + + // Copy the backup file into the container + emit("Copying backup file into container..."); + await execAsync( + `docker cp ${tempDir}/database.sql $(docker ps --filter "name=dokploy-postgres" -q):/tmp/database.sql`, + ); + + // Verify file in container + emit("Verifying file in container..."); + await execAsync( + `docker exec $(docker ps --filter "name=dokploy-postgres" -q) ls -l /tmp/database.sql`, + ); + + // Restore from the copied file + emit("Running database restore..."); + await execAsync( + `docker exec $(docker ps --filter "name=dokploy-postgres" -q) pg_restore -v -U dokploy -d dokploy /tmp/database.sql`, + ); + + // Cleanup the temporary file in the container + emit("Cleaning up container temp file..."); + await execAsync( + `docker exec $(docker ps --filter "name=dokploy-postgres" -q) rm /tmp/database.sql`, + ); + + // Restore filesystem + emit("Restoring filesystem..."); + await execAsync(`cp -r ${tempDir}/filesystem/* ${BASE_PATH}/`); + + emit("Restore completed successfully!"); + } finally { + // Cleanup + emit("Cleaning up temporary files..."); + await execAsync(`rm -rf ${tempDir}`); } - - emit("Restore completed successfully!"); } catch (error) { + console.error(error); emit( `Error: ${ error instanceof Error ? error.message - : "Error restoring postgres backup" + : "Error restoring web server backup" }`, ); throw error; } }; +// docker exec $(docker ps --filter "name=dokploy-postgres" -q) pg_restore -v -U dokploy -d dokploy /Users/mauricio/Documents/Github/Personal/dokploy/apps/dokploy/.docker/temp-restore-2025-03-30T01-09-27-203Z/database.sql +// server/webserver-backup-2025-03-30T00-38-08-836Z.zip