mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(multi server): add support for drag n drop
This commit is contained in:
@@ -130,7 +130,7 @@ export const SaveDragNDrop = ({ applicationId }: Props) => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
className="w-fit"
|
className="w-fit"
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
disabled={!zip}
|
disabled={!zip || isLoading}
|
||||||
>
|
>
|
||||||
Deploy{" "}
|
Deploy{" "}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const app = await findApplicationById(input.applicationId as string);
|
const app = await findApplicationById(input.applicationId as string);
|
||||||
await unzipDrop(zipFile, app.appName);
|
await unzipDrop(zipFile, app);
|
||||||
|
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: app.applicationId,
|
applicationId: app.applicationId,
|
||||||
@@ -405,6 +405,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
descriptionLog: "",
|
descriptionLog: "",
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
|
server: !!app.serverId,
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
|
|||||||
@@ -449,6 +449,9 @@ export const stopCompose = async (composeId: string) => {
|
|||||||
const compose = await findComposeById(composeId);
|
const compose = await findComposeById(composeId);
|
||||||
try {
|
try {
|
||||||
if (compose.composeType === "docker-compose") {
|
if (compose.composeType === "docker-compose") {
|
||||||
|
console.log(
|
||||||
|
`cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${compose.appName} stop`,
|
||||||
|
);
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
await execAsyncRemote(
|
await execAsyncRemote(
|
||||||
compose.serverId,
|
compose.serverId,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { COMPOSE_PATH } from "@/server/constants";
|
|||||||
import type { InferResultType } from "@/server/types/with";
|
import type { InferResultType } from "@/server/types/with";
|
||||||
import boxen from "boxen";
|
import boxen from "boxen";
|
||||||
import {
|
import {
|
||||||
getComposePath,
|
|
||||||
writeDomainsToCompose,
|
writeDomainsToCompose,
|
||||||
writeDomainsToComposeRemote,
|
writeDomainsToComposeRemote,
|
||||||
} from "../docker/domain";
|
} from "../docker/domain";
|
||||||
@@ -114,11 +113,6 @@ Compose Type: ${composeType} ✅`;
|
|||||||
|
|
||||||
cd "${projectPath}";
|
cd "${projectPath}";
|
||||||
|
|
||||||
if [ ! -f "${composePath}" ]; then
|
|
||||||
echo "❌ Error: Compose file not found" >> "${logPath}";
|
|
||||||
exit 1;
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker ${command.split(" ").join(" ")} >> "${logPath}" 2>&1 || { echo "Error: ❌ Docker command failed" >> "${logPath}"; exit 1; }
|
docker ${command.split(" ").join(" ")} >> "${logPath}" 2>&1 || { echo "Error: ❌ Docker command failed" >> "${logPath}"; exit 1; }
|
||||||
|
|
||||||
echo "Docker Compose Deployed: ✅" >> "${logPath}"
|
echo "Docker Compose Deployed: ✅" >> "${logPath}"
|
||||||
|
|||||||
@@ -2,12 +2,27 @@ import fs from "node:fs/promises";
|
|||||||
import path, { join } from "node:path";
|
import path, { join } from "node:path";
|
||||||
import { APPLICATIONS_PATH } from "@/server/constants";
|
import { APPLICATIONS_PATH } from "@/server/constants";
|
||||||
import AdmZip from "adm-zip";
|
import AdmZip from "adm-zip";
|
||||||
import { recreateDirectory } from "../filesystem/directory";
|
import {
|
||||||
|
recreateDirectory,
|
||||||
|
recreateDirectoryRemote,
|
||||||
|
} from "../filesystem/directory";
|
||||||
|
import type { Application } from "@/server/api/services/application";
|
||||||
|
import { execAsyncRemote } from "../process/execAsync";
|
||||||
|
import { Client, type SFTPWrapper } from "ssh2";
|
||||||
|
import { findServerById } from "@/server/api/services/server";
|
||||||
|
import { readSSHKey } from "../filesystem/ssh";
|
||||||
|
|
||||||
|
export const unzipDrop = async (zipFile: File, application: Application) => {
|
||||||
|
let sftp: SFTPWrapper | null = null;
|
||||||
|
|
||||||
export const unzipDrop = async (zipFile: File, appName: string) => {
|
|
||||||
try {
|
try {
|
||||||
|
const { appName } = application;
|
||||||
const outputPath = join(APPLICATIONS_PATH, appName, "code");
|
const outputPath = join(APPLICATIONS_PATH, appName, "code");
|
||||||
await recreateDirectory(outputPath);
|
if (application.serverId) {
|
||||||
|
await recreateDirectoryRemote(outputPath, application.serverId);
|
||||||
|
} else {
|
||||||
|
await recreateDirectory(outputPath);
|
||||||
|
}
|
||||||
const arrayBuffer = await zipFile.arrayBuffer();
|
const arrayBuffer = await zipFile.arrayBuffer();
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
@@ -28,6 +43,9 @@ export const unzipDrop = async (zipFile: File, appName: string) => {
|
|||||||
? rootEntries[0]?.entryName.split("/")[0]
|
? rootEntries[0]?.entryName.split("/")[0]
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
|
if (application.serverId) {
|
||||||
|
sftp = await getSFTPConnection(application.serverId);
|
||||||
|
}
|
||||||
for (const entry of zipEntries) {
|
for (const entry of zipEntries) {
|
||||||
let filePath = entry.entryName;
|
let filePath = entry.entryName;
|
||||||
|
|
||||||
@@ -42,15 +60,64 @@ export const unzipDrop = async (zipFile: File, appName: string) => {
|
|||||||
if (!filePath) continue;
|
if (!filePath) continue;
|
||||||
|
|
||||||
const fullPath = path.join(outputPath, filePath);
|
const fullPath = path.join(outputPath, filePath);
|
||||||
if (entry.isDirectory) {
|
|
||||||
await fs.mkdir(fullPath, { recursive: true });
|
if (application.serverId) {
|
||||||
|
if (entry.isDirectory) {
|
||||||
|
await execAsyncRemote(application.serverId, `mkdir -p ${fullPath}`);
|
||||||
|
} else {
|
||||||
|
if (sftp === null) throw new Error("No SFTP connection available");
|
||||||
|
await uploadFileToServer(sftp, entry.getData(), fullPath);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
if (entry.isDirectory) {
|
||||||
await fs.writeFile(fullPath, entry.getData());
|
await fs.mkdir(fullPath, { recursive: true });
|
||||||
|
} else {
|
||||||
|
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
||||||
|
await fs.writeFile(fullPath, entry.getData());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error processing ZIP file:", error);
|
console.error("Error processing ZIP file:", error);
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
sftp?.end();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSFTPConnection = async (serverId: string): Promise<SFTPWrapper> => {
|
||||||
|
const server = await findServerById(serverId);
|
||||||
|
if (!server.sshKeyId) throw new Error("No SSH key available for this server");
|
||||||
|
|
||||||
|
const keys = await readSSHKey(server.sshKeyId);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const conn = new Client();
|
||||||
|
conn
|
||||||
|
.on("ready", () => {
|
||||||
|
conn.sftp((err, sftp) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve(sftp);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.connect({
|
||||||
|
host: server.ipAddress,
|
||||||
|
port: server.port,
|
||||||
|
username: server.username,
|
||||||
|
privateKey: keys.privateKey,
|
||||||
|
timeout: 99999,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadFileToServer = (
|
||||||
|
sftp: SFTPWrapper,
|
||||||
|
data: Buffer,
|
||||||
|
remotePath: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
sftp.writeFile(remotePath, data, (err) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -151,14 +151,21 @@ export const writeDomainsToComposeRemote = async (
|
|||||||
try {
|
try {
|
||||||
const composeConverted = await addDomainToCompose(compose, domains);
|
const composeConverted = await addDomainToCompose(compose, domains);
|
||||||
const path = getComposePath(compose);
|
const path = getComposePath(compose);
|
||||||
|
|
||||||
|
if (!composeConverted) {
|
||||||
|
return `
|
||||||
|
echo "❌ Error: Compose file not found" >> ${logPath};
|
||||||
|
exit 1;
|
||||||
|
`;
|
||||||
|
}
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
const composeString = dump(composeConverted, { lineWidth: 1000 });
|
const composeString = dump(composeConverted, { lineWidth: 1000 });
|
||||||
const encodedContent = encodeBase64(composeString);
|
const encodedContent = encodeBase64(composeString);
|
||||||
return `echo "${encodedContent}" | base64 -d > "${path}";`;
|
return `echo "${encodedContent}" | base64 -d > "${path}";`;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return `
|
// @ts-ignore
|
||||||
echo "❌ Has occured an error: ${error?.message || error}" >> ${logPath};
|
return `echo "❌ Has occured an error: ${error?.message || error}" >> ${logPath};
|
||||||
exit 1;
|
exit 1;
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,20 @@ export const recreateDirectory = async (pathFolder: string): Promise<void> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const recreateDirectoryRemote = async (
|
||||||
|
pathFolder: string,
|
||||||
|
serverId: string | null,
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await execAsyncRemote(
|
||||||
|
serverId,
|
||||||
|
`rm -rf ${pathFolder}; mkdir -p ${pathFolder}`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error recreating directory '${pathFolder}':`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const removeDirectoryIfExistsContent = async (
|
export const removeDirectoryIfExistsContent = async (
|
||||||
path: string,
|
path: string,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
|
|||||||
Reference in New Issue
Block a user