From 6521491e2feed53497515917d9febbcf9f49aedf Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 2 Jul 2025 00:36:46 -0600 Subject: [PATCH] feat: enhance volume backup scheduling and management - Added initVolumeBackupsCronJobs function to initialize scheduled volume backups on server startup. - Updated volumeBackupsRouter to handle scheduling and removal of volume backup jobs based on user input. - Improved create and update volume backup logic to include scheduling functionality for both cloud and local environments. - Introduced utility functions for scheduling and removing volume backup jobs, enhancing overall backup management. --- .../server/api/routers/volume-backups.ts | 58 +++++++++++++++++-- apps/dokploy/server/server.ts | 2 + apps/dokploy/server/utils/backup.ts | 5 ++ .../server/src/services/volume-backups.ts | 15 ++--- .../server/src/utils/volume-backups/index.ts | 27 +++++++++ .../server/src/utils/volume-backups/utils.ts | 13 +++++ 6 files changed, 104 insertions(+), 16 deletions(-) diff --git a/apps/dokploy/server/api/routers/volume-backups.ts b/apps/dokploy/server/api/routers/volume-backups.ts index a6912f618..47e7cb6ec 100644 --- a/apps/dokploy/server/api/routers/volume-backups.ts +++ b/apps/dokploy/server/api/routers/volume-backups.ts @@ -6,6 +6,8 @@ import { runVolumeBackup, findVolumeBackupById, restoreVolume, + scheduleVolumeBackup, + removeVolumeBackupJob, } from "@dokploy/server"; import { createVolumeBackupSchema, @@ -21,6 +23,8 @@ import { execAsyncRemote, execAsyncStream, } from "@dokploy/server/utils/process/execAsync"; +import { removeJob, schedule, updateJob } from "@/server/utils/backup"; +import { TRPCError } from "@trpc/server"; export const volumeBackupsRouter = createTRPCRouter({ list: protectedProcedure @@ -55,7 +59,20 @@ export const volumeBackupsRouter = createTRPCRouter({ create: protectedProcedure .input(createVolumeBackupSchema) .mutation(async ({ input }) => { - return await createVolumeBackup(input); + const newVolumeBackup = await createVolumeBackup(input); + + if (newVolumeBackup?.enabled) { + if (IS_CLOUD) { + await schedule({ + cronSchedule: newVolumeBackup.cronExpression, + volumeBackupId: newVolumeBackup.volumeBackupId, + type: "volume-backup", + }); + } else { + await scheduleVolumeBackup(newVolumeBackup.volumeBackupId); + } + } + return newVolumeBackup; }), one: protectedProcedure .input( @@ -73,15 +90,46 @@ export const volumeBackupsRouter = createTRPCRouter({ }), ) .mutation(async ({ input }) => { - if (IS_CLOUD) { - return true; - } return await removeVolumeBackup(input.volumeBackupId); }), update: protectedProcedure .input(updateVolumeBackupSchema) .mutation(async ({ input }) => { - return await updateVolumeBackup(input.volumeBackupId, input); + const updatedVolumeBackup = await updateVolumeBackup( + input.volumeBackupId, + input, + ); + + if (!updatedVolumeBackup) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Volume backup not found", + }); + } + + if (IS_CLOUD) { + if (updatedVolumeBackup.enabled) { + await updateJob({ + cronSchedule: updatedVolumeBackup.cronExpression, + volumeBackupId: updatedVolumeBackup.volumeBackupId, + type: "volume-backup", + }); + } else { + await removeJob({ + cronSchedule: updatedVolumeBackup.cronExpression, + volumeBackupId: updatedVolumeBackup.volumeBackupId, + type: "volume-backup", + }); + } + } else { + if (updatedVolumeBackup?.enabled) { + removeVolumeBackupJob(updatedVolumeBackup.volumeBackupId); + scheduleVolumeBackup(updatedVolumeBackup.volumeBackupId); + } else { + removeVolumeBackupJob(updatedVolumeBackup.volumeBackupId); + } + } + return updatedVolumeBackup; }), runManually: protectedProcedure diff --git a/apps/dokploy/server/server.ts b/apps/dokploy/server/server.ts index 148edda0d..500a701ae 100644 --- a/apps/dokploy/server/server.ts +++ b/apps/dokploy/server/server.ts @@ -7,6 +7,7 @@ import { createDefaultTraefikConfig, initCronJobs, initSchedules, + initVolumeBackupsCronJobs, initializeNetwork, sendDokployRestartNotifications, setupDirectories, @@ -51,6 +52,7 @@ void app.prepare().then(async () => { await migration(); await initCronJobs(); await initSchedules(); + await initVolumeBackupsCronJobs(); await sendDokployRestartNotifications(); } diff --git a/apps/dokploy/server/utils/backup.ts b/apps/dokploy/server/utils/backup.ts index cf0b6c224..9263ecba8 100644 --- a/apps/dokploy/server/utils/backup.ts +++ b/apps/dokploy/server/utils/backup.ts @@ -19,6 +19,11 @@ type QueueJob = type: "schedule"; cronSchedule: string; scheduleId: string; + } + | { + type: "volume-backup"; + cronSchedule: string; + volumeBackupId: string; }; export const schedule = async (job: QueueJob) => { try { diff --git a/packages/server/src/services/volume-backups.ts b/packages/server/src/services/volume-backups.ts index 887d71258..1b50e1465 100644 --- a/packages/server/src/services/volume-backups.ts +++ b/packages/server/src/services/volume-backups.ts @@ -7,7 +7,6 @@ import { import { db } from "../db"; import { TRPCError } from "@trpc/server"; import type { z } from "zod"; -import { scheduleBackup } from "../utils/backups/utils"; export const findVolumeBackupById = async (volumeBackupId: string) => { const volumeBackup = await db.query.volumeBackups.findFirst({ @@ -43,14 +42,6 @@ export const createVolumeBackup = async ( .returning() .then((e) => e[0]); - await schedule({ - cronSchedule: backup.schedule, - backupId: backup.backupId, - type: "backup", - }); - - scheduleBackup(backup); - return newVolumeBackup; }; @@ -64,8 +55,10 @@ export const updateVolumeBackup = async ( volumeBackupId: string, volumeBackup: z.infer, ) => { - await db + return await db .update(volumeBackups) .set(volumeBackup) - .where(eq(volumeBackups.volumeBackupId, volumeBackupId)); + .where(eq(volumeBackups.volumeBackupId, volumeBackupId)) + .returning() + .then((e) => e[0]); }; diff --git a/packages/server/src/utils/volume-backups/index.ts b/packages/server/src/utils/volume-backups/index.ts index d35162f5e..eb80b09a7 100644 --- a/packages/server/src/utils/volume-backups/index.ts +++ b/packages/server/src/utils/volume-backups/index.ts @@ -1,3 +1,30 @@ export * from "./backup"; export * from "./restore"; export * from "./utils"; +import { volumeBackups } from "@dokploy/server/db/schema"; +import { eq } from "drizzle-orm"; +import { db } from "../../db/index"; +import { scheduleVolumeBackup } from "./utils"; + +export const initVolumeBackupsCronJobs = async () => { + console.log("Setting up volume backups cron jobs...."); + try { + const volumeBackupsResult = await db.query.volumeBackups.findMany({ + where: eq(volumeBackups.enabled, true), + with: { + application: true, + compose: true, + }, + }); + + console.log(`Initializing ${volumeBackupsResult.length} volume backups`); + for (const volumeBackup of volumeBackupsResult) { + scheduleVolumeBackup(volumeBackup.volumeBackupId); + console.log( + `Initialized volume backup: ${volumeBackup.name} ${volumeBackup.serviceType} ✅`, + ); + } + } catch (error) { + console.log(`Error initializing volume backups: ${error}`); + } +}; diff --git a/packages/server/src/utils/volume-backups/utils.ts b/packages/server/src/utils/volume-backups/utils.ts index 4185dd83f..69affad34 100644 --- a/packages/server/src/utils/volume-backups/utils.ts +++ b/packages/server/src/utils/volume-backups/utils.ts @@ -6,6 +6,19 @@ import { updateDeploymentStatus, } from "../.."; import { backupVolume } from "./backup"; +import { scheduleJob, scheduledJobs } from "node-schedule"; + +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(); +}; export const runVolumeBackup = async (volumeBackupId: string) => { const volumeBackup = await findVolumeBackupById(volumeBackupId);