chore: format backend server code

This commit is contained in:
James Murdza 2024-11-16 21:35:06 -05:00
parent 87d311a5d1
commit 7ecbd02fef
8 changed files with 448 additions and 400 deletions

View File

@ -1,58 +1,61 @@
import { Socket } from "socket.io" import { Socket } from "socket.io"
class Counter { class Counter {
private count: number = 0 private count: number = 0
increment() { increment() {
this.count++ this.count++
} }
decrement() { decrement() {
this.count = Math.max(0, this.count - 1) this.count = Math.max(0, this.count - 1)
} }
getValue(): number { getValue(): number {
return this.count return this.count
} }
} }
// Owner Connection Management // Owner Connection Management
export class ConnectionManager { export class ConnectionManager {
// Counts how many times the owner is connected to a sandbox // Counts how many times the owner is connected to a sandbox
private ownerConnections: Record<string, Counter> = {} private ownerConnections: Record<string, Counter> = {}
// Stores all sockets connected to a given sandbox // Stores all sockets connected to a given sandbox
private sockets: Record<string, Set<Socket>> = {} private sockets: Record<string, Set<Socket>> = {}
// Checks if the owner of a sandbox is connected // Checks if the owner of a sandbox is connected
ownerIsConnected(sandboxId: string): boolean { ownerIsConnected(sandboxId: string): boolean {
return this.ownerConnections[sandboxId]?.getValue() > 0 return this.ownerConnections[sandboxId]?.getValue() > 0
}
// Adds a connection for a sandbox
addConnectionForSandbox(socket: Socket, sandboxId: string, isOwner: boolean) {
this.sockets[sandboxId] ??= new Set()
this.sockets[sandboxId].add(socket)
// If the connection is for the owner, increments the owner connection counter
if (isOwner) {
this.ownerConnections[sandboxId] ??= new Counter()
this.ownerConnections[sandboxId].increment()
} }
}
// Adds a connection for a sandbox // Removes a connection for a sandbox
addConnectionForSandbox(socket: Socket, sandboxId: string, isOwner: boolean) { removeConnectionForSandbox(
this.sockets[sandboxId] ??= new Set() socket: Socket,
this.sockets[sandboxId].add(socket) sandboxId: string,
isOwner: boolean
) {
this.sockets[sandboxId]?.delete(socket)
// If the connection is for the owner, increments the owner connection counter // If the connection being removed is for the owner, decrements the owner connection counter
if (isOwner) { if (isOwner) {
this.ownerConnections[sandboxId] ??= new Counter() this.ownerConnections[sandboxId]?.decrement()
this.ownerConnections[sandboxId].increment()
}
}
// Removes a connection for a sandbox
removeConnectionForSandbox(socket: Socket, sandboxId: string, isOwner: boolean) {
this.sockets[sandboxId]?.delete(socket)
// If the connection being removed is for the owner, decrements the owner connection counter
if (isOwner) {
this.ownerConnections[sandboxId]?.decrement()
}
}
// Returns the set of sockets connected to a given sandbox
connectionsForSandbox(sandboxId: string): Set<Socket> {
return this.sockets[sandboxId] ?? new Set();
} }
}
// Returns the set of sockets connected to a given sandbox
connectionsForSandbox(sandboxId: string): Set<Socket> {
return this.sockets[sandboxId] ?? new Set()
}
} }

View File

