mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-22 16:35:26 +00:00
feat(backups): add optional custom name for backup files
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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}{}`;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user