feat: load project templates from custom E2B sandboxes instead of from Cloudflare

This commit is contained in:
James Murdza 2024-11-02 13:27:11 -06:00
parent 5633727bdb
commit 5a63ab7265
6 changed files with 37 additions and 38 deletions

View File

@ -140,6 +140,12 @@ export class FileManager {
}) })
await Promise.all(promises) await Promise.all(promises)
// 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 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.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()
@ -348,8 +354,9 @@ export class FileManager {
// Get file content // Get file content
async getFile(fileId: string): Promise<string | undefined> { async getFile(fileId: string): Promise<string | undefined> {
const file = this.fileData.find((f) => f.id === fileId) const filePath = path.posix.join(this.dirName, fileId)
return file?.data const fileContent = await this.sandbox.files.read(filePath)
return fileContent
} }
// Get folder content // Get folder content

View File

@ -13,9 +13,8 @@ import {
} from "./ratelimit" } from "./ratelimit"
import { SecureGitClient } from "./SecureGitClient" import { SecureGitClient } from "./SecureGitClient"
import { TerminalManager } from "./TerminalManager" import { TerminalManager } from "./TerminalManager"
import { TFile, TFolder } from "./types" import { TFile, TFileData, TFolder } from "./types"
import { LockManager } from "./utils" import { LockManager } from "./utils"
import { TFileData } from "./types"
const lockManager = new LockManager() const lockManager = new LockManager()
// Define a type for SocketHandler functions // Define a type for SocketHandler functions
@ -38,6 +37,7 @@ type ServerContext = {
export class Sandbox { export class Sandbox {
// Sandbox properties: // Sandbox properties:
sandboxId: string; sandboxId: string;
type: string;
fileManager: FileManager | null; fileManager: FileManager | null;
terminalManager: TerminalManager | null; terminalManager: TerminalManager | null;
container: E2BSandbox | null; container: E2BSandbox | null;
@ -46,9 +46,10 @@ export class Sandbox {
gitClient: SecureGitClient | null; gitClient: SecureGitClient | null;
aiWorker: AIWorker; aiWorker: AIWorker;
constructor(sandboxId: string, { aiWorker, dokkuClient, gitClient }: ServerContext) { constructor(sandboxId: string, type: string, { aiWorker, dokkuClient, gitClient }: ServerContext) {
// Sandbox properties: // Sandbox properties:
this.sandboxId = sandboxId; this.sandboxId = sandboxId;
this.type = type;
this.fileManager = null; this.fileManager = null;
this.terminalManager = null; this.terminalManager = null;
this.container = null; this.container = null;
@ -69,8 +70,12 @@ export class Sandbox {
console.log(`Found existing container ${this.sandboxId}`) console.log(`Found existing container ${this.sandboxId}`)
} else { } else {
console.log("Creating container", this.sandboxId) console.log("Creating container", this.sandboxId)
// Create a new container with a specified timeout // Create a new container with a specified template and timeout
this.container = await E2BSandbox.create({ 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, timeoutMs: CONTAINER_TIMEOUT,
}) })
} }

View File

@ -96,6 +96,7 @@ io.on("connection", async (socket) => {
userId: string userId: string
sandboxId: string sandboxId: string
isOwner: boolean isOwner: boolean
type: string
} }
// Register the connection // Register the connection
@ -111,6 +112,7 @@ io.on("connection", async (socket) => {
// 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 = sandboxes[data.sandboxId] ?? new Sandbox(
data.sandboxId, data.sandboxId,
data.type,
{ {
aiWorker, dokkuClient, gitClient, aiWorker, dokkuClient, gitClient,
} }

View File

@ -1,6 +1,6 @@
import { Socket } from "socket.io" import { Socket } from "socket.io"
import { z } from "zod" import { z } from "zod"
import { User } from "./types" 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) => {
@ -33,6 +33,17 @@ export const socketAuth = async (socket: Socket, next: Function) => {
) )
const dbUserJSON = (await dbUser.json()) as User 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 // Check if user data was retrieved successfully
if (!dbUserJSON) { if (!dbUserJSON) {
next(new Error("DB error.")) next(new Error("DB error."))
@ -56,6 +67,7 @@ export const socketAuth = async (socket: Socket, next: Function) => {
userId, userId,
sandboxId: sandboxId, sandboxId: sandboxId,
isOwner: sandbox !== undefined, isOwner: sandbox !== undefined,
type: dbSandboxJSON.type
} }
// Allow the connection // Allow the connection

View File

@ -1,4 +1,3 @@
import pLimit from "p-limit"
import { z } from "zod" import { z } from "zod"
export interface Env { export interface Env {
@ -136,33 +135,7 @@ export default {
return success return success
} else if (path === "/api/init" && method === "POST") { } else if (path === "/api/init" && method === "POST") {
const initSchema = z.object({ // This API path no longer does anything, because template files are stored in E2B sandbox templates.
sandboxId: z.string(),
type: z.string(),
})
const body = await request.json()
const { sandboxId, type } = initSchema.parse(body)
console.log(`Copying template: ${type}`)
// List all objects under the directory
const { objects } = await env.Templates.list({ prefix: type })
// Copy each object to the new directory with a 5 concurrency limit
const limit = pLimit(5)
await Promise.all(
objects.map(({ key }) =>
limit(async () => {
const destinationKey = key.replace(type, `projects/${sandboxId}`)
const fileBody = await env.Templates.get(key).then(
(res) => res?.body ?? ""
)
await env.R2.put(destinationKey, fileBody)
})
)
)
return success return success
} else { } else {
return notFound return notFound

View File

@ -44,8 +44,8 @@ export default function RunButtonModal({
} else if (!isRunning && terminals.length < 4) { } else if (!isRunning && terminals.length < 4) {
const command = const command =
sandboxData.type === "streamlit" sandboxData.type === "streamlit"
? "pip install -r requirements.txt && streamlit run main.py --server.runOnSave true" ? "./venv/bin/streamlit run main.py --server.runOnSave true"
: "yarn install && yarn dev" : "npm run dev"
try { try {
// Create a new terminal with the appropriate command // Create a new terminal with the appropriate command