191 lines · 5.6 KB · TypeScript
RawDownload
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,			);		}	}};