import path from "node:path";import { paths } from "@dokploy/server/constants";import { createDeploymentVolumeBackup, updateDeploymentStatus,} from "@dokploy/server/services/deployment";import { findDestinationById } from "@dokploy/server/services/destination";import { findVolumeBackupById } from "@dokploy/server/services/volume-backups";import { execAsync, execAsyncRemote,} from "@dokploy/server/utils/process/execAsync";import { scheduledJobs, scheduleJob } from "node-schedule";import { getS3Credentials, normalizeS3Path } from "../backups/utils";import { sendVolumeBackupNotifications } from "../notifications/volume-backup";import { backupVolume, getVolumeServiceAppName } from "./backup"; // Helper functions to extract project info from volume backupconst getProjectName = ( volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,): string => { const services = [ volumeBackup.application, volumeBackup.compose, volumeBackup.postgres, volumeBackup.mysql, volumeBackup.mariadb, volumeBackup.mongo, volumeBackup.redis, volumeBackup.libsql, ]; for (const service of services) { if (service?.environment?.project?.name) { return service.environment.project.name; } } return "Unknown Project";}; const getOrganizationId = ( volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,): string => { const services = [ volumeBackup.application, volumeBackup.compose, volumeBackup.postgres, volumeBackup.mysql, volumeBackup.mariadb, volumeBackup.mongo, volumeBackup.redis, volumeBackup.libsql, ]; for (const service of services) { if (service?.environment?.project?.organizationId) { return service.environment.project.organizationId; } } return "";}; export const scheduleVolumeBackup = async (volumeBackupId: string) => { const volumeBackup = await findVolumeBackupById(volumeBackupId); scheduleJob(volumeBackupId, volumeBackup.cronExpression, async () => { await runVolumeBackup(volumeBackupId); });}; export const removeVolumeBackupJob = async (volumeBackupId: string) => { const currentJob = scheduledJobs[volumeBackupId]; currentJob?.cancel();}; const cleanupOldVolumeBackups = async ( volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>, serverId?: string | null,) => { const { keepLatestCount, prefix, volumeName } = volumeBackup; const destination = await findDestinationById(volumeBackup.destinationId); if (!keepLatestCount) return; try { const rcloneFlags = getS3Credentials(destination); const s3AppName = getVolumeServiceAppName(volumeBackup); const backupFilesPath = `:s3:${destination.bucket}/${s3AppName}/${normalizeS3Path(prefix || "")}`; const listCommand = `rclone lsf ${rcloneFlags.join(" ")} --include \"${volumeName}-*.tar\" ${backupFilesPath}`; const sortAndPick = `sort -r | tail -n +$((${keepLatestCount}+1)) | xargs -I{}`; const deleteCommand = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}{}`; const fullCommand = `${listCommand} | ${sortAndPick} ${deleteCommand}`; if (serverId) { await execAsyncRemote(serverId, fullCommand); } else { await execAsync(fullCommand); } } catch (error) { console.error("Volume backup retention error", error); }}; export const runVolumeBackup = async (volumeBackupId: string) => { const volumeBackup = await findVolumeBackupById(volumeBackupId); const serverId = volumeBackup.application?.serverId || volumeBackup.compose?.serverId; const deployment = await createDeploymentVolumeBackup({ volumeBackupId: volumeBackup.volumeBackupId, title: "Volume Backup", description: "Volume Backup", }); const projectName = getProjectName(volumeBackup); const organizationId = getOrganizationId(volumeBackup); try { const command = await backupVolume(volumeBackup); const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; if (serverId) { await execAsyncRemote(serverId, commandWithLog); } else { await execAsync(commandWithLog); } if (volumeBackup.keepLatestCount && volumeBackup.keepLatestCount > 0) { await cleanupOldVolumeBackups(volumeBackup, serverId); } await updateDeploymentStatus(deployment.deploymentId, "done"); // Map service type to match notification function expectations const mappedServiceType = volumeBackup.serviceType === "mongo" ? "mongodb" : volumeBackup.serviceType; try { await sendVolumeBackupNotifications({ projectName, applicationName: volumeBackup.name, volumeName: volumeBackup.volumeName, serviceType: mappedServiceType, type: "success", organizationId, }); } catch (notificationError) { console.error( "Failed to send volume backup success notification", notificationError, ); } } catch (error) { const { VOLUME_BACKUPS_PATH } = paths(!!serverId); const volumeBackupPath = path.join( VOLUME_BACKUPS_PATH, volumeBackup.appName, ); // delete all the .tar files const command = `rm -rf ${volumeBackupPath}/*.tar`; if (serverId) { await execAsyncRemote(serverId, command); } else { await execAsync(command); } await updateDeploymentStatus(deployment.deploymentId, "error"); // Send error notification const mappedServiceType = volumeBackup.serviceType === "mongo" ? "mongodb" : volumeBackup.serviceType; try { await sendVolumeBackupNotifications({ projectName, applicationName: volumeBackup.name, volumeName: volumeBackup.volumeName, serviceType: mappedServiceType, type: "error", organizationId, errorMessage: error instanceof Error ? error.message : String(error), }); } catch (notificationError) { console.error( "Failed to send volume backup error notification", notificationError, ); } }};
import path from "node:path";
import { paths } from "@dokploy/server/constants";
import {
createDeploymentVolumeBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { findDestinationById } from "@dokploy/server/services/destination";
import { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import { scheduledJobs, scheduleJob } from "node-schedule";
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
import { sendVolumeBackupNotifications } from "../notifications/volume-backup";
import { backupVolume, getVolumeServiceAppName } from "./backup";
// Helper functions to extract project info from volume backup
const getProjectName = (
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
): string => {
const services = [
volumeBackup.application,
volumeBackup.compose,
volumeBackup.postgres,
volumeBackup.mysql,
volumeBackup.mariadb,
volumeBackup.mongo,
volumeBackup.redis,
volumeBackup.libsql,
];
for (const service of services) {
if (service?.environment?.project?.name) {
return service.environment.project.name;
}
}
return "Unknown Project";
};
const getOrganizationId = (
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
): string => {
const services = [
volumeBackup.application,
volumeBackup.compose,
volumeBackup.postgres,
volumeBackup.mysql,
volumeBackup.mariadb,
volumeBackup.mongo,
volumeBackup.redis,
volumeBackup.libsql,
];
for (const service of services) {
if (service?.environment?.project?.organizationId) {
return service.environment.project.organizationId;
}
}
return "";
};
export const scheduleVolumeBackup = async (volumeBackupId: string) => {
const volumeBackup = await findVolumeBackupById(volumeBackupId);
scheduleJob(volumeBackupId, volumeBackup.cronExpression, async () => {
await runVolumeBackup(volumeBackupId);
});
};
export const removeVolumeBackupJob = async (volumeBackupId: string) => {
const currentJob = scheduledJobs[volumeBackupId];
currentJob?.cancel();
};
const cleanupOldVolumeBackups = async (
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
serverId?: string | null,
) => {
const { keepLatestCount, prefix, volumeName } = volumeBackup;
const destination = await findDestinationById(volumeBackup.destinationId);
if (!keepLatestCount) return;
try {
const rcloneFlags = getS3Credentials(destination);
const s3AppName = getVolumeServiceAppName(volumeBackup);
const backupFilesPath = `:s3:${destination.bucket}/${s3AppName}/${normalizeS3Path(prefix || "")}`;
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} --include \"${volumeName}-*.tar\" ${backupFilesPath}`;
const sortAndPick = `sort -r | tail -n +$((${keepLatestCount}+1)) | xargs -I{}`;
const deleteCommand = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}{}`;
const fullCommand = `${listCommand} | ${sortAndPick} ${deleteCommand}`;
if (serverId) {
await execAsyncRemote(serverId, fullCommand);
} else {
await execAsync(fullCommand);
}
} catch (error) {
console.error("Volume backup retention error", error);
}
};
export const runVolumeBackup = async (volumeBackupId: string) => {
const volumeBackup = await findVolumeBackupById(volumeBackupId);
const serverId =
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
const deployment = await createDeploymentVolumeBackup({
volumeBackupId: volumeBackup.volumeBackupId,
title: "Volume Backup",
description: "Volume Backup",
});
const projectName = getProjectName(volumeBackup);
const organizationId = getOrganizationId(volumeBackup);
try {
const command = await backupVolume(volumeBackup);
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (serverId) {
await execAsyncRemote(serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
if (volumeBackup.keepLatestCount && volumeBackup.keepLatestCount > 0) {
await cleanupOldVolumeBackups(volumeBackup, serverId);
}
await updateDeploymentStatus(deployment.deploymentId, "done");
// Map service type to match notification function expectations
const mappedServiceType =
volumeBackup.serviceType === "mongo"
? "mongodb"
: volumeBackup.serviceType;
try {
await sendVolumeBackupNotifications({
projectName,
applicationName: volumeBackup.name,
volumeName: volumeBackup.volumeName,
serviceType: mappedServiceType,
type: "success",
organizationId,
});
} catch (notificationError) {
console.error(
"Failed to send volume backup success notification",
notificationError,
);
}
} catch (error) {
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
const volumeBackupPath = path.join(
VOLUME_BACKUPS_PATH,
volumeBackup.appName,
);
// delete all the .tar files
const command = `rm -rf ${volumeBackupPath}/*.tar`;
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
await updateDeploymentStatus(deployment.deploymentId, "error");
// Send error notification
const mappedServiceType =
volumeBackup.serviceType === "mongo"
? "mongodb"
: volumeBackup.serviceType;
try {
await sendVolumeBackupNotifications({
projectName,
applicationName: volumeBackup.name,
volumeName: volumeBackup.volumeName,
serviceType: mappedServiceType,
type: "error",
organizationId,
errorMessage: error instanceof Error ? error.message : String(error),
});
} catch (notificationError) {
console.error(
"Failed to send volume backup error notification",
notificationError,
);
}
}
};