feat(backups): add optional custom name for backup files

This commit is contained in:
Narciso
2026-08-04 17:15:01 -04:00
parent 0124613516
commit 623ed062bb
17 changed files with 9315 additions and 29 deletions
+5 -2
View File
@@ -11,7 +11,7 @@ import { sendDatabaseBackupNotifications } from "../notifications/database-backu
import { execAsync, execAsyncRemote } from "../process/execAsync";
import {
getBackupCommand,
getBackupTimestamp,
getBackupFileName,
getS3Credentials,
normalizeS3Path,
} from "./utils";
@@ -25,7 +25,10 @@ export const runComposeBackup = async (
const project = await findProjectById(environment.projectId);
const { prefix, databaseType, serviceName } = backup;
const destination = await findDestinationById(backup.destinationId);
const backupFileName = `${getBackupTimestamp()}.${databaseType === "mongo" ? "bson" : "sql"}.gz`;
const backupFileName = getBackupFileName(
backup.customName,
databaseType === "mongo" ? "bson.gz" : "sql.gz",
);
const s3AppName = serviceName ? `${appName}_${serviceName}` : appName;
const bucketDestination = `${s3AppName}/${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({
+3 -2
View File
@@ -139,9 +139,10 @@ export const keepLatestNBackups = async (
const backupFilesPath = `:s3:${destination.bucket}/${appName}/${normalizeS3Path(backup.prefix)}`;
// --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}`;
// --format "tp" prefixes each line with the real modification time so sorting reflects actual backup age instead of filename order
const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".{sql.gz,bson.gz}"}" --format "tp" ${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{}`;
const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${backup.keepLatestCount}+1)) | cut -d';' -f2- | 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}{}`;
+2 -2
View File
@@ -11,7 +11,7 @@ import { sendDatabaseBackupNotifications } from "../notifications/database-backu
import { execAsync, execAsyncRemote } from "../process/execAsync";
import {
getBackupCommand,
getBackupTimestamp,
getBackupFileName,
getS3Credentials,
normalizeS3Path,
} from "./utils";
@@ -31,7 +31,7 @@ export const runLibsqlBackup = async (
});
const { prefix } = backup;
const destination = await findDestinationById(backup.destinationId);
const backupFileName = `${getBackupTimestamp()}.sql.gz`;
const backupFileName = getBackupFileName(backup.customName, "sql.gz");
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
try {
const rcloneFlags = getS3Credentials(destination);
+2 -2
View File
@@ -11,7 +11,7 @@ import { sendDatabaseBackupNotifications } from "../notifications/database-backu
import { execAsync, execAsyncRemote } from "../process/execAsync";
import {
getBackupCommand,
getBackupTimestamp,
getBackupFileName,
getS3Credentials,
normalizeS3Path,
} from "./utils";
@@ -25,7 +25,7 @@ export const runMariadbBackup = async (
const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = await findDestinationById(backup.destinationId);
const backupFileName = `${getBackupTimestamp()}.sql.gz`;
const backupFileName = getBackupFileName(backup.customName, "sql.gz");
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({
backupId: backup.backupId,
+2 -2
View File
@@ -11,7 +11,7 @@ import { sendDatabaseBackupNotifications } from "../notifications/database-backu
import { execAsync, execAsyncRemote } from "../process/execAsync";
import {
getBackupCommand,
getBackupTimestamp,
getBackupFileName,
getS3Credentials,
normalizeS3Path,
} from "./utils";
@@ -22,7 +22,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = await findDestinationById(backup.destinationId);
const backupFileName = `${getBackupTimestamp()}.bson.gz`;
const backupFileName = getBackupFileName(backup.customName, "bson.gz");
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({
backupId: backup.backupId,
+2 -2
View File
@@ -11,7 +11,7 @@ import { sendDatabaseBackupNotifications } from "../notifications/database-backu
import { execAsync, execAsyncRemote } from "../process/execAsync";
import {
getBackupCommand,
getBackupTimestamp,
getBackupFileName,
getS3Credentials,
normalizeS3Path,
} from "./utils";
@@ -22,7 +22,7 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = await findDestinationById(backup.destinationId);
const backupFileName = `${getBackupTimestamp()}.sql.gz`;
const backupFileName = getBackupFileName(backup.customName, "sql.gz");
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({
backupId: backup.backupId,
@@ -11,7 +11,7 @@ import { sendDatabaseBackupNotifications } from "../notifications/database-backu
import { execAsync, execAsyncRemote } from "../process/execAsync";
import {
getBackupCommand,
getBackupTimestamp,
getBackupFileName,
getS3Credentials,
normalizeS3Path,
} from "./utils";
@@ -31,7 +31,7 @@ export const runPostgresBackup = async (
});
const { prefix } = backup;
const destination = await findDestinationById(backup.destinationId);
const backupFileName = `${getBackupTimestamp()}.sql.gz`;
const backupFileName = getBackupFileName(backup.customName, "sql.gz");
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
try {
const rcloneFlags = getS3Credentials(destination);
@@ -61,6 +61,32 @@ export const removeScheduleBackup = (backupId: string) => {
export const getBackupTimestamp = () =>
new Date().toISOString().replace(/[:.]/g, "-");
const UNSAFE_CUSTOM_NAME_CHARS = /[^a-zA-Z0-9._-]+/g;
export const sanitizeBackupCustomName = (
customName?: string | null,
): string => {
if (!customName) return "";
return customName
.trim()
.replace(UNSAFE_CUSTOM_NAME_CHARS, "-")
.replace(/^-+|-+$/g, "");
};
export const getBackupFileName = (
customName: string | null | undefined,
extension: string,
fixedPrefix?: string,
): string => {
const timestamp = getBackupTimestamp();
const parts = [fixedPrefix, sanitizeBackupCustomName(customName)].filter(
Boolean,
);
return parts.length
? `${parts.join("-")}-${timestamp}.${extension}`
: `${timestamp}.${extension}`;
};
export const normalizeS3Path = (prefix: string) => {
// Trim whitespace and remove leading/trailing slashes
const normalizedPrefix = prefix.trim().replace(/^\/+|\/+$/g, "");
@@ -16,7 +16,7 @@ import { findDestinationById } from "@dokploy/server/services/destination";
import { sendDokployBackupNotifications } from "../notifications/dokploy-backup";
import { execAsync } from "../process/execAsync";
import { redactRcloneCredentials } from "./redact";
import { getBackupTimestamp, getS3Credentials, normalizeS3Path } from "./utils";
import { getBackupFileName, getS3Credentials, normalizeS3Path } from "./utils";
function formatBytes(bytes?: number) {
if (bytes === undefined) return "Unknown size";
@@ -42,10 +42,13 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
try {
const destination = await findDestinationById(backup.destinationId);
const rcloneFlags = getS3Credentials(destination);
const timestamp = getBackupTimestamp();
const { BASE_PATH } = paths();
const tempDir = await mkdtemp(join(tmpdir(), "dokploy-backup-"));
const backupFileName = `webserver-backup-${timestamp}.zip`;
const backupFileName = getBackupFileName(
backup.customName,
"zip",
"webserver-backup",
);
const s3Path = `:s3:${destination.bucket}/${backup.appName}/${normalizeS3Path(backup.prefix)}${backupFileName}`;
try {