cerevox
Version:
TypeScript SDK for browser automation and secure command execution in highly available and scalable micro computer environments
414 lines • 16.5 kB
JavaScript
;
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