sandbox.container.app.ts•4.39 kB
import { exec } from 'node:child_process'
import * as fs from 'node:fs/promises'
import path from 'node:path'
import { serve } from '@hono/node-server'
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { streamText } from 'hono/streaming'
import mime from 'mime'
import { ExecParams, FileWrite } from '../shared/schema.ts'
import {
	DIRECTORY_CONTENT_TYPE,
	get_file_name_from_path,
	get_mime_type,
	list_files_in_directory,
} from './fileUtils.ts'
import type { FileList } from '../shared/schema.ts'
process.chdir('workdir')
const app = new Hono()
app.get('/ping', (c) => c.text('pong!'))
/**
 * GET /files/ls
 *
 * Gets all files in a directory
 */
app.get('/files/ls', async (c) => {
	const directoriesToRead = ['.']
	const files: FileList = { resources: [] }
	while (directoriesToRead.length > 0) {
		const curr = directoriesToRead.pop()
		if (!curr) {
			throw new Error('Popped empty stack, error while listing directories')
		}
		const fullPath = path.join(process.cwd(), curr)
		const dir = await fs.readdir(fullPath, { withFileTypes: true })
		for (const dirent of dir) {
			const relPath = path.relative(process.cwd(), `${fullPath}/${dirent.name}`)
			if (dirent.isDirectory()) {
				directoriesToRead.push(dirent.name)
				files.resources.push({
					uri: `file:///${relPath}`,
					name: dirent.name,
					mimeType: 'inode/directory',
				})
			} else {
				const mimeType = mime.getType(dirent.name)
				files.resources.push({
					uri: `file:///${relPath}`,
					name: dirent.name,
					mimeType: mimeType ?? undefined,
				})
			}
		}
	}
	return c.json(files)
})
/**
 * GET /files/contents/{filepath}
 *
 * Get the contents of a file or directory
 */
app.get('/files/contents/*', async (c) => {
	const reqPath = await get_file_name_from_path(c.req.path)
	try {
		const mimeType = await get_mime_type(reqPath)
		const headers = mimeType ? { 'Content-Type': mimeType } : undefined
		const contents = await fs.readFile(path.join(process.cwd(), reqPath))
		return c.newResponse(contents, 200, headers)
	} catch (e: any) {
		if (e.code) {
			if (e.code === 'EISDIR') {
				const files = await list_files_in_directory(reqPath)
				return c.newResponse(files.join('\n'), 200, {
					'Content-Type': DIRECTORY_CONTENT_TYPE,
				})
			}
			if (e.code === 'ENOENT') {
				return c.notFound()
			}
		}
		throw e
	}
})
/**
 * POST /files/contents
 *
 * Create or update file contents
 */
app.post('/files/contents', zValidator('json', FileWrite), async (c) => {
	const file = c.req.valid('json')
	const reqPath = await get_file_name_from_path(file.path)
	try {
		await fs.writeFile(reqPath, file.text)
		return c.newResponse(null, 200)
	} catch (e) {
		return c.newResponse(`Error: ${e}`, 400)
	}
})
/**
 * DELETE /files/contents/{filepath}
 *
 * Delete a file or directory
 */
app.delete('/files/contents/*', async (c) => {
	const reqPath = await get_file_name_from_path(c.req.path)
	try {
		await fs.rm(path.join(process.cwd(), reqPath), { recursive: true })
		return c.newResponse('ok', 200)
	} catch (e: any) {
		if (e.code) {
			if (e.code === 'ENOENT') {
				return c.notFound()
			}
		}
		throw e
	}
})
/**
 * POST /exec
 *
 * Execute a command in a shell
 */
app.post('/exec', zValidator('json', ExecParams), (c) => {
	const execParams = c.req.valid('json')
	const proc = exec(execParams.args)
	return streamText(c, async (stream) => {
		return new Promise((resolve, reject) => {
			if (proc.stdout) {
				// Stream data from stdout
				proc.stdout.on('data', async (data) => {
					await stream.write(data.toString())
				})
			} else {
				void stream.write('WARNING: no stdout stream for process')
			}
			if (execParams.streamStderr) {
				if (proc.stderr) {
					proc.stderr.on('data', async (data) => {
						await stream.write(data.toString())
					})
				} else {
					void stream.write('WARNING: no stderr stream for process')
				}
			}
			// Handle process exit
			proc.on('exit', async (code) => {
				await stream.write(`Process exited with code: ${code}`)
				if (code === 0) {
					await stream.close()
					resolve()
				} else {
					console.error(`Process exited with code ${code}`)
					reject(new Error(`Process failed with code ${code}`))
				}
			})
			proc.on('error', (err) => {
				console.error('Error with process: ', err)
				reject(err)
			})
		})
	})
})
serve({
	fetch: app.fetch,
	port: 8080,
})