mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Implement restore functionality for various database types
- Added `apiRestoreBackup` schema to define input requirements for restore operations. - Refactored restore utilities for PostgreSQL, MySQL, MariaDB, and MongoDB to utilize a unified command generation approach, enhancing maintainability. - Improved logging during restore processes to provide clearer feedback on command execution and success/failure states. - Streamlined the handling of database credentials and backup file paths across different database types, ensuring consistency and reducing redundancy.
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
|||||||
apiCreateBackup,
|
apiCreateBackup,
|
||||||
apiFindOneBackup,
|
apiFindOneBackup,
|
||||||
apiRemoveBackup,
|
apiRemoveBackup,
|
||||||
|
apiRestoreBackup,
|
||||||
apiUpdateBackup,
|
apiUpdateBackup,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
|
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
|
||||||
@@ -366,23 +367,7 @@ export const backupRouter = createTRPCRouter({
|
|||||||
override: true,
|
override: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.input(
|
.input(apiRestoreBackup)
|
||||||
z.object({
|
|
||||||
databaseId: z.string(),
|
|
||||||
databaseType: z.enum([
|
|
||||||
"postgres",
|
|
||||||
"mysql",
|
|
||||||
"mariadb",
|
|
||||||
"mongo",
|
|
||||||
"web-server",
|
|
||||||
]),
|
|
||||||
backupType: z.enum(["database", "compose"]),
|
|
||||||
databaseName: z.string().min(1),
|
|
||||||
backupFile: z.string().min(1),
|
|
||||||
destinationId: z.string().min(1),
|
|
||||||
metadata: z.any(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.subscription(async ({ input }) => {
|
.subscription(async ({ input }) => {
|
||||||
const destination = await findDestinationById(input.destinationId);
|
const destination = await findDestinationById(input.destinationId);
|
||||||
if (input.backupType === "database") {
|
if (input.backupType === "database") {
|
||||||
@@ -390,57 +375,33 @@ export const backupRouter = createTRPCRouter({
|
|||||||
const postgres = await findPostgresById(input.databaseId);
|
const postgres = await findPostgresById(input.databaseId);
|
||||||
|
|
||||||
return observable<string>((emit) => {
|
return observable<string>((emit) => {
|
||||||
restorePostgresBackup(
|
restorePostgresBackup(postgres, destination, input, (log) => {
|
||||||
postgres,
|
emit.next(log);
|
||||||
destination,
|
});
|
||||||
input.databaseName,
|
|
||||||
input.backupFile,
|
|
||||||
(log) => {
|
|
||||||
emit.next(log);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (input.databaseType === "mysql") {
|
if (input.databaseType === "mysql") {
|
||||||
const mysql = await findMySqlById(input.databaseId);
|
const mysql = await findMySqlById(input.databaseId);
|
||||||
return observable<string>((emit) => {
|
return observable<string>((emit) => {
|
||||||
restoreMySqlBackup(
|
restoreMySqlBackup(mysql, destination, input, (log) => {
|
||||||
mysql,
|
emit.next(log);
|
||||||
destination,
|
});
|
||||||
input.databaseName,
|
|
||||||
input.backupFile,
|
|
||||||
(log) => {
|
|
||||||
emit.next(log);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (input.databaseType === "mariadb") {
|
if (input.databaseType === "mariadb") {
|
||||||
const mariadb = await findMariadbById(input.databaseId);
|
const mariadb = await findMariadbById(input.databaseId);
|
||||||
return observable<string>((emit) => {
|
return observable<string>((emit) => {
|
||||||
restoreMariadbBackup(
|
restoreMariadbBackup(mariadb, destination, input, (log) => {
|
||||||
mariadb,
|
emit.next(log);
|
||||||
destination,
|
});
|
||||||
input.databaseName,
|
|
||||||
input.backupFile,
|
|
||||||
(log) => {
|
|
||||||
emit.next(log);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (input.databaseType === "mongo") {
|
if (input.databaseType === "mongo") {
|
||||||
const mongo = await findMongoById(input.databaseId);
|
const mongo = await findMongoById(input.databaseId);
|
||||||
return observable<string>((emit) => {
|
return observable<string>((emit) => {
|
||||||
restoreMongoBackup(
|
restoreMongoBackup(mongo, destination, input, (log) => {
|
||||||
mongo,
|
emit.next(log);
|
||||||
destination,
|
});
|
||||||
input.databaseName,
|
|
||||||
input.backupFile,
|
|
||||||
(log) => {
|
|
||||||
emit.next(log);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (input.databaseType === "web-server") {
|
if (input.databaseType === "web-server") {
|
||||||
@@ -454,16 +415,9 @@ export const backupRouter = createTRPCRouter({
|
|||||||
if (input.backupType === "compose") {
|
if (input.backupType === "compose") {
|
||||||
const compose = await findComposeById(input.databaseId);
|
const compose = await findComposeById(input.databaseId);
|
||||||
return observable<string>((emit) => {
|
return observable<string>((emit) => {
|
||||||
restoreComposeBackup(
|
restoreComposeBackup(compose, destination, input, (log) => {
|
||||||
compose,
|
emit.next(log);
|
||||||
destination,
|
});
|
||||||
input.databaseName,
|
|
||||||
input.backupFile,
|
|
||||||
input.metadata,
|
|
||||||
(log) => {
|
|
||||||
emit.next(log);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -191,3 +191,39 @@ export const apiUpdateBackup = createSchema
|
|||||||
databaseType: true,
|
databaseType: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
|
export const apiRestoreBackup = z.object({
|
||||||
|
databaseId: z.string(),
|
||||||
|
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo", "web-server"]),
|
||||||
|
backupType: z.enum(["database", "compose"]),
|
||||||
|
databaseName: z.string().min(1),
|
||||||
|
backupFile: z.string().min(1),
|
||||||
|
destinationId: z.string().min(1),
|
||||||
|
metadata: z
|
||||||
|
.object({
|
||||||
|
serviceName: z.string().optional(),
|
||||||
|
postgres: z
|
||||||
|
.object({
|
||||||
|
databaseUser: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
mariadb: z
|
||||||
|
.object({
|
||||||
|
databaseUser: z.string(),
|
||||||
|
databasePassword: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
mongo: z
|
||||||
|
.object({
|
||||||
|
databaseUser: z.string(),
|
||||||
|
databasePassword: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
mysql: z
|
||||||
|
.object({
|
||||||
|
databaseRootPassword: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|||||||
@@ -106,11 +106,11 @@ 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"`;
|
return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --archive --authenticationDatabase admin --gzip"`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getServiceContainerCommand = (appName: string) => {
|
export const getServiceContainerCommand = (appName: string) => {
|
||||||
return `docker ps -q --filter "status=running" --filter "label=com.docker.swarm.service.name=${appName}" | head -n 1`;
|
return `docker ps -q --filter "status=running" --filter "label=com.docker.swarm.service.name=${appName}" | head -n 1`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getComposeContainerCommand = (
|
export const getComposeContainerCommand = (
|
||||||
appName: string,
|
appName: string,
|
||||||
serviceName: string,
|
serviceName: string,
|
||||||
composeType: "stack" | "docker-compose" | undefined,
|
composeType: "stack" | "docker-compose" | undefined,
|
||||||
@@ -226,7 +226,7 @@ export const getBackupCommand = (
|
|||||||
CONTAINER_ID=$(${containerSearch})
|
CONTAINER_ID=$(${containerSearch})
|
||||||
|
|
||||||
if [ -z "$CONTAINER_ID" ]; then
|
if [ -z "$CONTAINER_ID" ]; then
|
||||||
echo "[$(date)] ❌ Container not found" >> ${logPath};
|
echo "[$(date)] ❌ Error: Container not found" >> ${logPath};
|
||||||
exit 1;
|
exit 1;
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ export const getBackupCommand = (
|
|||||||
|
|
||||||
# Run the backup command and capture the exit status
|
# Run the backup command and capture the exit status
|
||||||
BACKUP_OUTPUT=$(${backupCommand} 2>&1 >/dev/null) || {
|
BACKUP_OUTPUT=$(${backupCommand} 2>&1 >/dev/null) || {
|
||||||
echo "[$(date)] ❌ backup failed" >> ${logPath};
|
echo "[$(date)] ❌ Error: Backup failed" >> ${logPath};
|
||||||
echo "Error: $BACKUP_OUTPUT" >> ${logPath};
|
echo "Error: $BACKUP_OUTPUT" >> ${logPath};
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
@@ -244,7 +244,7 @@ export const getBackupCommand = (
|
|||||||
|
|
||||||
# Run the upload command and capture the exit status
|
# Run the upload command and capture the exit status
|
||||||
UPLOAD_OUTPUT=$(${backupCommand} | ${rcloneCommand} 2>&1 >/dev/null) || {
|
UPLOAD_OUTPUT=$(${backupCommand} | ${rcloneCommand} 2>&1 >/dev/null) || {
|
||||||
echo "[$(date)] ❌ Upload to S3 failed" >> ${logPath};
|
echo "[$(date)] ❌ Error: Upload to S3 failed" >> ${logPath};
|
||||||
echo "Error: $UPLOAD_OUTPUT" >> ${logPath};
|
echo "Error: $UPLOAD_OUTPUT" >> ${logPath};
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,76 +2,84 @@ import type { Destination } from "@dokploy/server/services/destination";
|
|||||||
import type { Compose } from "@dokploy/server/services/compose";
|
import type { Compose } from "@dokploy/server/services/compose";
|
||||||
import { getS3Credentials } from "../backups/utils";
|
import { getS3Credentials } from "../backups/utils";
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
import type { Backup } from "@dokploy/server/services/backup";
|
import { getRestoreCommand } from "./utils";
|
||||||
import { getComposeContainer } from "../docker/utils";
|
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||||
import {
|
import type { z } from "zod";
|
||||||
getMariadbRestoreCommand,
|
|
||||||
getMongoRestoreCommand,
|
interface DatabaseCredentials {
|
||||||
getMysqlRestoreCommand,
|
databaseUser?: string;
|
||||||
getPostgresRestoreCommand,
|
databasePassword?: string;
|
||||||
} from "./utils";
|
}
|
||||||
|
|
||||||
export const restoreComposeBackup = async (
|
export const restoreComposeBackup = async (
|
||||||
compose: Compose,
|
compose: Compose,
|
||||||
destination: Destination,
|
destination: Destination,
|
||||||
database: string,
|
backupInput: z.infer<typeof apiRestoreBackup>,
|
||||||
backupFile: string,
|
|
||||||
metadata: Backup["metadata"] & { serviceName: string },
|
|
||||||
emit: (log: string) => void,
|
emit: (log: string) => void,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const { serverId } = compose;
|
if (backupInput.databaseType === "web-server") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { serverId, appName, composeType } = compose;
|
||||||
|
|
||||||
const rcloneFlags = getS3Credentials(destination);
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
const bucketPath = `:s3:${destination.bucket}`;
|
const bucketPath = `:s3:${destination.bucket}`;
|
||||||
const backupPath = `${bucketPath}/${backupFile}`;
|
const backupPath = `${bucketPath}/${backupInput.backupFile}`;
|
||||||
|
let rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip`;
|
||||||
|
|
||||||
const { Id: containerId } = await getComposeContainer(
|
if (backupInput.metadata?.mongo) {
|
||||||
compose,
|
rcloneCommand = `rclone copy ${rcloneFlags.join(" ")} "${backupPath}"`;
|
||||||
metadata.serviceName || "",
|
|
||||||
);
|
|
||||||
let restoreCommand = "";
|
|
||||||
|
|
||||||
if (metadata.postgres) {
|
|
||||||
restoreCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | ${getPostgresRestoreCommand(containerId, database, metadata.postgres.databaseUser)}`;
|
|
||||||
} else if (metadata.mariadb) {
|
|
||||||
restoreCommand = `
|
|
||||||
rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | ${getMariadbRestoreCommand(containerId, database, metadata.mariadb.databaseUser, metadata.mariadb.databasePassword)}
|
|
||||||
`;
|
|
||||||
} else if (metadata.mysql) {
|
|
||||||
restoreCommand = `
|
|
||||||
rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | ${getMysqlRestoreCommand(containerId, database, metadata.mysql.databaseRootPassword)}
|
|
||||||
`;
|
|
||||||
} else if (metadata.mongo) {
|
|
||||||
const tempDir = "/tmp/dokploy-restore";
|
|
||||||
const fileName = backupFile.split("/").pop() || "backup.dump.gz";
|
|
||||||
const decompressedName = fileName.replace(".gz", "");
|
|
||||||
restoreCommand = `\
|
|
||||||
rm -rf ${tempDir} && \
|
|
||||||
mkdir -p ${tempDir} && \
|
|
||||||
rclone copy ${rcloneFlags.join(" ")} "${backupPath}" ${tempDir} && \
|
|
||||||
cd ${tempDir} && \
|
|
||||||
gunzip -f "${fileName}" && \
|
|
||||||
${getMongoRestoreCommand(containerId, database, metadata.mongo.databaseUser, metadata.mongo.databasePassword)} < "${decompressedName}" && \
|
|
||||||
rm -rf ${tempDir}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let credentials: DatabaseCredentials;
|
||||||
|
|
||||||
|
switch (backupInput.databaseType) {
|
||||||
|
case "postgres":
|
||||||
|
credentials = {
|
||||||
|
databaseUser: backupInput.metadata?.postgres?.databaseUser,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "mariadb":
|
||||||
|
credentials = {
|
||||||
|
databaseUser: backupInput.metadata?.mariadb?.databaseUser,
|
||||||
|
databasePassword: backupInput.metadata?.mariadb?.databasePassword,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "mysql":
|
||||||
|
credentials = {
|
||||||
|
databasePassword: backupInput.metadata?.mysql?.databaseRootPassword,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "mongo":
|
||||||
|
credentials = {
|
||||||
|
databaseUser: backupInput.metadata?.mongo?.databaseUser,
|
||||||
|
databasePassword: backupInput.metadata?.mongo?.databasePassword,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreCommand = getRestoreCommand({
|
||||||
|
appName: appName,
|
||||||
|
serviceName: backupInput.metadata?.serviceName,
|
||||||
|
type: backupInput.databaseType,
|
||||||
|
credentials: {
|
||||||
|
database: backupInput.databaseName,
|
||||||
|
...credentials,
|
||||||
|
},
|
||||||
|
restoreType: composeType,
|
||||||
|
rcloneCommand,
|
||||||
|
});
|
||||||
|
|
||||||
emit("Starting restore...");
|
emit("Starting restore...");
|
||||||
emit(`Backup path: ${backupPath}`);
|
emit(`Backup path: ${backupPath}`);
|
||||||
|
|
||||||
emit(`Executing command: ${restoreCommand}`);
|
emit(`Executing command: ${restoreCommand}`);
|
||||||
|
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
const { stdout, stderr } = await execAsyncRemote(
|
await execAsyncRemote(serverId, restoreCommand);
|
||||||
serverId,
|
|
||||||
restoreCommand,
|
|
||||||
);
|
|
||||||
emit(stdout);
|
|
||||||
emit(stderr);
|
|
||||||
} else {
|
} else {
|
||||||
const { stdout, stderr } = await execAsync(restoreCommand);
|
await execAsync(restoreCommand);
|
||||||
emit(stdout);
|
|
||||||
emit(stderr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
emit("Restore completed successfully!");
|
emit("Restore completed successfully!");
|
||||||
|
|||||||
@@ -1,36 +1,37 @@
|
|||||||
import type { Destination } from "@dokploy/server/services/destination";
|
import type { Destination } from "@dokploy/server/services/destination";
|
||||||
import type { Mariadb } from "@dokploy/server/services/mariadb";
|
import type { Mariadb } from "@dokploy/server/services/mariadb";
|
||||||
import { getS3Credentials } from "../backups/utils";
|
import { getS3Credentials } from "../backups/utils";
|
||||||
import { getServiceContainer } from "../docker/utils";
|
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
import { getMariadbRestoreCommand } from "./utils";
|
import { getRestoreCommand } from "./utils";
|
||||||
|
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
export const restoreMariadbBackup = async (
|
export const restoreMariadbBackup = async (
|
||||||
mariadb: Mariadb,
|
mariadb: Mariadb,
|
||||||
destination: Destination,
|
destination: Destination,
|
||||||
database: string,
|
backupInput: z.infer<typeof apiRestoreBackup>,
|
||||||
backupFile: string,
|
|
||||||
emit: (log: string) => void,
|
emit: (log: string) => void,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const { appName, databasePassword, databaseUser, serverId } = mariadb;
|
const { appName, serverId, databaseUser, databasePassword } = mariadb;
|
||||||
|
|
||||||
const rcloneFlags = getS3Credentials(destination);
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
const bucketPath = `:s3:${destination.bucket}`;
|
const bucketPath = `:s3:${destination.bucket}`;
|
||||||
const backupPath = `${bucketPath}/${backupFile}`;
|
const backupPath = `${bucketPath}/${backupInput.backupFile}`;
|
||||||
|
|
||||||
const { Id: containerId } = await getServiceContainer(appName, serverId);
|
const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip`;
|
||||||
|
|
||||||
const restoreCommand = getMariadbRestoreCommand(
|
const command = getRestoreCommand({
|
||||||
containerId,
|
appName,
|
||||||
database,
|
credentials: {
|
||||||
databaseUser,
|
database: backupInput.databaseName,
|
||||||
databasePassword || "",
|
databaseUser,
|
||||||
);
|
databasePassword,
|
||||||
|
},
|
||||||
const command = `
|
type: "mariadb",
|
||||||
rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | ${restoreCommand}
|
rcloneCommand,
|
||||||
`;
|
restoreType: "database",
|
||||||
|
});
|
||||||
|
|
||||||
emit("Starting restore...");
|
emit("Starting restore...");
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import type { Destination } from "@dokploy/server/services/destination";
|
import type { Destination } from "@dokploy/server/services/destination";
|
||||||
import type { Mongo } from "@dokploy/server/services/mongo";
|
import type { Mongo } from "@dokploy/server/services/mongo";
|
||||||
import { getS3Credentials } from "../backups/utils";
|
import { getS3Credentials } from "../backups/utils";
|
||||||
import { getServiceContainer } from "../docker/utils";
|
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
import { getMongoRestoreCommand } from "./utils";
|
import { getRestoreCommand } from "./utils";
|
||||||
|
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
export const restoreMongoBackup = async (
|
export const restoreMongoBackup = async (
|
||||||
mongo: Mongo,
|
mongo: Mongo,
|
||||||
destination: Destination,
|
destination: Destination,
|
||||||
database: string,
|
backupInput: z.infer<typeof apiRestoreBackup>,
|
||||||
backupFile: string,
|
|
||||||
emit: (log: string) => void,
|
emit: (log: string) => void,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
@@ -17,38 +17,30 @@ export const restoreMongoBackup = async (
|
|||||||
|
|
||||||
const rcloneFlags = getS3Credentials(destination);
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
const bucketPath = `:s3:${destination.bucket}`;
|
const bucketPath = `:s3:${destination.bucket}`;
|
||||||
const backupPath = `${bucketPath}/${backupFile}`;
|
const backupPath = `${bucketPath}/${backupInput.backupFile}`;
|
||||||
|
const rcloneCommand = `rclone copy ${rcloneFlags.join(" ")} "${backupPath}"`;
|
||||||
|
|
||||||
const { Id: containerId } = await getServiceContainer(appName, serverId);
|
const command = getRestoreCommand({
|
||||||
|
appName,
|
||||||
// For MongoDB, we need to first download the backup file since mongorestore expects a directory
|
type: "mongo",
|
||||||
const tempDir = "/tmp/dokploy-restore";
|
credentials: {
|
||||||
const fileName = backupFile.split("/").pop() || "backup.dump.gz";
|
database: backupInput.databaseName,
|
||||||
const decompressedName = fileName.replace(".gz", "");
|
databaseUser,
|
||||||
const restoreCommand = getMongoRestoreCommand(
|
databasePassword,
|
||||||
containerId,
|
},
|
||||||
database,
|
restoreType: "database",
|
||||||
databaseUser,
|
rcloneCommand,
|
||||||
databasePassword || "",
|
backupFile: backupInput.backupFile,
|
||||||
);
|
});
|
||||||
|
|
||||||
const downloadCommand = `\
|
|
||||||
rm -rf ${tempDir} && \
|
|
||||||
mkdir -p ${tempDir} && \
|
|
||||||
rclone copy ${rcloneFlags.join(" ")} "${backupPath}" ${tempDir} && \
|
|
||||||
cd ${tempDir} && \
|
|
||||||
gunzip -f "${fileName}" && \
|
|
||||||
${restoreCommand} < "${decompressedName}" && \
|
|
||||||
rm -rf ${tempDir}`;
|
|
||||||
|
|
||||||
emit("Starting restore...");
|
emit("Starting restore...");
|
||||||
|
|
||||||
emit(`Executing command: ${downloadCommand}`);
|
emit(`Executing command: ${command}`);
|
||||||
|
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
await execAsyncRemote(serverId, downloadCommand);
|
await execAsyncRemote(serverId, command);
|
||||||
} else {
|
} else {
|
||||||
await execAsync(downloadCommand);
|
await execAsync(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit("Restore completed successfully!");
|
emit("Restore completed successfully!");
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import type { Destination } from "@dokploy/server/services/destination";
|
import type { Destination } from "@dokploy/server/services/destination";
|
||||||
import type { MySql } from "@dokploy/server/services/mysql";
|
import type { MySql } from "@dokploy/server/services/mysql";
|
||||||
import { getS3Credentials } from "../backups/utils";
|
import { getS3Credentials } from "../backups/utils";
|
||||||
import { getServiceContainer } from "../docker/utils";
|
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
import { getMysqlRestoreCommand } from "./utils";
|
import { getRestoreCommand } from "./utils";
|
||||||
|
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
export const restoreMySqlBackup = async (
|
export const restoreMySqlBackup = async (
|
||||||
mysql: MySql,
|
mysql: MySql,
|
||||||
destination: Destination,
|
destination: Destination,
|
||||||
database: string,
|
backupInput: z.infer<typeof apiRestoreBackup>,
|
||||||
backupFile: string,
|
|
||||||
emit: (log: string) => void,
|
emit: (log: string) => void,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
@@ -17,19 +17,20 @@ export const restoreMySqlBackup = async (
|
|||||||
|
|
||||||
const rcloneFlags = getS3Credentials(destination);
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
const bucketPath = `:s3:${destination.bucket}`;
|
const bucketPath = `:s3:${destination.bucket}`;
|
||||||
const backupPath = `${bucketPath}/${backupFile}`;
|
const backupPath = `${bucketPath}/${backupInput.backupFile}`;
|
||||||
|
|
||||||
const { Id: containerId } = await getServiceContainer(appName, serverId);
|
const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip`;
|
||||||
|
|
||||||
const restoreCommand = getMysqlRestoreCommand(
|
const command = getRestoreCommand({
|
||||||
containerId,
|
appName,
|
||||||
database,
|
type: "mysql",
|
||||||
databaseRootPassword || "",
|
credentials: {
|
||||||
);
|
database: backupInput.databaseName,
|
||||||
|
databasePassword: databaseRootPassword,
|
||||||
const command = `
|
},
|
||||||
rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | ${restoreCommand}
|
restoreType: "database",
|
||||||
`;
|
rcloneCommand,
|
||||||
|
});
|
||||||
|
|
||||||
emit("Starting restore...");
|
emit("Starting restore...");
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import type { Destination } from "@dokploy/server/services/destination";
|
import type { Destination } from "@dokploy/server/services/destination";
|
||||||
import type { Postgres } from "@dokploy/server/services/postgres";
|
import type { Postgres } from "@dokploy/server/services/postgres";
|
||||||
import { getS3Credentials } from "../backups/utils";
|
import { getS3Credentials } from "../backups/utils";
|
||||||
import { getServiceContainer } from "../docker/utils";
|
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
import { getPostgresRestoreCommand } from "./utils";
|
import { getRestoreCommand } from "./utils";
|
||||||
|
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
export const restorePostgresBackup = async (
|
export const restorePostgresBackup = async (
|
||||||
postgres: Postgres,
|
postgres: Postgres,
|
||||||
destination: Destination,
|
destination: Destination,
|
||||||
database: string,
|
backupInput: z.infer<typeof apiRestoreBackup>,
|
||||||
backupFile: string,
|
|
||||||
emit: (log: string) => void,
|
emit: (log: string) => void,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
@@ -18,32 +18,30 @@ export const restorePostgresBackup = async (
|
|||||||
const rcloneFlags = getS3Credentials(destination);
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
const bucketPath = `:s3:${destination.bucket}`;
|
const bucketPath = `:s3:${destination.bucket}`;
|
||||||
|
|
||||||
const backupPath = `${bucketPath}/${backupFile}`;
|
const backupPath = `${bucketPath}/${backupInput.backupFile}`;
|
||||||
|
|
||||||
const { Id: containerId } = await getServiceContainer(appName, serverId);
|
const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip`;
|
||||||
|
|
||||||
emit("Starting restore...");
|
emit("Starting restore...");
|
||||||
emit(`Backup path: ${backupPath}`);
|
emit(`Backup path: ${backupPath}`);
|
||||||
|
|
||||||
const restoreCommand = getPostgresRestoreCommand(
|
const command = getRestoreCommand({
|
||||||
containerId,
|
appName,
|
||||||
database,
|
credentials: {
|
||||||
databaseUser,
|
database: backupInput.databaseName,
|
||||||
);
|
databaseUser,
|
||||||
|
},
|
||||||
const command = `\
|
type: "postgres",
|
||||||
rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | ${restoreCommand}`;
|
rcloneCommand,
|
||||||
|
restoreType: "database",
|
||||||
|
});
|
||||||
|
|
||||||
emit(`Executing command: ${command}`);
|
emit(`Executing command: ${command}`);
|
||||||
|
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
await execAsyncRemote(serverId, command);
|
||||||
emit(stdout);
|
|
||||||
emit(stderr);
|
|
||||||
} else {
|
} else {
|
||||||
const { stdout, stderr } = await execAsync(command);
|
await execAsync(command);
|
||||||
emit(stdout);
|
|
||||||
emit(stderr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
emit("Restore completed successfully!");
|
emit("Restore completed successfully!");
|
||||||
|
|||||||
@@ -1,33 +1,131 @@
|
|||||||
|
import {
|
||||||
|
getComposeContainerCommand,
|
||||||
|
getServiceContainerCommand,
|
||||||
|
} from "../backups/utils";
|
||||||
|
|
||||||
export const getPostgresRestoreCommand = (
|
export const getPostgresRestoreCommand = (
|
||||||
containerId: string,
|
|
||||||
database: string,
|
database: string,
|
||||||
databaseUser: string,
|
databaseUser: string,
|
||||||
) => {
|
) => {
|
||||||
return `docker exec -i ${containerId} sh -c "pg_restore -U ${databaseUser} -d ${database} --clean --if-exists"`;
|
return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U ${databaseUser} -d ${database} --clean --if-exists"`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMariadbRestoreCommand = (
|
export const getMariadbRestoreCommand = (
|
||||||
containerId: string,
|
|
||||||
database: string,
|
database: string,
|
||||||
databaseUser: string,
|
databaseUser: string,
|
||||||
databasePassword: string,
|
databasePassword: string,
|
||||||
) => {
|
) => {
|
||||||
return `docker exec -i ${containerId} sh -c "mariadb -u ${databaseUser} -p${databasePassword} ${database}"`;
|
return `docker exec -i $CONTAINER_ID sh -c "mariadb -u ${databaseUser} -p${databasePassword} ${database}"`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMysqlRestoreCommand = (
|
export const getMysqlRestoreCommand = (
|
||||||
containerId: string,
|
|
||||||
database: string,
|
database: string,
|
||||||
databasePassword: string,
|
databasePassword: string,
|
||||||
) => {
|
) => {
|
||||||
return `docker exec -i ${containerId} sh -c "mysql -u root -p${databasePassword} ${database}"`;
|
return `docker exec -i $CONTAINER_ID sh -c "mysql -u root -p${databasePassword} ${database}"`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMongoRestoreCommand = (
|
export const getMongoRestoreCommand = (
|
||||||
containerId: string,
|
|
||||||
database: string,
|
database: string,
|
||||||
databaseUser: string,
|
databaseUser: string,
|
||||||
databasePassword: string,
|
databasePassword: string,
|
||||||
) => {
|
) => {
|
||||||
return `docker exec -i ${containerId} 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"`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getComposeSearchCommand = (
|
||||||
|
appName: string,
|
||||||
|
type: "stack" | "docker-compose" | "database",
|
||||||
|
serviceName?: string,
|
||||||
|
) => {
|
||||||
|
if (type === "database") {
|
||||||
|
return getServiceContainerCommand(appName || "");
|
||||||
|
}
|
||||||
|
return getComposeContainerCommand(appName || "", serviceName || "", type);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DatabaseCredentials {
|
||||||
|
database: string;
|
||||||
|
databaseUser?: string;
|
||||||
|
databasePassword?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateRestoreCommand = (
|
||||||
|
type: "postgres" | "mariadb" | "mysql" | "mongo",
|
||||||
|
credentials: DatabaseCredentials,
|
||||||
|
) => {
|
||||||
|
const { database, databaseUser, databasePassword } = credentials;
|
||||||
|
switch (type) {
|
||||||
|
case "postgres":
|
||||||
|
return getPostgresRestoreCommand(database, databaseUser || "");
|
||||||
|
case "mariadb":
|
||||||
|
return getMariadbRestoreCommand(
|
||||||
|
database,
|
||||||
|
databaseUser || "",
|
||||||
|
databasePassword || "",
|
||||||
|
);
|
||||||
|
case "mysql":
|
||||||
|
return getMysqlRestoreCommand(database, databasePassword || "");
|
||||||
|
case "mongo":
|
||||||
|
return getMongoRestoreCommand(
|
||||||
|
database,
|
||||||
|
databaseUser || "",
|
||||||
|
databasePassword || "",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMongoSpecificCommand = (
|
||||||
|
rcloneCommand: string,
|
||||||
|
restoreCommand: string,
|
||||||
|
backupFile: string,
|
||||||
|
): string => {
|
||||||
|
const tempDir = "/tmp/dokploy-restore";
|
||||||
|
const fileName = backupFile.split("/").pop() || "backup.dump.gz";
|
||||||
|
const decompressedName = fileName.replace(".gz", "");
|
||||||
|
return `
|
||||||
|
rm -rf ${tempDir} && \
|
||||||
|
mkdir -p ${tempDir} && \
|
||||||
|
${rcloneCommand} ${tempDir} && \
|
||||||
|
cd ${tempDir} && \
|
||||||
|
gunzip -f "${fileName}" && \
|
||||||
|
${restoreCommand} < "${decompressedName}" && \
|
||||||
|
rm -rf ${tempDir}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RestoreOptions {
|
||||||
|
appName: string;
|
||||||
|
type: "postgres" | "mariadb" | "mysql" | "mongo";
|
||||||
|
restoreType: "stack" | "docker-compose" | "database";
|
||||||
|
credentials: DatabaseCredentials;
|
||||||
|
serviceName?: string;
|
||||||
|
rcloneCommand: string;
|
||||||
|
backupFile?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRestoreCommand = ({
|
||||||
|
appName,
|
||||||
|
type,
|
||||||
|
restoreType,
|
||||||
|
credentials,
|
||||||
|
serviceName,
|
||||||
|
rcloneCommand,
|
||||||
|
backupFile,
|
||||||
|
}: RestoreOptions) => {
|
||||||
|
const containerSearch = getComposeSearchCommand(
|
||||||
|
appName,
|
||||||
|
restoreType,
|
||||||
|
serviceName,
|
||||||
|
);
|
||||||
|
const restoreCommand = generateRestoreCommand(type, credentials);
|
||||||
|
let cmd = `CONTAINER_ID=$(${containerSearch})`;
|
||||||
|
|
||||||
|
if (type !== "mongo") {
|
||||||
|
cmd += ` && ${rcloneCommand} | ${restoreCommand}`;
|
||||||
|
} else {
|
||||||
|
cmd += ` && ${getMongoSpecificCommand(rcloneCommand, restoreCommand, backupFile || "")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user