From 66dd890448e22266984879fe2487dfc1e0e89b29 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 4 May 2025 00:15:09 -0600 Subject: [PATCH] Enhance backup command generation and logging for database backups - Refactored backup utilities for PostgreSQL, MySQL, MariaDB, and MongoDB to streamline command generation and improve logging. - Introduced a unified `getBackupCommand` function to encapsulate backup command logic, enhancing maintainability. - Improved error handling and logging during backup processes, providing clearer feedback on command execution and success/failure states. - Updated container retrieval methods to return null instead of throwing errors when no containers are found, improving robustness. --- .../pages/dashboard/project/[projectId].tsx | 6 + packages/server/src/services/deployment.ts | 4 +- packages/server/src/utils/backups/compose.ts | 86 ++-------- packages/server/src/utils/backups/mariadb.ts | 59 ++----- packages/server/src/utils/backups/mongo.ts | 63 ++----- packages/server/src/utils/backups/mysql.ts | 62 ++----- packages/server/src/utils/backups/postgres.ts | 64 ++----- packages/server/src/utils/backups/utils.ts | 160 +++++++++++++++++- packages/server/src/utils/docker/utils.ts | 4 +- .../server/src/utils/process/execAsync.ts | 1 - 10 files changed, 219 insertions(+), 290 deletions(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId].tsx b/apps/dokploy/pages/dashboard/project/[projectId].tsx index 728d83d1..6bc39642 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId].tsx @@ -80,6 +80,7 @@ import { Loader2, PlusIcon, Search, + ServerIcon, Trash2, X, } from "lucide-react"; @@ -968,6 +969,11 @@ const Project = ( }} className="flex flex-col group relative cursor-pointer bg-transparent transition-colors hover:bg-border" > + {service.serverId && ( +
+ +
+ )}
diff --git a/packages/server/src/services/deployment.ts b/packages/server/src/services/deployment.ts index afcfccc3..74f73f0e 100644 --- a/packages/server/src/services/deployment.ts +++ b/packages/server/src/services/deployment.ts @@ -316,7 +316,7 @@ export const createDeploymentBackup = async ( const command = ` mkdir -p ${LOGS_PATH}/${backup.appName}; -echo "Initializing backup" >> ${logFilePath}; +echo "Initializing backup\n" >> ${logFilePath}; `; await execAsyncRemote(server.serverId, command); @@ -324,7 +324,7 @@ echo "Initializing backup" >> ${logFilePath}; await fsPromises.mkdir(path.join(LOGS_PATH, backup.appName), { recursive: true, }); - await fsPromises.writeFile(logFilePath, "Initializing backup"); + await fsPromises.writeFile(logFilePath, "Initializing backup\n"); } const deploymentCreate = await db diff --git a/packages/server/src/utils/backups/compose.ts b/packages/server/src/utils/backups/compose.ts index d5459034..79cb69e8 100644 --- a/packages/server/src/utils/backups/compose.ts +++ b/packages/server/src/utils/backups/compose.ts @@ -2,21 +2,12 @@ import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { Compose } from "@dokploy/server/services/compose"; import { findProjectById } from "@dokploy/server/services/project"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; -import { execAsyncRemote, execAsyncStream } from "../process/execAsync"; -import { - getMariadbBackupCommand, - getMysqlBackupCommand, - getMongoBackupCommand, - getPostgresBackupCommand, - getS3Credentials, - normalizeS3Path, -} from "./utils"; +import { execAsync, execAsyncRemote } from "../process/execAsync"; +import { getS3Credentials, normalizeS3Path, getBackupCommand } from "./utils"; import { createDeploymentBackup, updateDeploymentStatus, } from "@dokploy/server/services/deployment"; -import { createWriteStream } from "node:fs"; -import { getComposeContainer } from "../docker/utils"; export const runComposeBackup = async ( compose: Compose, @@ -24,7 +15,7 @@ export const runComposeBackup = async ( ) => { const { projectId, name } = compose; const project = await findProjectById(projectId); - const { prefix, database } = backup; + const { prefix } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.dump.gz`; const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; @@ -37,74 +28,17 @@ export const runComposeBackup = async ( try { const rcloneFlags = getS3Credentials(destination); const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; - - const { Id: containerId } = await getComposeContainer( - compose, - backup.serviceName || "", - ); - const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`; - let backupCommand = ""; - if (backup.databaseType === "postgres") { - backupCommand = getPostgresBackupCommand( - containerId, - database, - backup.metadata?.postgres?.databaseUser || "", - ); - } else if (backup.databaseType === "mariadb") { - backupCommand = getMariadbBackupCommand( - containerId, - database, - backup.metadata?.mariadb?.databaseUser || "", - backup.metadata?.mariadb?.databasePassword || "", - ); - } else if (backup.databaseType === "mysql") { - backupCommand = getMysqlBackupCommand( - containerId, - database, - backup.metadata?.mysql?.databaseRootPassword || "", - ); - } else if (backup.databaseType === "mongo") { - backupCommand = getMongoBackupCommand( - containerId, - database, - backup.metadata?.mongo?.databaseUser || "", - backup.metadata?.mongo?.databasePassword || "", - ); - } + const backupCommand = getBackupCommand( + backup, + rcloneCommand, + deployment.logPath, + ); if (compose.serverId) { - await execAsyncRemote( - compose.serverId, - ` - set -e; - echo "Running command." >> ${deployment.logPath}; - export RCLONE_LOG_LEVEL=DEBUG; - ${backupCommand} | ${rcloneCommand} >> ${deployment.logPath} 2>> ${deployment.logPath} || { - echo "❌ Command failed" >> ${deployment.logPath}; - exit 1; - } - echo "✅ Command executed successfully" >> ${deployment.logPath}; - `, - ); + await execAsyncRemote(compose.serverId, backupCommand); } else { - const writeStream = createWriteStream(deployment.logPath, { flags: "a" }); - await execAsyncStream( - `${backupCommand} | ${rcloneCommand}`, - (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }, - { - env: { - ...process.env, - RCLONE_LOG_LEVEL: "DEBUG", - }, - }, - ); - writeStream.write("Backup done✅"); - writeStream.end(); + await execAsync(backupCommand); } await sendDatabaseBackupNotifications({ diff --git a/packages/server/src/utils/backups/mariadb.ts b/packages/server/src/utils/backups/mariadb.ts index f42e02c7..6f71939e 100644 --- a/packages/server/src/utils/backups/mariadb.ts +++ b/packages/server/src/utils/backups/mariadb.ts @@ -1,27 +1,21 @@ import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { Mariadb } from "@dokploy/server/services/mariadb"; import { findProjectById } from "@dokploy/server/services/project"; -import { getServiceContainer } from "../docker/utils"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; -import { execAsyncRemote, execAsyncStream } from "../process/execAsync"; -import { - getMariadbBackupCommand, - getS3Credentials, - normalizeS3Path, -} from "./utils"; +import { execAsync, execAsyncRemote } from "../process/execAsync"; +import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils"; import { createDeploymentBackup, updateDeploymentStatus, } from "@dokploy/server/services/deployment"; -import { createWriteStream } from "node:fs"; export const runMariadbBackup = async ( mariadb: Mariadb, backup: BackupSchedule, ) => { - const { appName, databasePassword, databaseUser, projectId, name } = mariadb; + const { projectId, name } = mariadb; const project = await findProjectById(projectId); - const { prefix, database } = backup; + const { prefix } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.sql.gz`; const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; @@ -33,50 +27,17 @@ export const runMariadbBackup = async ( try { const rcloneFlags = getS3Credentials(destination); const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; - - const { Id: containerId } = await getServiceContainer( - appName, - mariadb.serverId, - ); - const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`; - const command = getMariadbBackupCommand( - containerId, - database, - databaseUser, - databasePassword || "", + const backupCommand = getBackupCommand( + backup, + rcloneCommand, + deployment.logPath, ); if (mariadb.serverId) { - await execAsyncRemote( - mariadb.serverId, - ` - set -e; - echo "Running command." >> ${deployment.logPath}; - export RCLONE_LOG_LEVEL=DEBUG; - ${command} | ${rcloneCommand} >> ${deployment.logPath} 2>> ${deployment.logPath} || { - echo "❌ Command failed" >> ${deployment.logPath}; - exit 1; - } - echo "✅ Command executed successfully" >> ${deployment.logPath}; - `, - ); + await execAsyncRemote(mariadb.serverId, backupCommand); } else { - const writeStream = createWriteStream(deployment.logPath, { flags: "a" }); - await execAsyncStream( - `${command} | ${rcloneCommand}`, - (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }, - { - env: { - ...process.env, - RCLONE_LOG_LEVEL: "DEBUG", - }, - }, - ); + await execAsync(backupCommand); } await sendDatabaseBackupNotifications({ diff --git a/packages/server/src/utils/backups/mongo.ts b/packages/server/src/utils/backups/mongo.ts index 8aec00d9..815439d5 100644 --- a/packages/server/src/utils/backups/mongo.ts +++ b/packages/server/src/utils/backups/mongo.ts @@ -1,24 +1,18 @@ import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { Mongo } from "@dokploy/server/services/mongo"; import { findProjectById } from "@dokploy/server/services/project"; -import { getServiceContainer } from "../docker/utils"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; -import { execAsyncRemote, execAsyncStream } from "../process/execAsync"; -import { - getMongoBackupCommand, - getS3Credentials, - normalizeS3Path, -} from "./utils"; +import { execAsync, execAsyncRemote } from "../process/execAsync"; +import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils"; import { createDeploymentBackup, updateDeploymentStatus, } from "@dokploy/server/services/deployment"; -import { createWriteStream } from "node:fs"; export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => { - const { appName, databasePassword, databaseUser, projectId, name } = mongo; + const { projectId, name } = mongo; const project = await findProjectById(projectId); - const { prefix, database } = backup; + const { prefix } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.dump.gz`; const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; @@ -30,51 +24,18 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => { try { const rcloneFlags = getS3Credentials(destination); const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; - - const { Id: containerId } = await getServiceContainer( - appName, - mongo.serverId, - ); - const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`; - const command = getMongoBackupCommand( - containerId, - database, - databaseUser || "", - databasePassword || "", + + const backupCommand = getBackupCommand( + backup, + rcloneCommand, + deployment.logPath, ); + if (mongo.serverId) { - await execAsyncRemote( - mongo.serverId, - ` - set -e; - echo "Running command." >> ${deployment.logPath}; - export RCLONE_LOG_LEVEL=DEBUG; - ${command} | ${rcloneCommand} >> ${deployment.logPath} 2>> ${deployment.logPath} || { - echo "❌ Command failed" >> ${deployment.logPath}; - exit 1; - } - echo "✅ Command executed successfully" >> ${deployment.logPath}; - `, - ); + await execAsyncRemote(mongo.serverId, backupCommand); } else { - const writeStream = createWriteStream(deployment.logPath, { flags: "a" }); - await execAsyncStream( - `${command} | ${rcloneCommand}`, - (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }, - { - env: { - ...process.env, - RCLONE_LOG_LEVEL: "DEBUG", - }, - }, - ); - writeStream.write("Backup done✅"); - writeStream.end(); + await execAsync(backupCommand); } await sendDatabaseBackupNotifications({ diff --git a/packages/server/src/utils/backups/mysql.ts b/packages/server/src/utils/backups/mysql.ts index f20d5a84..80a24827 100644 --- a/packages/server/src/utils/backups/mysql.ts +++ b/packages/server/src/utils/backups/mysql.ts @@ -1,24 +1,18 @@ import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { MySql } from "@dokploy/server/services/mysql"; import { findProjectById } from "@dokploy/server/services/project"; -import { getServiceContainer } from "../docker/utils"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; -import { execAsyncRemote, execAsyncStream } from "../process/execAsync"; -import { - getMysqlBackupCommand, - getS3Credentials, - normalizeS3Path, -} from "./utils"; -import { createWriteStream } from "node:fs"; +import { execAsync, execAsyncRemote } from "../process/execAsync"; +import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils"; import { createDeploymentBackup, updateDeploymentStatus, } from "@dokploy/server/services/deployment"; export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => { - const { appName, databaseRootPassword, projectId, name } = mysql; + const { projectId, name } = mysql; const project = await findProjectById(projectId); - const { prefix, database } = backup; + const { prefix } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.sql.gz`; const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; @@ -27,53 +21,23 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => { title: "MySQL Backup", description: "MySQL Backup", }); + try { const rcloneFlags = getS3Credentials(destination); const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; - const { Id: containerId } = await getServiceContainer( - appName, - mysql.serverId, + const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`; + + const backupCommand = getBackupCommand( + backup, + rcloneCommand, + deployment.logPath, ); - const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`; - const command = getMysqlBackupCommand( - containerId, - database, - databaseRootPassword || "", - ); if (mysql.serverId) { - await execAsyncRemote( - mysql.serverId, - ` - set -e; - echo "Running command." >> ${deployment.logPath}; - export RCLONE_LOG_LEVEL=DEBUG; - ${command} | ${rcloneCommand} >> ${deployment.logPath} 2>> ${deployment.logPath} || { - echo "❌ Command failed" >> ${deployment.logPath}; - exit 1; - } - echo "✅ Command executed successfully" >> ${deployment.logPath}; - `, - ); + await execAsyncRemote(mysql.serverId, backupCommand); } else { - const writeStream = createWriteStream(deployment.logPath, { flags: "a" }); - await execAsyncStream( - `${command} | ${rcloneCommand}`, - (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }, - { - env: { - ...process.env, - RCLONE_LOG_LEVEL: "DEBUG", - }, - }, - ); - writeStream.write("Backup done✅"); - writeStream.end(); + await execAsync(backupCommand); } await sendDatabaseBackupNotifications({ applicationName: name, diff --git a/packages/server/src/utils/backups/postgres.ts b/packages/server/src/utils/backups/postgres.ts index e122b9ad..29974106 100644 --- a/packages/server/src/utils/backups/postgres.ts +++ b/packages/server/src/utils/backups/postgres.ts @@ -1,33 +1,27 @@ import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { Postgres } from "@dokploy/server/services/postgres"; import { findProjectById } from "@dokploy/server/services/project"; -import { getServiceContainer } from "../docker/utils"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; -import { execAsyncRemote, execAsyncStream } from "../process/execAsync"; -import { - getPostgresBackupCommand, - getS3Credentials, - normalizeS3Path, -} from "./utils"; +import { execAsync, execAsyncRemote } from "../process/execAsync"; +import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils"; import { createDeploymentBackup, updateDeploymentStatus, } from "@dokploy/server/services/deployment"; -import { createWriteStream } from "node:fs"; export const runPostgresBackup = async ( postgres: Postgres, backup: BackupSchedule, ) => { - const { appName, databaseUser, name, projectId } = postgres; + const { name, projectId } = postgres; const project = await findProjectById(projectId); const deployment = await createDeploymentBackup({ backupId: backup.backupId, - title: "Postgres Backup", - description: "Postgres Backup", + title: "Initializing Backup", + description: "Initializing Backup", }); - const { prefix, database } = backup; + const { prefix } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.sql.gz`; const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; @@ -37,49 +31,15 @@ export const runPostgresBackup = async ( const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`; - const { Id: containerId } = await getServiceContainer( - appName, - postgres.serverId, + const backupCommand = getBackupCommand( + backup, + rcloneCommand, + deployment.logPath, ); - - const command = getPostgresBackupCommand( - containerId, - database, - databaseUser || "", - ); - if (postgres.serverId) { - await execAsyncRemote( - postgres.serverId, - ` - set -e; - echo "Running command." >> ${deployment.logPath}; - export RCLONE_LOG_LEVEL=DEBUG; - ${command} | ${rcloneCommand} >> ${deployment.logPath} 2>> ${deployment.logPath} || { - echo "❌ Command failed" >> ${deployment.logPath}; - exit 1; - } - echo "✅ Command executed successfully" >> ${deployment.logPath}; - `, - ); + await execAsyncRemote(postgres.serverId, backupCommand); } else { - const writeStream = createWriteStream(deployment.logPath, { flags: "a" }); - await execAsyncStream( - `${command} | ${rcloneCommand}`, - (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }, - { - env: { - ...process.env, - RCLONE_LOG_LEVEL: "DEBUG", - }, - }, - ); - writeStream.write("Backup done✅"); - writeStream.end(); + await execAsync(backupCommand); } await sendDatabaseBackupNotifications({ diff --git a/packages/server/src/utils/backups/utils.ts b/packages/server/src/utils/backups/utils.ts index 632f5127..a0ec2b73 100644 --- a/packages/server/src/utils/backups/utils.ts +++ b/packages/server/src/utils/backups/utils.ts @@ -77,35 +77,179 @@ export const getS3Credentials = (destination: Destination) => { }; export const getPostgresBackupCommand = ( - containerId: string, database: string, databaseUser: string, ) => { - return `docker exec ${containerId} sh -c "pg_dump -Fc --no-acl --no-owner -h localhost -U ${databaseUser} --no-password '${database}' | gzip"`; + return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; pg_dump -Fc --no-acl --no-owner -h localhost -U ${databaseUser} --no-password '${database}' | gzip"`; }; export const getMariadbBackupCommand = ( - containerId: string, database: string, databaseUser: string, databasePassword: string, ) => { - return `docker exec ${containerId} sh -c "mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --databases ${database} | gzip"`; + return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --databases ${database} | gzip"`; }; export const getMysqlBackupCommand = ( - containerId: string, database: string, databasePassword: string, ) => { - return `docker exec ${containerId} sh -c "mysqldump --default-character-set=utf8mb4 -u 'root' --password='${databasePassword}' --single-transaction --no-tablespaces --quick '${database}' | gzip"`; + return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mysqldump --default-character-set=utf8mb4 -u 'root' --password='${databasePassword}' --single-transaction --no-tablespaces --quick '${database}' | gzip"`; }; export const getMongoBackupCommand = ( - containerId: string, database: string, databaseUser: string, databasePassword: string, ) => { - return `docker exec ${containerId} sh -c "mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --archive --authenticationDatabase admin --gzip"`; + return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --archive --authenticationDatabase admin --gzip"`; +}; + +const getServiceContainerCommand = (appName: string) => { + return `docker ps -q --filter "status=running" --filter "label=com.docker.swarm.service.name=${appName}" | head -n 1`; +}; + +const getComposeContainerCommand = ( + appName: string, + serviceName: string, + composeType: "stack" | "docker-compose" | undefined, +) => { + if (composeType === "stack") { + return `docker ps -q --filter "status=running" --filter "label=com.docker.stack.namespace=${appName}" --filter "label=com.docker.swarm.service.name=${serviceName}" | head -n 1`; + } + return `docker ps -q --filter "status=running" --filter "label=com.docker.compose.project=${appName}" --filter "label=com.docker.compose.service=${serviceName}" | head -n 1`; +}; + +const getContainerSearchCommand = (backup: BackupSchedule) => { + const { backupType, postgres, mysql, mariadb, mongo, compose, serviceName } = + backup; + + if (backupType === "database") { + const appName = + postgres?.appName || mysql?.appName || mariadb?.appName || mongo?.appName; + return getServiceContainerCommand(appName || ""); + } + if (backupType === "compose") { + const { appName, composeType } = compose || {}; + return getComposeContainerCommand( + appName || "", + serviceName || "", + composeType, + ); + } +}; + +export const generateBackupCommand = (backup: BackupSchedule) => { + const { backupType, databaseType } = backup; + switch (databaseType) { + case "postgres": { + const postgres = backup.postgres; + if (backupType === "database" && postgres) { + return getPostgresBackupCommand(backup.database, postgres.databaseUser); + } + if (backupType === "compose" && backup.metadata?.postgres) { + return getPostgresBackupCommand( + backup.database, + backup.metadata.postgres.databaseUser, + ); + } + break; + } + case "mysql": { + const mysql = backup.mysql; + if (backupType === "database" && mysql) { + return getMysqlBackupCommand(backup.database, mysql.databasePassword); + } + if (backupType === "compose" && backup.metadata?.mysql) { + return getMysqlBackupCommand( + backup.database, + backup.metadata?.mysql?.databaseRootPassword || "", + ); + } + break; + } + case "mariadb": { + const mariadb = backup.mariadb; + if (backupType === "database" && mariadb) { + return getMariadbBackupCommand( + backup.database, + mariadb.databaseUser, + mariadb.databasePassword, + ); + } + if (backupType === "compose" && backup.metadata?.mariadb) { + return getMariadbBackupCommand( + backup.database, + backup.metadata.mariadb.databaseUser, + backup.metadata.mariadb.databasePassword, + ); + } + break; + } + case "mongo": { + const mongo = backup.mongo; + if (backupType === "database" && mongo) { + return getMongoBackupCommand( + backup.database, + mongo.databaseUser, + mongo.databasePassword, + ); + } + if (backupType === "compose" && backup.metadata?.mongo) { + return getMongoBackupCommand( + backup.database, + backup.metadata.mongo.databaseUser, + backup.metadata.mongo.databasePassword, + ); + } + break; + } + default: + throw new Error(`Database type not supported: ${databaseType}`); + } + + return null; +}; + +export const getBackupCommand = ( + backup: BackupSchedule, + rcloneCommand: string, + logPath: string, +) => { + const containerSearch = getContainerSearchCommand(backup); + const backupCommand = generateBackupCommand(backup); + return ` + set -eo pipefail; + echo "[$(date)] Starting backup process..." >> ${logPath}; + echo "[$(date)] Executing backup command..." >> ${logPath}; + CONTAINER_ID=$(${containerSearch}) + + if [ -z "$CONTAINER_ID" ]; then + echo "[$(date)] ❌ Container not found" >> ${logPath}; + exit 1; + fi + + echo "[$(date)] Container Up: $CONTAINER_ID" >> ${logPath}; + + # Run the backup command and capture the exit status + BACKUP_OUTPUT=$(${backupCommand} 2>&1 >/dev/null) || { + echo "[$(date)] ❌ backup failed" >> ${logPath}; + echo "Error: $BACKUP_OUTPUT" >> ${logPath}; + exit 1; + } + + echo "[$(date)] ✅ backup completed successfully" >> ${logPath}; + echo "[$(date)] Starting upload to S3..." >> ${logPath}; + + # Run the upload command and capture the exit status + UPLOAD_OUTPUT=$(${backupCommand} | ${rcloneCommand} 2>&1 >/dev/null) || { + echo "[$(date)] ❌ Upload to S3 failed" >> ${logPath}; + echo "Error: $UPLOAD_OUTPUT" >> ${logPath}; + exit 1; + } + + echo "[$(date)] ✅ Upload to S3 completed successfully" >> ${logPath}; + echo "Backup done ✅" >> ${logPath}; + `; }; diff --git a/packages/server/src/utils/docker/utils.ts b/packages/server/src/utils/docker/utils.ts index 12d23043..be497242 100644 --- a/packages/server/src/utils/docker/utils.ts +++ b/packages/server/src/utils/docker/utils.ts @@ -509,7 +509,7 @@ export const getServiceContainer = async ( }); if (containers.length === 0 || !containers[0]) { - throw new Error(`No container found with name: ${appName}`); + return null; } const container = containers[0]; @@ -549,7 +549,7 @@ export const getComposeContainer = async ( }); if (containers.length === 0 || !containers[0]) { - throw new Error(`No container found with name: ${appName}`); + return null; } const container = containers[0]; diff --git a/packages/server/src/utils/process/execAsync.ts b/packages/server/src/utils/process/execAsync.ts index d82a488d..84f0701d 100644 --- a/packages/server/src/utils/process/execAsync.ts +++ b/packages/server/src/utils/process/execAsync.ts @@ -21,7 +21,6 @@ export const execAsyncStream = ( const childProcess = exec(command, options, (error) => { if (error) { - console.log(error); reject(error); return; }