import * as plugins from '../../plugins.ts'; import { logger } from '../../logging.ts'; import type { OpsServer } from '../classes.opsserver.ts'; import * as interfaces from '../../../ts_interfaces/index.ts'; import { requireValidIdentity } from '../helpers/guards.ts'; import { getErrorMessage } from '../../utils/error.ts'; export class WorkspaceHandler { public typedrouter = new plugins.typedrequest.TypedRouter(); constructor(private opsServerRef: OpsServer) { this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter); this.registerHandlers(); } /** * Resolve a service name to a container ID (handling Swarm service IDs) */ private async resolveContainerId(serviceName: string): Promise { const service = this.opsServerRef.oneboxRef.services.getService(serviceName); if (!service || !service.containerID) { throw new plugins.typedrequest.TypedResponseError(`Service not found or has no container: ${serviceName}`); } return service.containerID; } private registerHandlers(): void { // Read file from container this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'workspaceReadFile', async (dataArg) => { await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); const containerId = await this.resolveContainerId(dataArg.serviceName); const result = await this.opsServerRef.oneboxRef.docker.execInContainer( containerId, ['cat', dataArg.path], ); if (result.exitCode !== 0) { throw new plugins.typedrequest.TypedResponseError(`Failed to read file: ${result.stderr || 'File not found'}`); } return { content: result.stdout }; }, ), ); // Write file to container this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'workspaceWriteFile', async (dataArg) => { await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); const containerId = await this.resolveContainerId(dataArg.serviceName); // Use sh -c with printf to write content (handles special characters) const escaped = dataArg.content.replace(/'/g, "'\\''"); const result = await this.opsServerRef.oneboxRef.docker.execInContainer( containerId, ['sh', '-c', `printf '%s' '${escaped}' > ${dataArg.path}`], ); if (result.exitCode !== 0) { throw new plugins.typedrequest.TypedResponseError(`Failed to write file: ${result.stderr}`); } return {}; }, ), ); // Read directory from container this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'workspaceReadDir', async (dataArg) => { await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); const containerId = await this.resolveContainerId(dataArg.serviceName); // Use ls with -1 -F to get entries with type indicators (/ for dirs) const result = await this.opsServerRef.oneboxRef.docker.execInContainer( containerId, ['ls', '-1', '-F', dataArg.path], ); if (result.exitCode !== 0) { throw new plugins.typedrequest.TypedResponseError(`Failed to read directory: ${result.stderr}`); } const entries = result.stdout .split('\n') .filter((line) => line.trim()) .map((line) => { const isDir = line.endsWith('/'); const name = isDir ? line.slice(0, -1) : line.replace(/[*@=|]$/, ''); const basePath = dataArg.path.endsWith('/') ? dataArg.path : dataArg.path + '/'; return { type: (isDir ? 'directory' : 'file') as 'file' | 'directory', name, path: basePath + name, }; }); return { entries }; }, ), ); // Create directory in container this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'workspaceMkdir', async (dataArg) => { await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); const containerId = await this.resolveContainerId(dataArg.serviceName); const result = await this.opsServerRef.oneboxRef.docker.execInContainer( containerId, ['mkdir', '-p', dataArg.path], ); if (result.exitCode !== 0) { throw new plugins.typedrequest.TypedResponseError(`Failed to create directory: ${result.stderr}`); } return {}; }, ), ); // Remove file/directory from container this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'workspaceRm', async (dataArg) => { await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); const containerId = await this.resolveContainerId(dataArg.serviceName); const args = dataArg.recursive ? ['rm', '-rf', dataArg.path] : ['rm', '-f', dataArg.path]; const result = await this.opsServerRef.oneboxRef.docker.execInContainer( containerId, args, ); if (result.exitCode !== 0) { throw new plugins.typedrequest.TypedResponseError(`Failed to remove: ${result.stderr}`); } return {}; }, ), ); // Check if path exists in container this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'workspaceExists', async (dataArg) => { await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); const containerId = await this.resolveContainerId(dataArg.serviceName); const result = await this.opsServerRef.oneboxRef.docker.execInContainer( containerId, ['test', '-e', dataArg.path], ); return { exists: result.exitCode === 0 }; }, ), ); // Execute a command in the container (non-interactive) this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'workspaceExec', async (dataArg) => { await requireValidIdentity(this.opsServerRef.adminHandler, dataArg); const containerId = await this.resolveContainerId(dataArg.serviceName); const cmd = dataArg.args ? [dataArg.command, ...dataArg.args] : [dataArg.command]; const result = await this.opsServerRef.oneboxRef.docker.execInContainer( containerId, cmd, ); return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode, }; }, ), ); logger.info('Workspace handler registered'); } }