diff --git a/backend/server/package-lock.json b/backend/server/package-lock.json index d167660..288efe9 100644 --- a/backend/server/package-lock.json +++ b/backend/server/package-lock.json @@ -15,6 +15,7 @@ "e2b": "^0.16.1", "express": "^4.19.2", "rate-limiter-flexible": "^5.0.3", + "simple-git": "^3.25.0", "socket.io": "^4.7.5", "ssh2": "^1.15.0", "zod": "^3.22.4" @@ -77,6 +78,40 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/file-exists/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@kwsites/file-exists/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.1.tgz", @@ -1695,6 +1730,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-git": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.25.0.tgz", + "integrity": "sha512-KIY5sBnzc4yEcJXW7Tdv4viEz8KyG+nU0hay+DWZasvdFOYKeUZ6Xc25LUHHjw0tinPT7O1eY6pzX7pRT1K8rw==", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/simple-git/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/simple-git/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", diff --git a/backend/server/package.json b/backend/server/package.json index dc0f09c..19e5ee5 100644 --- a/backend/server/package.json +++ b/backend/server/package.json @@ -17,6 +17,7 @@ "e2b": "^0.16.1", "express": "^4.19.2", "rate-limiter-flexible": "^5.0.3", + "simple-git": "^3.25.0", "socket.io": "^4.7.5", "ssh2": "^1.15.0", "zod": "^3.22.4" diff --git a/backend/server/src/SecureGitClient.ts b/backend/server/src/SecureGitClient.ts new file mode 100644 index 0000000..5c115b8 --- /dev/null +++ b/backend/server/src/SecureGitClient.ts @@ -0,0 +1,80 @@ +import simpleGit, { SimpleGit } from "simple-git"; +import path from "path"; +import fs from "fs"; +import os from "os"; + +export type FileData = { + id: string; + data: string; +}; + +export class SecureGitClient { + private gitUrl: string; + private sshKeyPath: string; + + constructor(gitUrl: string, sshKeyPath: string) { + this.gitUrl = gitUrl; + this.sshKeyPath = sshKeyPath; + } + + async pushFiles(fileData: FileData[], repository: string): Promise { + let tempDir: string | undefined; + + try { + // Create a temporary directory + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'git-push-')); + console.log(`Temporary directory created: ${tempDir}`); + + // Write files to the temporary directory + for (const { id, data } of fileData) { + const filePath = path.join(tempDir, id); + const dirPath = path.dirname(filePath); + + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + + console.log("Writing ", filePath, data); + fs.writeFileSync(filePath, data); + } + + // Initialize the simple-git instance with the temporary directory and custom SSH command + const git: SimpleGit = simpleGit(tempDir, { + config: [ + 'core.sshCommand=ssh -i ' + this.sshKeyPath + ' -o IdentitiesOnly=yes' + ] + }); + + // Initialize a new Git repository + await git.init(); + + // Add remote repository + await git.addRemote("origin", `${this.gitUrl}:${repository}`); + + // Add files to the repository + for (const {id, data} of fileData) { + await git.add(id); + } + + // Commit the changes + await git.commit("Add files."); + + // Push the changes to the remote repository + await git.push("origin", "master"); + + console.log("Files successfully pushed to the repository"); + + if (tempDir) { + fs.rmSync(tempDir, { recursive: true, force: true }); + console.log(`Temporary directory removed: ${tempDir}`); + } + } catch (error) { + if (tempDir) { + fs.rmSync(tempDir, { recursive: true, force: true }); + console.log(`Temporary directory removed: ${tempDir}`); + } + console.error("Error pushing files to the repository:", error); + throw error; + } + } +} \ No newline at end of file diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 187db67..9a2ac0d 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -1,11 +1,11 @@ -import os from "os"; import path from "path"; import cors from "cors"; import express, { Express } from "express"; import dotenv from "dotenv"; import { createServer } from "http"; import { Server } from "socket.io"; -import { DokkuClient, SSHConfig } from "./DokkuClient"; +import { DokkuClient } from "./DokkuClient"; +import { SecureGitClient, FileData } from "./SecureGitClient"; import fs from "fs"; import { z } from "zod"; @@ -126,6 +126,11 @@ const client = new DokkuClient({ client.connect(); +const git = new SecureGitClient( + "dokku@gitwit.app", + process.env.DOKKU_KEY +) + io.on("connection", async (socket) => { try { if (inactivityTimeout) clearTimeout(inactivityTimeout); @@ -292,6 +297,33 @@ io.on("connection", async (socket) => { } ); + socket.on( + "deploy", + async (callback: (response: CallbackResponse) => void) => { + try { + // Push the project files to the Dokku server + console.log("Deploying project ${data.sandboxId}..."); + // Remove the /project/[id]/ component of each file path: + const fixedFilePaths = sandboxFiles.fileData.map((file) => { + return { + ...file, + id: file.id.split("/").slice(2).join("/"), + }; + }); + // Push all files to Dokku. + await git.pushFiles(fixedFilePaths, data.sandboxId); + callback({ + success: true, + }); + } catch (error) { + callback({ + success: false, + message: "Failed to deploy project: " + error, + }); + } + } + ); + socket.on("createFile", async (name: string, callback) => { try { const size: number = await getProjectSize(data.sandboxId);