mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-26 09:35:29 +02:00
Merge branch 'canary' into fix/openapi-bigint-serialization
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import {
|
||||
getWebServerSettings,
|
||||
@@ -12,8 +14,6 @@ export const startLogCleanup = async (
|
||||
cronExpression = "0 0 * * *",
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
||||
|
||||
const existingJob = scheduledJobs[LOG_CLEANUP_JOB_NAME];
|
||||
if (existingJob) {
|
||||
existingJob.cancel();
|
||||
@@ -21,10 +21,17 @@ export const startLogCleanup = async (
|
||||
|
||||
scheduleJob(LOG_CLEANUP_JOB_NAME, cronExpression, async () => {
|
||||
try {
|
||||
await execAsync(
|
||||
`tail -n 1000 ${DYNAMIC_TRAEFIK_PATH}/access.log > ${DYNAMIC_TRAEFIK_PATH}/access.log.tmp && mv ${DYNAMIC_TRAEFIK_PATH}/access.log.tmp ${DYNAMIC_TRAEFIK_PATH}/access.log`,
|
||||
);
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
||||
const accessLogPath = path.join(DYNAMIC_TRAEFIK_PATH, "access.log");
|
||||
|
||||
if (!fs.existsSync(accessLogPath)) {
|
||||
console.error("Access log file does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
await execAsync(
|
||||
`tail -n 1000 ${accessLogPath} > ${accessLogPath}.tmp && mv ${accessLogPath}.tmp ${accessLogPath}`,
|
||||
);
|
||||
await execAsync("docker exec dokploy-traefik kill -USR1 1");
|
||||
} catch (error) {
|
||||
console.error("Error during log cleanup:", error);
|
||||
|
||||
@@ -30,6 +30,18 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
|
||||
baseURL: config.apiUrl,
|
||||
});
|
||||
case "azure":
|
||||
// Azure OpenAI-compatible endpoints already include /v1 in the path.
|
||||
// Using createAzure with such URLs causes a doubled /v1//v1/ suffix.
|
||||
if (config.apiUrl.includes("/v1")) {
|
||||
return createOpenAICompatible({
|
||||
name: "azure",
|
||||
baseURL: config.apiUrl,
|
||||
headers: {
|
||||
"api-key": config.apiKey,
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
return createAzure({
|
||||
apiKey: config.apiKey,
|
||||
baseURL: config.apiUrl,
|
||||
|
||||
@@ -8,19 +8,25 @@ import { findEnvironmentById } from "@dokploy/server/services/environment";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
import {
|
||||
getBackupCommand,
|
||||
getBackupTimestamp,
|
||||
getS3Credentials,
|
||||
normalizeS3Path,
|
||||
} from "./utils";
|
||||
|
||||
export const runComposeBackup = async (
|
||||
compose: Compose,
|
||||
backup: BackupSchedule,
|
||||
) => {
|
||||
const { environmentId, name } = compose;
|
||||
const { environmentId, name, appName } = compose;
|
||||
const environment = await findEnvironmentById(environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
const { prefix, databaseType } = backup;
|
||||
const { prefix, databaseType, serviceName } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const backupFileName = `${getBackupTimestamp()}.${databaseType === "mongo" ? "bson" : "sql"}.gz`;
|
||||
const s3AppName = serviceName ? `${appName}_${serviceName}` : appName;
|
||||
const bucketDestination = `${s3AppName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const deployment = await createDeploymentBackup({
|
||||
backupId: backup.backupId,
|
||||
title: "Compose Backup",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import path from "node:path";
|
||||
import { CLEANUP_CRON_JOB } from "@dokploy/server/constants";
|
||||
import { member } from "@dokploy/server/db/schema";
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
@@ -11,7 +10,7 @@ import { startLogCleanup } from "../access-log/handler";
|
||||
import { cleanupAll } from "../docker/utils";
|
||||
import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getS3Credentials, scheduleBackup } from "./utils";
|
||||
import { getS3Credentials, normalizeS3Path, scheduleBackup } from "./utils";
|
||||
|
||||
export const initCronJobs = async () => {
|
||||
console.log("Setting up cron jobs....");
|
||||
@@ -30,15 +29,19 @@ export const initCronJobs = async () => {
|
||||
const webServerSettings = await getWebServerSettings();
|
||||
|
||||
if (webServerSettings?.enableDockerCleanup) {
|
||||
scheduleJob("docker-cleanup", CLEANUP_CRON_JOB, async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
|
||||
);
|
||||
try {
|
||||
scheduleJob("docker-cleanup", CLEANUP_CRON_JOB, async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
|
||||
);
|
||||
|
||||
await cleanupAll();
|
||||
await cleanupAll();
|
||||
|
||||
await sendDockerCleanupNotifications(admin.user.id);
|
||||
});
|
||||
await sendDockerCleanupNotifications(admin.user.id);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[Backup] Docker Cleanup Error", error);
|
||||
}
|
||||
}
|
||||
|
||||
const servers = await getAllServers();
|
||||
@@ -46,18 +49,22 @@ export const initCronJobs = async () => {
|
||||
for (const server of servers) {
|
||||
const { serverId, enableDockerCleanup, name } = server;
|
||||
if (enableDockerCleanup) {
|
||||
scheduleJob(serverId, CLEANUP_CRON_JOB, async () => {
|
||||
console.log(
|
||||
`SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${name}`,
|
||||
);
|
||||
try {
|
||||
scheduleJob(serverId, CLEANUP_CRON_JOB, async () => {
|
||||
console.log(
|
||||
`SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${name}`,
|
||||
);
|
||||
|
||||
await cleanupAll(serverId);
|
||||
await cleanupAll(serverId);
|
||||
|
||||
await sendDockerCleanupNotifications(
|
||||
admin.user.id,
|
||||
`Docker cleanup for Server ${name} (${serverId})`,
|
||||
);
|
||||
});
|
||||
await sendDockerCleanupNotifications(
|
||||
admin.user.id,
|
||||
`Docker cleanup for Server ${name} (${serverId})`,
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[Backup] ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +75,7 @@ export const initCronJobs = async () => {
|
||||
mariadb: true,
|
||||
mysql: true,
|
||||
mongo: true,
|
||||
libsql: true,
|
||||
user: true,
|
||||
compose: true,
|
||||
},
|
||||
@@ -87,14 +95,33 @@ export const initCronJobs = async () => {
|
||||
}
|
||||
|
||||
if (webServerSettings?.logCleanupCron) {
|
||||
console.log(
|
||||
"Starting log requests cleanup",
|
||||
webServerSettings.logCleanupCron,
|
||||
);
|
||||
await startLogCleanup(webServerSettings.logCleanupCron);
|
||||
try {
|
||||
console.log(
|
||||
"Starting log requests cleanup",
|
||||
webServerSettings.logCleanupCron,
|
||||
);
|
||||
await startLogCleanup(webServerSettings.logCleanupCron);
|
||||
} catch (error) {
|
||||
console.error("[Backup] Log Cleanup Error", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getServiceAppName = (backup: BackupSchedule): string => {
|
||||
if (backup.compose?.appName) {
|
||||
return backup.serviceName
|
||||
? `${backup.compose.appName}_${backup.serviceName}`
|
||||
: backup.compose.appName;
|
||||
}
|
||||
const serviceAppName =
|
||||
backup.postgres?.appName ||
|
||||
backup.mysql?.appName ||
|
||||
backup.mariadb?.appName ||
|
||||
backup.mongo?.appName ||
|
||||
backup.libsql?.appName;
|
||||
return serviceAppName || backup.appName;
|
||||
};
|
||||
|
||||
export const keepLatestNBackups = async (
|
||||
backup: BackupSchedule,
|
||||
serverId?: string | null,
|
||||
@@ -105,18 +132,16 @@ export const keepLatestNBackups = async (
|
||||
|
||||
try {
|
||||
const rcloneFlags = getS3Credentials(backup.destination);
|
||||
const backupFilesPath = path.join(
|
||||
`:s3:${backup.destination.bucket}`,
|
||||
backup.prefix,
|
||||
);
|
||||
const appName = getServiceAppName(backup);
|
||||
const backupFilesPath = `:s3:${backup.destination.bucket}/${appName}/${normalizeS3Path(backup.prefix)}`;
|
||||
|
||||
// --include "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone
|
||||
const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".sql.gz"}" ${backupFilesPath}`;
|
||||
// --include "*.bson.gz" or "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone
|
||||
const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".{sql.gz,bson.gz}"}" ${backupFilesPath}`;
|
||||
// when we pipe the above command with this one, we only get the list of files we want to delete
|
||||
const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${backup.keepLatestCount}+1)) | xargs -I{}`;
|
||||
// this command deletes the files
|
||||
// to test the deletion before actually deleting we can add --dry-run before ${backupFilesPath}/{}
|
||||
const rcloneDelete = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}/{}`;
|
||||
// to test the deletion before actually deleting we can add --dry-run before ${backupFilesPath}{}
|
||||
const rcloneDelete = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}{}`;
|
||||
|
||||
const rcloneCommand = `${rcloneList} | ${sortAndPickUnwantedBackups} ${rcloneDelete}`;
|
||||
|
||||
|
||||
80
packages/server/src/utils/backups/libsql.ts
Normal file
80
packages/server/src/utils/backups/libsql.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import { findEnvironmentById } from "@dokploy/server/services/environment";
|
||||
import type { Libsql } from "@dokploy/server/services/libsql";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import {
|
||||
getBackupCommand,
|
||||
getBackupTimestamp,
|
||||
getS3Credentials,
|
||||
normalizeS3Path,
|
||||
} from "./utils";
|
||||
|
||||
export const runLibsqlBackup = async (
|
||||
libsql: Libsql,
|
||||
backup: BackupSchedule,
|
||||
) => {
|
||||
const { name, environmentId, appName } = libsql;
|
||||
const environment = await findEnvironmentById(environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
const deployment = await createDeploymentBackup({
|
||||
backupId: backup.backupId,
|
||||
title: "Initializing Backup",
|
||||
description: "Initializing Backup",
|
||||
});
|
||||
const { prefix } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${getBackupTimestamp()}.sql.gz`;
|
||||
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
try {
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||
|
||||
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
|
||||
const backupCommand = getBackupCommand(
|
||||
backup,
|
||||
rcloneCommand,
|
||||
deployment.logPath,
|
||||
);
|
||||
if (libsql.serverId) {
|
||||
await execAsyncRemote(libsql.serverId, backupCommand);
|
||||
} else {
|
||||
await execAsync(backupCommand, {
|
||||
shell: "/bin/bash",
|
||||
});
|
||||
}
|
||||
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "libsql",
|
||||
type: "success",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
} catch (error) {
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "libsql",
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -8,19 +8,24 @@ import type { Mariadb } from "@dokploy/server/services/mariadb";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
import {
|
||||
getBackupCommand,
|
||||
getBackupTimestamp,
|
||||
getS3Credentials,
|
||||
normalizeS3Path,
|
||||
} from "./utils";
|
||||
|
||||
export const runMariadbBackup = async (
|
||||
mariadb: Mariadb,
|
||||
backup: BackupSchedule,
|
||||
) => {
|
||||
const { environmentId, name } = mariadb;
|
||||
const { environmentId, name, appName } = mariadb;
|
||||
const environment = await findEnvironmentById(environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
const { prefix } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const backupFileName = `${getBackupTimestamp()}.sql.gz`;
|
||||
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const deployment = await createDeploymentBackup({
|
||||
backupId: backup.backupId,
|
||||
title: "MariaDB Backup",
|
||||
|
||||
@@ -8,16 +8,21 @@ import type { Mongo } from "@dokploy/server/services/mongo";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
import {
|
||||
getBackupCommand,
|
||||
getBackupTimestamp,
|
||||
getS3Credentials,
|
||||
normalizeS3Path,
|
||||
} from "./utils";
|
||||
|
||||
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
const { environmentId, name } = mongo;
|
||||
const { environmentId, name, appName } = mongo;
|
||||
const environment = await findEnvironmentById(environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
const { prefix } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const backupFileName = `${getBackupTimestamp()}.bson.gz`;
|
||||
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const deployment = await createDeploymentBackup({
|
||||
backupId: backup.backupId,
|
||||
title: "MongoDB Backup",
|
||||
|
||||
@@ -8,16 +8,21 @@ import type { MySql } from "@dokploy/server/services/mysql";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
import {
|
||||
getBackupCommand,
|
||||
getBackupTimestamp,
|
||||
getS3Credentials,
|
||||
normalizeS3Path,
|
||||
} from "./utils";
|
||||
|
||||
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||
const { environmentId, name } = mysql;
|
||||
const { environmentId, name, appName } = mysql;
|
||||
const environment = await findEnvironmentById(environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
const { prefix } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const backupFileName = `${getBackupTimestamp()}.sql.gz`;
|
||||
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const deployment = await createDeploymentBackup({
|
||||
backupId: backup.backupId,
|
||||
title: "MySQL Backup",
|
||||
|
||||
@@ -8,13 +8,18 @@ import type { Postgres } from "@dokploy/server/services/postgres";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
import {
|
||||
getBackupCommand,
|
||||
getBackupTimestamp,
|
||||
getS3Credentials,
|
||||
normalizeS3Path,
|
||||
} from "./utils";
|
||||
|
||||
export const runPostgresBackup = async (
|
||||
postgres: Postgres,
|
||||
backup: BackupSchedule,
|
||||
) => {
|
||||
const { name, environmentId } = postgres;
|
||||
const { name, environmentId, appName } = postgres;
|
||||
const environment = await findEnvironmentById(environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
@@ -25,8 +30,8 @@ export const runPostgresBackup = async (
|
||||
});
|
||||
const { prefix } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const backupFileName = `${getBackupTimestamp()}.sql.gz`;
|
||||
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
try {
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Destination } from "@dokploy/server/services/destination";
|
||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||
import { keepLatestNBackups } from ".";
|
||||
import { runComposeBackup } from "./compose";
|
||||
import { runLibsqlBackup } from "./libsql";
|
||||
import { runMariadbBackup } from "./mariadb";
|
||||
import { runMongoBackup } from "./mongo";
|
||||
import { runMySqlBackup } from "./mysql";
|
||||
@@ -19,6 +20,7 @@ export const scheduleBackup = (backup: BackupSchedule) => {
|
||||
mysql,
|
||||
mongo,
|
||||
mariadb,
|
||||
libsql,
|
||||
compose,
|
||||
} = backup;
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
@@ -35,6 +37,9 @@ export const scheduleBackup = (backup: BackupSchedule) => {
|
||||
} else if (databaseType === "mariadb" && mariadb) {
|
||||
await runMariadbBackup(mariadb, backup);
|
||||
await keepLatestNBackups(backup, mariadb.serverId);
|
||||
} else if (databaseType === "libsql" && libsql) {
|
||||
await runLibsqlBackup(libsql, backup);
|
||||
await keepLatestNBackups(backup, libsql.serverId);
|
||||
} else if (databaseType === "web-server") {
|
||||
await runWebServerBackup(backup);
|
||||
await keepLatestNBackups(backup);
|
||||
@@ -51,6 +56,9 @@ export const removeScheduleBackup = (backupId: string) => {
|
||||
currentJob?.cancel();
|
||||
};
|
||||
|
||||
export const getBackupTimestamp = () =>
|
||||
new Date().toISOString().replace(/[:.]/g, "-");
|
||||
|
||||
export const normalizeS3Path = (prefix: string) => {
|
||||
// Trim whitespace and remove leading/trailing slashes
|
||||
const normalizedPrefix = prefix.trim().replace(/^\/+|\/+$/g, "");
|
||||
@@ -74,6 +82,10 @@ export const getS3Credentials = (destination: Destination) => {
|
||||
rcloneFlags.unshift(`--s3-provider="${provider}"`);
|
||||
}
|
||||
|
||||
if (destination.additionalFlags?.length) {
|
||||
rcloneFlags.push(...destination.additionalFlags);
|
||||
}
|
||||
|
||||
return rcloneFlags;
|
||||
};
|
||||
|
||||
@@ -107,6 +119,10 @@ export const getMongoBackupCommand = (
|
||||
return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --archive --authenticationDatabase admin --gzip"`;
|
||||
};
|
||||
|
||||
export const getLibsqlBackupCommand = (database: string) => {
|
||||
return `docker exec -i $CONTAINER_ID sh -c "tar cf - -C /var/lib/sqld ${database} | gzip"`;
|
||||
};
|
||||
|
||||
export const getServiceContainerCommand = (appName: string) => {
|
||||
return `docker ps -q --filter "status=running" --filter "label=com.docker.swarm.service.name=${appName}" | head -n 1`;
|
||||
};
|
||||
@@ -123,12 +139,24 @@ export const getComposeContainerCommand = (
|
||||
};
|
||||
|
||||
const getContainerSearchCommand = (backup: BackupSchedule) => {
|
||||
const { backupType, postgres, mysql, mariadb, mongo, compose, serviceName } =
|
||||
backup;
|
||||
const {
|
||||
backupType,
|
||||
postgres,
|
||||
mysql,
|
||||
mariadb,
|
||||
mongo,
|
||||
libsql,
|
||||
compose,
|
||||
serviceName,
|
||||
} = backup;
|
||||
|
||||
if (backupType === "database") {
|
||||
const appName =
|
||||
postgres?.appName || mysql?.appName || mariadb?.appName || mongo?.appName;
|
||||
postgres?.appName ||
|
||||
mysql?.appName ||
|
||||
mariadb?.appName ||
|
||||
mongo?.appName ||
|
||||
libsql?.appName;
|
||||
return getServiceContainerCommand(appName || "");
|
||||
}
|
||||
if (backupType === "compose") {
|
||||
@@ -209,6 +237,12 @@ export const generateBackupCommand = (backup: BackupSchedule) => {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "libsql": {
|
||||
if (backupType === "database") {
|
||||
return getLibsqlBackupCommand(backup.database);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Database type not supported: ${databaseType}`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { mkdtemp, rm, stat } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
||||
@@ -9,8 +9,18 @@ import {
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||
import { sendDokployBackupNotifications } from "../notifications/dokploy-backup";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { getS3Credentials, normalizeS3Path } from "./utils";
|
||||
import { getBackupTimestamp, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
|
||||
function formatBytes(bytes?: number) {
|
||||
if (bytes === undefined) return "Unknown size";
|
||||
if (bytes === 0) return "0 B";
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
const value = bytes / Math.pow(1024, i);
|
||||
return `${value.toFixed(2)} ${sizes[i]} (${bytes} bytes)`;
|
||||
}
|
||||
|
||||
export const runWebServerBackup = async (backup: BackupSchedule) => {
|
||||
if (IS_CLOUD) {
|
||||
@@ -23,15 +33,15 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
||||
description: "Web Server Backup",
|
||||
});
|
||||
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
|
||||
|
||||
let computedBackupSize: number | undefined;
|
||||
try {
|
||||
const destination = await findDestinationById(backup.destinationId);
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const timestamp = getBackupTimestamp();
|
||||
const { BASE_PATH } = paths();
|
||||
const tempDir = await mkdtemp(join(tmpdir(), "dokploy-backup-"));
|
||||
const backupFileName = `webserver-backup-${timestamp}.zip`;
|
||||
const s3Path = `:s3:${destination.bucket}/${normalizeS3Path(backup.prefix)}${backupFileName}`;
|
||||
const s3Path = `:s3:${destination.bucket}/${backup.appName}/${normalizeS3Path(backup.prefix)}${backupFileName}`;
|
||||
|
||||
try {
|
||||
await execAsync(`mkdir -p ${tempDir}/filesystem`);
|
||||
@@ -67,7 +77,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
||||
await execAsync(cleanupCommand);
|
||||
|
||||
await execAsync(
|
||||
`rsync -a --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
|
||||
`rsync -a --ignore-errors --no-specials --no-devices --exclude='volume-backups/' ${BASE_PATH}/ ${tempDir}/filesystem/`,
|
||||
);
|
||||
|
||||
writeStream.write("Copied filesystem to temp directory\n");
|
||||
@@ -79,11 +89,24 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
||||
|
||||
writeStream.write("Zipped database and filesystem\n");
|
||||
|
||||
const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`;
|
||||
const zipPath = join(tempDir, backupFileName);
|
||||
try {
|
||||
const { size } = await stat(zipPath);
|
||||
computedBackupSize = size;
|
||||
writeStream.write(`Backup size: ${size} bytes\n`);
|
||||
} catch {
|
||||
// If stat fails, keep undefined
|
||||
}
|
||||
|
||||
const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${zipPath}" "${s3Path}"`;
|
||||
writeStream.write("Running command to upload backup to S3\n");
|
||||
await execAsync(uploadCommand);
|
||||
writeStream.write("Uploaded backup to S3 ✅\n");
|
||||
writeStream.end();
|
||||
await sendDokployBackupNotifications({
|
||||
type: "success",
|
||||
backupSize: formatBytes(computedBackupSize),
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
return true;
|
||||
} finally {
|
||||
@@ -100,6 +123,12 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
||||
error instanceof Error ? error.message : "Unknown error\n",
|
||||
);
|
||||
writeStream.end();
|
||||
await sendDokployBackupNotifications({
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
backupSize: formatBytes(computedBackupSize),
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { quote } from "shell-quote";
|
||||
import { writeDomainsToCompose } from "../docker/domain";
|
||||
import {
|
||||
encodeBase64,
|
||||
getEnviromentVariablesObject,
|
||||
getEnvironmentVariablesObject,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
|
||||
@@ -46,17 +46,17 @@ Compose Type: ${composeType} ✅`;
|
||||
set -e
|
||||
{
|
||||
echo "${logBox}";
|
||||
|
||||
|
||||
${newCompose}
|
||||
|
||||
|
||||
${envCommand}
|
||||
|
||||
|
||||
cd "${projectPath}";
|
||||
|
||||
${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""}
|
||||
${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create ${compose.composeType === "stack" ? "--driver overlay" : ""} --attachable ${compose.appName}` : ""}
|
||||
env -i PATH="$PATH" ${exportEnvCommand} docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; }
|
||||
${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""}
|
||||
|
||||
|
||||
echo "Docker Compose Deployed: ✅";
|
||||
} || {
|
||||
echo "Error: ❌ Script execution failed";
|
||||
@@ -131,7 +131,7 @@ echo "${encodedContent}" | base64 -d > "${envFilePath}";
|
||||
const getExportEnvCommand = (compose: ComposeNested) => {
|
||||
if (compose.composeType !== "stack") return "";
|
||||
|
||||
const envVars = getEnviromentVariablesObject(
|
||||
const envVars = getEnvironmentVariablesObject(
|
||||
compose.env,
|
||||
compose.environment.project.env,
|
||||
compose.environment.env,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
getEnviromentVariablesObject,
|
||||
getEnvironmentVariablesObject,
|
||||
prepareEnvironmentVariablesForShell,
|
||||
} from "@dokploy/server/utils/docker/utils";
|
||||
import { quote } from "shell-quote";
|
||||
@@ -52,7 +52,7 @@ export const getDockerCommand = (application: ApplicationNested) => {
|
||||
commandArgs.push("--build-arg", arg);
|
||||
}
|
||||
|
||||
const secrets = getEnviromentVariablesObject(
|
||||
const secrets = getEnvironmentVariablesObject(
|
||||
buildSecrets,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
@@ -86,12 +86,12 @@ export const getDockerCommand = (application: ApplicationNested) => {
|
||||
|
||||
command += `
|
||||
echo "Building ${appName}" ;
|
||||
cd ${dockerContextPath} || {
|
||||
cd ${dockerContextPath} || {
|
||||
echo "❌ The path ${dockerContextPath} does not exist" ;
|
||||
exit 1;
|
||||
}
|
||||
|
||||
${joinedSecrets} docker ${commandArgs.join(" ")} || {
|
||||
${joinedSecrets} docker ${commandArgs.join(" ")} || {
|
||||
echo "❌ Docker build failed" ;
|
||||
exit 1;
|
||||
}
|
||||
|
||||
@@ -182,7 +182,11 @@ export const mechanizeDockerContainer = async (
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await docker.createService(settings);
|
||||
if (authConfig) {
|
||||
await docker.createService(authConfig, settings);
|
||||
} else {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import { db } from "../../db/index";
|
||||
import { user as userSchema } from "../../db/schema/user";
|
||||
|
||||
export const LICENSE_KEY_URL =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:4002"
|
||||
: "https://licenses-api.dokploy.com";
|
||||
// process.env.NODE_ENV === "development"
|
||||
// ? "http://localhost:4002"
|
||||
"https://licenses-api.dokploy.com";
|
||||
|
||||
export const initEnterpriseBackupCronJobs = async () => {
|
||||
scheduleJob("enterprise-check", "0 0 */3 * *", async () => {
|
||||
|
||||
155
packages/server/src/utils/databases/libsql.ts
Normal file
155
packages/server/src/utils/databases/libsql.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import type { CreateServiceOptions, PortConfig } from "dockerode";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
generateConfigContainer,
|
||||
generateFileMounts,
|
||||
generateVolumeMounts,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
export type LibsqlNested = InferResultType<
|
||||
"libsql",
|
||||
{
|
||||
mounts: true;
|
||||
environment: { with: { project: true } };
|
||||
}
|
||||
>;
|
||||
export const buildLibsql = async (libsql: LibsqlNested) => {
|
||||
const {
|
||||
appName,
|
||||
env,
|
||||
externalPort,
|
||||
externalGRPCPort,
|
||||
externalAdminPort,
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
databaseUser,
|
||||
databasePassword,
|
||||
sqldNode,
|
||||
sqldPrimaryUrl,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
command,
|
||||
mounts,
|
||||
enableNamespaces,
|
||||
} = libsql;
|
||||
|
||||
const basicAuth = Buffer.from(
|
||||
`${databaseUser}:${databasePassword}`,
|
||||
"utf-8",
|
||||
).toString("base64");
|
||||
|
||||
const defaultLibsqlEnv = `SQLD_NODE="${sqldNode}"\nSQLD_HTTP_AUTH="basic:${basicAuth}"${
|
||||
env ? `\n${env}` : ""
|
||||
}${sqldNode === "replica" ? `\nSQLD_PRIMARY_URL="${sqldPrimaryUrl}"` : ""}`;
|
||||
|
||||
const {
|
||||
HealthCheck,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Labels,
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
UpdateConfig,
|
||||
Networks,
|
||||
} = generateConfigContainer(libsql);
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
defaultLibsqlEnv,
|
||||
libsql.environment.project.env,
|
||||
libsql.environment.env,
|
||||
);
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, libsql);
|
||||
|
||||
const docker = await getRemoteDocker(libsql.serverId);
|
||||
|
||||
let finalCommand =
|
||||
command ??
|
||||
"sqld --db-path iku.db --http-listen-addr 0.0.0.0:8080 --grpc-listen-addr 0.0.0.0:5001 --admin-listen-addr 0.0.0.0:5000";
|
||||
if (enableNamespaces) {
|
||||
finalCommand += " --enable-namespaces";
|
||||
}
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
HealthCheck,
|
||||
Image: "ghcr.io/tursodatabase/libsql-server:v0.24.32",
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(finalCommand
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", finalCommand],
|
||||
}
|
||||
: {}),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
},
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
EndpointSpec: {
|
||||
Mode: "dnsrr",
|
||||
Ports: [
|
||||
...(externalPort
|
||||
? [
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 8080,
|
||||
PublishedPort: externalPort,
|
||||
PublishMode: "host",
|
||||
} as PortConfig,
|
||||
]
|
||||
: []),
|
||||
...(externalGRPCPort
|
||||
? [
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 5001,
|
||||
PublishedPort: externalGRPCPort,
|
||||
PublishMode: "host",
|
||||
} as PortConfig,
|
||||
]
|
||||
: []),
|
||||
...(externalAdminPort
|
||||
? [
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 5000,
|
||||
PublishedPort: externalAdminPort,
|
||||
PublishMode: "host",
|
||||
} as PortConfig,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
UpdateConfig,
|
||||
};
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
const inspect = await service.inspect();
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
@@ -74,8 +74,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
|
||||
Image: dockerImage,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(StopGracePeriod !== null &&
|
||||
StopGracePeriod !== undefined && { StopGracePeriod }),
|
||||
StopGracePeriod: StopGracePeriod ?? 30_000_000_000,
|
||||
...(command && {
|
||||
Command: command.split(" "),
|
||||
}),
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
libsql,
|
||||
mariadb,
|
||||
mongo,
|
||||
mysql,
|
||||
postgres,
|
||||
redis,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { deployLibsql } from "@dokploy/server/services/libsql";
|
||||
import { deployMariadb } from "@dokploy/server/services/mariadb";
|
||||
import { deployMongo } from "@dokploy/server/services/mongo";
|
||||
import { deployMySql } from "@dokploy/server/services/mysql";
|
||||
@@ -15,7 +17,13 @@ import { eq } from "drizzle-orm";
|
||||
import { removeService } from "../docker/utils";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
|
||||
type DatabaseType = "postgres" | "mysql" | "mariadb" | "mongo" | "redis";
|
||||
type DatabaseType =
|
||||
| "libsql"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "postgres"
|
||||
| "redis";
|
||||
|
||||
export const rebuildDatabase = async (
|
||||
databaseId: string,
|
||||
@@ -41,31 +49,25 @@ export const rebuildDatabase = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "postgres") {
|
||||
await deployPostgres(databaseId);
|
||||
} else if (type === "mysql") {
|
||||
await deployMySql(databaseId);
|
||||
if (type === "libsql") {
|
||||
await deployLibsql(databaseId);
|
||||
} else if (type === "mariadb") {
|
||||
await deployMariadb(databaseId);
|
||||
} else if (type === "mongo") {
|
||||
await deployMongo(databaseId);
|
||||
} else if (type === "mysql") {
|
||||
await deployMySql(databaseId);
|
||||
} else if (type === "postgres") {
|
||||
await deployPostgres(databaseId);
|
||||
} else if (type === "redis") {
|
||||
await deployRedis(databaseId);
|
||||
}
|
||||
};
|
||||
|
||||
const findDatabaseById = async (databaseId: string, type: DatabaseType) => {
|
||||
if (type === "postgres") {
|
||||
return await db.query.postgres.findFirst({
|
||||
where: eq(postgres.postgresId, databaseId),
|
||||
with: {
|
||||
mounts: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (type === "mysql") {
|
||||
return await db.query.mysql.findFirst({
|
||||
where: eq(mysql.mysqlId, databaseId),
|
||||
if (type === "libsql") {
|
||||
return await db.query.libsql.findFirst({
|
||||
where: eq(libsql.libsqlId, databaseId),
|
||||
with: {
|
||||
mounts: true,
|
||||
},
|
||||
@@ -87,6 +89,22 @@ const findDatabaseById = async (databaseId: string, type: DatabaseType) => {
|
||||
},
|
||||
});
|
||||
}
|
||||
if (type === "mysql") {
|
||||
return await db.query.mysql.findFirst({
|
||||
where: eq(mysql.mysqlId, databaseId),
|
||||
with: {
|
||||
mounts: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (type === "postgres") {
|
||||
return await db.query.postgres.findFirst({
|
||||
where: eq(postgres.postgresId, databaseId),
|
||||
with: {
|
||||
mounts: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (type === "redis") {
|
||||
return await db.query.redis.findFirst({
|
||||
where: eq(redis.redisId, databaseId),
|
||||
|
||||
@@ -18,7 +18,9 @@ export const randomizeComposeFile = async (
|
||||
) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
const composeFile = compose.composeFile;
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
const composeData = parse(composeFile, {
|
||||
maxAliasCount: 10000,
|
||||
}) as ComposeSpecification;
|
||||
|
||||
const randomSuffix = suffix || generateRandomHash();
|
||||
|
||||
|
||||
@@ -63,7 +63,9 @@ export const loadDockerCompose = async (
|
||||
|
||||
if (existsSync(path)) {
|
||||
const yamlStr = readFileSync(path, "utf8");
|
||||
const parsedConfig = parse(yamlStr) as ComposeSpecification;
|
||||
const parsedConfig = parse(yamlStr, {
|
||||
maxAliasCount: 10000,
|
||||
}) as ComposeSpecification;
|
||||
return parsedConfig;
|
||||
}
|
||||
return null;
|
||||
@@ -86,7 +88,9 @@ export const loadDockerComposeRemote = async (
|
||||
return null;
|
||||
}
|
||||
if (!stdout) return null;
|
||||
const parsedConfig = parse(stdout) as ComposeSpecification;
|
||||
const parsedConfig = parse(stdout, {
|
||||
maxAliasCount: 10000,
|
||||
}) as ComposeSpecification;
|
||||
return parsedConfig;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -106,10 +110,6 @@ export const writeDomainsToCompose = async (
|
||||
compose: Compose,
|
||||
domains: Domain[],
|
||||
) => {
|
||||
if (!domains.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
const composeConverted = await addDomainToCompose(compose, domains);
|
||||
const path = getComposePath(compose);
|
||||
@@ -145,7 +145,7 @@ export const addDomainToCompose = async (
|
||||
result = await loadDockerCompose(compose);
|
||||
}
|
||||
|
||||
if (!result || domains.length === 0) {
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -172,8 +172,12 @@ export const addDomainToCompose = async (
|
||||
);
|
||||
}
|
||||
|
||||
const httpLabels = createDomainLabels(appName, domain, "web");
|
||||
if (https) {
|
||||
const httpLabels = createDomainLabels(
|
||||
appName,
|
||||
domain,
|
||||
domain.customEntrypoint || "web",
|
||||
);
|
||||
if (!domain.customEntrypoint && https) {
|
||||
const httpsLabels = createDomainLabels(appName, domain, "websecure");
|
||||
httpLabels.push(...httpsLabels);
|
||||
}
|
||||
@@ -251,11 +255,12 @@ export const writeComposeFile = async (
|
||||
export const createDomainLabels = (
|
||||
appName: string,
|
||||
domain: Domain,
|
||||
entrypoint: "web" | "websecure",
|
||||
entrypoint: string,
|
||||
) => {
|
||||
const {
|
||||
host,
|
||||
port,
|
||||
customEntrypoint,
|
||||
https,
|
||||
uniqueConfigKey,
|
||||
certificateType,
|
||||
@@ -274,34 +279,45 @@ export const createDomainLabels = (
|
||||
|
||||
// Collect middlewares for this router
|
||||
const middlewares: string[] = [];
|
||||
const isRedirectRouter = entrypoint === "web" && https && !customEntrypoint;
|
||||
|
||||
// Add HTTPS redirect for web entrypoint (must be first)
|
||||
if (entrypoint === "web" && https) {
|
||||
// Web router with HTTPS only needs redirect — all other middlewares
|
||||
// run on the websecure router where the request actually lands.
|
||||
if (isRedirectRouter) {
|
||||
middlewares.push("redirect-to-https@file");
|
||||
}
|
||||
|
||||
// Add stripPath middleware if needed
|
||||
if (stripPath && path && path !== "/") {
|
||||
const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`;
|
||||
// Only define middleware once (on web entrypoint)
|
||||
if (entrypoint === "web") {
|
||||
// Define middleware on web (or custom) entrypoint so Traefik registers it
|
||||
if (entrypoint === "web" || customEntrypoint) {
|
||||
labels.push(
|
||||
`traefik.http.middlewares.${middlewareName}.stripprefix.prefixes=${path}`,
|
||||
);
|
||||
}
|
||||
middlewares.push(middlewareName);
|
||||
if (!isRedirectRouter) {
|
||||
middlewares.push(middlewareName);
|
||||
}
|
||||
}
|
||||
|
||||
// Add internalPath middleware if needed
|
||||
if (internalPath && internalPath !== "/" && internalPath.startsWith("/")) {
|
||||
const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`;
|
||||
// Only define middleware once (on web entrypoint)
|
||||
if (entrypoint === "web") {
|
||||
// Define middleware on web (or custom) entrypoint so Traefik registers it
|
||||
if (entrypoint === "web" || customEntrypoint) {
|
||||
labels.push(
|
||||
`traefik.http.middlewares.${middlewareName}.addprefix.prefix=${internalPath}`,
|
||||
);
|
||||
}
|
||||
middlewares.push(middlewareName);
|
||||
if (!isRedirectRouter) {
|
||||
middlewares.push(middlewareName);
|
||||
}
|
||||
}
|
||||
|
||||
// Add custom middlewares (skip for redirect-only router)
|
||||
if (!isRedirectRouter && domain.middlewares?.length) {
|
||||
middlewares.push(...domain.middlewares);
|
||||
}
|
||||
|
||||
// Apply middlewares to router if any exist
|
||||
@@ -312,7 +328,7 @@ export const createDomainLabels = (
|
||||
}
|
||||
|
||||
// Add TLS configuration for websecure
|
||||
if (entrypoint === "websecure") {
|
||||
if (entrypoint === "websecure" || (customEntrypoint && https)) {
|
||||
if (certificateType === "letsencrypt") {
|
||||
labels.push(
|
||||
`traefik.http.routers.${routerName}.tls.certresolver=letsencrypt`,
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { ContainerInfo, ResourceRequirements } from "dockerode";
|
||||
import { parse } from "dotenv";
|
||||
import { quote } from "shell-quote";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
import type { LibsqlNested } from "../databases/libsql";
|
||||
import type { MariadbNested } from "../databases/mariadb";
|
||||
import type { MongoNested } from "../databases/mongo";
|
||||
import type { MysqlNested } from "../databases/mysql";
|
||||
@@ -258,6 +259,48 @@ export const cleanupSystem = async (serverId?: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export interface DockerDiskUsageItem {
|
||||
type: string;
|
||||
totalCount: number;
|
||||
active: number;
|
||||
size: string;
|
||||
reclaimable: string;
|
||||
sizeBytes: number;
|
||||
}
|
||||
|
||||
const parseSizeToBytes = (size: string): number => {
|
||||
const match = size.match(/^([\d.]+)\s*([KMGT]?B)$/i);
|
||||
if (!match) return 0;
|
||||
const value = Number.parseFloat(match[1] as string);
|
||||
const unit = (match[2] as string).toUpperCase();
|
||||
const multipliers: Record<string, number> = {
|
||||
B: 1,
|
||||
KB: 1024,
|
||||
MB: 1024 ** 2,
|
||||
GB: 1024 ** 3,
|
||||
TB: 1024 ** 4,
|
||||
};
|
||||
return value * (multipliers[unit] || 0);
|
||||
};
|
||||
|
||||
export const getDockerDiskUsage = async (): Promise<DockerDiskUsageItem[]> => {
|
||||
const command = "docker system df --format '{{json .}}'";
|
||||
const { stdout } = await execAsync(command);
|
||||
|
||||
const lines = stdout.trim().split("\n").filter(Boolean);
|
||||
return lines.map((line) => {
|
||||
const data = JSON.parse(line);
|
||||
return {
|
||||
type: data.Type,
|
||||
totalCount: Number.parseInt(data.TotalCount, 10) || 0,
|
||||
active: Number.parseInt(data.Active, 10) || 0,
|
||||
size: data.Size,
|
||||
reclaimable: data.Reclaimable,
|
||||
sizeBytes: parseSizeToBytes(data.Size),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Volume cleanup should always be performed manually by the user. The reason is that during automatic cleanup, a volume may be deleted due to a stopped container, which is a dangerous situation.
|
||||
*
|
||||
@@ -433,7 +476,7 @@ export const parseEnvironmentKeyValuePair = (
|
||||
return [key, valueParts.join("=")];
|
||||
};
|
||||
|
||||
export const getEnviromentVariablesObject = (
|
||||
export const getEnvironmentVariablesObject = (
|
||||
input: string | null,
|
||||
projectEnv?: string | null,
|
||||
environmentEnv?: string | null,
|
||||
@@ -605,6 +648,7 @@ export const generateFileMounts = (
|
||||
appName: string,
|
||||
service:
|
||||
| ApplicationNested
|
||||
| LibsqlNested
|
||||
| MongoNested
|
||||
| MariadbNested
|
||||
| MysqlNested
|
||||
@@ -736,3 +780,177 @@ export const getComposeContainer = async (
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
type ServiceHealthStatus = {
|
||||
status: "healthy" | "unhealthy";
|
||||
message?: string;
|
||||
};
|
||||
|
||||
const checkSwarmServiceRunning = async (
|
||||
serviceName: string,
|
||||
): Promise<ServiceHealthStatus> => {
|
||||
try {
|
||||
const service = docker.getService(serviceName);
|
||||
const info = await service.inspect();
|
||||
const replicas = info.Spec?.Mode?.Replicated?.Replicas ?? 0;
|
||||
if (replicas === 0) {
|
||||
return {
|
||||
status: "unhealthy",
|
||||
message: "Service has 0 replicas configured",
|
||||
};
|
||||
}
|
||||
|
||||
// Check that at least one task is actually running
|
||||
const tasks = await docker.listTasks({
|
||||
filters: JSON.stringify({
|
||||
service: [serviceName],
|
||||
"desired-state": ["running"],
|
||||
}),
|
||||
});
|
||||
|
||||
const runningTask = tasks.find((t) => t.Status?.State === "running");
|
||||
|
||||
if (!runningTask) {
|
||||
const latestTask = tasks[0];
|
||||
const taskState = latestTask?.Status?.State ?? "unknown";
|
||||
return {
|
||||
status: "unhealthy",
|
||||
message: `No running tasks (current state: ${taskState})`,
|
||||
};
|
||||
}
|
||||
|
||||
return { status: "healthy" };
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "unhealthy",
|
||||
message: error instanceof Error ? error.message : "Service not found",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getSwarmServiceContainerId = async (
|
||||
serviceName: string,
|
||||
): Promise<string | null> => {
|
||||
try {
|
||||
const tasks = await docker.listTasks({
|
||||
filters: JSON.stringify({
|
||||
service: [serviceName],
|
||||
"desired-state": ["running"],
|
||||
}),
|
||||
});
|
||||
|
||||
const runningTask = tasks.find((t) => t.Status?.State === "running");
|
||||
|
||||
return runningTask?.Status?.ContainerStatus?.ContainerID ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const checkPostgresHealth = async (): Promise<ServiceHealthStatus> => {
|
||||
const serviceCheck = await checkSwarmServiceRunning("dokploy-postgres");
|
||||
if (serviceCheck.status === "unhealthy") {
|
||||
return serviceCheck;
|
||||
}
|
||||
|
||||
// Verify PostgreSQL actually accepts connections
|
||||
const containerId = await getSwarmServiceContainerId("dokploy-postgres");
|
||||
if (!containerId) {
|
||||
return { status: "unhealthy", message: "Could not find running container" };
|
||||
}
|
||||
|
||||
try {
|
||||
const exec = await docker.getContainer(containerId).exec({
|
||||
Cmd: ["pg_isready", "-U", "dokploy"],
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
});
|
||||
const stream = await exec.start({});
|
||||
|
||||
const output = await new Promise<string>((resolve) => {
|
||||
let data = "";
|
||||
stream.on("data", (chunk: Buffer) => {
|
||||
data += chunk.toString();
|
||||
});
|
||||
stream.on("end", () => resolve(data));
|
||||
});
|
||||
|
||||
const inspectResult = await exec.inspect();
|
||||
if (inspectResult.ExitCode !== 0) {
|
||||
return {
|
||||
status: "unhealthy",
|
||||
message: `PostgreSQL not ready: ${output.trim()}`,
|
||||
};
|
||||
}
|
||||
|
||||
return { status: "healthy" };
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "unhealthy",
|
||||
message:
|
||||
error instanceof Error ? error.message : "Failed to check PostgreSQL",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const checkRedisHealth = async (): Promise<ServiceHealthStatus> => {
|
||||
const serviceCheck = await checkSwarmServiceRunning("dokploy-redis");
|
||||
if (serviceCheck.status === "unhealthy") {
|
||||
return serviceCheck;
|
||||
}
|
||||
|
||||
// Verify Redis actually responds to PING
|
||||
const containerId = await getSwarmServiceContainerId("dokploy-redis");
|
||||
if (!containerId) {
|
||||
return { status: "unhealthy", message: "Could not find running container" };
|
||||
}
|
||||
|
||||
try {
|
||||
const exec = await docker.getContainer(containerId).exec({
|
||||
Cmd: ["redis-cli", "ping"],
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
});
|
||||
const stream = await exec.start({});
|
||||
|
||||
const output = await new Promise<string>((resolve) => {
|
||||
let data = "";
|
||||
stream.on("data", (chunk: Buffer) => {
|
||||
data += chunk.toString();
|
||||
});
|
||||
stream.on("end", () => resolve(data));
|
||||
});
|
||||
|
||||
if (!output.includes("PONG")) {
|
||||
return {
|
||||
status: "unhealthy",
|
||||
message: `Redis did not respond with PONG: ${output.trim()}`,
|
||||
};
|
||||
}
|
||||
|
||||
return { status: "healthy" };
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "unhealthy",
|
||||
message: error instanceof Error ? error.message : "Failed to check Redis",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const checkTraefikHealth = async (): Promise<ServiceHealthStatus> => {
|
||||
// Traefik can run as a standalone container or a swarm service
|
||||
try {
|
||||
const container = docker.getContainer("dokploy-traefik");
|
||||
const info = await container.inspect();
|
||||
if (!info.State.Running) {
|
||||
return {
|
||||
status: "unhealthy",
|
||||
message: "Container is not running",
|
||||
};
|
||||
}
|
||||
return { status: "healthy" };
|
||||
} catch {
|
||||
// Not a standalone container, check as swarm service
|
||||
return checkSwarmServiceRunning("dokploy-traefik");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -50,6 +51,7 @@ export const sendBuildErrorNotifications = async ({
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -66,6 +68,7 @@ export const sendBuildErrorNotifications = async ({
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
@@ -250,6 +253,26 @@ export const sendBuildErrorNotifications = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `:warning: **Build Failed**
|
||||
|
||||
**Project:** ${projectName}
|
||||
**Application:** ${applicationName}
|
||||
**Type:** ${applicationType}
|
||||
**Time:** ${date.toLocaleString()}
|
||||
|
||||
**Error:**
|
||||
\`\`\`
|
||||
${errorMessage}
|
||||
\`\`\`
|
||||
|
||||
[View Build Details](${buildLink})`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy Bot",
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: "Build Error",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -53,6 +54,7 @@ export const sendBuildSuccessNotifications = async ({
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -69,6 +71,7 @@ export const sendBuildSuccessNotifications = async ({
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
@@ -266,6 +269,14 @@ export const sendBuildSuccessNotifications = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**✅ Build Success**\n\n**Project:** ${projectName}\n**Application:** ${applicationName}\n**Type:** ${applicationType}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}\n\n[View Build Details](${buildLink})`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: "Build Success",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -29,7 +30,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
}: {
|
||||
projectName: string;
|
||||
applicationName: string;
|
||||
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
|
||||
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb" | "libsql";
|
||||
type: "error" | "success";
|
||||
organizationId: string;
|
||||
errorMessage?: string;
|
||||
@@ -50,6 +51,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -66,6 +68,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
@@ -153,7 +156,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
? [
|
||||
{
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
value: `\`\`\`${errorMessage.length > 1010 ? `${errorMessage.substring(0, 1010)}...` : errorMessage}\`\`\``,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -272,6 +275,21 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg =
|
||||
type === "error" && errorMessage
|
||||
? `\n\n**Error:**\n\`\`\`\n${errorMessage}\n\`\`\``
|
||||
: "";
|
||||
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**${statusEmoji} Database Backup ${typeStatus}**\n\n**Project:** ${projectName}\n**Application:** ${applicationName}\n**Type:** ${databaseType}\n**Database Name:** ${databaseName}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}${errorMsg}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: `Database Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -37,6 +38,7 @@ export const sendDockerCleanupNotifications = async (
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -53,6 +55,7 @@ export const sendDockerCleanupNotifications = async (
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
@@ -168,6 +171,14 @@ export const sendDockerCleanupNotifications = async (
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**✅ Docker Cleanup**\n\n**Message:** ${message}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: "Docker Cleanup",
|
||||
|
||||
416
packages/server/src/utils/notifications/dokploy-backup.ts
Normal file
416
packages/server/src/utils/notifications/dokploy-backup.ts
Normal file
@@ -0,0 +1,416 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import DokployBackupEmail from "@dokploy/server/emails/emails/dokploy-backup";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { format } from "date-fns";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
sendCustomNotification,
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
sendSlackNotification,
|
||||
sendTeamsNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
|
||||
export const sendDokployBackupNotifications = async ({
|
||||
type,
|
||||
errorMessage,
|
||||
backupSize,
|
||||
}: {
|
||||
type: "error" | "success";
|
||||
errorMessage?: string;
|
||||
backupSize?: string;
|
||||
}) => {
|
||||
const date = new Date();
|
||||
const unixDate = ~~(Number(date) / 1000);
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: eq(notifications.dokployBackup, true),
|
||||
with: {
|
||||
email: true,
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const {
|
||||
email,
|
||||
discord,
|
||||
telegram,
|
||||
slack,
|
||||
resend,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
} = notification;
|
||||
|
||||
try {
|
||||
if (email || resend) {
|
||||
const template = await renderAsync(
|
||||
DokployBackupEmail({
|
||||
type,
|
||||
errorMessage,
|
||||
date: date.toLocaleString(),
|
||||
backupSize,
|
||||
}),
|
||||
).catch();
|
||||
|
||||
if (email) {
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Dokploy instance backup",
|
||||
template,
|
||||
);
|
||||
}
|
||||
|
||||
if (resend) {
|
||||
await sendResendNotification(
|
||||
resend,
|
||||
"Dokploy instance backup",
|
||||
template,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title:
|
||||
type === "success"
|
||||
? decorate(">", "`✅` Dokploy Backup Successful")
|
||||
: decorate(">", "`❌` Dokploy Backup Failed"),
|
||||
color: type === "success" ? 0x57f287 : 0xed4245,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`📦`", "Backup Type"),
|
||||
value: "Complete Dokploy Instance",
|
||||
inline: true,
|
||||
},
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
name: decorate("`💾`", "Backup Size"),
|
||||
value: backupSize,
|
||||
inline: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Status"),
|
||||
value: type
|
||||
.replace("error", "Failed")
|
||||
.replace("success", "Successful"),
|
||||
inline: true,
|
||||
},
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Instance Backup Notification",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate(
|
||||
type === "success" ? "✅" : "❌",
|
||||
`Dokploy Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
),
|
||||
`${decorate("📦", "Backup Type: Complete Dokploy Instance")}` +
|
||||
`${backupSize ? decorate("💾", `Backup Size: ${backupSize}`) : ""}` +
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (ntfy) {
|
||||
await sendNtfyNotification(
|
||||
ntfy,
|
||||
`Dokploy Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`${type === "success" ? "white_check_mark" : "x"}`,
|
||||
"",
|
||||
"📦Backup Type: Complete Dokploy Instance\n" +
|
||||
`${backupSize ? `💾Backup Size: ${backupSize}\n` : ""}` +
|
||||
`🕒Date: ${date.toLocaleString()}\n` +
|
||||
`${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
const isError = type === "error" && errorMessage;
|
||||
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg = isError
|
||||
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
|
||||
: "";
|
||||
const sizeInfo = backupSize
|
||||
? `\n<b>Backup Size:</b> ${backupSize}`
|
||||
: "";
|
||||
|
||||
const messageText = `<b>${statusEmoji} Dokploy Backup ${typeStatus}</b>\n\n<b>Backup Type:</b> Complete Dokploy Instance${sizeInfo}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
|
||||
|
||||
await sendTelegramNotification(telegram, messageText);
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: type === "success" ? "#00FF00" : "#FF0000",
|
||||
pretext:
|
||||
type === "success"
|
||||
? ":white_check_mark: *Dokploy Backup Successful*"
|
||||
: ":x: *Dokploy Backup Failed*",
|
||||
fields: [
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
title: "Error Message",
|
||||
value: errorMessage,
|
||||
short: false,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Backup Type",
|
||||
value: "Complete Dokploy Instance",
|
||||
short: true,
|
||||
},
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
title: "Backup Size",
|
||||
value: backupSize,
|
||||
short: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
value: type === "success" ? "Successful" : "Failed",
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
const limitCharacter = 800;
|
||||
const truncatedErrorMessage =
|
||||
errorMessage && errorMessage.length > limitCharacter
|
||||
? errorMessage.substring(0, limitCharacter)
|
||||
: errorMessage;
|
||||
|
||||
await sendLarkNotification(lark, {
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
schema: "2.0",
|
||||
config: {
|
||||
update_multi: true,
|
||||
style: {
|
||||
text_size: {
|
||||
normal_v2: {
|
||||
default: "normal",
|
||||
pc: "normal",
|
||||
mobile: "heading",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content:
|
||||
type === "success"
|
||||
? "✅ Dokploy Backup Successful"
|
||||
: "❌ Dokploy Backup Failed",
|
||||
},
|
||||
subtitle: {
|
||||
tag: "plain_text",
|
||||
content: "",
|
||||
},
|
||||
template: type === "success" ? "green" : "red",
|
||||
padding: "12px 12px 12px 12px",
|
||||
},
|
||||
body: {
|
||||
direction: "vertical",
|
||||
padding: "12px 12px 12px 12px",
|
||||
elements: [
|
||||
{
|
||||
tag: "column_set",
|
||||
columns: [
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content:
|
||||
"**Backup Type:**\nComplete Dokploy Instance",
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Status:**\n${type === "success" ? "Successful" : "Failed"}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Backup Size:**\n${backupSize}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Date:**\n${format(date, "PP pp")}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
...(type === "error" && truncatedErrorMessage
|
||||
? [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
const statusEmoji = type === "success" ? ":white_check_mark:" : ":x:";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `${statusEmoji} **Dokploy Backup ${typeStatus}**
|
||||
|
||||
**Backup Type:** Complete Dokploy Instance${backupSize ? `\n**Backup Size:** ${backupSize}` : ""}
|
||||
**Date:** ${date.toLocaleString()}
|
||||
**Status:** ${typeStatus}${type === "error" && errorMessage ? `\n\n**Error:**\n\`\`\`\n${errorMessage}\n\`\`\`` : ""}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy Bot",
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: `Dokploy Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
message: `Dokploy instance backup ${type === "success" ? "completed successfully" : "failed"}`,
|
||||
backupType: "Complete Dokploy Instance",
|
||||
...(backupSize ? { backupSize } : {}),
|
||||
...(type === "error" && errorMessage ? { errorMessage } : {}),
|
||||
timestamp: date.toISOString(),
|
||||
date: date.toLocaleString(),
|
||||
status: type,
|
||||
type: "dokploy-backup",
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
`Dokploy Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`Backup Type: Complete Dokploy Instance${backupSize ? `\nBackup Size: ${backupSize}` : ""}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
await sendTeamsNotification(teams, {
|
||||
title: `${type === "success" ? "✅" : "❌"} Dokploy Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
facts: [
|
||||
{ name: "Backup Type", value: "Complete Dokploy Instance" },
|
||||
...(backupSize ? [{ name: "Backup Size", value: backupSize }] : []),
|
||||
{ name: "Date", value: format(date, "PP pp") },
|
||||
{
|
||||
name: "Status",
|
||||
value: type === "success" ? "Successful" : "Failed",
|
||||
},
|
||||
...(type === "error" && errorMessage
|
||||
? [{ name: "Error Message", value: errorMessage }]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -32,6 +33,7 @@ export const sendDokployRestartNotifications = async () => {
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -41,233 +43,242 @@ export const sendDokployRestartNotifications = async () => {
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const {
|
||||
email,
|
||||
resend,
|
||||
discord,
|
||||
telegram,
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
} = notification;
|
||||
email,
|
||||
resend,
|
||||
discord,
|
||||
telegram,
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
} = notification;
|
||||
|
||||
try {
|
||||
if (email || resend) {
|
||||
const template = await renderAsync(
|
||||
DokployRestartEmail({ date: date.toLocaleString() }),
|
||||
).catch();
|
||||
try {
|
||||
if (email || resend) {
|
||||
const template = await renderAsync(
|
||||
DokployRestartEmail({ date: date.toLocaleString() }),
|
||||
).catch();
|
||||
|
||||
if (email) {
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Dokploy Server Restarted",
|
||||
template,
|
||||
);
|
||||
if (email) {
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Dokploy Server Restarted",
|
||||
template,
|
||||
);
|
||||
}
|
||||
|
||||
if (resend) {
|
||||
await sendResendNotification(
|
||||
resend,
|
||||
"Dokploy Server Restarted",
|
||||
template,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (resend) {
|
||||
await sendResendNotification(
|
||||
resend,
|
||||
"Dokploy Server Restarted",
|
||||
template,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title: decorate(">", "`✅` Dokploy Server Restarted"),
|
||||
color: 0x57f287,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Restart Notification",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate("✅", "Dokploy Server Restarted"),
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (ntfy) {
|
||||
await sendNtfyNotification(
|
||||
ntfy,
|
||||
"Dokploy Server Restarted",
|
||||
"white_check_mark",
|
||||
"",
|
||||
`🕒Date: ${date.toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(
|
||||
date,
|
||||
"PP",
|
||||
)}\n<b>Time:</b> ${format(date, "pp")}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: "#00FF00",
|
||||
pretext: ":white_check_mark: *Dokploy Server Restarted*",
|
||||
fields: [
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
try {
|
||||
await sendCustomNotification(custom, {
|
||||
title: "Dokploy Server Restarted",
|
||||
message: "Dokploy server has been restarted successfully",
|
||||
await sendDiscordNotification(discord, {
|
||||
title: decorate(">", "`✅` Dokploy Server Restarted"),
|
||||
color: 0x57f287,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
date: date.toLocaleString(),
|
||||
status: "success",
|
||||
type: "dokploy-restart",
|
||||
footer: {
|
||||
text: "Dokploy Restart Notification",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
await sendLarkNotification(lark, {
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
schema: "2.0",
|
||||
config: {
|
||||
update_multi: true,
|
||||
style: {
|
||||
text_size: {
|
||||
normal_v2: {
|
||||
default: "normal",
|
||||
pc: "normal",
|
||||
mobile: "heading",
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate("✅", "Dokploy Server Restarted"),
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (ntfy) {
|
||||
await sendNtfyNotification(
|
||||
ntfy,
|
||||
"Dokploy Server Restarted",
|
||||
"white_check_mark",
|
||||
"",
|
||||
`🕒Date: ${date.toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(
|
||||
date,
|
||||
"PP",
|
||||
)}\n<b>Time:</b> ${format(date, "pp")}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: "#00FF00",
|
||||
pretext: ":white_check_mark: *Dokploy Server Restarted*",
|
||||
fields: [
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**✅ Dokploy Server Restarted**\n\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
try {
|
||||
await sendCustomNotification(custom, {
|
||||
title: "Dokploy Server Restarted",
|
||||
message: "Dokploy server has been restarted successfully",
|
||||
timestamp: date.toISOString(),
|
||||
date: date.toLocaleString(),
|
||||
status: "success",
|
||||
type: "dokploy-restart",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
await sendLarkNotification(lark, {
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
schema: "2.0",
|
||||
config: {
|
||||
update_multi: true,
|
||||
style: {
|
||||
text_size: {
|
||||
normal_v2: {
|
||||
default: "normal",
|
||||
pc: "normal",
|
||||
mobile: "heading",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content: "✅ Dokploy Server Restarted",
|
||||
},
|
||||
subtitle: {
|
||||
tag: "plain_text",
|
||||
content: "",
|
||||
},
|
||||
template: "green",
|
||||
padding: "12px 12px 12px 12px",
|
||||
},
|
||||
body: {
|
||||
direction: "vertical",
|
||||
padding: "12px 12px 12px 12px",
|
||||
elements: [
|
||||
{
|
||||
tag: "column_set",
|
||||
columns: [
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: "**Status:**\nSuccessful",
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Restart Time:**\n${format(
|
||||
date,
|
||||
"PP pp",
|
||||
)}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content: "✅ Dokploy Server Restarted",
|
||||
},
|
||||
],
|
||||
subtitle: {
|
||||
tag: "plain_text",
|
||||
content: "",
|
||||
},
|
||||
template: "green",
|
||||
padding: "12px 12px 12px 12px",
|
||||
},
|
||||
body: {
|
||||
direction: "vertical",
|
||||
padding: "12px 12px 12px 12px",
|
||||
elements: [
|
||||
{
|
||||
tag: "column_set",
|
||||
columns: [
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: "**Status:**\nSuccessful",
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Restart Time:**\n${format(
|
||||
date,
|
||||
"PP pp",
|
||||
)}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
"Dokploy Server Restarted",
|
||||
`Date: ${date.toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
"Dokploy Server Restarted",
|
||||
`Date: ${date.toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
await sendTeamsNotification(teams, {
|
||||
title: "✅ Dokploy Server Restarted",
|
||||
facts: [
|
||||
{ name: "Status", value: "Successful" },
|
||||
{ name: "Restart Time", value: format(date, "PP pp") },
|
||||
],
|
||||
});
|
||||
if (teams) {
|
||||
await sendTeamsNotification(teams, {
|
||||
title: "✅ Dokploy Server Restarted",
|
||||
facts: [
|
||||
{ name: "Status", value: "Successful" },
|
||||
{ name: "Restart Time", value: format(date, "PP pp") },
|
||||
],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Dokploy] Restart notifications failed:", error);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
sendCustomNotification,
|
||||
sendDiscordNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendPushoverNotification,
|
||||
sendSlackNotification,
|
||||
sendTeamsNotification,
|
||||
@@ -38,6 +39,7 @@ export const sendServerThresholdNotifications = async (
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -49,63 +51,72 @@ export const sendServerThresholdNotifications = async (
|
||||
const typeColor = 0xff0000; // Rojo para indicar alerta
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { discord, telegram, slack, custom, lark, pushover, teams } =
|
||||
notification;
|
||||
const {
|
||||
discord,
|
||||
telegram,
|
||||
slack,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
} = notification;
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
try {
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title: decorate(">", `\`⚠️\` Server ${payload.Type} Alert`),
|
||||
color: typeColor,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`🏷️`", "Server Name"),
|
||||
value: payload.ServerName,
|
||||
inline: true,
|
||||
await sendDiscordNotification(discord, {
|
||||
title: decorate(">", `\`⚠️\` Server ${payload.Type} Alert`),
|
||||
color: typeColor,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`🏷️`", "Server Name"),
|
||||
value: payload.ServerName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate(typeEmoji, "Type"),
|
||||
value: payload.Type,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("📊", "Current Value"),
|
||||
value: `${payload.Value.toFixed(2)}%`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("⚠️", "Threshold"),
|
||||
value: `${payload.Threshold.toFixed(2)}%`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📜`", "Message"),
|
||||
value: `\`\`\`${payload.Message}\`\`\``,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Server Monitoring Alert",
|
||||
},
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate(typeEmoji, "Type"),
|
||||
value: payload.Type,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("📊", "Current Value"),
|
||||
value: `${payload.Value.toFixed(2)}%`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("⚠️", "Threshold"),
|
||||
value: `${payload.Threshold.toFixed(2)}%`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📜`", "Message"),
|
||||
value: `\`\`\`${payload.Message}\`\`\``,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Server Monitoring Alert",
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`
|
||||
if (telegram) {
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`
|
||||
<b>⚠️ Server ${payload.Type} Alert</b>
|
||||
<b>Server Name:</b> ${payload.ServerName}
|
||||
<b>Type:</b> ${payload.Type}
|
||||
@@ -114,170 +125,181 @@ export const sendServerThresholdNotifications = async (
|
||||
<b>Message:</b> ${payload.Message}
|
||||
<b>Time:</b> ${date.toLocaleString()}
|
||||
`,
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: "#FF0000",
|
||||
pretext: `:warning: *Server ${payload.Type} Alert*`,
|
||||
fields: [
|
||||
{
|
||||
title: "Server Name",
|
||||
value: payload.ServerName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
value: payload.Type,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Current Value",
|
||||
value: `${payload.Value.toFixed(2)}%`,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Threshold",
|
||||
value: `${payload.Threshold.toFixed(2)}%`,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Message",
|
||||
value: payload.Message,
|
||||
},
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: "#FF0000",
|
||||
pretext: `:warning: *Server ${payload.Type} Alert*`,
|
||||
fields: [
|
||||
{
|
||||
title: "Server Name",
|
||||
value: payload.ServerName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
value: payload.Type,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Current Value",
|
||||
value: `${payload.Value.toFixed(2)}%`,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Threshold",
|
||||
value: `${payload.Threshold.toFixed(2)}%`,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Message",
|
||||
value: payload.Message,
|
||||
},
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: `Server ${payload.Type} Alert`,
|
||||
message: payload.Message,
|
||||
serverName: payload.ServerName,
|
||||
type: payload.Type,
|
||||
currentValue: payload.Value,
|
||||
threshold: payload.Threshold,
|
||||
timestamp: date.toISOString(),
|
||||
date: date.toLocaleString(),
|
||||
status: "alert",
|
||||
alertType: "server-threshold",
|
||||
});
|
||||
}
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**⚠️ Server ${payload.Type} Alert**\n\n**Server Name:** ${payload.ServerName}\n**Type:** ${payload.Type}\n**Current Value:** ${payload.Value.toFixed(2)}%\n**Threshold:** ${payload.Threshold.toFixed(2)}%\n**Message:** ${payload.Message}\n**Time:** ${date.toLocaleString()}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
await sendLarkNotification(lark, {
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
schema: "2.0",
|
||||
config: {
|
||||
update_multi: true,
|
||||
style: {
|
||||
text_size: {
|
||||
normal_v2: {
|
||||
default: "normal",
|
||||
pc: "normal",
|
||||
mobile: "heading",
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: `Server ${payload.Type} Alert`,
|
||||
message: payload.Message,
|
||||
serverName: payload.ServerName,
|
||||
type: payload.Type,
|
||||
currentValue: payload.Value,
|
||||
threshold: payload.Threshold,
|
||||
timestamp: date.toISOString(),
|
||||
date: date.toLocaleString(),
|
||||
status: "alert",
|
||||
alertType: "server-threshold",
|
||||
});
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
await sendLarkNotification(lark, {
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
schema: "2.0",
|
||||
config: {
|
||||
update_multi: true,
|
||||
style: {
|
||||
text_size: {
|
||||
normal_v2: {
|
||||
default: "normal",
|
||||
pc: "normal",
|
||||
mobile: "heading",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content: `⚠️ Server ${payload.Type} Alert`,
|
||||
},
|
||||
subtitle: {
|
||||
tag: "plain_text",
|
||||
content: "",
|
||||
},
|
||||
template: "red",
|
||||
padding: "12px 12px 12px 12px",
|
||||
},
|
||||
body: {
|
||||
direction: "vertical",
|
||||
padding: "12px 12px 12px 12px",
|
||||
elements: [
|
||||
{
|
||||
tag: "column_set",
|
||||
columns: [
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Server Name:**\n${payload.ServerName}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Current Value:**\n${payload.Value.toFixed(2)}%`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Alert Message:**\n${payload.Message}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Type:**\n${payload.Type === "CPU" ? "🔲" : "💾"} ${payload.Type}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Threshold:**\n${payload.Threshold.toFixed(2)}%`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Alert Time:**\n${date.toLocaleString()}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content: `⚠️ Server ${payload.Type} Alert`,
|
||||
},
|
||||
],
|
||||
subtitle: {
|
||||
tag: "plain_text",
|
||||
content: "",
|
||||
},
|
||||
template: "red",
|
||||
padding: "12px 12px 12px 12px",
|
||||
},
|
||||
body: {
|
||||
direction: "vertical",
|
||||
padding: "12px 12px 12px 12px",
|
||||
elements: [
|
||||
{
|
||||
tag: "column_set",
|
||||
columns: [
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Server Name:**\n${payload.ServerName}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Current Value:**\n${payload.Value.toFixed(2)}%`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Alert Message:**\n${payload.Message}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Type:**\n${payload.Type === "CPU" ? "🔲" : "💾"} ${payload.Type}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Threshold:**\n${payload.Threshold.toFixed(2)}%`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Alert Time:**\n${date.toLocaleString()}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
`Server ${payload.Type} Alert`,
|
||||
`Server: ${payload.ServerName}\nType: ${payload.Type}\nCurrent: ${payload.Value.toFixed(2)}%\nThreshold: ${payload.Threshold.toFixed(2)}%\nMessage: ${payload.Message}\nTime: ${date.toLocaleString()}`,
|
||||
);
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
`Server ${payload.Type} Alert`,
|
||||
`Server: ${payload.ServerName}\nType: ${payload.Type}\nCurrent: ${payload.Value.toFixed(2)}%\nThreshold: ${payload.Threshold.toFixed(2)}%\nMessage: ${payload.Message}\nTime: ${date.toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
email,
|
||||
gotify,
|
||||
lark,
|
||||
mattermost,
|
||||
ntfy,
|
||||
pushover,
|
||||
resend,
|
||||
@@ -206,6 +207,33 @@ export const sendNtfyNotification = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const sendMattermostNotification = async (
|
||||
connection: typeof mattermost.$inferInsert,
|
||||
message: any,
|
||||
) => {
|
||||
const payload = {
|
||||
...message,
|
||||
// Only include username if it's provided and not empty
|
||||
...(message.username?.trim() && { username: message.username }),
|
||||
// Only include channel if it's provided and not empty
|
||||
...(message.channel?.trim() && {
|
||||
channel: `#${message.channel.replace("#", "")}`,
|
||||
}),
|
||||
};
|
||||
|
||||
const response = await fetch(connection.webhookUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to send Mattermost notification: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const sendCustomNotification = async (
|
||||
connection: typeof custom.$inferInsert,
|
||||
payload: Record<string, any>,
|
||||
|
||||
@@ -5,9 +5,12 @@ import { renderAsync } from "@react-email/components";
|
||||
import { format } from "date-fns";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendCustomNotification,
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -36,7 +39,8 @@ export const sendVolumeBackupNotifications = async ({
|
||||
| "mongodb"
|
||||
| "mariadb"
|
||||
| "redis"
|
||||
| "compose";
|
||||
| "compose"
|
||||
| "libsql";
|
||||
type: "error" | "success";
|
||||
organizationId: string;
|
||||
errorMessage?: string;
|
||||
@@ -57,6 +61,9 @@ export const sendVolumeBackupNotifications = async ({
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
@@ -71,257 +78,420 @@ export const sendVolumeBackupNotifications = async ({
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
} = notification;
|
||||
|
||||
if (email || resend) {
|
||||
const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`;
|
||||
const htmlContent = await renderAsync(
|
||||
VolumeBackupEmail({
|
||||
try {
|
||||
if (email || resend) {
|
||||
const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`;
|
||||
const htmlContent = await renderAsync(
|
||||
VolumeBackupEmail({
|
||||
projectName,
|
||||
applicationName,
|
||||
volumeName,
|
||||
serviceType,
|
||||
type,
|
||||
errorMessage,
|
||||
backupSize,
|
||||
date: date.toISOString(),
|
||||
}),
|
||||
);
|
||||
if (email) {
|
||||
await sendEmailNotification(email, subject, htmlContent);
|
||||
}
|
||||
if (resend) {
|
||||
await sendResendNotification(resend, subject, htmlContent);
|
||||
}
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title:
|
||||
type === "success"
|
||||
? decorate(">", "`✅` Volume Backup Successful")
|
||||
: decorate(">", "`❌` Volume Backup Failed"),
|
||||
color: type === "success" ? 0x57f287 : 0xed4245,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`🛠️`", "Project"),
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⚙️`", "Application"),
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`💾`", "Volume Name"),
|
||||
value: volumeName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`🔧`", "Service Type"),
|
||||
value: serviceType,
|
||||
inline: true,
|
||||
},
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
name: decorate("`📊`", "Backup Size"),
|
||||
value: backupSize,
|
||||
inline: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: type
|
||||
.replace("error", "Failed")
|
||||
.replace("success", "Successful"),
|
||||
inline: true,
|
||||
},
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Volume Backup Notification",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate(
|
||||
type === "success" ? "✅" : "❌",
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
),
|
||||
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
||||
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
||||
`${decorate("💾", `Volume Name: ${volumeName}`)}` +
|
||||
`${decorate("🔧", `Service Type: ${serviceType}`)}` +
|
||||
`${backupSize ? decorate("📊", `Backup Size: ${backupSize}`) : ""}` +
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (ntfy) {
|
||||
await sendNtfyNotification(
|
||||
ntfy,
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`${type === "success" ? "white_check_mark" : "x"}`,
|
||||
"",
|
||||
`🛠️Project: ${projectName}\n` +
|
||||
`⚙️Application: ${applicationName}\n` +
|
||||
`💾Volume Name: ${volumeName}\n` +
|
||||
`🔧Service Type: ${serviceType}\n` +
|
||||
`${backupSize ? `📊Backup Size: ${backupSize}\n` : ""}` +
|
||||
`🕒Date: ${date.toLocaleString()}\n` +
|
||||
`${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
const isError = type === "error" && errorMessage;
|
||||
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg = isError
|
||||
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
|
||||
: "";
|
||||
const sizeInfo = backupSize
|
||||
? `\n<b>Backup Size:</b> ${backupSize}`
|
||||
: "";
|
||||
|
||||
const messageText = `<b>${statusEmoji} Volume Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Volume Name:</b> ${volumeName}\n<b>Service Type:</b> ${serviceType}${sizeInfo}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
|
||||
|
||||
await sendTelegramNotification(telegram, messageText);
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: type === "success" ? "#00FF00" : "#FF0000",
|
||||
pretext:
|
||||
type === "success"
|
||||
? ":white_check_mark: *Volume Backup Successful*"
|
||||
: ":x: *Volume Backup Failed*",
|
||||
fields: [
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
title: "Error Message",
|
||||
value: errorMessage,
|
||||
short: false,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Project",
|
||||
value: projectName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Application",
|
||||
value: applicationName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Volume Name",
|
||||
value: volumeName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Service Type",
|
||||
value: serviceType,
|
||||
short: true,
|
||||
},
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
title: "Backup Size",
|
||||
value: backupSize,
|
||||
short: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
value: type,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
value: type === "success" ? "Successful" : "Failed",
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg =
|
||||
type === "error" && errorMessage
|
||||
? `\n\n**Error:**\n\`\`\`\n${errorMessage}\n\`\`\``
|
||||
: "";
|
||||
const sizeInfo = backupSize ? `\n**Backup Size:** ${backupSize}` : "";
|
||||
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**${statusEmoji} Volume Backup ${typeStatus}**\n\n**Project:** ${projectName}\n**Application:** ${applicationName}\n**Volume Name:** ${volumeName}\n**Service Type:** ${serviceType}${sizeInfo}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}${errorMsg}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
const limitCharacter = 800;
|
||||
const truncatedErrorMessage =
|
||||
errorMessage && errorMessage.length > limitCharacter
|
||||
? errorMessage.substring(0, limitCharacter)
|
||||
: errorMessage;
|
||||
|
||||
await sendLarkNotification(lark, {
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
schema: "2.0",
|
||||
config: {
|
||||
update_multi: true,
|
||||
style: {
|
||||
text_size: {
|
||||
normal_v2: {
|
||||
default: "normal",
|
||||
pc: "normal",
|
||||
mobile: "heading",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content:
|
||||
type === "success"
|
||||
? "✅ Volume Backup Successful"
|
||||
: "❌ Volume Backup Failed",
|
||||
},
|
||||
subtitle: {
|
||||
tag: "plain_text",
|
||||
content: "",
|
||||
},
|
||||
template: type === "success" ? "green" : "red",
|
||||
padding: "12px 12px 12px 12px",
|
||||
},
|
||||
body: {
|
||||
direction: "vertical",
|
||||
padding: "12px 12px 12px 12px",
|
||||
elements: [
|
||||
{
|
||||
tag: "column_set",
|
||||
columns: [
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Project:**\n${projectName}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Volume Name:**\n${volumeName}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Status:**\n${type === "success" ? "Successful" : "Failed"}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Application:**\n${applicationName}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Service Type:**\n${serviceType}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Date:**\n${format(date, "PP pp")}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
...(type === "error" && truncatedErrorMessage
|
||||
? [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`Project: ${projectName}\nApplication: ${applicationName}\nVolume: ${volumeName}\nService Type: ${serviceType}${backupSize ? `\nBackup Size: ${backupSize}` : ""}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
const facts = [
|
||||
{ name: "Project", value: projectName },
|
||||
{ name: "Application", value: applicationName },
|
||||
{ name: "Volume Name", value: volumeName },
|
||||
{ name: "Service Type", value: serviceType },
|
||||
{ name: "Date", value: format(date, "PP pp") },
|
||||
{
|
||||
name: "Status",
|
||||
value: type === "success" ? "Successful" : "Failed",
|
||||
},
|
||||
];
|
||||
if (backupSize) {
|
||||
facts.push({ name: "Backup Size", value: backupSize });
|
||||
}
|
||||
if (type === "error" && errorMessage) {
|
||||
facts.push({ name: "Error", value: errorMessage.substring(0, 500) });
|
||||
}
|
||||
await sendTeamsNotification(teams, {
|
||||
title:
|
||||
type === "success"
|
||||
? "✅ Volume Backup Successful"
|
||||
: "❌ Volume Backup Failed",
|
||||
facts,
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: `Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
message:
|
||||
type === "success"
|
||||
? "Volume backup completed successfully"
|
||||
: "Volume backup failed",
|
||||
projectName,
|
||||
applicationName,
|
||||
volumeName,
|
||||
serviceType,
|
||||
type,
|
||||
errorMessage,
|
||||
backupSize,
|
||||
date: date.toISOString(),
|
||||
}),
|
||||
);
|
||||
if (email) {
|
||||
await sendEmailNotification(email, subject, htmlContent);
|
||||
errorMessage: errorMessage ?? "",
|
||||
backupSize: backupSize ?? "",
|
||||
timestamp: date.toISOString(),
|
||||
date: date.toLocaleString(),
|
||||
status: type,
|
||||
});
|
||||
}
|
||||
if (resend) {
|
||||
await sendResendNotification(resend, subject, htmlContent);
|
||||
}
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title:
|
||||
type === "success"
|
||||
? decorate(">", "`✅` Volume Backup Successful")
|
||||
: decorate(">", "`❌` Volume Backup Failed"),
|
||||
color: type === "success" ? 0x57f287 : 0xed4245,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`🛠️`", "Project"),
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⚙️`", "Application"),
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`💾`", "Volume Name"),
|
||||
value: volumeName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`🔧`", "Service Type"),
|
||||
value: serviceType,
|
||||
inline: true,
|
||||
},
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
name: decorate("`📊`", "Backup Size"),
|
||||
value: backupSize,
|
||||
inline: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: type
|
||||
.replace("error", "Failed")
|
||||
.replace("success", "Successful"),
|
||||
inline: true,
|
||||
},
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Volume Backup Notification",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate(
|
||||
type === "success" ? "✅" : "❌",
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
),
|
||||
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
||||
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
||||
`${decorate("💾", `Volume Name: ${volumeName}`)}` +
|
||||
`${decorate("🔧", `Service Type: ${serviceType}`)}` +
|
||||
`${backupSize ? decorate("📊", `Backup Size: ${backupSize}`) : ""}` +
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (ntfy) {
|
||||
await sendNtfyNotification(
|
||||
ntfy,
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`${type === "success" ? "white_check_mark" : "x"}`,
|
||||
"",
|
||||
`🛠️Project: ${projectName}\n` +
|
||||
`⚙️Application: ${applicationName}\n` +
|
||||
`💾Volume Name: ${volumeName}\n` +
|
||||
`🔧Service Type: ${serviceType}\n` +
|
||||
`${backupSize ? `📊Backup Size: ${backupSize}\n` : ""}` +
|
||||
`🕒Date: ${date.toLocaleString()}\n` +
|
||||
`${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
const isError = type === "error" && errorMessage;
|
||||
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg = isError
|
||||
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
|
||||
: "";
|
||||
const sizeInfo = backupSize ? `\n<b>Backup Size:</b> ${backupSize}` : "";
|
||||
|
||||
const messageText = `<b>${statusEmoji} Volume Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Volume Name:</b> ${volumeName}\n<b>Service Type:</b> ${serviceType}${sizeInfo}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
|
||||
|
||||
await sendTelegramNotification(telegram, messageText);
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: type === "success" ? "#00FF00" : "#FF0000",
|
||||
pretext:
|
||||
type === "success"
|
||||
? ":white_check_mark: *Volume Backup Successful*"
|
||||
: ":x: *Volume Backup Failed*",
|
||||
fields: [
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
title: "Error Message",
|
||||
value: errorMessage,
|
||||
short: false,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Project",
|
||||
value: projectName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Application",
|
||||
value: applicationName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Volume Name",
|
||||
value: volumeName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Service Type",
|
||||
value: serviceType,
|
||||
short: true,
|
||||
},
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
title: "Backup Size",
|
||||
value: backupSize,
|
||||
short: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
value: type,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
value: type === "success" ? "Successful" : "Failed",
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`Project: ${projectName}\nApplication: ${applicationName}\nVolume: ${volumeName}\nService Type: ${serviceType}${backupSize ? `\nBackup Size: ${backupSize}` : ""}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
const facts = [
|
||||
{ name: "Project", value: projectName },
|
||||
{ name: "Application", value: applicationName },
|
||||
{ name: "Volume Name", value: volumeName },
|
||||
{ name: "Service Type", value: serviceType },
|
||||
{ name: "Date", value: format(date, "PP pp") },
|
||||
{ name: "Status", value: type === "success" ? "Successful" : "Failed" },
|
||||
];
|
||||
if (backupSize) {
|
||||
facts.push({ name: "Backup Size", value: backupSize });
|
||||
}
|
||||
if (type === "error" && errorMessage) {
|
||||
facts.push({ name: "Error", value: errorMessage.substring(0, 500) });
|
||||
}
|
||||
await sendTeamsNotification(teams, {
|
||||
title:
|
||||
type === "success"
|
||||
? "✅ Volume Backup Successful"
|
||||
: "❌ Volume Backup Failed",
|
||||
facts,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -201,14 +201,31 @@ export const execAsyncRemote = async (
|
||||
.on("error", (err) => {
|
||||
conn.end();
|
||||
if (err.level === "client-authentication") {
|
||||
const technicalDetail = `Error: ${err.message} ${err.level}`;
|
||||
const friendlyMessage = [
|
||||
"",
|
||||
"❌ Couldn't connect to your server — the SSH key was not accepted.",
|
||||
"",
|
||||
"This usually means the key doesn't match what's on the server, or the key format is invalid.",
|
||||
"",
|
||||
`Technical details: ${technicalDetail}`,
|
||||
"",
|
||||
"💡 Hints:",
|
||||
" • Check that the SSH key you added in Dokploy is the same one installed on the server (e.g. in ~/.ssh/authorized_keys).",
|
||||
" • Try generating a new SSH key in Dokploy and add only the public key to the server, then try again.",
|
||||
" • Make sure to follow the instructions on the Setup Server Button on the SSH Keys tab and then click on deployments tab and check the logs for more details.",
|
||||
].join("\n");
|
||||
const errorMsg = `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`;
|
||||
onData?.(errorMsg);
|
||||
onData?.(friendlyMessage);
|
||||
reject(
|
||||
new ExecError(errorMsg, {
|
||||
command,
|
||||
serverId,
|
||||
originalError: err,
|
||||
}),
|
||||
new ExecError(
|
||||
`Authentication failed: Invalid SSH private key. ${friendlyMessage}`,
|
||||
{
|
||||
command,
|
||||
serverId,
|
||||
originalError: err,
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
const errorMsg = `SSH connection error: ${err.message}`;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@dokploy/server/services/bitbucket";
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import type { z } from "zod";
|
||||
|
||||
export type ApplicationWithBitbucket = InferResultType<
|
||||
"applications",
|
||||
@@ -179,7 +180,7 @@ export const getBitbucketRepositories = async (bitbucketId?: string) => {
|
||||
};
|
||||
|
||||
export const getBitbucketBranches = async (
|
||||
input: typeof apiFindBitbucketBranches._type,
|
||||
input: z.infer<typeof apiFindBitbucketBranches>,
|
||||
) => {
|
||||
if (!input.bitbucketId) {
|
||||
return [];
|
||||
@@ -234,7 +235,7 @@ export const getBitbucketBranches = async (
|
||||
};
|
||||
|
||||
export const testBitbucketConnection = async (
|
||||
input: typeof apiBitbucketTestConnection._type,
|
||||
input: z.infer<typeof apiBitbucketTestConnection>,
|
||||
) => {
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ export const cloneGiteaRepository = async ({
|
||||
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const cloneUrl = buildGiteaCloneUrl(
|
||||
giteaProvider.giteaUrl,
|
||||
giteaProvider.giteaInternalUrl || giteaProvider.giteaUrl,
|
||||
giteaProvider.accessToken!,
|
||||
giteaOwner!,
|
||||
giteaRepository!,
|
||||
@@ -211,7 +211,10 @@ export const testGiteaConnection = async (input: { giteaId: string }) => {
|
||||
});
|
||||
}
|
||||
|
||||
const baseUrl = provider.giteaUrl.replace(/\/+$/, "");
|
||||
const baseUrl = (provider.giteaInternalUrl || provider.giteaUrl).replace(
|
||||
/\/+$/,
|
||||
"",
|
||||
);
|
||||
|
||||
// Use /user/repos to get authenticated user's repositories with pagination
|
||||
let allRepos = 0;
|
||||
@@ -268,7 +271,9 @@ export const getGiteaRepositories = async (giteaId?: string) => {
|
||||
await refreshGiteaToken(giteaId);
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
|
||||
const baseUrl = (
|
||||
giteaProvider.giteaInternalUrl || giteaProvider.giteaUrl
|
||||
).replace(/\/+$/, "");
|
||||
|
||||
// Use /user/repos to get authenticated user's repositories with pagination
|
||||
let allRepositories: any[] = [];
|
||||
@@ -333,7 +338,9 @@ export const getGiteaBranches = async (input: {
|
||||
|
||||
const giteaProvider = await findGiteaById(input.giteaId);
|
||||
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
|
||||
const baseUrl = (
|
||||
giteaProvider.giteaInternalUrl || giteaProvider.giteaUrl
|
||||
).replace(/\/+$/, "");
|
||||
|
||||
// Handle pagination for branches
|
||||
let allBranches: any[] = [];
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import { createAppAuth } from "@octokit/auth-app";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { Octokit } from "octokit";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const authGithub = (githubProvider: Github): Octokit => {
|
||||
if (!haveGithubRequirements(githubProvider)) {
|
||||
@@ -197,7 +198,7 @@ export const getGithubRepositories = async (githubId?: string) => {
|
||||
};
|
||||
|
||||
export const getGithubBranches = async (
|
||||
input: typeof apiFindGithubBranches._type,
|
||||
input: z.infer<typeof apiFindGithubBranches>,
|
||||
) => {
|
||||
if (!input.githubId) {
|
||||
return [];
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@dokploy/server/services/gitlab";
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const refreshGitlabToken = async (gitlabProviderId: string) => {
|
||||
const gitlabProvider = await findGitlabById(gitlabProviderId);
|
||||
@@ -89,12 +90,14 @@ const getGitlabRepoClone = (
|
||||
gitlab: GitlabInfo,
|
||||
gitlabPathNamespace: string | null,
|
||||
) => {
|
||||
const repoClone = `${gitlab?.gitlabUrl.replace(/^https?:\/\//, "")}/${gitlabPathNamespace}.git`;
|
||||
const url = gitlab?.gitlabInternalUrl || gitlab?.gitlabUrl;
|
||||
const repoClone = `${url?.replace(/^https?:\/\//, "")}/${gitlabPathNamespace}.git`;
|
||||
return repoClone;
|
||||
};
|
||||
|
||||
const getGitlabCloneUrl = (gitlab: GitlabInfo, repoClone: string) => {
|
||||
const isSecure = gitlab?.gitlabUrl.startsWith("https://");
|
||||
const url = gitlab?.gitlabInternalUrl || gitlab?.gitlabUrl;
|
||||
const isSecure = url?.startsWith("https://");
|
||||
const cloneUrl = `http${isSecure ? "s" : ""}://oauth2:${gitlab?.accessToken}@${repoClone}`;
|
||||
return cloneUrl;
|
||||
};
|
||||
@@ -171,7 +174,7 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
|
||||
if (groupName) {
|
||||
return groupName
|
||||
.split(",")
|
||||
.some((name) =>
|
||||
.some((name: string) =>
|
||||
full_path.toLowerCase().startsWith(name.trim().toLowerCase()),
|
||||
);
|
||||
}
|
||||
@@ -213,10 +216,13 @@ export const getGitlabBranches = async (input: {
|
||||
const allBranches = [];
|
||||
let page = 1;
|
||||
const perPage = 100; // GitLab's max per page is 100
|
||||
const baseUrl = (
|
||||
gitlabProvider.gitlabInternalUrl || gitlabProvider.gitlabUrl
|
||||
).replace(/\/+$/, "");
|
||||
|
||||
while (true) {
|
||||
const branchesResponse = await fetch(
|
||||
`${gitlabProvider.gitlabUrl}/api/v4/projects/${input.id}/repository/branches?page=${page}&per_page=${perPage}`,
|
||||
`${baseUrl}/api/v4/projects/${input.id}/repository/branches?page=${page}&per_page=${perPage}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
@@ -256,7 +262,7 @@ export const getGitlabBranches = async (input: {
|
||||
};
|
||||
|
||||
export const testGitlabConnection = async (
|
||||
input: typeof apiGitlabTestConnection._type,
|
||||
input: z.infer<typeof apiGitlabTestConnection>,
|
||||
) => {
|
||||
const { gitlabId, groupName } = input;
|
||||
|
||||
@@ -276,7 +282,7 @@ export const testGitlabConnection = async (
|
||||
if (groupName) {
|
||||
return groupName
|
||||
.split(",")
|
||||
.some((name) =>
|
||||
.some((name: string) =>
|
||||
full_path.toLowerCase().startsWith(name.trim().toLowerCase()),
|
||||
);
|
||||
}
|
||||
@@ -291,10 +297,13 @@ export const validateGitlabProvider = async (gitlabProvider: Gitlab) => {
|
||||
const allProjects = [];
|
||||
let page = 1;
|
||||
const perPage = 100; // GitLab's max per page is 100
|
||||
const baseUrl = (
|
||||
gitlabProvider.gitlabInternalUrl || gitlabProvider.gitlabUrl
|
||||
).replace(/\/+$/, "");
|
||||
|
||||
while (true) {
|
||||
const response = await fetch(
|
||||
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&page=${page}&per_page=${perPage}`,
|
||||
`${baseUrl}/api/v4/projects?membership=true&page=${page}&per_page=${perPage}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
|
||||
@@ -32,7 +32,7 @@ export const restoreComposeBackup = async (
|
||||
rcloneCommand = `rclone copy ${rcloneFlags.join(" ")} "${backupPath}"`;
|
||||
}
|
||||
|
||||
let credentials: DatabaseCredentials;
|
||||
let credentials: DatabaseCredentials = {};
|
||||
|
||||
switch (backupInput.databaseType) {
|
||||
case "postgres":
|
||||
@@ -62,13 +62,18 @@ export const restoreComposeBackup = async (
|
||||
const restoreCommand = getRestoreCommand({
|
||||
appName: appName,
|
||||
serviceName: backupInput.metadata?.serviceName,
|
||||
type: backupInput.databaseType,
|
||||
type: backupInput.databaseType as
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mysql"
|
||||
| "mongo",
|
||||
credentials: {
|
||||
database: backupInput.databaseName,
|
||||
...credentials,
|
||||
},
|
||||
restoreType: composeType,
|
||||
rcloneCommand,
|
||||
backupFile: backupInput.backupFile,
|
||||
});
|
||||
|
||||
emit("Starting restore...");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { restoreComposeBackup } from "./compose";
|
||||
export { restoreLibsqlBackup } from "./libsql";
|
||||
export { restoreMariadbBackup } from "./mariadb";
|
||||
export { restoreMongoBackup } from "./mongo";
|
||||
export { restoreMySqlBackup } from "./mysql";
|
||||
|
||||
49
packages/server/src/utils/restore/libsql.ts
Normal file
49
packages/server/src/utils/restore/libsql.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import type { Libsql } from "@dokploy/server/services/libsql";
|
||||
import type { z } from "zod";
|
||||
import { getS3Credentials, getServiceContainerCommand } from "../backups/utils";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
|
||||
export const restoreLibsqlBackup = async (
|
||||
libsql: Libsql,
|
||||
destination: Destination,
|
||||
backupInput: z.infer<typeof apiRestoreBackup>,
|
||||
emit: (log: string) => void,
|
||||
) => {
|
||||
try {
|
||||
const { appName, serverId } = libsql;
|
||||
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const bucketPath = `:s3:${destination.bucket}`;
|
||||
|
||||
const backupPath = `${bucketPath}/${backupInput.backupFile}`;
|
||||
|
||||
const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}"`;
|
||||
|
||||
emit("Starting restore...");
|
||||
emit(`Backup path: ${backupPath}`);
|
||||
|
||||
const containerSearch = getServiceContainerCommand(appName);
|
||||
const restoreCommand = `docker exec -i $CONTAINER_ID sh -c "tar xzf - -C /var/lib/sqld"`;
|
||||
|
||||
const command = `CONTAINER_ID=$(${containerSearch}) && ${rcloneCommand} | ${restoreCommand}`;
|
||||
|
||||
emit(`Executing command: ${command}`);
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
|
||||
emit("Restore completed successfully!");
|
||||
} catch (error) {
|
||||
emit(
|
||||
`Error: ${
|
||||
error instanceof Error ? error.message : "Error restoring libsql backup"
|
||||
}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -30,7 +30,7 @@ export const getMongoRestoreCommand = (
|
||||
databaseUser: string,
|
||||
databasePassword: string,
|
||||
) => {
|
||||
return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username '${databaseUser}' --password '${databasePassword}' --authenticationDatabase admin --db ${database} --archive"`;
|
||||
return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username '${databaseUser}' --password '${databasePassword}' --authenticationDatabase admin --db ${database} --archive --drop"`;
|
||||
};
|
||||
|
||||
export const getComposeSearchCommand = (
|
||||
|
||||
@@ -5,7 +5,7 @@ import { paths } from "@dokploy/server/constants";
|
||||
import type { Domain } from "@dokploy/server/services/domain";
|
||||
import { parse, stringify } from "yaml";
|
||||
import { encodeBase64 } from "../docker/utils";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import type { FileConfig, HttpLoadBalancerService } from "./file-types";
|
||||
|
||||
export const createTraefikConfig = (appName: string) => {
|
||||
@@ -57,18 +57,16 @@ export const removeTraefikConfig = async (
|
||||
try {
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
|
||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
||||
const command = `rm -f ${configPath}`;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, `rm ${configPath}`);
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
if (fs.existsSync(configPath)) {
|
||||
await fs.promises.unlink(configPath);
|
||||
}
|
||||
await execAsync(command);
|
||||
}
|
||||
if (fs.existsSync(configPath)) {
|
||||
await fs.promises.unlink(configPath);
|
||||
}
|
||||
} catch {}
|
||||
} catch (error) {
|
||||
console.error(`Error removing traefik config for ${appName}:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeTraefikConfigRemote = async (
|
||||
@@ -78,8 +76,13 @@ export const removeTraefikConfigRemote = async (
|
||||
try {
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths(true);
|
||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
||||
await execAsyncRemote(serverId, `rm ${configPath}`);
|
||||
} catch {}
|
||||
await execAsyncRemote(serverId, `rm -f ${configPath}`);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error removing remote traefik config for ${appName}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const loadOrCreateConfig = (appName: string): FileConfig => {
|
||||
|
||||
@@ -32,10 +32,10 @@ export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
|
||||
config.http.routers[routerName] = await createRouterConfig(
|
||||
app,
|
||||
domain,
|
||||
"web",
|
||||
domain.customEntrypoint || "web",
|
||||
);
|
||||
|
||||
if (domain.https) {
|
||||
if (!domain.customEntrypoint && domain.https) {
|
||||
config.http.routers[routerNameSecure] = await createRouterConfig(
|
||||
app,
|
||||
domain,
|
||||
@@ -121,13 +121,20 @@ const toPunycode = (host: string): string => {
|
||||
export const createRouterConfig = async (
|
||||
app: ApplicationNested,
|
||||
domain: Domain,
|
||||
entryPoint: "web" | "websecure",
|
||||
entryPoint: string,
|
||||
) => {
|
||||
const { appName, redirects, security } = app;
|
||||
const { certificateType } = domain;
|
||||
|
||||
const { host, path, https, uniqueConfigKey, internalPath, stripPath } =
|
||||
domain;
|
||||
const {
|
||||
host,
|
||||
path,
|
||||
https,
|
||||
uniqueConfigKey,
|
||||
internalPath,
|
||||
stripPath,
|
||||
customEntrypoint,
|
||||
} = domain;
|
||||
const punycodeHost = toPunycode(host);
|
||||
const routerConfig: HttpRouter = {
|
||||
rule: `Host(\`${punycodeHost}\`)${path !== null && path !== "/" ? ` && PathPrefix(\`${path}\`)` : ""}`,
|
||||
@@ -136,32 +143,31 @@ export const createRouterConfig = async (
|
||||
entryPoints: [entryPoint],
|
||||
};
|
||||
|
||||
// Add path rewriting middleware if needed
|
||||
if (internalPath && internalPath !== "/" && internalPath !== path) {
|
||||
const pathMiddleware = `addprefix-${appName}-${uniqueConfigKey}`;
|
||||
routerConfig.middlewares?.push(pathMiddleware);
|
||||
}
|
||||
const isRedirectRouter = entryPoint === "web" && https && !customEntrypoint;
|
||||
|
||||
if (stripPath && path && path !== "/") {
|
||||
const stripMiddleware = `stripprefix-${appName}-${uniqueConfigKey}`;
|
||||
routerConfig.middlewares?.push(stripMiddleware);
|
||||
}
|
||||
// Web router with HTTPS only needs redirect — all other middlewares
|
||||
// run on the websecure router where the request actually lands.
|
||||
if (isRedirectRouter) {
|
||||
routerConfig.middlewares?.push("redirect-to-https");
|
||||
} else {
|
||||
// Add path rewriting middleware if needed
|
||||
if (internalPath && internalPath !== "/" && internalPath !== path) {
|
||||
const pathMiddleware = `addprefix-${appName}-${uniqueConfigKey}`;
|
||||
routerConfig.middlewares?.push(pathMiddleware);
|
||||
}
|
||||
|
||||
if (entryPoint === "web" && https) {
|
||||
routerConfig.middlewares = ["redirect-to-https"];
|
||||
}
|
||||
if (stripPath && path && path !== "/") {
|
||||
const stripMiddleware = `stripprefix-${appName}-${uniqueConfigKey}`;
|
||||
routerConfig.middlewares?.push(stripMiddleware);
|
||||
}
|
||||
|
||||
if ((entryPoint === "websecure" && https) || !https) {
|
||||
// redirects
|
||||
for (const redirect of redirects) {
|
||||
let middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`;
|
||||
if (domain.domainType === "preview") {
|
||||
middlewareName = `redirect-${appName.replace(
|
||||
/^preview-(.+)-[^-]+$/,
|
||||
"$1",
|
||||
)}-${redirect.uniqueConfigKey}`;
|
||||
// redirects - skip for preview deployments as wildcard subdomains
|
||||
// should not inherit parent redirect rules (e.g., www-redirect)
|
||||
if (domain.domainType !== "preview") {
|
||||
for (const redirect of redirects) {
|
||||
const middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`;
|
||||
routerConfig.middlewares?.push(middlewareName);
|
||||
}
|
||||
routerConfig.middlewares?.push(middlewareName);
|
||||
}
|
||||
|
||||
// security
|
||||
@@ -175,9 +181,14 @@ export const createRouterConfig = async (
|
||||
}
|
||||
routerConfig.middlewares?.push(middlewareName);
|
||||
}
|
||||
|
||||
// custom middlewares from domain
|
||||
if (domain.middlewares && domain.middlewares.length > 0) {
|
||||
routerConfig.middlewares?.push(...domain.middlewares);
|
||||
}
|
||||
}
|
||||
|
||||
if (entryPoint === "websecure") {
|
||||
if (entryPoint === "websecure" || (customEntrypoint && https)) {
|
||||
if (certificateType === "letsencrypt") {
|
||||
routerConfig.tls = { certResolver: "letsencrypt" };
|
||||
} else if (certificateType === "custom" && domain.customCertResolver) {
|
||||
|
||||
@@ -2,7 +2,30 @@ import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { findComposeById } from "@dokploy/server/services/compose";
|
||||
import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
|
||||
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
|
||||
import {
|
||||
getBackupTimestamp,
|
||||
getS3Credentials,
|
||||
normalizeS3Path,
|
||||
} from "../backups/utils";
|
||||
|
||||
export const getVolumeServiceAppName = (
|
||||
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
|
||||
): string => {
|
||||
if (volumeBackup.compose?.appName) {
|
||||
return volumeBackup.serviceName
|
||||
? `${volumeBackup.compose.appName}_${volumeBackup.serviceName}`
|
||||
: volumeBackup.compose.appName;
|
||||
}
|
||||
const serviceAppName =
|
||||
volumeBackup.application?.appName ||
|
||||
volumeBackup.postgres?.appName ||
|
||||
volumeBackup.mysql?.appName ||
|
||||
volumeBackup.mariadb?.appName ||
|
||||
volumeBackup.mongo?.appName ||
|
||||
volumeBackup.redis?.appName ||
|
||||
volumeBackup.libsql?.appName;
|
||||
return serviceAppName || volumeBackup.appName;
|
||||
};
|
||||
|
||||
export const backupVolume = async (
|
||||
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
|
||||
@@ -12,15 +35,16 @@ export const backupVolume = async (
|
||||
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
|
||||
const { VOLUME_BACKUPS_PATH, VOLUME_BACKUP_LOCK_PATH } = paths(!!serverId);
|
||||
const destination = volumeBackup.destination;
|
||||
const backupFileName = `${volumeName}-${new Date().toISOString()}.tar`;
|
||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const s3AppName = getVolumeServiceAppName(volumeBackup);
|
||||
const backupFileName = `${volumeName}-${getBackupTimestamp()}.tar`;
|
||||
const bucketDestination = `${s3AppName}/${normalizeS3Path(prefix || "")}${backupFileName}`;
|
||||
const rcloneFlags = getS3Credentials(volumeBackup.destination);
|
||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||
const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName);
|
||||
|
||||
const rcloneCommand = `rclone copyto ${rcloneFlags.join(" ")} "${volumeBackupPath}/${backupFileName}" "${rcloneDestination}"`;
|
||||
|
||||
const baseCommand = `
|
||||
const backupCommand = `
|
||||
set -e
|
||||
echo "Volume name: ${volumeName}"
|
||||
echo "Backup file name: ${backupFileName}"
|
||||
@@ -33,6 +57,9 @@ export const backupVolume = async (
|
||||
ubuntu \
|
||||
bash -c "cd /volume_data && tar cvf /backup/${backupFileName} ."
|
||||
echo "Volume backup done ✅"
|
||||
`;
|
||||
|
||||
const uploadCommand = `
|
||||
echo "Starting upload to S3..."
|
||||
${rcloneCommand}
|
||||
echo "Upload to S3 done ✅"
|
||||
@@ -42,7 +69,10 @@ export const backupVolume = async (
|
||||
`;
|
||||
|
||||
if (!turnOff) {
|
||||
return baseCommand;
|
||||
return `
|
||||
${backupCommand}
|
||||
${uploadCommand}
|
||||
`;
|
||||
}
|
||||
|
||||
const serviceLockId =
|
||||
@@ -91,9 +121,10 @@ export const backupVolume = async (
|
||||
ACTUAL_REPLICAS=$(docker service inspect ${volumeBackup.application?.appName} --format "{{.Spec.Mode.Replicated.Replicas}}")
|
||||
echo "Actual replicas: $ACTUAL_REPLICAS"
|
||||
docker service update --replicas=0 ${volumeBackup.application?.appName}
|
||||
${baseCommand}
|
||||
${backupCommand}
|
||||
echo "Starting application to $ACTUAL_REPLICAS replicas"
|
||||
docker service update --replicas=$ACTUAL_REPLICAS --with-registry-auth ${volumeBackup.application?.appName}
|
||||
${uploadCommand}
|
||||
`);
|
||||
}
|
||||
if (serviceType === "compose") {
|
||||
@@ -128,8 +159,9 @@ export const backupVolume = async (
|
||||
}
|
||||
return lockWrapper(`
|
||||
${stopCommand}
|
||||
${baseCommand}
|
||||
${backupCommand}
|
||||
${startCommand}
|
||||
${uploadCommand}
|
||||
`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
|
||||
import { sendVolumeBackupNotifications } from "../notifications/volume-backup";
|
||||
import { backupVolume } from "./backup";
|
||||
import { backupVolume, getVolumeServiceAppName } from "./backup";
|
||||
|
||||
// Helper functions to extract project info from volume backup
|
||||
const getProjectName = (
|
||||
@@ -26,6 +26,7 @@ const getProjectName = (
|
||||
volumeBackup.mariadb,
|
||||
volumeBackup.mongo,
|
||||
volumeBackup.redis,
|
||||
volumeBackup.libsql,
|
||||
];
|
||||
|
||||
for (const service of services) {
|
||||
@@ -48,6 +49,7 @@ const getOrganizationId = (
|
||||
volumeBackup.mariadb,
|
||||
volumeBackup.mongo,
|
||||
volumeBackup.redis,
|
||||
volumeBackup.libsql,
|
||||
];
|
||||
|
||||
for (const service of services) {
|
||||
@@ -81,9 +83,9 @@ const cleanupOldVolumeBackups = async (
|
||||
|
||||
try {
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const normalizedPrefix = normalizeS3Path(prefix);
|
||||
const backupFilesPath = `:s3:${destination.bucket}/${normalizedPrefix}`;
|
||||
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} --include \"${volumeName}-*.tar\" :s3:${destination.bucket}/${normalizedPrefix}`;
|
||||
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}`;
|
||||
@@ -131,14 +133,21 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
|
||||
? "mongodb"
|
||||
: volumeBackup.serviceType;
|
||||
|
||||
await sendVolumeBackupNotifications({
|
||||
projectName,
|
||||
applicationName: volumeBackup.name,
|
||||
volumeName: volumeBackup.volumeName,
|
||||
serviceType: mappedServiceType,
|
||||
type: "success",
|
||||
organizationId,
|
||||
});
|
||||
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(
|
||||
@@ -160,14 +169,21 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
|
||||
? "mongodb"
|
||||
: volumeBackup.serviceType;
|
||||
|
||||
await sendVolumeBackupNotifications({
|
||||
projectName,
|
||||
applicationName: volumeBackup.name,
|
||||
volumeName: volumeBackup.volumeName,
|
||||
serviceType: mappedServiceType,
|
||||
type: "error",
|
||||
organizationId,
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user