@sstar/embedlink_agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -0
- package/dist/.platform +1 -0
- package/dist/board/docs.js +59 -0
- package/dist/board/notes.js +11 -0
- package/dist/board_uart/history.js +81 -0
- package/dist/board_uart/index.js +66 -0
- package/dist/board_uart/manager.js +313 -0
- package/dist/board_uart/resource.js +578 -0
- package/dist/board_uart/sessions.js +559 -0
- package/dist/config/index.js +341 -0
- package/dist/core/activity.js +7 -0
- package/dist/core/errors.js +45 -0
- package/dist/core/log_stream.js +26 -0
- package/dist/files/__tests__/files_manager.test.js +209 -0
- package/dist/files/artifact_manager.js +68 -0
- package/dist/files/file_operation_logger.js +271 -0
- package/dist/files/files_manager.js +511 -0
- package/dist/files/index.js +87 -0
- package/dist/files/types.js +5 -0
- package/dist/firmware/burn_recover.js +733 -0
- package/dist/firmware/prepare_images.js +184 -0
- package/dist/firmware/user_guide.js +43 -0
- package/dist/index.js +449 -0
- package/dist/logger.js +245 -0
- package/dist/macro/index.js +241 -0
- package/dist/macro/runner.js +168 -0
- package/dist/nfs/index.js +105 -0
- package/dist/plugins/loader.js +30 -0
- package/dist/proto/agent.proto +473 -0
- package/dist/resources/docs/board-interaction.md +115 -0
- package/dist/resources/docs/firmware-upgrade.md +404 -0
- package/dist/resources/docs/nfs-mount-guide.md +78 -0
- package/dist/resources/docs/tftp-transfer-guide.md +81 -0
- package/dist/secrets/index.js +9 -0
- package/dist/server/grpc.js +1069 -0
- package/dist/server/web.js +2284 -0
- package/dist/ssh/adapter.js +126 -0
- package/dist/ssh/candidates.js +85 -0
- package/dist/ssh/index.js +3 -0
- package/dist/ssh/paircheck.js +35 -0
- package/dist/ssh/tunnel.js +111 -0
- package/dist/tftp/client.js +345 -0
- package/dist/tftp/index.js +284 -0
- package/dist/tftp/server.js +731 -0
- package/dist/uboot/index.js +45 -0
- package/dist/ui/assets/index-BlnLVmbt.js +374 -0
- package/dist/ui/assets/index-xMbarYXA.css +32 -0
- package/dist/ui/index.html +21 -0
- package/dist/utils/network.js +150 -0
- package/dist/utils/platform.js +83 -0
- package/dist/utils/port-check.js +153 -0
- package/dist/utils/user-prompt.js +139 -0
- package/package.json +64 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createReadStream, createWriteStream } from 'fs';
|
|
4
|
+
import { pipeline } from 'stream/promises';
|
|
5
|
+
import { FileType } from './types.js';
|
|
6
|
+
import { FileOperationLogger, FileOperationEvent } from './file_operation_logger.js';
|
|
7
|
+
export class FilesManager {
|
|
8
|
+
constructor(config, agentLogger, agentConfig) {
|
|
9
|
+
this.rootDir = path.resolve(config.rootDir || './files');
|
|
10
|
+
this.maxFileSize = config.maxFileSize || 100 * 1024 * 1024; // 默认 100MB
|
|
11
|
+
this.maxTotalBytes = config.maxTotalBytes || 5 * 1024 * 1024 * 1024; // 默认 5GB
|
|
12
|
+
this.maxTotalFiles = config.maxTotalFiles || 50000;
|
|
13
|
+
this.allowedPaths = config.allowedPaths || [this.rootDir];
|
|
14
|
+
this.agentConfig = agentConfig;
|
|
15
|
+
// 如果启用了文件操作日志,创建FileOperationLogger
|
|
16
|
+
if (agentLogger && agentConfig?.logging?.files?.enabled) {
|
|
17
|
+
this.fileLogger = new FileOperationLogger(agentLogger, agentConfig.agent?.id || 'unknown');
|
|
18
|
+
}
|
|
19
|
+
// 确保根目录存在
|
|
20
|
+
this.ensureDirectory(this.rootDir);
|
|
21
|
+
}
|
|
22
|
+
// ============ 公开接口 - 对应 MCP 工具 ============
|
|
23
|
+
/**
|
|
24
|
+
* 上传文件到 Agent
|
|
25
|
+
*/
|
|
26
|
+
async putFile(srcPath, dstPath, requestId) {
|
|
27
|
+
// 如果没有传入requestId,开始一个新的操作记录
|
|
28
|
+
if (!requestId && this.fileLogger) {
|
|
29
|
+
requestId = this.fileLogger.startOperation('put', {
|
|
30
|
+
originalSourcePath: srcPath,
|
|
31
|
+
agentReceivedTargetPath: dstPath,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
if (this.fileLogger && requestId) {
|
|
36
|
+
// 记录路径解析开始
|
|
37
|
+
this.fileLogger.logDebug(requestId, FileOperationEvent.PATH_RESOLUTION_START, {
|
|
38
|
+
agentReceivedTargetPath: dstPath,
|
|
39
|
+
inputPath: dstPath || path.basename(srcPath),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// 验证并解析目标路径
|
|
43
|
+
let targetPath;
|
|
44
|
+
let inputPath;
|
|
45
|
+
if (dstPath && dstPath.trim()) {
|
|
46
|
+
targetPath = this.resolvePath(dstPath);
|
|
47
|
+
inputPath = dstPath;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const baseName = path.basename(srcPath);
|
|
51
|
+
inputPath = baseName;
|
|
52
|
+
if (!baseName) {
|
|
53
|
+
// 如果srcPath是根目录或空路径,使用当前目录
|
|
54
|
+
targetPath = this.rootDir;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
targetPath = this.resolvePath(baseName);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// 详细记录路径转换过程
|
|
61
|
+
let pathTransformation;
|
|
62
|
+
if (this.fileLogger && requestId && this.agentConfig?.logging?.files?.pathResolution) {
|
|
63
|
+
pathTransformation = this.fileLogger.logPathTransformation(requestId, inputPath, this.rootDir);
|
|
64
|
+
}
|
|
65
|
+
if (this.fileLogger && requestId) {
|
|
66
|
+
this.fileLogger.logDebug(requestId, FileOperationEvent.PATH_RESOLUTION_SUCCESS, {
|
|
67
|
+
agentReceivedTargetPath: dstPath,
|
|
68
|
+
resolvedTargetPath: targetPath,
|
|
69
|
+
pathTransformation,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// 安全检查记录
|
|
73
|
+
if (this.fileLogger && requestId && this.agentConfig?.logging?.files?.securityChecks) {
|
|
74
|
+
this.fileLogger.logDebug(requestId, FileOperationEvent.SECURITY_CHECK_START, {
|
|
75
|
+
resolvedTargetPath: targetPath,
|
|
76
|
+
});
|
|
77
|
+
const securityResult = await this.performSecurityChecks(targetPath, srcPath);
|
|
78
|
+
this.fileLogger.logDebug(requestId, FileOperationEvent.SECURITY_CHECK_SUCCESS, {
|
|
79
|
+
resolvedTargetPath: targetPath,
|
|
80
|
+
securityChecks: securityResult,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// 检查文件大小
|
|
84
|
+
const stats = await fs.stat(srcPath);
|
|
85
|
+
if (stats.size > this.maxFileSize) {
|
|
86
|
+
const error = new Error(`File size ${stats.size} exceeds maximum allowed size ${this.maxFileSize}`);
|
|
87
|
+
if (this.fileLogger && requestId) {
|
|
88
|
+
this.fileLogger.logError(requestId, error, {
|
|
89
|
+
originalSourcePath: srcPath,
|
|
90
|
+
agentReceivedTargetPath: dstPath,
|
|
91
|
+
fileSize: stats.size,
|
|
92
|
+
maxSize: this.maxFileSize,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
// 记录文件复制开始
|
|
98
|
+
const actualStoragePath = await this.copyFileWithLogging(srcPath, targetPath, requestId);
|
|
99
|
+
// 获取文件信息
|
|
100
|
+
const fileInfo = await this.getFileInfo(actualStoragePath);
|
|
101
|
+
// 记录操作成功
|
|
102
|
+
if (this.fileLogger && requestId) {
|
|
103
|
+
this.fileLogger.logSuccess(requestId, {
|
|
104
|
+
originalSourcePath: srcPath,
|
|
105
|
+
agentReceivedTargetPath: dstPath,
|
|
106
|
+
resolvedTargetPath: targetPath,
|
|
107
|
+
actualStoragePath: actualStoragePath,
|
|
108
|
+
pathTransformation,
|
|
109
|
+
fileSize: stats.size,
|
|
110
|
+
fileType: stats.isFile() ? 'file' : 'directory',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return fileInfo;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (this.fileLogger && requestId) {
|
|
117
|
+
this.fileLogger.logError(requestId, error, {
|
|
118
|
+
originalSourcePath: srcPath,
|
|
119
|
+
agentReceivedTargetPath: dstPath,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 从 Agent 获取文件内容
|
|
127
|
+
*/
|
|
128
|
+
async getFile(srcPath, requestId) {
|
|
129
|
+
// 如果没有传入requestId,开始一个新的操作记录
|
|
130
|
+
if (!requestId && this.fileLogger) {
|
|
131
|
+
requestId = this.fileLogger.startOperation('get', {
|
|
132
|
+
agentReceivedTargetPath: srcPath,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
if (this.fileLogger && requestId) {
|
|
137
|
+
this.fileLogger.logDebug(requestId, FileOperationEvent.GET_VALIDATION_START, {
|
|
138
|
+
agentReceivedTargetPath: srcPath,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const fullPath = this.resolvePath(srcPath);
|
|
142
|
+
if (this.fileLogger && requestId) {
|
|
143
|
+
this.fileLogger.logDebug(requestId, FileOperationEvent.GET_VALIDATION_SUCCESS, {
|
|
144
|
+
agentReceivedTargetPath: srcPath,
|
|
145
|
+
resolvedTargetPath: fullPath,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const stats = await fs.stat(fullPath);
|
|
149
|
+
if (!stats.isFile()) {
|
|
150
|
+
const error = new Error(`Path is not a file: ${srcPath}`);
|
|
151
|
+
if (this.fileLogger && requestId) {
|
|
152
|
+
this.fileLogger.logError(requestId, error, {
|
|
153
|
+
agentReceivedTargetPath: srcPath,
|
|
154
|
+
resolvedTargetPath: fullPath,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
const content = await fs.readFile(fullPath);
|
|
160
|
+
if (this.fileLogger && requestId) {
|
|
161
|
+
this.fileLogger.logSuccess(requestId, {
|
|
162
|
+
agentReceivedTargetPath: srcPath,
|
|
163
|
+
resolvedTargetPath: fullPath,
|
|
164
|
+
fileSize: content.length,
|
|
165
|
+
fileType: 'file',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return content;
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (this.fileLogger && requestId) {
|
|
172
|
+
this.fileLogger.logError(requestId, error, {
|
|
173
|
+
agentReceivedTargetPath: srcPath,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (error.code === 'ENOENT') {
|
|
177
|
+
throw new Error(`File not found: ${srcPath}`);
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 创建文件的可读流(用于大文件传输)
|
|
184
|
+
*/
|
|
185
|
+
async createFileReadStream(srcPath) {
|
|
186
|
+
const fullPath = this.resolvePath(srcPath);
|
|
187
|
+
try {
|
|
188
|
+
const stats = await fs.stat(fullPath);
|
|
189
|
+
if (!stats.isFile()) {
|
|
190
|
+
throw new Error(`Path is not a file: ${srcPath}`);
|
|
191
|
+
}
|
|
192
|
+
return createReadStream(fullPath);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (error.code === 'ENOENT') {
|
|
196
|
+
throw new Error(`File not found: ${srcPath}`);
|
|
197
|
+
}
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* 从流保存文件
|
|
203
|
+
*/
|
|
204
|
+
async saveFileFromStream(dstPath, stream) {
|
|
205
|
+
const fullPath = this.resolvePath(dstPath);
|
|
206
|
+
// 确保目标目录存在
|
|
207
|
+
await this.ensureDirectory(path.dirname(fullPath));
|
|
208
|
+
// 创建写入流
|
|
209
|
+
const writeStream = createWriteStream(fullPath);
|
|
210
|
+
try {
|
|
211
|
+
await pipeline(stream, writeStream);
|
|
212
|
+
return await this.getFileInfo(fullPath);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
// 如果失败,清理部分写入的文件
|
|
216
|
+
try {
|
|
217
|
+
await fs.unlink(fullPath);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// 忽略删除错误
|
|
221
|
+
}
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// ============ 内部实现 - 不暴露为 MCP 工具 ============
|
|
226
|
+
/**
|
|
227
|
+
* 列出目录内容
|
|
228
|
+
*/
|
|
229
|
+
async listDir(dirPath, options) {
|
|
230
|
+
const targetPath = dirPath ? this.resolvePath(dirPath) : this.rootDir;
|
|
231
|
+
try {
|
|
232
|
+
const entries = await fs.readdir(targetPath, { withFileTypes: true });
|
|
233
|
+
const result = [];
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
// 跳过隐藏文件(除非要求显示所有文件)
|
|
236
|
+
if (!options?.all && entry.name.startsWith('.')) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
// 模式匹配过滤
|
|
240
|
+
if (options?.pattern && !this.matchPattern(entry.name, options.pattern)) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const fullPath = path.join(targetPath, entry.name);
|
|
244
|
+
const stats = await fs.stat(fullPath);
|
|
245
|
+
const fileEntry = {
|
|
246
|
+
name: entry.name,
|
|
247
|
+
path: path.relative(this.rootDir, fullPath),
|
|
248
|
+
type: entry.isDirectory() ? FileType.DIRECTORY : FileType.FILE,
|
|
249
|
+
size: stats.size,
|
|
250
|
+
mtime: stats.mtimeMs,
|
|
251
|
+
};
|
|
252
|
+
result.push(fileEntry);
|
|
253
|
+
// 递归处理子目录
|
|
254
|
+
if (options?.recursive && entry.isDirectory()) {
|
|
255
|
+
const subEntries = await this.listDir(path.relative(this.rootDir, fullPath), options);
|
|
256
|
+
result.push(...subEntries);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return result.sort((a, b) => {
|
|
260
|
+
// 目录优先,然后按名称排序
|
|
261
|
+
if (a.type !== b.type) {
|
|
262
|
+
return a.type === FileType.DIRECTORY ? -1 : 1;
|
|
263
|
+
}
|
|
264
|
+
return a.name.localeCompare(b.name);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
if (error.code === 'ENOENT') {
|
|
269
|
+
throw new Error(`Directory not found: ${dirPath || 'root'}`);
|
|
270
|
+
}
|
|
271
|
+
if (error.code === 'ENOTDIR') {
|
|
272
|
+
throw new Error(`Path is not a directory: ${dirPath || 'root'}`);
|
|
273
|
+
}
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 删除文件或目录
|
|
279
|
+
*/
|
|
280
|
+
async remove(targetPath, recursive = false) {
|
|
281
|
+
const fullPath = this.resolvePath(targetPath);
|
|
282
|
+
try {
|
|
283
|
+
const stats = await fs.stat(fullPath);
|
|
284
|
+
if (stats.isDirectory()) {
|
|
285
|
+
if (recursive) {
|
|
286
|
+
await fs.rm(fullPath, { recursive: true });
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
// 检查目录是否为空
|
|
290
|
+
const entries = await fs.readdir(fullPath);
|
|
291
|
+
if (entries.length > 0) {
|
|
292
|
+
throw new Error(`Directory is not empty: ${targetPath}. Use recursive=true to delete non-empty directories.`);
|
|
293
|
+
}
|
|
294
|
+
await fs.rm(fullPath);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
await fs.unlink(fullPath);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
if (error.code === 'ENOENT') {
|
|
303
|
+
// 文件不存在,视为成功
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* 创建目录
|
|
311
|
+
*/
|
|
312
|
+
async makeDir(dirPath, parents = false) {
|
|
313
|
+
const fullPath = this.resolvePath(dirPath);
|
|
314
|
+
try {
|
|
315
|
+
await fs.mkdir(fullPath, { recursive: parents });
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
if (error.code === 'EEXIST') {
|
|
319
|
+
// 目录已存在,视为成功
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* 获取文件状态信息
|
|
327
|
+
*/
|
|
328
|
+
async getStat(targetPath) {
|
|
329
|
+
const fullPath = this.resolvePath(targetPath);
|
|
330
|
+
try {
|
|
331
|
+
const stats = await fs.stat(fullPath);
|
|
332
|
+
const relativePath = path.relative(this.rootDir, fullPath);
|
|
333
|
+
return {
|
|
334
|
+
path: relativePath,
|
|
335
|
+
name: path.basename(fullPath),
|
|
336
|
+
size: stats.size,
|
|
337
|
+
isFile: stats.isFile(),
|
|
338
|
+
isDirectory: stats.isDirectory(),
|
|
339
|
+
modifiedAt: stats.mtimeMs,
|
|
340
|
+
permissions: stats.mode.toString(8),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
if (error.code === 'ENOENT') {
|
|
345
|
+
throw new Error(`Path not found: ${targetPath}`);
|
|
346
|
+
}
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// ============ 私有辅助方法 ============
|
|
351
|
+
/**
|
|
352
|
+
* 解析并验证路径安全性
|
|
353
|
+
*/
|
|
354
|
+
resolvePath(relativePath) {
|
|
355
|
+
if (!relativePath || typeof relativePath !== 'string') {
|
|
356
|
+
throw new Error(`Invalid path: expected string, got ${typeof relativePath}`);
|
|
357
|
+
}
|
|
358
|
+
// 清理路径,防止目录遍历攻击
|
|
359
|
+
const cleanPath = path.normalize(relativePath).replace(/^(\.\.[\/\\])+/, '');
|
|
360
|
+
const fullPath = path.join(this.rootDir, cleanPath);
|
|
361
|
+
// 确保路径在允许的范围内
|
|
362
|
+
const resolvedPath = path.resolve(fullPath);
|
|
363
|
+
const isAllowed = this.allowedPaths.some((allowedPath) => resolvedPath.startsWith(path.resolve(allowedPath)));
|
|
364
|
+
if (!isAllowed) {
|
|
365
|
+
throw new Error(`Access denied: Path '${relativePath}' is outside allowed directories`);
|
|
366
|
+
}
|
|
367
|
+
return resolvedPath;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* 确保目录存在
|
|
371
|
+
*/
|
|
372
|
+
async ensureDirectory(dirPath) {
|
|
373
|
+
if (!dirPath || typeof dirPath !== 'string') {
|
|
374
|
+
throw new Error(`Invalid directory path: expected string, got ${typeof dirPath}`);
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
if (error.code !== 'EEXIST') {
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* 获取文件信息
|
|
387
|
+
*/
|
|
388
|
+
async getFileInfo(fullPath) {
|
|
389
|
+
const stats = await fs.stat(fullPath);
|
|
390
|
+
const relativePath = path.relative(this.rootDir, fullPath);
|
|
391
|
+
return {
|
|
392
|
+
path: relativePath,
|
|
393
|
+
name: path.basename(fullPath),
|
|
394
|
+
size: stats.size,
|
|
395
|
+
mtime: stats.mtimeMs,
|
|
396
|
+
isFile: stats.isFile(),
|
|
397
|
+
isDirectory: stats.isDirectory(),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* 执行安全检查
|
|
402
|
+
*/
|
|
403
|
+
async performSecurityChecks(targetPath, srcPath) {
|
|
404
|
+
const checks = {
|
|
405
|
+
pathTraversal: true,
|
|
406
|
+
sizeLimit: true,
|
|
407
|
+
permission: true,
|
|
408
|
+
diskSpace: true,
|
|
409
|
+
};
|
|
410
|
+
try {
|
|
411
|
+
// 1. 路径遍历检查(已在resolvePath中完成)
|
|
412
|
+
// 这里只是再次确认
|
|
413
|
+
const resolvedPath = path.resolve(targetPath);
|
|
414
|
+
const isAllowed = this.allowedPaths.some((allowedPath) => resolvedPath.startsWith(path.resolve(allowedPath)));
|
|
415
|
+
checks.pathTraversal = isAllowed;
|
|
416
|
+
// 2. 文件大小检查(如果提供了源路径)
|
|
417
|
+
if (srcPath) {
|
|
418
|
+
try {
|
|
419
|
+
const stats = await fs.stat(srcPath);
|
|
420
|
+
checks.sizeLimit = stats.size <= this.maxFileSize;
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
checks.sizeLimit = false;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// 3. 权限检查
|
|
427
|
+
try {
|
|
428
|
+
// 检查目标目录的写权限
|
|
429
|
+
const targetDir = path.dirname(targetPath);
|
|
430
|
+
await fs.access(targetDir, fs.constants.W_OK);
|
|
431
|
+
checks.permission = true;
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
checks.permission = false;
|
|
435
|
+
}
|
|
436
|
+
// 4. 磁盘空间检查(简化版)
|
|
437
|
+
try {
|
|
438
|
+
// 获取根目录的磁盘空间信息(需要额外的库,这里简化为总是通过)
|
|
439
|
+
checks.diskSpace = true;
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
checks.diskSpace = false;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
// 如果检查过程中出现错误,标记为失败
|
|
447
|
+
checks.permission = false;
|
|
448
|
+
}
|
|
449
|
+
return checks;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* 带日志的文件复制
|
|
453
|
+
*/
|
|
454
|
+
async copyFileWithLogging(srcPath, dstPath, requestId) {
|
|
455
|
+
if (this.fileLogger && requestId) {
|
|
456
|
+
this.fileLogger.logDebug(requestId, FileOperationEvent.COPY_START, {
|
|
457
|
+
sourcePath: srcPath,
|
|
458
|
+
destinationPath: dstPath,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
try {
|
|
462
|
+
// 确保目标目录存在
|
|
463
|
+
const targetDir = path.dirname(dstPath);
|
|
464
|
+
await this.ensureDirectory(targetDir);
|
|
465
|
+
if (this.fileLogger && requestId) {
|
|
466
|
+
this.fileLogger.logDebug(requestId, FileOperationEvent.DIRECTORY_CREATED, {
|
|
467
|
+
directory: targetDir,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
// 执行文件复制
|
|
471
|
+
await fs.copyFile(srcPath, dstPath);
|
|
472
|
+
// 验证文件确实存在
|
|
473
|
+
const stats = await fs.stat(dstPath);
|
|
474
|
+
if (this.fileLogger && requestId) {
|
|
475
|
+
this.fileLogger.logDebug(requestId, FileOperationEvent.COPY_SUCCESS, {
|
|
476
|
+
sourcePath: srcPath,
|
|
477
|
+
actualStoragePath: dstPath,
|
|
478
|
+
fileSize: stats.size,
|
|
479
|
+
fileModified: stats.mtime,
|
|
480
|
+
fileExists: true,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return dstPath;
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
if (this.fileLogger && requestId) {
|
|
487
|
+
this.fileLogger.logDebug(requestId, 'files.copy.error', {
|
|
488
|
+
sourcePath: srcPath,
|
|
489
|
+
destinationPath: dstPath,
|
|
490
|
+
error: error.message,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* 简单的文件名模式匹配
|
|
498
|
+
*/
|
|
499
|
+
matchPattern(fileName, pattern) {
|
|
500
|
+
// 简单的 glob 风格匹配实现
|
|
501
|
+
const regexPattern = pattern.replace(/\*/g, '.*').replace(/\?/g, '.');
|
|
502
|
+
return new RegExp(`^${regexPattern}$`).test(fileName);
|
|
503
|
+
}
|
|
504
|
+
getLimits() {
|
|
505
|
+
return {
|
|
506
|
+
maxFileSize: this.maxFileSize,
|
|
507
|
+
maxTotalBytes: this.maxTotalBytes,
|
|
508
|
+
maxTotalFiles: this.maxTotalFiles,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { exec } from 'node:child_process';
|
|
5
|
+
import util from 'node:util';
|
|
6
|
+
const execAsync = util.promisify(exec);
|
|
7
|
+
async function getDrives() {
|
|
8
|
+
if (os.platform() !== 'win32') {
|
|
9
|
+
return [{ name: '/', path: '/', type: 'dir', size: 0 }];
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const { stdout } = await execAsync('wmic logicaldisk get name');
|
|
13
|
+
const lines = stdout.split('\r\n').filter((l) => l.trim());
|
|
14
|
+
const drives = [];
|
|
15
|
+
// Skip header "Name"
|
|
16
|
+
for (let i = 1; i < lines.length; i++) {
|
|
17
|
+
const drive = lines[i].trim();
|
|
18
|
+
if (drive && /^[A-Z]:$/.test(drive)) {
|
|
19
|
+
drives.push({ name: drive, path: drive + '\\', type: 'dir', size: 0 });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return drives;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return [{ name: 'C:\\', path: 'C:\\', type: 'dir', size: 0 }];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function listFiles(targetPath) {
|
|
29
|
+
// Check if we need to list drives (empty path on Windows)
|
|
30
|
+
const isWin = os.platform() === 'win32';
|
|
31
|
+
if (!targetPath || targetPath.trim() === '') {
|
|
32
|
+
if (isWin) {
|
|
33
|
+
const drives = await getDrives();
|
|
34
|
+
return { ok: true, files: drives, current: '' };
|
|
35
|
+
}
|
|
36
|
+
// Unix: default to root
|
|
37
|
+
targetPath = '/';
|
|
38
|
+
}
|
|
39
|
+
const resolved = path.resolve(targetPath);
|
|
40
|
+
try {
|
|
41
|
+
const st = await fs.stat(resolved);
|
|
42
|
+
if (!st.isDirectory()) {
|
|
43
|
+
return { ok: false, files: [], current: resolved, error: 'Not a directory' };
|
|
44
|
+
}
|
|
45
|
+
const entries = await fs.readdir(resolved, { withFileTypes: true });
|
|
46
|
+
const files = [];
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
try {
|
|
49
|
+
const fullPath = path.join(resolved, entry.name);
|
|
50
|
+
// Basic filter: ignore dotfiles if needed, but usually we want to see them
|
|
51
|
+
// Let's rely on stat for correct type/size
|
|
52
|
+
const stat = await fs.stat(fullPath);
|
|
53
|
+
files.push({
|
|
54
|
+
name: entry.name,
|
|
55
|
+
path: fullPath,
|
|
56
|
+
type: stat.isDirectory() ? 'dir' : 'file',
|
|
57
|
+
size: stat.size,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Ignore files we can't stat (permission denied etc)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Sort: Directories first, then files
|
|
65
|
+
files.sort((a, b) => {
|
|
66
|
+
if (a.type === b.type)
|
|
67
|
+
return a.name.localeCompare(b.name);
|
|
68
|
+
return a.type === 'dir' ? -1 : 1;
|
|
69
|
+
});
|
|
70
|
+
const parent = path.dirname(resolved);
|
|
71
|
+
const hasParent = parent !== resolved;
|
|
72
|
+
return {
|
|
73
|
+
ok: true,
|
|
74
|
+
files,
|
|
75
|
+
current: resolved,
|
|
76
|
+
parent: hasParent ? parent : undefined,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
files: [],
|
|
83
|
+
current: resolved,
|
|
84
|
+
error: e.message || String(e),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|