UNPKG

cerevox

Version:

TypeScript SDK for browser automation and secure command execution in highly available and scalable micro computer environments

414 lines 16.5 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.FileSystem = void 0; const promises_1 = __importDefault(require("node:fs/promises")); const node_path_1 = __importDefault(require("node:path")); const base_1 = require("./base"); const constants_1 = require("../utils/constants"); const session_1 = require("./session"); /** * FileSystem 类 - 提供沙箱环境中的文件系统操作功能 * * 主要功能: * - 文件和目录的读写操作 * - 安全的文件同步(本地 ↔ 沙箱) * - 递归目录同步 * - 二进制文件检测和处理 * - 路径安全验证 * * @example * ```typescript * const fileSystem = new FileSystem(sandbox); * * // 读取文件 * const content = await fileSystem.read('/path/to/file.txt'); * * // 写入文件 * await fileSystem.write('/path/to/file.txt', 'Hello World'); * * // 同步目录 * const localPath = await fileSystem.syncDownloadsDirectory(); * ``` */ let FileSystem = class FileSystem extends base_1.BaseClass { /** * 创建 FileSystem 实例 * @param sandbox - E2B 沙箱实例 * @param session - Session 实例(用于获取 API 主机地址) * @param options - 可选配置参数 */ constructor(session) { super(session.getLogger().level); this.session = session; this.logger.debug('FileSystem 类初始化完成'); } /** * 验证路径是否安全,只允许同步 /home/user 目录下的子目录 * @param path - 要验证的路径 * @returns 是否为安全路径 * @private */ validateSafePathToDownload(path) { // 规范化路径 const normalizedPath = path.replace(/\/+/g, '/').replace(/\/$/, ''); // 检查是否为绝对路径且在 /home/user 下 if (normalizedPath.startsWith('/')) { // 绝对路径必须在 /home/user 目录下,且不能是 /home/user 本身 return (normalizedPath.startsWith('/home/user/') && normalizedPath !== '/home/user'); } // 相对路径检查 // 不允许包含 .. 来访问上级目录 if (normalizedPath.includes('..')) { return false; } // 不允许同步当前目录本身(. 或空字符串) if (normalizedPath === '.' || normalizedPath === '' || normalizedPath === './') { return false; } // 相对路径必须是子目录(以 ./ 开头或直接是目录名) return normalizedPath.startsWith('./') || !normalizedPath.startsWith('/'); } /** * 列出沙箱目录中的文件 * @param path - 沙箱中的目录路径 * @returns 文件名列表的 Promise * @throws {Error} 当沙箱未运行或列表操作失败时抛出错误 */ async listFiles(path) { try { this.logger.debug('开始列出目录文件', { path }); const result = (await (await this.session.terminal.run(`ls -la "${path}"`)).json()); if (result.exitCode) { this.logger.error('列出文件失败', { path, exitCode: result.exitCode, stderr: result.stderr, }); throw new Error(`Failed to list files: ${result.stderr}`); } // 解析 ls 输出以提取文件名 const files = result.stdout ?.split('\n') .slice(1) // 跳过第一行(总计) .filter(line => line.trim()) .map(line => line.split(/\s+/).pop()) .filter(name => name && name !== '.' && name !== '..') || []; this.logger.debug('成功列出文件', { path, fileCount: files.length }); return files; } catch (error) { this.logger.error('列出文件时发生错误', { path, error }); throw new Error(`Failed to list files: ${error}`); } } /** * 检查远程路径是否为目录 * @param remotePath - 远程路径 * @returns 是否为目录 * @private */ async isDirectory(remotePath) { try { const result = (await (await this.session.terminal.run(`test -d "${remotePath}" && echo "true" || echo "false"`)).json()); return result.stdout?.trim() === 'true'; } catch (error) { this.logger.debug('Error checking if path is directory', { remotePath, error, }); return false; } } async mkdir(path) { try { this.logger.debug('开始创建目录', { path }); await (await this.session.terminal.run(`mkdir -p "${path}"`)).json(); this.logger.debug('目录创建成功', { path }); } catch (error) { this.logger.error('创建目录失败', { path, error }); throw new Error(`Failed to create directory: ${error}`); } } /** * 递归同步目录 * @param remotePath - 远程目录路径 * @param localPath - 本地目录路径 * @returns 同步的文件数量 * @private */ async syncDirectoryRecursive(remotePath, localPath, onProgress) { let syncedCount = 0; try { // 获取目录中的所有文件和子目录 const items = await this.listFiles(remotePath); if (items.length === 0) { this.logger.debug('No items found in directory', { remotePath }); return 0; } // 确保本地目录存在 await promises_1.default.mkdir(localPath, { recursive: true }); // 处理每个项目 for (const item of items) { const remoteItemPath = `${remotePath}/${item}`; const localItemPath = node_path_1.default.join(localPath, item); // 检查是否为目录 const isDir = await this.isDirectory(remoteItemPath); if (isDir) { this.logger.debug('Syncing subdirectory', { from: remoteItemPath, to: localItemPath, }); // 递归同步子目录 const subSyncedCount = await this.syncDirectoryRecursive(remoteItemPath, localItemPath, onProgress); syncedCount += subSyncedCount; } else { this.logger.debug('Syncing file', { from: remoteItemPath, to: localItemPath, }); // 同步文件 await this.download(remoteItemPath, localItemPath); if (onProgress) { onProgress({ from: remoteItemPath, to: localItemPath, count: syncedCount, }); } syncedCount++; } } return syncedCount; } catch (error) { this.logger.error('Error in recursive directory sync', { remotePath, localPath, error, }); throw error; } } /** * 向沙箱中写入文件(通过 API 接口) * @param path - 沙箱中的文件路径 * @param content - 要写入的文本内容(仅支持字符串) */ async write(path, content) { try { this.logger.debug('开始写入文件', { path, contentLength: content.length, }); const response = await this.session.sandbox.request(`/api/files/write`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ dist: path, content: content, }), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`写入失败: ${response.status} ${response.statusText} - ${errorText}`); } const result = await response.json(); this.logger.debug('文件写入成功', { path, result, }); } catch (error) { this.logger.error('文件写入失败', { path, error }); throw error; } } /** * 从沙箱中读取文件(通过 API 接口,仅支持 UTF-8 编码) * @param remotePath - 沙箱中的文件路径 * @returns 文件的文本内容 */ async read(remotePath) { try { this.logger.debug('开始读取文件', { remotePath }); const response = await this.session.sandbox.request(`/api/files/read`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ path: remotePath, }), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`读取失败: ${response.status} ${response.statusText} - ${errorText}`); } const { data } = (await response.json()); this.logger.debug('文件读取成功', { remotePath, contentLength: data.content.length, }); return data.content; } catch (error) { this.logger.debug('文件读取失败', { remotePath, error }); throw error; } } /** * 上传本地文件到沙箱(通过 API 接口) * @param localPath - 本地文件路径 * @param remotePath - 沙箱中的目标路径 */ async upload(localPath, remotePath) { try { this.logger.debug('开始上传文件', { localPath, remotePath }); // 检查本地文件是否存在 const stats = await promises_1.default.stat(localPath); if (!stats.isFile()) { throw new Error(`路径不是文件: ${localPath}`); } const form = new FormData(); const fileBuffer = await promises_1.default.readFile(localPath); const fileName = node_path_1.default.basename(localPath); const blob = new Blob([new Uint8Array(fileBuffer)], { type: 'application/octet-stream', }); form.append('file', blob, fileName); form.append('source', localPath); form.append('dist', remotePath); const response = await this.session.sandbox.request(`/api/files/upload`, { method: 'POST', body: form, }); if (!response.ok) { const errorText = await response.text(); throw new Error(`上传失败: ${response.status} ${response.statusText} - ${errorText}`); } const result = await response.json(); this.logger.debug('文件上传成功', { localPath, remotePath, result, }); } catch (error) { this.logger.error('文件上传失败', { localPath, remotePath, error }); throw error; } } /** * 从沙箱下载文件到本地(通过 API 接口) * @param remotePath - 沙箱中的文件路径 * @param localPath - 本地目标路径 */ async download(remotePath, localPath) { try { this.logger.debug('开始下载文件', { remotePath, localPath }); // 确保本地目录存在 const localDir = node_path_1.default.dirname(localPath); await promises_1.default.mkdir(localDir, { recursive: true }); const response = await this.session.sandbox.request(`/api/files/download`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ path: remotePath, }), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`下载失败: ${response.status} ${response.statusText} - ${errorText}`); } // 获取文件内容并写入本地 const buffer = Buffer.from(await response.arrayBuffer()); await promises_1.default.writeFile(localPath, buffer); this.logger.debug('文件下载成功', { remotePath, localPath, size: buffer.length, }); } catch (error) { this.logger.error('文件下载失败', { remotePath, localPath, error }); throw error; } } /** * 同步 sandbox 中的 downloads 目录到本地(递归同步所有子目录) * @param dist - 本地目标目录路径,默认为 '/tmp/cerevox/downloads' * @param src - 远程源目录路径,默认为 '/home/user/downloads' * @returns 本地同步目录路径 * @throws {Error} 当路径不安全时抛出错误 */ async syncDownloadsDirectory(dist = '/tmp/cerevox/downloads', src = '/home/user/downloads', onProgress) { // 验证路径安全性 if (!this.validateSafePathToDownload(src)) { const error = new Error(`Unsafe path detected: ${src}. Only subdirectories under /home/user are allowed.`); this.logger.error('Path validation failed', { path: src, error: error.message, }); throw error; } const remotePath = src; const localPath = dist; try { this.logger.debug('🗂️ Starting recursive directory sync', { remotePath, localPath, }); // 检查远程目录是否存在 const dirExists = await this.isDirectory(remotePath); if (!dirExists) { // 尝试作为文件处理 const files = await this.listFiles(node_path_1.default.dirname(remotePath)); const fileName = node_path_1.default.basename(remotePath); if (!files.includes(fileName)) { this.logger.debug('Remote path does not exist', { remotePath }); return ''; } } // 递归同步目录 const syncedCount = await this.syncDirectoryRecursive(remotePath, localPath, onProgress); if (syncedCount === 0) { this.logger.debug('No files found to sync'); return ''; } this.logger.debug(`🎉 Successfully synced ${syncedCount} files recursively from ${remotePath}`); return localPath; } catch (error) { this.logger.error('Error syncing downloads directory:', error); throw error; } } }; exports.FileSystem = FileSystem; exports.FileSystem = FileSystem = __decorate([ (0, base_1.Logger)({ VERSION: constants_1.VERSION }), __metadata("design:paramtypes", [session_1.Session]) ], FileSystem); //# sourceMappingURL=file-system.js.map