From c042c8c0c5834bbfc2404334214f56876e2b85f1 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 30 Jun 2025 01:25:50 -0600 Subject: [PATCH] feat: implement service-based volume selection for backups - Added a new query to load mounts by service name, enhancing the volume backup form's functionality. - Updated the form to allow users to select a service and corresponding volume from a dropdown, improving user experience. - Retained the option for manual input of volume names, ensuring flexibility in volume selection. - Refactored the component to streamline the handling of service and volume selections. --- .../volume-backups/handle-volume-backups.tsx | 370 ++++++++++-------- apps/dokploy/server/api/routers/compose.ts | 22 ++ .../server/src/utils/volume-backups/utils.ts | 62 +++ 3 files changed, 292 insertions(+), 162 deletions(-) create mode 100644 packages/server/src/utils/volume-backups/utils.ts diff --git a/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx b/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx index 04a027cb8..f0f00bd26 100644 --- a/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx +++ b/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx @@ -154,6 +154,18 @@ export const HandleVolumeBackups = ({ }, ); + const serviceName = form.watch("serviceName"); + + const { data: mountsByService } = api.compose.loadMountsByService.useQuery( + { + composeId: id || "", + serviceName, + }, + { + enabled: !!id && volumeBackupType === "compose" && !!serviceName, + }, + ); + useEffect(() => { if (volumeBackupId && volumeBackup) { form.reset({ @@ -261,117 +273,6 @@ export const HandleVolumeBackups = ({
- {serviceTypeForm === "compose" && ( -
- {errorServices && ( - - {errorServices?.message} - - )} - ( - - Service Name -
- - - - - - - -

- Fetch: Will clone the repository and load the - services -

-
-
-
- - - - - - -

- Cache: If you previously deployed this compose, - it will read the services from the last - deployment/fetch from the repository -

-
-
-
-
- - -
- )} - /> -
- )} - )} /> - - {serviceTypeForm === "application" && ( + {serviceTypeForm === "compose" && ( <> - ( - - Volumes - - - Choose the volume to backup, if you dont see the volume - here, you can type the volume name manually - - - +
+ {errorServices && ( + + {errorServices?.message} + )} - /> + ( + + Service Name +
+ - - - The name of the Docker volume to backup - - - - )} - /> + + {services?.map((service, index) => ( + + {service} + + ))} + + Empty + + + + + + + + + +

+ Fetch: Will clone the repository and load the + services +

+
+
+
+ + + + + + +

+ Cache: If you previously deployed this + compose, it will read the services from the + last deployment/fetch from the repository +

+
+
+
+
+ + +
+ )} + /> +
+ {mountsByService && mountsByService.length > 0 && ( + ( + + Volumes + + + Choose the volume to backup, if you dont see the + volume here, you can type the volume name manually + + + + )} + /> + )} )} + {serviceTypeForm === "application" && ( + ( + + Volumes + + + Choose the volume to backup, if you dont see the volume + here, you can type the volume name manually + + + + )} + /> + )} + + ( + + Volume Name + + + + + The name of the Docker volume to backup + + + + )} + /> { + const compose = await findComposeById(input.composeId); + if (compose.project.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to load this compose", + }); + } + const container = await getComposeContainer(compose, input.serviceName); + const mounts = container?.Mounts.filter( + (mount) => mount.Type === "volume" && mount.Source !== "", + ); + return mounts; + }), fetchSourceType: protectedProcedure .input(apiFindCompose) .mutation(async ({ input, ctx }) => { diff --git a/packages/server/src/utils/volume-backups/utils.ts b/packages/server/src/utils/volume-backups/utils.ts new file mode 100644 index 000000000..375d3139a --- /dev/null +++ b/packages/server/src/utils/volume-backups/utils.ts @@ -0,0 +1,62 @@ +import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups"; +import { getComposeContainer } from "../docker/utils"; +import { findComposeById, paths, paths } from "../.."; + +export const createVolumeBackup = async ( + volumeBackup: Awaited>, +) => { + const serverId = + volumeBackup.application?.serverId || volumeBackup.compose?.serverId; + + if (serverId) { + } else { + } +}; + +const backupVolume = async ( + volumeBackup: Awaited>, +) => { + const { serviceType, volumeName, turnOff } = volumeBackup; + + if (turnOff) { + return `docker run --rm \ + -v ${volumeName}:/volume_data \ + -v $(pwd):/backup \ + ubuntu \ + bash -c "cd /volume_data && tar cvf /backup/${volumeName}.tar ."`; + } + + if (serviceType === "application") { + return ` + docker service scale ${volumeBackup.application?.appName}=0 + docker run --rm \ + -v ${volumeName}:/volume_data \ + -v $(pwd):/backup \ + ubuntu \ + bash -c "cd /volume_data && tar cvf /backup/${volumeName}.tar . + docker service scale ${volumeBackup.application?.appName}=1 + "`; + } + if (serviceType === "compose") { + const compose = await findComposeById( + volumeBackup.compose?.composeId || "", + ); + const { COMPOSE_PATH } = paths(!!compose.serverId); + let stopCommand = ""; + + if (compose.composeType === "stack") { + stopCommand = `docker service scale ${compose.appName}_${volumeBackup.serviceName}=0`; + } else { + stopCommand = `docker compose down --remove-orphans`; + } + return ` + + docker run --rm \ + -v ${volumeName}:/volume_data \ + -v $(pwd):/backup \ + ubuntu \ + bash -c "cd /volume_data && tar cvf /backup/${volumeName}.tar ."`; + } + + return ``; +};