diff --git a/backend/server/nodemon.json b/backend/server/nodemon.json index 5554d0f..c71b99b 100644 --- a/backend/server/nodemon.json +++ b/backend/server/nodemon.json @@ -1,5 +1,7 @@ { - "watch": ["src"], + "watch": [ + "src" + ], "ext": "ts", "exec": "concurrently \"npx tsc --watch\" \"ts-node src/index.ts\"" } \ No newline at end of file diff --git a/backend/server/package.json b/backend/server/package.json index 40c9c18..435cd1b 100644 --- a/backend/server/package.json +++ b/backend/server/package.json @@ -31,4 +31,4 @@ "ts-node": "^10.9.2", "typescript": "^5.4.5" } -} +} \ No newline at end of file diff --git a/backend/server/src/DokkuClient.ts b/backend/server/src/DokkuClient.ts index fd0adcd..e2e9911 100644 --- a/backend/server/src/DokkuClient.ts +++ b/backend/server/src/DokkuClient.ts @@ -1,37 +1,33 @@ -import { SSHSocketClient, SSHConfig } from "./SSHSocketClient" +import { SSHConfig, SSHSocketClient } from "./SSHSocketClient" export interface DokkuResponse { - ok: boolean; - output: string; + ok: boolean + output: string } export class DokkuClient extends SSHSocketClient { - constructor(config: SSHConfig) { - super( - config, - "/var/run/dokku-daemon/dokku-daemon.sock" - ) + super(config, "/var/run/dokku-daemon/dokku-daemon.sock") } async sendCommand(command: string): Promise { try { - const response = await this.sendData(command); + const response = await this.sendData(command) if (typeof response !== "string") { - throw new Error("Received data is not a string"); + throw new Error("Received data is not a string") } - return JSON.parse(response); + return JSON.parse(response) } catch (error: any) { - throw new Error(`Failed to send command: ${error.message}`); + throw new Error(`Failed to send command: ${error.message}`) } } async listApps(): Promise { - const response = await this.sendCommand("apps:list"); - return response.output.split("\n").slice(1); // Split by newline and ignore the first line (header) + const response = await this.sendCommand("apps:list") + return response.output.split("\n").slice(1) // Split by newline and ignore the first line (header) } } -export { SSHConfig }; \ No newline at end of file +export { SSHConfig } diff --git a/backend/server/src/SSHSocketClient.ts b/backend/server/src/SSHSocketClient.ts index e0dc043..c653b23 100644 --- a/backend/server/src/SSHSocketClient.ts +++ b/backend/server/src/SSHSocketClient.ts @@ -1,90 +1,90 @@ -import { Client } from "ssh2"; +import { Client } from "ssh2" export interface SSHConfig { - host: string; - port?: number; - username: string; - privateKey: Buffer; + host: string + port?: number + username: string + privateKey: Buffer } export class SSHSocketClient { - private conn: Client; - private config: SSHConfig; - private socketPath: string; - private isConnected: boolean = false; - - constructor(config: SSHConfig, socketPath: string) { - this.conn = new Client(); - this.config = { ...config, port: 22}; - this.socketPath = socketPath; - - this.setupTerminationHandlers(); - } - - private setupTerminationHandlers() { - process.on("SIGINT", this.closeConnection.bind(this)); - process.on("SIGTERM", this.closeConnection.bind(this)); - } - - private closeConnection() { - console.log("Closing SSH connection..."); - this.conn.end(); - this.isConnected = false; - process.exit(0); - } - - connect(): Promise { - return new Promise((resolve, reject) => { - this.conn - .on("ready", () => { - console.log("SSH connection established"); - this.isConnected = true; - resolve(); - }) - .on("error", (err) => { - console.error("SSH connection error:", err); - this.isConnected = false; - reject(err); - }) - .on("close", () => { - console.log("SSH connection closed"); - this.isConnected = false; - }) - .connect(this.config); - }); - } - - sendData(data: string): Promise { - return new Promise((resolve, reject) => { - if (!this.isConnected) { - reject(new Error("SSH connection is not established")); - return; - } - - this.conn.exec( - `echo "${data}" | nc -U ${this.socketPath}`, - (err, stream) => { - if (err) { - reject(err); - return; - } - - stream - .on("close", (code: number, signal: string) => { - reject( - new Error( - `Stream closed with code ${code} and signal ${signal}` - ) - ); - }) - .on("data", (data: Buffer) => { - resolve(data.toString()); - }) - .stderr.on("data", (data: Buffer) => { - reject(new Error(data.toString())); - }); + private conn: Client + private config: SSHConfig + private socketPath: string + private isConnected: boolean = false + + constructor(config: SSHConfig, socketPath: string) { + this.conn = new Client() + this.config = { ...config, port: 22 } + this.socketPath = socketPath + + this.setupTerminationHandlers() + } + + private setupTerminationHandlers() { + process.on("SIGINT", this.closeConnection.bind(this)) + process.on("SIGTERM", this.closeConnection.bind(this)) + } + + private closeConnection() { + console.log("Closing SSH connection...") + this.conn.end() + this.isConnected = false + process.exit(0) + } + + connect(): Promise { + return new Promise((resolve, reject) => { + this.conn + .on("ready", () => { + console.log("SSH connection established") + this.isConnected = true + resolve() + }) + .on("error", (err) => { + console.error("SSH connection error:", err) + this.isConnected = false + reject(err) + }) + .on("close", () => { + console.log("SSH connection closed") + this.isConnected = false + }) + .connect(this.config) + }) + } + + sendData(data: string): Promise { + return new Promise((resolve, reject) => { + if (!this.isConnected) { + reject(new Error("SSH connection is not established")) + return + } + + this.conn.exec( + `echo "${data}" | nc -U ${this.socketPath}`, + (err, stream) => { + if (err) { + reject(err) + return } - ); - }); - } - } \ No newline at end of file + + stream + .on("close", (code: number, signal: string) => { + reject( + new Error( + `Stream closed with code ${code} and signal ${signal}` + ) + ) + }) + .on("data", (data: Buffer) => { + resolve(data.toString()) + }) + .stderr.on("data", (data: Buffer) => { + reject(new Error(data.toString())) + }) + } + ) + }) + } +} diff --git a/backend/server/src/SecureGitClient.ts b/backend/server/src/SecureGitClient.ts index 6fabce6..34f5322 100644 --- a/backend/server/src/SecureGitClient.ts +++ b/backend/server/src/SecureGitClient.ts @@ -1,82 +1,84 @@ -import simpleGit, { SimpleGit } from "simple-git"; -import path from "path"; -import fs from "fs"; -import os from "os"; +import fs from "fs" +import os from "os" +import path from "path" +import simpleGit, { SimpleGit } from "simple-git" export type FileData = { - id: string; - data: string; -}; + id: string + data: string +} export class SecureGitClient { - private gitUrl: string; - private sshKeyPath: string; + private gitUrl: string + private sshKeyPath: string constructor(gitUrl: string, sshKeyPath: string) { - this.gitUrl = gitUrl; - this.sshKeyPath = sshKeyPath; + this.gitUrl = gitUrl + this.sshKeyPath = sshKeyPath } async pushFiles(fileData: FileData[], repository: string): Promise { - let tempDir: string | undefined; + let tempDir: string | undefined try { // Create a temporary directory - tempDir = fs.mkdtempSync(path.posix.join(os.tmpdir(), 'git-push-')); - console.log(`Temporary directory created: ${tempDir}`); + tempDir = fs.mkdtempSync(path.posix.join(os.tmpdir(), "git-push-")) + console.log(`Temporary directory created: ${tempDir}`) // Write files to the temporary directory - console.log(`Writing ${fileData.length} files.`); + console.log(`Writing ${fileData.length} files.`) for (const { id, data } of fileData) { - const filePath = path.posix.join(tempDir, id); - const dirPath = path.dirname(filePath); - + const filePath = path.posix.join(tempDir, id) + const dirPath = path.dirname(filePath) + if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath, { recursive: true }); + fs.mkdirSync(dirPath, { recursive: true }) } - fs.writeFileSync(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' - ] + "core.sshCommand=ssh -i " + + this.sshKeyPath + + " -o IdentitiesOnly=yes", + ], }).outputHandler((_command, stdout, stderr) => { - stdout.pipe(process.stdout); - stderr.pipe(process.stderr); - });; + stdout.pipe(process.stdout) + stderr.pipe(process.stderr) + }) // Initialize a new Git repository - await git.init(); + await git.init() // Add remote repository - await git.addRemote("origin", `${this.gitUrl}:${repository}`); + await git.addRemote("origin", `${this.gitUrl}:${repository}`) // Add files to the repository - for (const {id, data} of fileData) { - await git.add(id); + for (const { id, data } of fileData) { + await git.add(id) } // Commit the changes - await git.commit("Add files."); + await git.commit("Add files.") // Push the changes to the remote repository - await git.push("origin", "master", {'--force': null}); + await git.push("origin", "master", { "--force": null }) - console.log("Files successfully pushed to the repository"); + console.log("Files successfully pushed to the repository") if (tempDir) { - fs.rmSync(tempDir, { recursive: true, force: true }); - console.log(`Temporary directory removed: ${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}`); + fs.rmSync(tempDir, { recursive: true, force: true }) + console.log(`Temporary directory removed: ${tempDir}`) } - console.error("Error pushing files to the repository:", error); - throw error; + console.error("Error pushing files to the repository:", error) + throw error } } -} \ No newline at end of file +} diff --git a/backend/server/src/Terminal.ts b/backend/server/src/Terminal.ts index e30f022..482b8a4 100644 --- a/backend/server/src/Terminal.ts +++ b/backend/server/src/Terminal.ts @@ -1,13 +1,13 @@ -import { Sandbox, ProcessHandle } from "e2b"; +import { ProcessHandle, Sandbox } from "e2b" // Terminal class to manage a pseudo-terminal (PTY) in a sandbox environment export class Terminal { - private pty: ProcessHandle | undefined; // Holds the PTY process handle - private sandbox: Sandbox; // Reference to the sandbox environment + private pty: ProcessHandle | undefined // Holds the PTY process handle + private sandbox: Sandbox // Reference to the sandbox environment // Constructor initializes the Terminal with a sandbox constructor(sandbox: Sandbox) { - this.sandbox = sandbox; + this.sandbox = sandbox } // Initialize the terminal with specified rows, columns, and data handler @@ -16,9 +16,9 @@ export class Terminal { cols = 80, onData, }: { - rows?: number; - cols?: number; - onData: (responseData: string) => void; + rows?: number + cols?: number + onData: (responseData: string) => void }): Promise { // Create a new PTY process this.pty = await this.sandbox.pty.create({ @@ -26,35 +26,38 @@ export class Terminal { cols, timeout: 0, onData: (data: Uint8Array) => { - onData(new TextDecoder().decode(data)); // Convert received data to string and pass to handler + onData(new TextDecoder().decode(data)) // Convert received data to string and pass to handler }, - }); + }) } // Send data to the terminal async sendData(data: string) { if (this.pty) { - await this.sandbox.pty.sendInput(this.pty.pid, new TextEncoder().encode(data)); + await this.sandbox.pty.sendInput( + this.pty.pid, + new TextEncoder().encode(data) + ) } else { - console.log("Cannot send data because pty is not initialized."); + console.log("Cannot send data because pty is not initialized.") } } // Resize the terminal async resize(size: { cols: number; rows: number }): Promise { if (this.pty) { - await this.sandbox.pty.resize(this.pty.pid, size); + await this.sandbox.pty.resize(this.pty.pid, size) } else { - console.log("Cannot resize terminal because pty is not initialized."); + console.log("Cannot resize terminal because pty is not initialized.") } } // Close the terminal, killing the PTY process and stopping the input stream async close(): Promise { if (this.pty) { - await this.pty.kill(); + await this.pty.kill() } else { - console.log("Cannot kill pty because it is not initialized."); + console.log("Cannot kill pty because it is not initialized.") } } } @@ -64,4 +67,4 @@ export class Terminal { // await terminal.init(); // terminal.sendData('ls -la'); // await terminal.resize({ cols: 100, rows: 30 }); -// await terminal.close(); \ No newline at end of file +// await terminal.close(); diff --git a/backend/server/src/fileoperations.ts b/backend/server/src/fileoperations.ts index 5e0e249..1157487 100644 --- a/backend/server/src/fileoperations.ts +++ b/backend/server/src/fileoperations.ts @@ -1,14 +1,7 @@ -import * as dotenv from "dotenv"; -import { - R2FileBody, - R2Files, - Sandbox, - TFile, - TFileData, - TFolder, -} from "./types"; +import * as dotenv from "dotenv" +import { R2Files, TFile, TFileData, TFolder } from "./types" -dotenv.config(); +dotenv.config() export const getSandboxFiles = async (id: string) => { const res = await fetch( @@ -18,13 +11,13 @@ export const getSandboxFiles = async (id: string) => { Authorization: `${process.env.WORKERS_KEY}`, }, } - ); - const data: R2Files = await res.json(); + ) + const data: R2Files = await res.json() - const paths = data.objects.map((obj) => obj.key); - const processedFiles = await processFiles(paths, id); - return processedFiles; -}; + const paths = data.objects.map((obj) => obj.key) + const processedFiles = await processFiles(paths, id) + return processedFiles +} export const getFolder = async (folderId: string) => { const res = await fetch( @@ -34,39 +27,39 @@ export const getFolder = async (folderId: string) => { Authorization: `${process.env.WORKERS_KEY}`, }, } - ); - const data: R2Files = await res.json(); + ) + const data: R2Files = await res.json() - return data.objects.map((obj) => obj.key); -}; + return data.objects.map((obj) => obj.key) +} const processFiles = async (paths: string[], id: string) => { - const root: TFolder = { id: "/", type: "folder", name: "/", children: [] }; - const fileData: TFileData[] = []; + const root: TFolder = { id: "/", type: "folder", name: "/", children: [] } + const fileData: TFileData[] = [] paths.forEach((path) => { - const allParts = path.split("/"); + const allParts = path.split("/") if (allParts[1] !== id) { - return; + return } - const parts = allParts.slice(2); - let current: TFolder = root; + const parts = allParts.slice(2) + let current: TFolder = root for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - const isFile = i === parts.length - 1 && part.length; - const existing = current.children.find((child) => child.name === part); + const part = parts[i] + const isFile = i === parts.length - 1 && part.length + const existing = current.children.find((child) => child.name === part) if (existing) { if (!isFile) { - current = existing as TFolder; + current = existing as TFolder } } else { if (isFile) { - const file: TFile = { id: path, type: "file", name: part }; - current.children.push(file); - fileData.push({ id: path, data: "" }); + const file: TFile = { id: path, type: "file", name: part } + current.children.push(file) + fileData.push({ id: path, data: "" }) } else { const folder: TFolder = { // id: path, // todo: wrong id. for example, folder "src" ID is: projects/a7vgttfqbgy403ratp7du3ln/src/App.css @@ -74,26 +67,26 @@ const processFiles = async (paths: string[], id: string) => { type: "folder", name: part, children: [], - }; - current.children.push(folder); - current = folder; + } + current.children.push(folder) + current = folder } } } - }); + }) await Promise.all( fileData.map(async (file) => { - const data = await fetchFileContent(file.id); - file.data = data; + const data = await fetchFileContent(file.id) + file.data = data }) - ); + ) return { files: root.children, fileData, - }; -}; + } +} const fetchFileContent = async (fileId: string): Promise => { try { @@ -104,13 +97,13 @@ const fetchFileContent = async (fileId: string): Promise => { Authorization: `${process.env.WORKERS_KEY}`, }, } - ); - return await fileRes.text(); + ) + return await fileRes.text() } catch (error) { - console.error("ERROR fetching file:", error); - return ""; + console.error("ERROR fetching file:", error) + return "" } -}; +} export const createFile = async (fileId: string) => { const res = await fetch(`${process.env.STORAGE_WORKER_URL}/api`, { @@ -120,9 +113,9 @@ export const createFile = async (fileId: string) => { Authorization: `${process.env.WORKERS_KEY}`, }, body: JSON.stringify({ fileId }), - }); - return res.ok; -}; + }) + return res.ok +} export const renameFile = async ( fileId: string, @@ -136,9 +129,9 @@ export const renameFile = async ( Authorization: `${process.env.WORKERS_KEY}`, }, body: JSON.stringify({ fileId, newFileId, data }), - }); - return res.ok; -}; + }) + return res.ok +} export const saveFile = async (fileId: string, data: string) => { const res = await fetch(`${process.env.STORAGE_WORKER_URL}/api/save`, { @@ -148,9 +141,9 @@ export const saveFile = async (fileId: string, data: string) => { Authorization: `${process.env.WORKERS_KEY}`, }, body: JSON.stringify({ fileId, data }), - }); - return res.ok; -}; + }) + return res.ok +} export const deleteFile = async (fileId: string) => { const res = await fetch(`${process.env.STORAGE_WORKER_URL}/api`, { @@ -160,9 +153,9 @@ export const deleteFile = async (fileId: string) => { Authorization: `${process.env.WORKERS_KEY}`, }, body: JSON.stringify({ fileId }), - }); - return res.ok; -}; + }) + return res.ok +} export const getProjectSize = async (id: string) => { const res = await fetch( @@ -172,6 +165,6 @@ export const getProjectSize = async (id: string) => { Authorization: `${process.env.WORKERS_KEY}`, }, } - ); - return (await res.json()).size; -}; \ No newline at end of file + ) + return (await res.json()).size +} diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index fb8bec1..f659c7f 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -1,20 +1,14 @@ -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 } from "./DokkuClient"; -import { SecureGitClient, FileData } from "./SecureGitClient"; -import fs, { readFile } from "fs"; +import cors from "cors" +import dotenv from "dotenv" +import express, { Express } from "express" +import fs from "fs" +import { createServer } from "http" +import path from "path" +import { Server } from "socket.io" +import { DokkuClient } from "./DokkuClient" +import { SecureGitClient } from "./SecureGitClient" -import { z } from "zod"; -import { - TFile, - TFileData, - TFolder, - User -} from "./types"; +import { z } from "zod" import { createFile, deleteFile, @@ -23,10 +17,17 @@ import { getSandboxFiles, renameFile, saveFile, -} from "./fileoperations"; -import { LockManager } from "./utils"; +} from "./fileoperations" +import { TFile, TFileData, TFolder, User } from "./types" +import { LockManager } from "./utils" -import { Sandbox, Filesystem, FilesystemEvent, EntryInfo, WatchHandle } from "e2b"; +import { + EntryInfo, + Filesystem, + FilesystemEvent, + Sandbox, + WatchHandle, +} from "e2b" import { Terminal } from "./Terminal" @@ -37,53 +38,57 @@ import { deleteFileRL, renameFileRL, saveFileRL, -} from "./ratelimit"; +} from "./ratelimit" -process.on('uncaughtException', (error) => { - console.error('Uncaught Exception:', error); +process.on("uncaughtException", (error) => { + console.error("Uncaught Exception:", error) // Do not exit the process // You can add additional logging or recovery logic here -}); +}) -process.on('unhandledRejection', (reason, promise) => { - console.error('Unhandled Rejection at:', promise, 'reason:', reason); +process.on("unhandledRejection", (reason, promise) => { + console.error("Unhandled Rejection at:", promise, "reason:", reason) // Do not exit the process // You can also handle the rejected promise here if needed -}); +}) // The amount of time in ms that a container will stay alive without a hearbeat. -const CONTAINER_TIMEOUT = 60_000; +const CONTAINER_TIMEOUT = 60_000 -dotenv.config(); +dotenv.config() -const app: Express = express(); -const port = process.env.PORT || 4000; -app.use(cors()); -const httpServer = createServer(app); +const app: Express = express() +const port = process.env.PORT || 4000 +app.use(cors()) +const httpServer = createServer(app) const io = new Server(httpServer, { cors: { origin: "*", }, -}); +}) -let inactivityTimeout: NodeJS.Timeout | null = null; -let isOwnerConnected = false; +let inactivityTimeout: NodeJS.Timeout | null = null +let isOwnerConnected = false -const containers: Record = {}; -const connections: Record = {}; -const terminals: Record = {}; +const containers: Record = {} +const connections: Record = {} +const terminals: Record = {} -const dirName = "/home/user"; +const dirName = "/home/user" -const moveFile = async (filesystem: Filesystem, filePath: string, newFilePath: string) => { +const moveFile = async ( + filesystem: Filesystem, + filePath: string, + newFilePath: string +) => { try { - const fileContents = await filesystem.read(filePath); - await filesystem.write(newFilePath, fileContents); - await filesystem.remove(filePath); + const fileContents = await filesystem.read(filePath) + await filesystem.write(newFilePath, fileContents) + await filesystem.remove(filePath) } catch (e) { - console.error(`Error moving file from ${filePath} to ${newFilePath}:`, e); + console.error(`Error moving file from ${filePath} to ${newFilePath}:`, e) } -}; +} io.use(async (socket, next) => { const handshakeSchema = z.object({ @@ -91,17 +96,17 @@ io.use(async (socket, next) => { sandboxId: z.string(), EIO: z.string(), transport: z.string(), - }); + }) - const q = socket.handshake.query; - const parseQuery = handshakeSchema.safeParse(q); + const q = socket.handshake.query + const parseQuery = handshakeSchema.safeParse(q) if (!parseQuery.success) { - next(new Error("Invalid request.")); - return; + next(new Error("Invalid request.")) + return } - const { sandboxId, userId } = parseQuery.data; + const { sandboxId, userId } = parseQuery.data const dbUser = await fetch( `${process.env.DATABASE_WORKER_URL}/api/user?id=${userId}`, { @@ -109,38 +114,41 @@ io.use(async (socket, next) => { Authorization: `${process.env.WORKERS_KEY}`, }, } - ); - const dbUserJSON = (await dbUser.json()) as User; + ) + const dbUserJSON = (await dbUser.json()) as User if (!dbUserJSON) { - next(new Error("DB error.")); - return; + next(new Error("DB error.")) + return } - const sandbox = dbUserJSON.sandbox.find((s) => s.id === sandboxId); + const sandbox = dbUserJSON.sandbox.find((s) => s.id === sandboxId) const sharedSandboxes = dbUserJSON.usersToSandboxes.find( (uts) => uts.sandboxId === sandboxId - ); + ) if (!sandbox && !sharedSandboxes) { - next(new Error("Invalid credentials.")); - return; + next(new Error("Invalid credentials.")) + return } socket.data = { userId, sandboxId: sandboxId, isOwner: sandbox !== undefined, - }; + } - next(); -}); + next() +}) -const lockManager = new LockManager(); +const lockManager = new LockManager() -if (!process.env.DOKKU_HOST) console.error('Environment variable DOKKU_HOST is not defined'); -if (!process.env.DOKKU_USERNAME) console.error('Environment variable DOKKU_USERNAME is not defined'); -if (!process.env.DOKKU_KEY) console.error('Environment variable DOKKU_KEY is not defined'); +if (!process.env.DOKKU_HOST) + console.error("Environment variable DOKKU_HOST is not defined") +if (!process.env.DOKKU_USERNAME) + console.error("Environment variable DOKKU_USERNAME is not defined") +if (!process.env.DOKKU_KEY) + console.error("Environment variable DOKKU_KEY is not defined") const client = process.env.DOKKU_HOST && process.env.DOKKU_KEY && process.env.DOKKU_USERNAME @@ -149,498 +157,576 @@ const client = username: process.env.DOKKU_USERNAME, privateKey: fs.readFileSync(process.env.DOKKU_KEY), }) - : null; -client?.connect(); + : null +client?.connect() -const git = process.env.DOKKU_HOST && process.env.DOKKU_KEY ? new SecureGitClient( - `dokku@${process.env.DOKKU_HOST}`, - process.env.DOKKU_KEY -) : null; +const git = + process.env.DOKKU_HOST && process.env.DOKKU_KEY + ? new SecureGitClient( + `dokku@${process.env.DOKKU_HOST}`, + process.env.DOKKU_KEY + ) + : null io.on("connection", async (socket) => { try { - if (inactivityTimeout) clearTimeout(inactivityTimeout); + if (inactivityTimeout) clearTimeout(inactivityTimeout) const data = socket.data as { - userId: string; - sandboxId: string; - isOwner: boolean; - }; + userId: string + sandboxId: string + isOwner: boolean + } if (data.isOwner) { - isOwnerConnected = true; - connections[data.sandboxId] = (connections[data.sandboxId] ?? 0) + 1; + isOwnerConnected = true + connections[data.sandboxId] = (connections[data.sandboxId] ?? 0) + 1 } else { if (!isOwnerConnected) { - socket.emit("disableAccess", "The sandbox owner is not connected."); - return; + socket.emit("disableAccess", "The sandbox owner is not connected.") + return } } - const createdContainer = await lockManager.acquireLock(data.sandboxId, async () => { - try { - // Start a new container if the container doesn't exist or it timed out. - if (!containers[data.sandboxId] || !(await containers[data.sandboxId].isRunning())) { - containers[data.sandboxId] = await Sandbox.create({ timeoutMs: CONTAINER_TIMEOUT }); - console.log("Created container ", data.sandboxId); - return true; + const createdContainer = await lockManager.acquireLock( + data.sandboxId, + async () => { + try { + // Start a new container if the container doesn't exist or it timed out. + if ( + !containers[data.sandboxId] || + !(await containers[data.sandboxId].isRunning()) + ) { + containers[data.sandboxId] = await Sandbox.create({ + timeoutMs: CONTAINER_TIMEOUT, + }) + console.log("Created container ", data.sandboxId) + return true + } + } catch (e: any) { + console.error(`Error creating container ${data.sandboxId}:`, e) + io.emit("error", `Error: container creation. ${e.message ?? e}`) } - } catch (e: any) { - console.error(`Error creating container ${data.sandboxId}:`, e); - io.emit("error", `Error: container creation. ${e.message ?? e}`); } - }); + ) - const sandboxFiles = await getSandboxFiles(data.sandboxId); - const projectDirectory = path.posix.join(dirName, "projects", data.sandboxId); - const containerFiles = containers[data.sandboxId].files; - const fileWatchers: WatchHandle[] = []; + const sandboxFiles = await getSandboxFiles(data.sandboxId) + const projectDirectory = path.posix.join( + dirName, + "projects", + data.sandboxId + ) + const containerFiles = containers[data.sandboxId].files + const fileWatchers: WatchHandle[] = [] // Change the owner of the project directory to user const fixPermissions = async (projectDirectory: string) => { try { await containers[data.sandboxId].commands.run( `sudo chown -R user "${projectDirectory}"` - ); + ) } catch (e: any) { - console.log("Failed to fix permissions: " + e); + console.log("Failed to fix permissions: " + e) } - }; + } // Check if the given path is a directory const isDirectory = async (projectDirectory: string): Promise => { try { const result = await containers[data.sandboxId].commands.run( `[ -d "${projectDirectory}" ] && echo "true" || echo "false"` - ); - return result.stdout.trim() === "true"; + ) + return result.stdout.trim() === "true" } catch (e: any) { - console.log("Failed to check if directory: " + e); - return false; + console.log("Failed to check if directory: " + e) + return false } - }; + } // Only continue to container setup if a new container was created if (createdContainer) { - // Copy all files from the project to the container const promises = sandboxFiles.fileData.map(async (file) => { try { - const filePath = path.posix.join(dirName, file.id); - const parentDirectory = path.dirname(filePath); + const filePath = path.posix.join(dirName, file.id) + const parentDirectory = path.dirname(filePath) if (!containerFiles.exists(parentDirectory)) { - await containerFiles.makeDir(parentDirectory); + await containerFiles.makeDir(parentDirectory) } - await containerFiles.write(filePath, file.data); + await containerFiles.write(filePath, file.data) } catch (e: any) { - console.log("Failed to create file: " + e); + console.log("Failed to create file: " + e) } - }); - await Promise.all(promises); + }) + await Promise.all(promises) // Make the logged in user the owner of all project files - fixPermissions(projectDirectory); - + fixPermissions(projectDirectory) } // Start filesystem watcher for the project directory - const watchDirectory = async (directory: string): Promise => { + const watchDirectory = async ( + directory: string + ): Promise => { try { - return await containerFiles.watch(directory, async (event: FilesystemEvent) => { - try { - - function removeDirName(path : string, dirName : string) { - return path.startsWith(dirName) ? path.slice(dirName.length) : path; - } - - // This is the absolute file path in the container - const containerFilePath = path.posix.join(directory, event.name); - // This is the file path relative to the home directory - const sandboxFilePath = removeDirName(containerFilePath, dirName + "/"); - // This is the directory being watched relative to the home directory - const sandboxDirectory = removeDirName(directory, dirName + "/"); - - // Helper function to find a folder by id - function findFolderById(files: (TFolder | TFile)[], folderId : string) { - return files.find((file : TFolder | TFile) => file.type === "folder" && file.id === folderId); - } - - // A new file or directory was created. - if (event.type === "create") { - const folder = findFolderById(sandboxFiles.files, sandboxDirectory) as TFolder; - const isDir = await isDirectory(containerFilePath); - - const newItem = isDir - ? { id: sandboxFilePath, name: event.name, type: "folder", children: [] } as TFolder - : { id: sandboxFilePath, name: event.name, type: "file" } as TFile; - - if (folder) { - // If the folder exists, add the new item (file/folder) as a child - folder.children.push(newItem); - } else { - // If folder doesn't exist, add the new item to the root - sandboxFiles.files.push(newItem); + return await containerFiles.watch( + directory, + async (event: FilesystemEvent) => { + try { + function removeDirName(path: string, dirName: string) { + return path.startsWith(dirName) + ? path.slice(dirName.length) + : path } - if (!isDir) { - const fileData = await containers[data.sandboxId].files.read(containerFilePath); - const fileContents = typeof fileData === "string" ? fileData : ""; - sandboxFiles.fileData.push({ id: sandboxFilePath, data: fileContents }); + // This is the absolute file path in the container + const containerFilePath = path.posix.join(directory, event.name) + // This is the file path relative to the home directory + const sandboxFilePath = removeDirName( + containerFilePath, + dirName + "/" + ) + // This is the directory being watched relative to the home directory + const sandboxDirectory = removeDirName(directory, dirName + "/") + + // Helper function to find a folder by id + function findFolderById( + files: (TFolder | TFile)[], + folderId: string + ) { + return files.find( + (file: TFolder | TFile) => + file.type === "folder" && file.id === folderId + ) } - console.log(`Create ${sandboxFilePath}`); - } + // A new file or directory was created. + if (event.type === "create") { + const folder = findFolderById( + sandboxFiles.files, + sandboxDirectory + ) as TFolder + const isDir = await isDirectory(containerFilePath) - // A file or directory was removed or renamed. - else if (event.type === "remove" || event.type == "rename") { - const folder = findFolderById(sandboxFiles.files, sandboxDirectory) as TFolder; - const isDir = await isDirectory(containerFilePath); + const newItem = isDir + ? ({ + id: sandboxFilePath, + name: event.name, + type: "folder", + children: [], + } as TFolder) + : ({ + id: sandboxFilePath, + name: event.name, + type: "file", + } as TFile) - const isFileMatch = (file: TFolder | TFile | TFileData) => file.id === sandboxFilePath || file.id.startsWith(containerFilePath + '/'); + if (folder) { + // If the folder exists, add the new item (file/folder) as a child + folder.children.push(newItem) + } else { + // If folder doesn't exist, add the new item to the root + sandboxFiles.files.push(newItem) + } - if (folder) { - // Remove item from its parent folder - folder.children = folder.children.filter((file: TFolder | TFile) => !isFileMatch(file)); - } else { - // Remove from the root if it's not inside a folder - sandboxFiles.files = sandboxFiles.files.filter((file: TFolder | TFile) => !isFileMatch(file)); + if (!isDir) { + const fileData = await containers[data.sandboxId].files.read( + containerFilePath + ) + const fileContents = + typeof fileData === "string" ? fileData : "" + sandboxFiles.fileData.push({ + id: sandboxFilePath, + data: fileContents, + }) + } + + console.log(`Create ${sandboxFilePath}`) } - // Also remove any corresponding file data - sandboxFiles.fileData = sandboxFiles.fileData.filter((file: TFileData) => !isFileMatch(file)); + // A file or directory was removed or renamed. + else if (event.type === "remove" || event.type == "rename") { + const folder = findFolderById( + sandboxFiles.files, + sandboxDirectory + ) as TFolder + const isDir = await isDirectory(containerFilePath) - console.log(`Removed: ${sandboxFilePath}`); - } + const isFileMatch = (file: TFolder | TFile | TFileData) => + file.id === sandboxFilePath || + file.id.startsWith(containerFilePath + "/") - // The contents of a file were changed. - else if (event.type === "write") { - const folder = findFolderById(sandboxFiles.files, sandboxDirectory) as TFolder; - const fileToWrite = sandboxFiles.fileData.find(file => file.id === sandboxFilePath); + if (folder) { + // Remove item from its parent folder + folder.children = folder.children.filter( + (file: TFolder | TFile) => !isFileMatch(file) + ) + } else { + // Remove from the root if it's not inside a folder + sandboxFiles.files = sandboxFiles.files.filter( + (file: TFolder | TFile) => !isFileMatch(file) + ) + } - if (fileToWrite) { - fileToWrite.data = await containers[data.sandboxId].files.read(containerFilePath); - console.log(`Write to ${sandboxFilePath}`); - } else { - // If the file is part of a folder structure, locate it and update its data - const fileInFolder = folder?.children.find(file => file.id === sandboxFilePath); - if (fileInFolder) { - const fileData = await containers[data.sandboxId].files.read(containerFilePath); - const fileContents = typeof fileData === "string" ? fileData : ""; - sandboxFiles.fileData.push({ id: sandboxFilePath, data: fileContents }); - console.log(`Write to ${sandboxFilePath}`); + // Also remove any corresponding file data + sandboxFiles.fileData = sandboxFiles.fileData.filter( + (file: TFileData) => !isFileMatch(file) + ) + + console.log(`Removed: ${sandboxFilePath}`) + } + + // The contents of a file were changed. + else if (event.type === "write") { + const folder = findFolderById( + sandboxFiles.files, + sandboxDirectory + ) as TFolder + const fileToWrite = sandboxFiles.fileData.find( + (file) => file.id === sandboxFilePath + ) + + if (fileToWrite) { + fileToWrite.data = await containers[ + data.sandboxId + ].files.read(containerFilePath) + console.log(`Write to ${sandboxFilePath}`) + } else { + // If the file is part of a folder structure, locate it and update its data + const fileInFolder = folder?.children.find( + (file) => file.id === sandboxFilePath + ) + if (fileInFolder) { + const fileData = await containers[ + data.sandboxId + ].files.read(containerFilePath) + const fileContents = + typeof fileData === "string" ? fileData : "" + sandboxFiles.fileData.push({ + id: sandboxFilePath, + data: fileContents, + }) + console.log(`Write to ${sandboxFilePath}`) + } } } + + // Tell the client to reload the file list + socket.emit("loaded", sandboxFiles.files) + } catch (error) { + console.error( + `Error handling ${event.type} event for ${event.name}:`, + error + ) } - - // Tell the client to reload the file list - socket.emit("loaded", sandboxFiles.files); - - } catch (error) { - console.error(`Error handling ${event.type} event for ${event.name}:`, error); - } - }, { "timeout": 0 } ) + }, + { timeout: 0 } + ) } catch (error) { - console.error(`Error watching filesystem:`, error); + console.error(`Error watching filesystem:`, error) } - }; + } // Watch the project directory - const handle = await watchDirectory(projectDirectory); + const handle = await watchDirectory(projectDirectory) // Keep track of watch handlers to close later - if (handle) fileWatchers.push(handle); + if (handle) fileWatchers.push(handle) // Watch all subdirectories of the project directory, but not deeper // This also means directories created after the container is created won't be watched - const dirContent = await containerFiles.list(projectDirectory); - await Promise.all(dirContent.map(async (item : EntryInfo) => { - if (item.type === "dir") { - console.log("Watching " + item.path); - // Keep track of watch handlers to close later - const handle = await watchDirectory(item.path); - if (handle) fileWatchers.push(handle); - } - })) - - socket.emit("loaded", sandboxFiles.files); + const dirContent = await containerFiles.list(projectDirectory) + await Promise.all( + dirContent.map(async (item: EntryInfo) => { + if (item.type === "dir") { + console.log("Watching " + item.path) + // Keep track of watch handlers to close later + const handle = await watchDirectory(item.path) + if (handle) fileWatchers.push(handle) + } + }) + ) + + socket.emit("loaded", sandboxFiles.files) socket.on("heartbeat", async () => { try { // This keeps the container alive for another CONTAINER_TIMEOUT seconds. - // The E2B docs are unclear, but the timeout is relative to the time of this method call. - await containers[data.sandboxId].setTimeout(CONTAINER_TIMEOUT); + // The E2B docs are unclear, but the timeout is relative to the time of this method call. + await containers[data.sandboxId].setTimeout(CONTAINER_TIMEOUT) } catch (e: any) { - console.error("Error setting timeout:", e); - io.emit("error", `Error: set timeout. ${e.message ?? e}`); + console.error("Error setting timeout:", e) + io.emit("error", `Error: set timeout. ${e.message ?? e}`) } - }); + }) socket.on("getFile", (fileId: string, callback) => { - console.log(fileId); + console.log(fileId) try { - const file = sandboxFiles.fileData.find((f) => f.id === fileId); - if (!file) return; + const file = sandboxFiles.fileData.find((f) => f.id === fileId) + if (!file) return - callback(file.data); + callback(file.data) } catch (e: any) { - console.error("Error getting file:", e); - io.emit("error", `Error: get file. ${e.message ?? e}`); + console.error("Error getting file:", e) + io.emit("error", `Error: get file. ${e.message ?? e}`) } - }); + }) socket.on("getFolder", async (folderId: string, callback) => { try { - const files = await getFolder(folderId); - callback(files); + const files = await getFolder(folderId) + callback(files) } catch (e: any) { - console.error("Error getting folder:", e); - io.emit("error", `Error: get folder. ${e.message ?? e}`); + console.error("Error getting folder:", e) + io.emit("error", `Error: get folder. ${e.message ?? e}`) } - }); + }) // todo: send diffs + debounce for efficiency socket.on("saveFile", async (fileId: string, body: string) => { - if (!fileId) return; // handles saving when no file is open + if (!fileId) return // handles saving when no file is open try { if (Buffer.byteLength(body, "utf-8") > MAX_BODY_SIZE) { socket.emit( "error", "Error: file size too large. Please reduce the file size." - ); - return; + ) + return } try { - await saveFileRL.consume(data.userId, 1); - await saveFile(fileId, body); + await saveFileRL.consume(data.userId, 1) + await saveFile(fileId, body) } catch (e) { - io.emit("error", "Rate limited: file saving. Please slow down."); - return; + io.emit("error", "Rate limited: file saving. Please slow down.") + return } - const file = sandboxFiles.fileData.find((f) => f.id === fileId); - if (!file) return; - file.data = body; + const file = sandboxFiles.fileData.find((f) => f.id === fileId) + if (!file) return + file.data = body await containers[data.sandboxId].files.write( path.posix.join(dirName, file.id), body - ); - fixPermissions(projectDirectory); + ) + fixPermissions(projectDirectory) } catch (e: any) { - console.error("Error saving file:", e); - io.emit("error", `Error: file saving. ${e.message ?? e}`); + console.error("Error saving file:", e) + io.emit("error", `Error: file saving. ${e.message ?? e}`) } - }); + }) socket.on( "moveFile", async (fileId: string, folderId: string, callback) => { try { - const file = sandboxFiles.fileData.find((f) => f.id === fileId); - if (!file) return; + const file = sandboxFiles.fileData.find((f) => f.id === fileId) + if (!file) return - const parts = fileId.split("/"); - const newFileId = folderId + "/" + parts.pop(); + const parts = fileId.split("/") + const newFileId = folderId + "/" + parts.pop() await moveFile( containers[data.sandboxId].files, path.posix.join(dirName, fileId), path.posix.join(dirName, newFileId) - ); - fixPermissions(projectDirectory); + ) + fixPermissions(projectDirectory) - file.id = newFileId; + file.id = newFileId - await renameFile(fileId, newFileId, file.data); - const newFiles = await getSandboxFiles(data.sandboxId); - callback(newFiles.files); + await renameFile(fileId, newFileId, file.data) + const newFiles = await getSandboxFiles(data.sandboxId) + callback(newFiles.files) } catch (e: any) { - console.error("Error moving file:", e); - io.emit("error", `Error: file moving. ${e.message ?? e}`); + console.error("Error moving file:", e) + io.emit("error", `Error: file moving. ${e.message ?? e}`) } } - ); + ) interface CallbackResponse { - success: boolean; - apps?: string[]; - message?: string; + success: boolean + apps?: string[] + message?: string } socket.on( "list", async (callback: (response: CallbackResponse) => void) => { - console.log("Retrieving apps list..."); + console.log("Retrieving apps list...") try { - if (!client) throw Error("Failed to retrieve apps list: No Dokku client") + if (!client) + throw Error("Failed to retrieve apps list: No Dokku client") callback({ success: true, - apps: await client.listApps() - }); + apps: await client.listApps(), + }) } catch (error) { callback({ success: false, message: "Failed to retrieve apps list", - }); + }) } } - ); + ) socket.on( "deploy", async (callback: (response: CallbackResponse) => void) => { try { // Push the project files to the Dokku server - console.log("Deploying project ${data.sandboxId}..."); + console.log("Deploying project ${data.sandboxId}...") if (!git) throw Error("Failed to retrieve apps list: No git client") // 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); + 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); + const size: number = await getProjectSize(data.sandboxId) // limit is 200mb if (size > 200 * 1024 * 1024) { io.emit( "error", "Rate limited: project size exceeded. Please delete some files." - ); - callback({ success: false }); - return; + ) + callback({ success: false }) + return } try { - await createFileRL.consume(data.userId, 1); + await createFileRL.consume(data.userId, 1) } catch (e) { - io.emit("error", "Rate limited: file creation. Please slow down."); - return; + io.emit("error", "Rate limited: file creation. Please slow down.") + return } - const id = `projects/${data.sandboxId}/${name}`; + const id = `projects/${data.sandboxId}/${name}` await containers[data.sandboxId].files.write( path.posix.join(dirName, id), "" - ); - fixPermissions(projectDirectory); + ) + fixPermissions(projectDirectory) sandboxFiles.files.push({ id, name, type: "file", - }); + }) sandboxFiles.fileData.push({ id, data: "", - }); + }) - await createFile(id); + await createFile(id) - callback({ success: true }); + callback({ success: true }) } catch (e: any) { - console.error("Error creating file:", e); - io.emit("error", `Error: file creation. ${e.message ?? e}`); + console.error("Error creating file:", e) + io.emit("error", `Error: file creation. ${e.message ?? e}`) } - }); + }) socket.on("createFolder", async (name: string, callback) => { try { try { - await createFolderRL.consume(data.userId, 1); + await createFolderRL.consume(data.userId, 1) } catch (e) { - io.emit("error", "Rate limited: folder creation. Please slow down."); - return; + io.emit("error", "Rate limited: folder creation. Please slow down.") + return } - const id = `projects/${data.sandboxId}/${name}`; + const id = `projects/${data.sandboxId}/${name}` await containers[data.sandboxId].files.makeDir( path.posix.join(dirName, id) - ); + ) - callback(); + callback() } catch (e: any) { - console.error("Error creating folder:", e); - io.emit("error", `Error: folder creation. ${e.message ?? e}`); + console.error("Error creating folder:", e) + io.emit("error", `Error: folder creation. ${e.message ?? e}`) } - }); + }) socket.on("renameFile", async (fileId: string, newName: string) => { try { try { - await renameFileRL.consume(data.userId, 1); + await renameFileRL.consume(data.userId, 1) } catch (e) { - io.emit("error", "Rate limited: file renaming. Please slow down."); - return; + io.emit("error", "Rate limited: file renaming. Please slow down.") + return } - const file = sandboxFiles.fileData.find((f) => f.id === fileId); - if (!file) return; - file.id = newName; + const file = sandboxFiles.fileData.find((f) => f.id === fileId) + if (!file) return + file.id = newName - const parts = fileId.split("/"); + const parts = fileId.split("/") const newFileId = - parts.slice(0, parts.length - 1).join("/") + "/" + newName; + parts.slice(0, parts.length - 1).join("/") + "/" + newName await moveFile( containers[data.sandboxId].files, path.posix.join(dirName, fileId), path.posix.join(dirName, newFileId) - ); - fixPermissions(projectDirectory); - await renameFile(fileId, newFileId, file.data); + ) + fixPermissions(projectDirectory) + await renameFile(fileId, newFileId, file.data) } catch (e: any) { - console.error("Error renaming folder:", e); - io.emit("error", `Error: folder renaming. ${e.message ?? e}`); + console.error("Error renaming folder:", e) + io.emit("error", `Error: folder renaming. ${e.message ?? e}`) } - }); + }) socket.on("deleteFile", async (fileId: string, callback) => { try { try { - await deleteFileRL.consume(data.userId, 1); + await deleteFileRL.consume(data.userId, 1) } catch (e) { - io.emit("error", "Rate limited: file deletion. Please slow down."); + io.emit("error", "Rate limited: file deletion. Please slow down.") } - const file = sandboxFiles.fileData.find((f) => f.id === fileId); - if (!file) return; + const file = sandboxFiles.fileData.find((f) => f.id === fileId) + if (!file) return await containers[data.sandboxId].files.remove( path.posix.join(dirName, fileId) - ); + ) sandboxFiles.fileData = sandboxFiles.fileData.filter( (f) => f.id !== fileId - ); + ) - await deleteFile(fileId); + await deleteFile(fileId) - const newFiles = await getSandboxFiles(data.sandboxId); - callback(newFiles.files); + const newFiles = await getSandboxFiles(data.sandboxId) + callback(newFiles.files) } catch (e: any) { - console.error("Error deleting file:", e); - io.emit("error", `Error: file deletion. ${e.message ?? e}`); + console.error("Error deleting file:", e) + io.emit("error", `Error: file deletion. ${e.message ?? e}`) } - }); + }) // todo // socket.on("renameFolder", async (folderId: string, newName: string) => { @@ -648,36 +734,36 @@ io.on("connection", async (socket) => { socket.on("deleteFolder", async (folderId: string, callback) => { try { - const files = await getFolder(folderId); + const files = await getFolder(folderId) await Promise.all( files.map(async (file) => { await containers[data.sandboxId].files.remove( path.posix.join(dirName, file) - ); + ) sandboxFiles.fileData = sandboxFiles.fileData.filter( (f) => f.id !== file - ); + ) - await deleteFile(file); + await deleteFile(file) }) - ); + ) - const newFiles = await getSandboxFiles(data.sandboxId); + const newFiles = await getSandboxFiles(data.sandboxId) - callback(newFiles.files); + callback(newFiles.files) } catch (e: any) { - console.error("Error deleting folder:", e); - io.emit("error", `Error: folder deletion. ${e.message ?? e}`); + console.error("Error deleting folder:", e) + io.emit("error", `Error: folder deletion. ${e.message ?? e}`) } - }); + }) socket.on("createTerminal", async (id: string, callback) => { try { // Note: The number of terminals per window is limited on the frontend, but not backend if (terminals[id]) { - return; + return } await lockManager.acquireLock(data.sandboxId, async () => { @@ -685,95 +771,103 @@ io.on("connection", async (socket) => { terminals[id] = new Terminal(containers[data.sandboxId]) await terminals[id].init({ onData: (responseString: string) => { - io.emit("terminalResponse", { id, data: responseString }); + io.emit("terminalResponse", { id, data: responseString }) function extractPortNumber(inputString: string) { // Remove ANSI escape codes - const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, ''); + const cleanedString = inputString.replace( + /\x1B\[[0-9;]*m/g, + "" + ) // Regular expression to match port number - const regex = /http:\/\/localhost:(\d+)/; + const regex = /http:\/\/localhost:(\d+)/ // If a match is found, return the port number - const match = cleanedString.match(regex); - return match ? match[1] : null; + const match = cleanedString.match(regex) + return match ? match[1] : null } - const port = parseInt(extractPortNumber(responseString) ?? ""); + const port = parseInt(extractPortNumber(responseString) ?? "") if (port) { io.emit( "previewURL", "https://" + containers[data.sandboxId].getHost(port) - ); + ) } }, cols: 80, rows: 20, //onExit: () => console.log("Terminal exited", id), - }); + }) - const defaultDirectory = path.posix.join(dirName, "projects", data.sandboxId); + const defaultDirectory = path.posix.join( + dirName, + "projects", + data.sandboxId + ) const defaultCommands = [ `cd "${defaultDirectory}"`, "export PS1='user> '", - "clear" + "clear", ] - for (const command of defaultCommands) await terminals[id].sendData(command + "\r"); + for (const command of defaultCommands) + await terminals[id].sendData(command + "\r") - console.log("Created terminal", id); + console.log("Created terminal", id) } catch (e: any) { - console.error(`Error creating terminal ${id}:`, e); - io.emit("error", `Error: terminal creation. ${e.message ?? e}`); + console.error(`Error creating terminal ${id}:`, e) + io.emit("error", `Error: terminal creation. ${e.message ?? e}`) } - }); + }) - callback(); + callback() } catch (e: any) { - console.error(`Error creating terminal ${id}:`, e); - io.emit("error", `Error: terminal creation. ${e.message ?? e}`); + console.error(`Error creating terminal ${id}:`, e) + io.emit("error", `Error: terminal creation. ${e.message ?? e}`) } - }); + }) socket.on( "resizeTerminal", (dimensions: { cols: number; rows: number }) => { try { Object.values(terminals).forEach((t) => { - t.resize(dimensions); - }); + t.resize(dimensions) + }) } catch (e: any) { - console.error("Error resizing terminal:", e); - io.emit("error", `Error: terminal resizing. ${e.message ?? e}`); + console.error("Error resizing terminal:", e) + io.emit("error", `Error: terminal resizing. ${e.message ?? e}`) } } - ); + ) socket.on("terminalData", async (id: string, data: string) => { try { if (!terminals[id]) { - return; + return } - await terminals[id].sendData(data); + await terminals[id].sendData(data) } catch (e: any) { - console.error("Error writing to terminal:", e); - io.emit("error", `Error: writing to terminal. ${e.message ?? e}`); + console.error("Error writing to terminal:", e) + io.emit("error", `Error: writing to terminal. ${e.message ?? e}`) } - }); + }) socket.on("closeTerminal", async (id: string, callback) => { try { if (!terminals[id]) { - return; + return } - await terminals[id].close(); - delete terminals[id]; + await terminals[id].close() + delete terminals[id] - callback(); + callback() } catch (e: any) { - console.error("Error closing terminal:", e); - io.emit("error", `Error: closing terminal. ${e.message ?? e}`); + console.error("Error closing terminal:", e) + io.emit("error", `Error: closing terminal. ${e.message ?? e}`) } - }); + }) socket.on( "generateCode", @@ -797,50 +891,56 @@ io.on("connection", async (socket) => { userId: data.userId, }), } - ); + ) // Generate code from cloudflare workers AI const generateCodePromise = fetch( - `${process.env.AI_WORKER_URL}/api?fileName=${encodeURIComponent(fileName)}&code=${encodeURIComponent(code)}&line=${encodeURIComponent(line)}&instructions=${encodeURIComponent(instructions)}`, + `${process.env.AI_WORKER_URL}/api?fileName=${encodeURIComponent( + fileName + )}&code=${encodeURIComponent(code)}&line=${encodeURIComponent( + line + )}&instructions=${encodeURIComponent(instructions)}`, { headers: { "Content-Type": "application/json", Authorization: `${process.env.CF_AI_KEY}`, }, } - ); + ) const [fetchResponse, generateCodeResponse] = await Promise.all([ fetchPromise, generateCodePromise, - ]); + ]) - const json = await generateCodeResponse.json(); + const json = await generateCodeResponse.json() - callback({ response: json.response, success: true }); + callback({ response: json.response, success: true }) } catch (e: any) { - console.error("Error generating code:", e); - io.emit("error", `Error: code generation. ${e.message ?? e}`); + console.error("Error generating code:", e) + io.emit("error", `Error: code generation. ${e.message ?? e}`) } } - ); + ) socket.on("disconnect", async () => { try { if (data.isOwner) { - connections[data.sandboxId]--; + connections[data.sandboxId]-- } // Stop watching file changes in the container - Promise.all(fileWatchers.map(async (handle : WatchHandle) => { - await handle.close(); - })); + Promise.all( + fileWatchers.map(async (handle: WatchHandle) => { + await handle.close() + }) + ) if (data.isOwner && connections[data.sandboxId] <= 0) { socket.broadcast.emit( "disableAccess", "The sandbox owner has disconnected." - ); + ) } // const sockets = await io.fetchSockets(); @@ -860,16 +960,16 @@ io.on("connection", async (socket) => { // console.log("number of sockets", sockets.length); // } } catch (e: any) { - console.log("Error disconnecting:", e); - io.emit("error", `Error: disconnecting. ${e.message ?? e}`); + console.log("Error disconnecting:", e) + io.emit("error", `Error: disconnecting. ${e.message ?? e}`) } - }); + }) } catch (e: any) { - console.error("Error connecting:", e); - io.emit("error", `Error: connection. ${e.message ?? e}`); + console.error("Error connecting:", e) + io.emit("error", `Error: connection. ${e.message ?? e}`) } -}); +}) httpServer.listen(port, () => { - console.log(`Server running on port ${port}`); -}); + console.log(`Server running on port ${port}`) +}) diff --git a/backend/server/src/ratelimit.ts b/backend/server/src/ratelimit.ts index f0d99fa..f40ab1e 100644 --- a/backend/server/src/ratelimit.ts +++ b/backend/server/src/ratelimit.ts @@ -30,4 +30,4 @@ export const deleteFileRL = new RateLimiterMemory({ export const deleteFolderRL = new RateLimiterMemory({ points: 1, duration: 2, -}) \ No newline at end of file +}) diff --git a/backend/server/src/types.ts b/backend/server/src/types.ts index b71592a..42ad6d0 100644 --- a/backend/server/src/types.ts +++ b/backend/server/src/types.ts @@ -1,70 +1,70 @@ // DB Types export type User = { - id: string; - name: string; - email: string; - generations: number; - sandbox: Sandbox[]; - usersToSandboxes: UsersToSandboxes[]; -}; + id: string + name: string + email: string + generations: number + sandbox: Sandbox[] + usersToSandboxes: UsersToSandboxes[] +} export type Sandbox = { - id: string; - name: string; - type: "react" | "node"; - visibility: "public" | "private"; - createdAt: Date; - userId: string; - usersToSandboxes: UsersToSandboxes[]; -}; + id: string + name: string + type: "react" | "node" + visibility: "public" | "private" + createdAt: Date + userId: string + usersToSandboxes: UsersToSandboxes[] +} export type UsersToSandboxes = { - userId: string; - sandboxId: string; - sharedOn: Date; -}; + userId: string + sandboxId: string + sharedOn: Date +} export type TFolder = { - id: string; - type: "folder"; - name: string; - children: (TFile | TFolder)[]; -}; + id: string + type: "folder" + name: string + children: (TFile | TFolder)[] +} export type TFile = { - id: string; - type: "file"; - name: string; -}; + id: string + type: "file" + name: string +} export type TFileData = { - id: string; - data: string; -}; + id: string + data: string +} export type R2Files = { - objects: R2FileData[]; - truncated: boolean; - delimitedPrefixes: any[]; -}; + objects: R2FileData[] + truncated: boolean + delimitedPrefixes: any[] +} export type R2FileData = { - storageClass: string; - uploaded: string; - checksums: any; - httpEtag: string; - etag: string; - size: number; - version: string; - key: string; -}; + storageClass: string + uploaded: string + checksums: any + httpEtag: string + etag: string + size: number + version: string + key: string +} export type R2FileBody = R2FileData & { - body: ReadableStream; - bodyUsed: boolean; - arrayBuffer: Promise; - text: Promise; - json: Promise; - blob: Promise; -}; + body: ReadableStream + bodyUsed: boolean + arrayBuffer: Promise + text: Promise + json: Promise + blob: Promise +} diff --git a/backend/server/src/utils.ts b/backend/server/src/utils.ts index 0aebb03..5ae1377 100644 --- a/backend/server/src/utils.ts +++ b/backend/server/src/utils.ts @@ -1,23 +1,23 @@ export class LockManager { - private locks: { [key: string]: Promise }; + private locks: { [key: string]: Promise } constructor() { - this.locks = {}; + this.locks = {} } async acquireLock(key: string, task: () => Promise): Promise { if (!this.locks[key]) { this.locks[key] = new Promise(async (resolve, reject) => { try { - const result = await task(); - resolve(result); + const result = await task() + resolve(result) } catch (error) { - reject(error); + reject(error) } finally { - delete this.locks[key]; + delete this.locks[key] } - }); + }) } - return await this.locks[key]; + return await this.locks[key] } -} \ No newline at end of file +}