mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-21 22:05:23 +02:00
- Added a check to prevent empty values from being processed in the onValueChange handler for Bitbucket, Gitea, GitHub, and GitLab providers. - Removed unnecessary defaultValue prop from Select components to streamline the code. - Updated button styles to remove background color for better consistency across the UI. - Enhanced volume backup selection to display a message when no volumes are found. This update enhances user experience by ensuring that empty selections are handled gracefully and improves the overall visual consistency of the UI components.
355 lines
9.4 KiB
TypeScript
355 lines
9.4 KiB
TypeScript
import {
|
|
createVolumeBackup,
|
|
findVolumeBackupById,
|
|
IS_CLOUD,
|
|
removeVolumeBackup,
|
|
removeVolumeBackupJob,
|
|
restoreVolume,
|
|
runVolumeBackup,
|
|
scheduleVolumeBackup,
|
|
updateVolumeBackup,
|
|
} from "@dokploy/server";
|
|
import { db } from "@dokploy/server/db";
|
|
import {
|
|
createVolumeBackupSchema,
|
|
updateVolumeBackupSchema,
|
|
volumeBackups,
|
|
} from "@dokploy/server/db/schema";
|
|
import { findDestinationById } from "@dokploy/server/services/destination";
|
|
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
|
import { findServerById } from "@dokploy/server/services/server";
|
|
import {
|
|
execAsyncRemote,
|
|
execAsyncStream,
|
|
} from "@dokploy/server/utils/process/execAsync";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { observable } from "@trpc/server/observable";
|
|
import { desc, eq } from "drizzle-orm";
|
|
import { z } from "zod";
|
|
import { audit } from "@/server/api/utils/audit";
|
|
import { assertVolumeBackupLimit } from "@/server/api/utils/plan-limits";
|
|
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
|
|
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
|
|
|
|
export const volumeBackupsRouter = createTRPCRouter({
|
|
list: protectedProcedure
|
|
.input(
|
|
z.object({
|
|
id: z.string().min(1),
|
|
volumeBackupType: z.enum([
|
|
"application",
|
|
"postgres",
|
|
"mysql",
|
|
"mariadb",
|
|
"mongo",
|
|
"redis",
|
|
"compose",
|
|
"libsql",
|
|
]),
|
|
}),
|
|
)
|
|
.query(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.id, {
|
|
volumeBackup: ["read"],
|
|
});
|
|
return await db.query.volumeBackups.findMany({
|
|
where: eq(volumeBackups[`${input.volumeBackupType}Id`], input.id),
|
|
with: {
|
|
application: true,
|
|
postgres: true,
|
|
mysql: true,
|
|
mariadb: true,
|
|
mongo: true,
|
|
redis: true,
|
|
compose: true,
|
|
libsql: true,
|
|
},
|
|
orderBy: [desc(volumeBackups.createdAt)],
|
|
});
|
|
}),
|
|
create: protectedProcedure
|
|
.input(createVolumeBackupSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const serviceType = (
|
|
[
|
|
"application",
|
|
"postgres",
|
|
"mysql",
|
|
"mariadb",
|
|
"mongo",
|
|
"redis",
|
|
"libsql",
|
|
"compose",
|
|
] as const
|
|
).find((type) => input[`${type}Id`]);
|
|
const serviceId = serviceType ? input[`${serviceType}Id`] : undefined;
|
|
if (serviceId) {
|
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
|
volumeBackup: ["create"],
|
|
});
|
|
}
|
|
if (IS_CLOUD && serviceType && serviceId) {
|
|
const existingVolumeBackups = await db.query.volumeBackups.findMany({
|
|
where: eq(volumeBackups[`${serviceType}Id`], serviceId),
|
|
});
|
|
await assertVolumeBackupLimit(
|
|
ctx.session.activeOrganizationId,
|
|
existingVolumeBackups.length,
|
|
);
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
await audit(ctx, {
|
|
action: "create",
|
|
resourceType: "volumeBackup",
|
|
resourceId: newVolumeBackup?.volumeBackupId,
|
|
});
|
|
return newVolumeBackup;
|
|
}),
|
|
one: protectedProcedure
|
|
.input(
|
|
z.object({
|
|
volumeBackupId: z.string().min(1),
|
|
}),
|
|
)
|
|
.query(async ({ input, ctx }) => {
|
|
const vb = await findVolumeBackupById(input.volumeBackupId);
|
|
const serviceId =
|
|
vb.applicationId ||
|
|
vb.postgresId ||
|
|
vb.mysqlId ||
|
|
vb.mariadbId ||
|
|
vb.mongoId ||
|
|
vb.redisId ||
|
|
vb.libsqlId ||
|
|
vb.composeId;
|
|
if (serviceId) {
|
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
|
volumeBackup: ["read"],
|
|
});
|
|
}
|
|
return vb;
|
|
}),
|
|
delete: protectedProcedure
|
|
.input(
|
|
z.object({
|
|
volumeBackupId: z.string().min(1),
|
|
}),
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const vb = await findVolumeBackupById(input.volumeBackupId);
|
|
const serviceId =
|
|
vb.applicationId ||
|
|
vb.postgresId ||
|
|
vb.mysqlId ||
|
|
vb.mariadbId ||
|
|
vb.mongoId ||
|
|
vb.redisId ||
|
|
vb.libsqlId ||
|
|
vb.composeId;
|
|
if (serviceId) {
|
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
|
volumeBackup: ["delete"],
|
|
});
|
|
}
|
|
const result = await removeVolumeBackup(input.volumeBackupId);
|
|
await audit(ctx, {
|
|
action: "delete",
|
|
resourceType: "volumeBackup",
|
|
resourceId: input.volumeBackupId,
|
|
});
|
|
return result;
|
|
}),
|
|
update: protectedProcedure
|
|
.input(updateVolumeBackupSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const existingVb = await findVolumeBackupById(input.volumeBackupId);
|
|
const serviceId =
|
|
existingVb.applicationId ||
|
|
existingVb.postgresId ||
|
|
existingVb.mysqlId ||
|
|
existingVb.mariadbId ||
|
|
existingVb.mongoId ||
|
|
existingVb.redisId ||
|
|
existingVb.libsqlId ||
|
|
existingVb.composeId;
|
|
if (serviceId) {
|
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
|
volumeBackup: ["update"],
|
|
});
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "volumeBackup",
|
|
resourceId: updatedVolumeBackup.volumeBackupId,
|
|
});
|
|
return updatedVolumeBackup;
|
|
}),
|
|
|
|
runManually: protectedProcedure
|
|
.input(z.object({ volumeBackupId: z.string().min(1) }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const vb = await findVolumeBackupById(input.volumeBackupId);
|
|
const serviceId =
|
|
vb.applicationId ||
|
|
vb.postgresId ||
|
|
vb.mysqlId ||
|
|
vb.mariadbId ||
|
|
vb.mongoId ||
|
|
vb.redisId ||
|
|
vb.libsqlId ||
|
|
vb.composeId;
|
|
if (serviceId) {
|
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
|
volumeBackup: ["create"],
|
|
});
|
|
}
|
|
try {
|
|
const result = await runVolumeBackup(input.volumeBackupId);
|
|
await audit(ctx, {
|
|
action: "run",
|
|
resourceType: "volumeBackup",
|
|
resourceId: input.volumeBackupId,
|
|
});
|
|
return result;
|
|
} catch (error) {
|
|
console.error(error);
|
|
return false;
|
|
}
|
|
}),
|
|
restoreVolumeBackupWithLogs: withPermission("volumeBackup", "restore")
|
|
.meta({
|
|
openapi: {
|
|
enabled: false,
|
|
path: "/restore-volume-backup-with-logs",
|
|
method: "POST",
|
|
override: true,
|
|
},
|
|
})
|
|
.input(
|
|
z.object({
|
|
backupFileName: z.string().min(1),
|
|
destinationId: z.string().min(1),
|
|
volumeName: z.string().min(1),
|
|
id: z.string().min(1),
|
|
serviceType: z.enum(["application", "compose"]),
|
|
serverId: z.string().optional(),
|
|
}),
|
|
)
|
|
.subscription(async ({ input, ctx }) => {
|
|
const destination = await findDestinationById(input.destinationId);
|
|
if (destination.organizationId !== ctx.session.activeOrganizationId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "You don't have access to this destination.",
|
|
});
|
|
}
|
|
if (input.serverId) {
|
|
const targetServer = await findServerById(input.serverId);
|
|
if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "You don't have access to this server.",
|
|
});
|
|
}
|
|
}
|
|
return observable<string>((emit) => {
|
|
const runRestore = async () => {
|
|
try {
|
|
emit.next("🚀 Starting volume restore process...");
|
|
emit.next(`📂 Backup File: ${input.backupFileName}`);
|
|
emit.next(`🔧 Volume Name: ${input.volumeName}`);
|
|
emit.next(`🏷️ Service Type: ${input.serviceType}`);
|
|
emit.next(""); // Empty line for better readability
|
|
|
|
// Generate the restore command
|
|
const restoreCommand = await restoreVolume(
|
|
input.id,
|
|
input.destinationId,
|
|
input.volumeName,
|
|
input.backupFileName,
|
|
input.serverId || "",
|
|
input.serviceType,
|
|
);
|
|
|
|
emit.next("📋 Generated restore command:");
|
|
emit.next("▶️ Executing restore...");
|
|
emit.next(""); // Empty line
|
|
|
|
// Execute the restore command with real-time output
|
|
if (input.serverId) {
|
|
emit.next(`🌐 Executing on remote server: ${input.serverId}`);
|
|
await execAsyncRemote(input.serverId, restoreCommand, (data) => {
|
|
emit.next(data);
|
|
});
|
|
} else {
|
|
emit.next("🖥️ Executing on local server");
|
|
await execAsyncStream(restoreCommand, (data) => {
|
|
emit.next(data);
|
|
});
|
|
}
|
|
|
|
emit.next("");
|
|
emit.next("✅ Volume restore completed successfully!");
|
|
emit.next(
|
|
"🎉 All containers/services have been restarted with the restored volume.",
|
|
);
|
|
} catch {
|
|
emit.next("");
|
|
emit.next("❌ Volume restore failed!");
|
|
} finally {
|
|
emit.complete();
|
|
}
|
|
};
|
|
|
|
// Start the restore process
|
|
runRestore();
|
|
});
|
|
}),
|
|
});
|