@ -23,7 +23,11 @@ function generateFileStructure(paths: string[]): (TFolder | TFile)[] {
} }
} else { } else {
if (isFile) { if (isFile) {
const file: TFile = { id: `/${parts.join("/")}`, type: "file", name: part } const file: TFile = {
id: `/${parts.join("/")}`,
type: "file",
name: part,
}
current.children.push(file) current.children.push(file)
} else { } else {
const folder: TFolder = { const folder: TFolder = {
@ -75,7 +79,9 @@ export class FileManager {
if (isFile) { if (isFile) {
const fileId = `/${parts.join("/")}` const fileId = `/${parts.join("/")}`
const data = await RemoteFileStorage.fetchFileContent(`projects/${this.sandboxId}${fileId}`) const data = await RemoteFileStorage.fetchFileContent(
`projects/${this.sandboxId}${fileId}`
)
fileData.push({ id: fileId, data }) fileData.push({ id: fileId, data })
} }
} }
@ -91,7 +97,7 @@ export class FileManager {
// Convert remote file path to local file path // Convert remote file path to local file path
private getLocalFileId(remoteId: string): string | undefined { private getLocalFileId(remoteId: string): string | undefined {
const allParts = remoteId.split("/") const allParts = remoteId.split("/")
if (allParts[1] !== this.sandboxId) return undefined; if (allParts[1] !== this.sandboxId) return undefined
return allParts.slice(2).join("/") return allParts.slice(2).join("/")
} }
@ -99,7 +105,7 @@ export class FileManager {
private getLocalFileIds(remoteIds: string[]): string[] { private getLocalFileIds(remoteIds: string[]): string[] {
return remoteIds return remoteIds
.map(this.getLocalFileId.bind(this)) .map(this.getLocalFileId.bind(this))
.filter((id) => id !== undefined); .filter((id) => id !== undefined)
} }
// Download files from remote storage // Download files from remote storage
@ -120,7 +126,6 @@ export class FileManager {
// Initialize the FileManager // Initialize the FileManager
async initialize() { async initialize() {
// Download files from remote file storage // Download files from remote file storage
await this.updateFileStructure() await this.updateFileStructure()
await this.updateFileData() await this.updateFileData()
@ -141,10 +146,14 @@ export class FileManager {
await Promise.all(promises) await Promise.all(promises)
// Reload file list from the container to include template files // Reload file list from the container to include template files
const result = await this.sandbox.commands.run(`find "${this.dirName}" -type f`); // List all files recursively const result = await this.sandbox.commands.run(
const localPaths = result.stdout.split('\n').filter(path => path); // Split the output into an array and filter out empty strings `find "${this.dirName}" -type f`
const relativePaths = localPaths.map(filePath => path.posix.relative(this.dirName, filePath)); // Convert absolute paths to relative paths ) // List all files recursively
this.files = generateFileStructure(relativePaths); const localPaths = result.stdout.split("\n").filter((path) => path) // Split the output into an array and filter out empty strings
const relativePaths = localPaths.map((filePath) =>
path.posix.relative(this.dirName, filePath)
) // Convert absolute paths to relative paths
this.files = generateFileStructure(relativePaths)
// Make the logged in user the owner of all project files // Make the logged in user the owner of all project files
this.fixPermissions() this.fixPermissions()
@ -169,9 +178,7 @@ export class FileManager {
// Change the owner of the project directory to user // Change the owner of the project directory to user
private async fixPermissions() { private async fixPermissions() {
try { try {
await this.sandbox.commands.run( await this.sandbox.commands.run(`sudo chown -R user "${this.dirName}"`)
`sudo chown -R user "${this.dirName}"`
)
} catch (e: any) { } catch (e: any) {
console.log("Failed to fix permissions: " + e) console.log("Failed to fix permissions: " + e)
} }
@ -193,7 +200,10 @@ export class FileManager {
// This is the absolute file path in the container // This is the absolute file path in the container
const containerFilePath = path.posix.join(directory, event.name) const containerFilePath = path.posix.join(directory, event.name)
// This is the file path relative to the project directory // This is the file path relative to the project directory
const sandboxFilePath = removeDirName(containerFilePath, this.dirName) const sandboxFilePath = removeDirName(
containerFilePath,
this.dirName
)
// This is the directory being watched relative to the project directory // This is the directory being watched relative to the project directory
const sandboxDirectory = removeDirName(directory, this.dirName) const sandboxDirectory = removeDirName(directory, this.dirName)
@ -218,16 +228,16 @@ export class FileManager {
const newItem = isDir const newItem = isDir
? ({ ? ({
id: sandboxFilePath, id: sandboxFilePath,
name: event.name, name: event.name,
type: "folder", type: "folder",
children: [], children: [],
} as TFolder) } as TFolder)
: ({ : ({
id: sandboxFilePath, id: sandboxFilePath,
name: event.name, name: event.name,
type: "file", type: "file",
} as TFile) } as TFile)
if (folder) { if (folder) {
// If the folder exists, add the new item (file/folder) as a child // If the folder exists, add the new item (file/folder) as a child
@ -361,7 +371,9 @@ export class FileManager {
// Get folder content // Get folder content
async getFolder(folderId: string): Promise<string[]> { async getFolder(folderId: string): Promise<string[]> {
const remotePaths = await RemoteFileStorage.getFolder(this.getRemoteFileId(folderId)) const remotePaths = await RemoteFileStorage.getFolder(
this.getRemoteFileId(folderId)
)
return this.getLocalFileIds(remotePaths) return this.getLocalFileIds(remotePaths)
} }
@ -400,7 +412,11 @@ export class FileManager {
fileData.id = newFileId fileData.id = newFileId
file.id = newFileId file.id = newFileId
await RemoteFileStorage.renameFile(this.getRemoteFileId(fileId), this.getRemoteFileId(newFileId), fileData.data) await RemoteFileStorage.renameFile(
this.getRemoteFileId(fileId),
this.getRemoteFileId(newFileId),
fileData.data
)
return this.updateFileStructure() return this.updateFileStructure()
} }
@ -465,7 +481,11 @@ export class FileManager {
await this.moveFileInContainer(fileId, newFileId) await this.moveFileInContainer(fileId, newFileId)
await this.fixPermissions() await this.fixPermissions()
await RemoteFileStorage.renameFile(this.getRemoteFileId(fileId), this.getRemoteFileId(newFileId), fileData.data) await RemoteFileStorage.renameFile(
this.getRemoteFileId(fileId),
this.getRemoteFileId(newFileId),
fileData.data
)
fileData.id = newFileId fileData.id = newFileId
file.id = newFileId file.id = newFileId
@ -477,9 +497,7 @@ export class FileManager {
if (!file) return this.files if (!file) return this.files
await this.sandbox.files.remove(path.posix.join(this.dirName, fileId)) await this.sandbox.files.remove(path.posix.join(this.dirName, fileId))
this.fileData = this.fileData.filter( this.fileData = this.fileData.filter((f) => f.id !== fileId)
(f) => f.id !== fileId
)
await RemoteFileStorage.deleteFile(this.getRemoteFileId(fileId)) await RemoteFileStorage.deleteFile(this.getRemoteFileId(fileId))
return this.updateFileStructure() return this.updateFileStructure()
@ -487,14 +505,14 @@ export class FileManager {
// Delete a folder // Delete a folder
async deleteFolder(folderId: string): Promise<(TFolder | TFile)[]> { async deleteFolder(folderId: string): Promise<(TFolder | TFile)[]> {
const files = await RemoteFileStorage.getFolder(this.getRemoteFileId(folderId)) const files = await RemoteFileStorage.getFolder(
this.getRemoteFileId(folderId)
)
await Promise.all( await Promise.all(
files.map(async (file) => { files.map(async (file) => {
await this.sandbox.files.remove(path.posix.join(this.dirName, file)) await this.sandbox.files.remove(path.posix.join(this.dirName, file))
this.fileData = this.fileData.filter( this.fileData = this.fileData.filter((f) => f.id !== file)
(f) => f.id !== file
)
await RemoteFileStorage.deleteFile(this.getRemoteFileId(file)) await RemoteFileStorage.deleteFile(this.getRemoteFileId(file))
}) })
) )

View File

@ -61,11 +61,7 @@ export const RemoteFileStorage = {
return res.ok return res.ok
}, },
renameFile: async ( renameFile: async (fileId: string, newFileId: string, data: string) => {
fileId: string,
newFileId: string,
data: string
) => {
const res = await fetch(`${process.env.STORAGE_WORKER_URL}/api/rename`, { const res = await fetch(`${process.env.STORAGE_WORKER_URL}/api/rename`, {
method: "POST", method: "POST",
headers: { headers: {
@ -111,7 +107,7 @@ export const RemoteFileStorage = {
} }
) )
return (await res.json()).size return (await res.json()).size
} },
} }
export default RemoteFileStorage export default RemoteFileStorage

View File

@ -5,11 +5,11 @@ import { CONTAINER_TIMEOUT } from "./constants"
import { DokkuClient } from "./DokkuClient" import { DokkuClient } from "./DokkuClient"
import { FileManager } from "./FileManager" import { FileManager } from "./FileManager"
import { import {
createFileRL, createFileRL,
createFolderRL, createFolderRL,
deleteFileRL, deleteFileRL,
renameFileRL, renameFileRL,
saveFileRL, saveFileRL,
} from "./ratelimit" } from "./ratelimit"
import { SecureGitClient } from "./SecureGitClient" import { SecureGitClient } from "./SecureGitClient"
import { TerminalManager } from "./TerminalManager" import { TerminalManager } from "./TerminalManager"
@ -18,245 +18,267 @@ import { LockManager } from "./utils"
const lockManager = new LockManager() const lockManager = new LockManager()
// Define a type for SocketHandler functions // Define a type for SocketHandler functions
type SocketHandler<T = Record<string, any>> = (args: T) => any; type SocketHandler<T = Record<string, any>> = (args: T) => any
// Extract port number from a string // Extract port number from a string
function extractPortNumber(inputString: string): number | null { function extractPortNumber(inputString: string): number | null {
const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, "") const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, "")
const regex = /http:\/\/localhost:(\d+)/ const regex = /http:\/\/localhost:(\d+)/
const match = cleanedString.match(regex) const match = cleanedString.match(regex)
return match ? parseInt(match[1]) : null return match ? parseInt(match[1]) : null
} }
type ServerContext = { type ServerContext = {
aiWorker: AIWorker; aiWorker: AIWorker
dokkuClient: DokkuClient | null; dokkuClient: DokkuClient | null
gitClient: SecureGitClient | null; gitClient: SecureGitClient | null
}; }
export class Sandbox { export class Sandbox {
// Sandbox properties:
sandboxId: string
type: string
fileManager: FileManager | null
terminalManager: TerminalManager | null
container: E2BSandbox | null
// Server context:
dokkuClient: DokkuClient | null
gitClient: SecureGitClient | null
aiWorker: AIWorker
constructor(
sandboxId: string,
type: string,
{ aiWorker, dokkuClient, gitClient }: ServerContext
) {
// Sandbox properties: // Sandbox properties:
sandboxId: string; this.sandboxId = sandboxId
type: string; this.type = type
fileManager: FileManager | null; this.fileManager = null
terminalManager: TerminalManager | null; this.terminalManager = null
container: E2BSandbox | null; this.container = null
// Server context: // Server context:
dokkuClient: DokkuClient | null; this.aiWorker = aiWorker
gitClient: SecureGitClient | null; this.dokkuClient = dokkuClient
aiWorker: AIWorker; this.gitClient = gitClient
}
constructor(sandboxId: string, type: string, { aiWorker, dokkuClient, gitClient }: ServerContext) { // Initializes the container for the sandbox environment
// Sandbox properties: async initialize(
this.sandboxId = sandboxId; fileWatchCallback: ((files: (TFolder | TFile)[]) => void) | undefined
this.type = type; ) {
this.fileManager = null; // Acquire a lock to ensure exclusive access to the sandbox environment
this.terminalManager = null; await lockManager.acquireLock(this.sandboxId, async () => {
this.container = null; // Check if a container already exists and is running
// Server context: if (this.container && (await this.container.isRunning())) {
this.aiWorker = aiWorker; console.log(`Found existing container ${this.sandboxId}`)
this.dokkuClient = dokkuClient; } else {
this.gitClient = gitClient; console.log("Creating container", this.sandboxId)
} // Create a new container with a specified template and timeout
const templateTypes = ["vanillajs", "reactjs", "nextjs", "streamlit"]
// Initializes the container for the sandbox environment const template = templateTypes.includes(this.type)
async initialize( ? `gitwit-${this.type}`
fileWatchCallback: ((files: (TFolder | TFile)[]) => void) | undefined : `base`
) { this.container = await E2BSandbox.create(template, {
// Acquire a lock to ensure exclusive access to the sandbox environment timeoutMs: CONTAINER_TIMEOUT,
await lockManager.acquireLock(this.sandboxId, async () => {
// Check if a container already exists and is running
if (this.container && await this.container.isRunning()) {
console.log(`Found existing container ${this.sandboxId}`)
} else {
console.log("Creating container", this.sandboxId)
// Create a new container with a specified template and timeout
const templateTypes = ["vanillajs", "reactjs", "nextjs", "streamlit"];
const template = templateTypes.includes(this.type)
? `gitwit-${this.type}`
: `base`;
this.container = await E2BSandbox.create(template, {
timeoutMs: CONTAINER_TIMEOUT,
})
}
}) })
// Ensure a container was successfully created }
if (!this.container) throw new Error("Failed to create container") })
// Ensure a container was successfully created
if (!this.container) throw new Error("Failed to create container")
// Initialize the terminal manager if it hasn't been set up yet // Initialize the terminal manager if it hasn't been set up yet
if (!this.terminalManager) { if (!this.terminalManager) {
this.terminalManager = new TerminalManager(this.container) this.terminalManager = new TerminalManager(this.container)
console.log(`Terminal manager set up for ${this.sandboxId}`) console.log(`Terminal manager set up for ${this.sandboxId}`)
}
// Initialize the file manager if it hasn't been set up yet
if (!this.fileManager) {
this.fileManager = new FileManager(
this.sandboxId,
this.container,
fileWatchCallback ?? null
)
// Initialize the file manager and emit the initial files
await this.fileManager.initialize()
}
} }
// Called when the client disconnects from the Sandbox // Initialize the file manager if it hasn't been set up yet
async disconnect() { if (!this.fileManager) {
// Close all terminals managed by the terminal manager this.fileManager = new FileManager(
await this.terminalManager?.closeAllTerminals() this.sandboxId,
// This way the terminal manager will be set up again if we reconnect this.container,
this.terminalManager = null; fileWatchCallback ?? null
// Close all file watchers managed by the file manager )
await this.fileManager?.closeWatchers() // Initialize the file manager and emit the initial files
// This way the file manager will be set up again if we reconnect await this.fileManager.initialize()
this.fileManager = null; }
}
// Called when the client disconnects from the Sandbox
async disconnect() {
// Close all terminals managed by the terminal manager
await this.terminalManager?.closeAllTerminals()
// This way the terminal manager will be set up again if we reconnect
this.terminalManager = null
// Close all file watchers managed by the file manager
await this.fileManager?.closeWatchers()
// This way the file manager will be set up again if we reconnect
this.fileManager = null
}
handlers(connection: { userId: string; isOwner: boolean; socket: Socket }) {
// Handle heartbeat from a socket connection
const handleHeartbeat: SocketHandler = (_: any) => {
// Only keep the sandbox alive if the owner is still connected
if (connection.isOwner) {
this.container?.setTimeout(CONTAINER_TIMEOUT)
}
} }
handlers(connection: { userId: string, isOwner: boolean, socket: Socket }) { // Handle getting a file
const handleGetFile: SocketHandler = ({ fileId }: any) => {
return this.fileManager?.getFile(fileId)
}
// Handle heartbeat from a socket connection // Handle getting a folder
const handleHeartbeat: SocketHandler = (_: any) => { const handleGetFolder: SocketHandler = ({ folderId }: any) => {
// Only keep the sandbox alive if the owner is still connected return this.fileManager?.getFolder(folderId)
if (connection.isOwner) { }
this.container?.setTimeout(CONTAINER_TIMEOUT)
}
}
// Handle getting a file // Handle saving a file
const handleGetFile: SocketHandler = ({ fileId }: any) => { const handleSaveFile: SocketHandler = async ({ fileId, body }: any) => {
return this.fileManager?.getFile(fileId) await saveFileRL.consume(connection.userId, 1)
} return this.fileManager?.saveFile(fileId, body)
}
// Handle getting a folder // Handle moving a file
const handleGetFolder: SocketHandler = ({ folderId }: any) => { const handleMoveFile: SocketHandler = ({ fileId, folderId }: any) => {
return this.fileManager?.getFolder(folderId) return this.fileManager?.moveFile(fileId, folderId)
} }
// Handle saving a file // Handle listing apps
const handleSaveFile: SocketHandler = async ({ fileId, body }: any) => { const handleListApps: SocketHandler = async (_: any) => {
await saveFileRL.consume(connection.userId, 1); if (!this.dokkuClient)
return this.fileManager?.saveFile(fileId, body) throw Error("Failed to retrieve apps list: No Dokku client")
} return { success: true, apps: await this.dokkuClient.listApps() }
}
// Handle moving a file // Handle deploying code
const handleMoveFile: SocketHandler = ({ fileId, folderId }: any) => { const handleDeploy: SocketHandler = async (_: any) => {
return this.fileManager?.moveFile(fileId, folderId) if (!this.gitClient) throw Error("No git client")
} if (!this.fileManager) throw Error("No file manager")
await this.gitClient.pushFiles(this.fileManager?.fileData, this.sandboxId)
return { success: true }
}
// Handle listing apps // Handle creating a file
const handleListApps: SocketHandler = async (_: any) => { const handleCreateFile: SocketHandler = async ({ name }: any) => {
if (!this.dokkuClient) throw Error("Failed to retrieve apps list: No Dokku client") await createFileRL.consume(connection.userId, 1)
return { success: true, apps: await this.dokkuClient.listApps() } return { success: await this.fileManager?.createFile(name) }
} }
// Handle deploying code // Handle creating a folder
const handleDeploy: SocketHandler = async (_: any) => { const handleCreateFolder: SocketHandler = async ({ name }: any) => {
if (!this.gitClient) throw Error("No git client") await createFolderRL.consume(connection.userId, 1)
if (!this.fileManager) throw Error("No file manager") return { success: await this.fileManager?.createFolder(name) }
await this.gitClient.pushFiles(this.fileManager?.fileData, this.sandboxId) }
return { success: true }
}
// Handle creating a file // Handle renaming a file
const handleCreateFile: SocketHandler = async ({ name }: any) => { const handleRenameFile: SocketHandler = async ({
await createFileRL.consume(connection.userId, 1); fileId,
return { "success": await this.fileManager?.createFile(name) } newName,
} }: any) => {
await renameFileRL.consume(connection.userId, 1)
return this.fileManager?.renameFile(fileId, newName)
}
// Handle creating a folder // Handle deleting a file
const handleCreateFolder: SocketHandler = async ({ name }: any) => { const handleDeleteFile: SocketHandler = async ({ fileId }: any) => {
await createFolderRL.consume(connection.userId, 1); await deleteFileRL.consume(connection.userId, 1)
return { "success": await this.fileManager?.createFolder(name) } return this.fileManager?.deleteFile(fileId)
} }
// Handle renaming a file // Handle deleting a folder
const handleRenameFile: SocketHandler = async ({ fileId, newName }: any) => { const handleDeleteFolder: SocketHandler = ({ folderId }: any) => {
await renameFileRL.consume(connection.userId, 1) return this.fileManager?.deleteFolder(folderId)
return this.fileManager?.renameFile(fileId, newName) }
}
// Handle deleting a file // Handle creating a terminal session
const handleDeleteFile: SocketHandler = async ({ fileId }: any) => { const handleCreateTerminal: SocketHandler = async ({ id }: any) => {
await deleteFileRL.consume(connection.userId, 1) await lockManager.acquireLock(this.sandboxId, async () => {
return this.fileManager?.deleteFile(fileId) await this.terminalManager?.createTerminal(
} id,
(responseString: string) => {
// Handle deleting a folder connection.socket.emit("terminalResponse", {
const handleDeleteFolder: SocketHandler = ({ folderId }: any) => { id,
return this.fileManager?.deleteFolder(folderId) data: responseString,
}
// Handle creating a terminal session
const handleCreateTerminal: SocketHandler = async ({ id }: any) => {
await lockManager.acquireLock(this.sandboxId, async () => {
await this.terminalManager?.createTerminal(id, (responseString: string) => {
connection.socket.emit("terminalResponse", { id, data: responseString })
const port = extractPortNumber(responseString)
if (port) {
connection.socket.emit(
"previewURL",
"https://" + this.container?.getHost(port)
)
}
})
}) })
} const port = extractPortNumber(responseString)
if (port) {
// Handle resizing a terminal connection.socket.emit(
const handleResizeTerminal: SocketHandler = ({ dimensions }: any) => { "previewURL",
this.terminalManager?.resizeTerminal(dimensions) "https://" + this.container?.getHost(port)
} )
}
// Handle sending data to a terminal }
const handleTerminalData: SocketHandler = ({ id, data }: any) => { )
return this.terminalManager?.sendTerminalData(id, data) })
}
// Handle closing a terminal
const handleCloseTerminal: SocketHandler = ({ id }: any) => {
return this.terminalManager?.closeTerminal(id)
}
// Handle generating code
const handleGenerateCode: SocketHandler = ({ fileName, code, line, instructions }: any) => {
return this.aiWorker.generateCode(connection.userId, fileName, code, line, instructions)
}
// Handle downloading files by download button
const handleDownloadFiles: SocketHandler = async () => {
if (!this.fileManager) throw Error("No file manager")
// Get all files with their data through fileManager
const files = this.fileManager.fileData.map((file: TFileData) => ({
path: file.id.startsWith('/') ? file.id.slice(1) : file.id,
content: file.data
}))
return { files }
}
return {
"heartbeat": handleHeartbeat,
"getFile": handleGetFile,
"downloadFiles": handleDownloadFiles,
"getFolder": handleGetFolder,
"saveFile": handleSaveFile,
"moveFile": handleMoveFile,
"list": handleListApps,
"deploy": handleDeploy,
"createFile": handleCreateFile,
"createFolder": handleCreateFolder,
"renameFile": handleRenameFile,
"deleteFile": handleDeleteFile,
"deleteFolder": handleDeleteFolder,
"createTerminal": handleCreateTerminal,
"resizeTerminal": handleResizeTerminal,
"terminalData": handleTerminalData,
"closeTerminal": handleCloseTerminal,
"generateCode": handleGenerateCode,
};
} }
// Handle resizing a terminal
const handleResizeTerminal: SocketHandler = ({ dimensions }: any) => {
this.terminalManager?.resizeTerminal(dimensions)
}
// Handle sending data to a terminal
const handleTerminalData: SocketHandler = ({ id, data }: any) => {
return this.terminalManager?.sendTerminalData(id, data)
}
// Handle closing a terminal
const handleCloseTerminal: SocketHandler = ({ id }: any) => {
return this.terminalManager?.closeTerminal(id)
}
// Handle generating code
const handleGenerateCode: SocketHandler = ({
fileName,
code,
line,
instructions,
}: any) => {
return this.aiWorker.generateCode(
connection.userId,
fileName,
code,
line,
instructions
)
}
// Handle downloading files by download button
const handleDownloadFiles: SocketHandler = async () => {
if (!this.fileManager) throw Error("No file manager")
// Get all files with their data through fileManager
const files = this.fileManager.fileData.map((file: TFileData) => ({
path: file.id.startsWith("/") ? file.id.slice(1) : file.id,
content: file.data,
}))
return { files }
}
return {
heartbeat: handleHeartbeat,
getFile: handleGetFile,
downloadFiles: handleDownloadFiles,
getFolder: handleGetFolder,
saveFile: handleSaveFile,
moveFile: handleMoveFile,
list: handleListApps,
deploy: handleDeploy,
createFile: handleCreateFile,
createFolder: handleCreateFolder,
renameFile: handleRenameFile,
deleteFile: handleDeleteFile,
deleteFolder: handleDeleteFolder,
createTerminal: handleCreateTerminal,
resizeTerminal: handleResizeTerminal,
terminalData: handleTerminalData,
closeTerminal: handleCloseTerminal,
generateCode: handleGenerateCode,
}
}
} }

View File

@ -10,14 +10,14 @@ import { ConnectionManager } from "./ConnectionManager"
import { DokkuClient } from "./DokkuClient" import { DokkuClient } from "./DokkuClient"
import { Sandbox } from "./Sandbox" import { Sandbox } from "./Sandbox"
import { SecureGitClient } from "./SecureGitClient" import { SecureGitClient } from "./SecureGitClient"
import { socketAuth } from "./socketAuth"; // Import the new socketAuth middleware import { socketAuth } from "./socketAuth" // Import the new socketAuth middleware
import { TFile, TFolder } from "./types" import { TFile, TFolder } from "./types"
// Log errors and send a notification to the client // Log errors and send a notification to the client
export const handleErrors = (message: string, error: any, socket: Socket) => { export const handleErrors = (message: string, error: any, socket: Socket) => {
console.error(message, error); console.error(message, error)
socket.emit("error", `${message} ${error.message ?? error}`); socket.emit("error", `${message} ${error.message ?? error}`)
}; }
// Handle uncaught exceptions // Handle uncaught exceptions
process.on("uncaughtException", (error) => { process.on("uncaughtException", (error) => {
@ -64,10 +64,10 @@ if (!process.env.DOKKU_KEY)
const dokkuClient = const dokkuClient =
process.env.DOKKU_HOST && process.env.DOKKU_KEY && process.env.DOKKU_USERNAME process.env.DOKKU_HOST && process.env.DOKKU_KEY && process.env.DOKKU_USERNAME
? new DokkuClient({ ? new DokkuClient({
host: process.env.DOKKU_HOST, host: process.env.DOKKU_HOST,
username: process.env.DOKKU_USERNAME, username: process.env.DOKKU_USERNAME,
privateKey: fs.readFileSync(process.env.DOKKU_KEY), privateKey: fs.readFileSync(process.env.DOKKU_KEY),
}) })
: null : null
dokkuClient?.connect() dokkuClient?.connect()
@ -75,9 +75,9 @@ dokkuClient?.connect()
const gitClient = const gitClient =
process.env.DOKKU_HOST && process.env.DOKKU_KEY process.env.DOKKU_HOST && process.env.DOKKU_KEY
? new SecureGitClient( ? new SecureGitClient(
`dokku@${process.env.DOKKU_HOST}`, `dokku@${process.env.DOKKU_HOST}`,
process.env.DOKKU_KEY process.env.DOKKU_KEY
) )
: null : null
// Add this near the top of the file, after other initializations // Add this near the top of the file, after other initializations
@ -110,21 +110,23 @@ io.on("connection", async (socket) => {
try { try {
// Create or retrieve the sandbox manager for the given sandbox ID // Create or retrieve the sandbox manager for the given sandbox ID
const sandbox = sandboxes[data.sandboxId] ?? new Sandbox( const sandbox =
data.sandboxId, sandboxes[data.sandboxId] ??
data.type, new Sandbox(data.sandboxId, data.type, {
{ aiWorker,
aiWorker, dokkuClient, gitClient, dokkuClient,
} gitClient,
) })
sandboxes[data.sandboxId] = sandbox sandboxes[data.sandboxId] = sandbox
// This callback recieves an update when the file list changes, and notifies all relevant connections. // This callback recieves an update when the file list changes, and notifies all relevant connections.
const sendFileNotifications = (files: (TFolder | TFile)[]) => { const sendFileNotifications = (files: (TFolder | TFile)[]) => {
connections.connectionsForSandbox(data.sandboxId).forEach((socket: Socket) => { connections
socket.emit("loaded", files); .connectionsForSandbox(data.sandboxId)
}); .forEach((socket: Socket) => {
}; socket.emit("loaded", files)
})
}
// Initialize the sandbox container // Initialize the sandbox container
// The file manager and terminal managers will be set up if they have been closed // The file manager and terminal managers will be set up if they have been closed
@ -134,26 +136,35 @@ io.on("connection", async (socket) => {
// Register event handlers for the sandbox // Register event handlers for the sandbox
// For each event handler, listen on the socket for that event // For each event handler, listen on the socket for that event
// Pass connection-specific information to the handlers // Pass connection-specific information to the handlers
Object.entries(sandbox.handlers({ Object.entries(
userId: data.userId, sandbox.handlers({
isOwner: data.isOwner, userId: data.userId,
socket isOwner: data.isOwner,
})).forEach(([event, handler]) => { socket,
socket.on(event, async (options: any, callback?: (response: any) => void) => { })
try { ).forEach(([event, handler]) => {
const result = await handler(options) socket.on(
callback?.(result); event,
} catch (e: any) { async (options: any, callback?: (response: any) => void) => {
handleErrors(`Error processing event "${event}":`, e, socket); try {
const result = await handler(options)
callback?.(result)
} catch (e: any) {
handleErrors(`Error processing event "${event}":`, e, socket)
}
} }
}); )
}); })
// Handle disconnection event // Handle disconnection event
socket.on("disconnect", async () => { socket.on("disconnect", async () => {
try { try {
// Deregister the connection // Deregister the connection
connections.removeConnectionForSandbox(socket, data.sandboxId, data.isOwner) connections.removeConnectionForSandbox(
socket,
data.sandboxId,
data.isOwner
)
// If the owner has disconnected from all sockets, close open terminals and file watchers.o // If the owner has disconnected from all sockets, close open terminals and file watchers.o
// The sandbox itself will timeout after the heartbeat stops. // The sandbox itself will timeout after the heartbeat stops.
@ -165,16 +176,14 @@ io.on("connection", async (socket) => {
) )
} }
} catch (e: any) { } catch (e: any) {
handleErrors("Error disconnecting:", e, socket); handleErrors("Error disconnecting:", e, socket)
} }
}) })
} catch (e: any) { } catch (e: any) {
handleErrors(`Error initializing sandbox ${data.sandboxId}:`, e, socket); handleErrors(`Error initializing sandbox ${data.sandboxId}:`, e, socket)
} }
} catch (e: any) { } catch (e: any) {
handleErrors("Error connecting:", e, socket); handleErrors("Error connecting:", e, socket)
} }
}) })

View File

@ -4,72 +4,72 @@ import { Sandbox, User } from "./types"
// Middleware for socket authentication // Middleware for socket authentication
export const socketAuth = async (socket: Socket, next: Function) => { export const socketAuth = async (socket: Socket, next: Function) => {
// Define the schema for handshake query validation // Define the schema for handshake query validation
const handshakeSchema = z.object({ const handshakeSchema = z.object({
userId: z.string(), userId: z.string(),
sandboxId: z.string(), sandboxId: z.string(),
EIO: z.string(), EIO: z.string(),
transport: z.string(), transport: z.string(),
}) })
const q = socket.handshake.query const q = socket.handshake.query
const parseQuery = handshakeSchema.safeParse(q) const parseQuery = handshakeSchema.safeParse(q)
// Check if the query is valid according to the schema // Check if the query is valid according to the schema
if (!parseQuery.success) { if (!parseQuery.success) {
next(new Error("Invalid request.")) next(new Error("Invalid request."))
return return
}
const { sandboxId, userId } = parseQuery.data
// Fetch user data from the database
const dbUser = await fetch(
`${process.env.DATABASE_WORKER_URL}/api/user?id=${userId}`,
{
headers: {
Authorization: `${process.env.WORKERS_KEY}`,
},
} }
)
const dbUserJSON = (await dbUser.json()) as User
const { sandboxId, userId } = parseQuery.data // Fetch sandbox data from the database
// Fetch user data from the database const dbSandbox = await fetch(
const dbUser = await fetch( `${process.env.DATABASE_WORKER_URL}/api/sandbox?id=${sandboxId}`,
`${process.env.DATABASE_WORKER_URL}/api/user?id=${userId}`, {
{ headers: {
headers: { Authorization: `${process.env.WORKERS_KEY}`,
Authorization: `${process.env.WORKERS_KEY}`, },
},
}
)
const dbUserJSON = (await dbUser.json()) as User
// Fetch sandbox data from the database
const dbSandbox = await fetch(
`${process.env.DATABASE_WORKER_URL}/api/sandbox?id=${sandboxId}`,
{
headers: {
Authorization: `${process.env.WORKERS_KEY}`,
},
}
)
const dbSandboxJSON = (await dbSandbox.json()) as Sandbox
// Check if user data was retrieved successfully
if (!dbUserJSON) {
next(new Error("DB error."))
return
} }
)
const dbSandboxJSON = (await dbSandbox.json()) as Sandbox
// Check if the user owns the sandbox or has shared access // Check if user data was retrieved successfully
const sandbox = dbUserJSON.sandbox.find((s) => s.id === sandboxId) if (!dbUserJSON) {
const sharedSandboxes = dbUserJSON.usersToSandboxes.find( next(new Error("DB error."))
(uts) => uts.sandboxId === sandboxId return
) }
// If user doesn't own or have shared access to the sandbox, deny access // Check if the user owns the sandbox or has shared access
if (!sandbox && !sharedSandboxes) { const sandbox = dbUserJSON.sandbox.find((s) => s.id === sandboxId)
next(new Error("Invalid credentials.")) const sharedSandboxes = dbUserJSON.usersToSandboxes.find(
return (uts) => uts.sandboxId === sandboxId
} )
// Set socket data with user information // If user doesn't own or have shared access to the sandbox, deny access
socket.data = { if (!sandbox && !sharedSandboxes) {
userId, next(new Error("Invalid credentials."))
sandboxId: sandboxId, return
isOwner: sandbox !== undefined, }
type: dbSandboxJSON.type
}
// Allow the connection // Set socket data with user information
next() socket.data = {
userId,
sandboxId: sandboxId,
isOwner: sandbox !== undefined,
type: dbSandboxJSON.type,
}
// Allow the connection
next()
} }