aws-runtime-bridge 1.4.0 → 1.6.1
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 +1 -1
- package/dist/adapter/AdapterRegistry.d.ts +1 -1
- package/dist/adapter/AdapterRegistry.d.ts.map +1 -1
- package/dist/adapter/AdapterRegistry.js +0 -2
- package/dist/adapter/ClaudeSdkAdapter.d.ts +4 -0
- package/dist/adapter/ClaudeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/ClaudeSdkAdapter.js +11 -2
- package/dist/adapter/CodexSdkAdapter.js +1 -1
- package/dist/adapter/OpencodeSdkAdapter.d.ts +13 -1
- package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/OpencodeSdkAdapter.js +58 -6
- package/dist/adapter/OpencodeSdkAdapter.test.js +57 -1
- package/dist/adapter/types.d.ts +10 -0
- package/dist/adapter/types.d.ts.map +1 -1
- package/dist/index.js +14 -43
- package/dist/middleware/auth.d.ts +5 -0
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +9 -1
- package/dist/routes/file-browser.d.ts +10 -0
- package/dist/routes/file-browser.d.ts.map +1 -1
- package/dist/routes/file-browser.js +226 -4
- package/dist/routes/file-browser.test.js +31 -0
- package/dist/routes/instance.d.ts +10 -0
- package/dist/routes/instance.d.ts.map +1 -1
- package/dist/routes/instance.js +93 -2
- package/dist/routes/instance.test.js +50 -0
- package/dist/routes/pty.d.ts +106 -0
- package/dist/routes/pty.d.ts.map +1 -0
- package/dist/routes/pty.js +526 -0
- package/dist/routes/pty.test.d.ts +2 -0
- package/dist/routes/pty.test.d.ts.map +1 -0
- package/dist/routes/pty.test.js +73 -0
- package/dist/routes/sessions.d.ts +1 -1
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +32 -213
- package/dist/routes/terminal.d.ts +32 -3
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +411 -243
- package/dist/routes/terminal.test.js +105 -29
- package/dist/services/agent-process-manager.d.ts +2 -2
- package/dist/services/agent-process-manager.d.ts.map +1 -1
- package/dist/services/agent-process-manager.js +3 -3
- package/dist/services/process-detector.d.ts +2 -4
- package/dist/services/process-detector.d.ts.map +1 -1
- package/dist/services/process-detector.js +9 -16
- package/dist/services/process-registry.d.ts +2 -2
- package/dist/services/process-registry.d.ts.map +1 -1
- package/dist/services/process-registry.js +1 -1
- package/dist/services/session-output.d.ts +27 -5
- package/dist/services/session-output.d.ts.map +1 -1
- package/dist/services/session-output.js +48 -3
- package/dist/services/session-output.test.js +43 -29
- package/dist/services/terminal-persistence.d.ts +9 -0
- package/dist/services/terminal-persistence.d.ts.map +1 -1
- package/dist/services/terminal-persistence.js +20 -0
- package/dist/services/tool-installer.d.ts +10 -0
- package/dist/services/tool-installer.d.ts.map +1 -1
- package/dist/services/tool-installer.js +126 -5
- package/dist/services/tool-installer.test.js +32 -1
- package/dist/services/workspace-files.d.ts +86 -0
- package/dist/services/workspace-files.d.ts.map +1 -1
- package/dist/services/workspace-files.js +571 -21
- package/dist/services/workspace-files.test.js +471 -11
- package/dist/services/workspace-watch.d.ts +21 -0
- package/dist/services/workspace-watch.d.ts.map +1 -0
- package/dist/services/workspace-watch.js +123 -0
- package/dist/services/workspace-watch.test.d.ts +2 -0
- package/dist/services/workspace-watch.test.d.ts.map +1 -0
- package/dist/services/workspace-watch.test.js +38 -0
- package/dist/types.d.ts +8 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -1
|
@@ -2,8 +2,23 @@
|
|
|
2
2
|
* 工作区文件服务
|
|
3
3
|
* 负责在工作区范围内安全地浏览、读取与保存文件。
|
|
4
4
|
*/
|
|
5
|
+
import { createReadStream, createWriteStream, promises as fs } from 'node:fs';
|
|
5
6
|
import path from 'node:path';
|
|
6
|
-
import {
|
|
7
|
+
import { Readable } from 'node:stream';
|
|
8
|
+
import { pipeline } from 'node:stream/promises';
|
|
9
|
+
import { TextDecoder } from 'node:util';
|
|
10
|
+
import { createGunzip } from 'node:zlib';
|
|
11
|
+
import * as tar from 'tar';
|
|
12
|
+
import unzipper from 'unzipper';
|
|
13
|
+
const { ZipArchive } = await import('archiver');
|
|
14
|
+
const ARCHIVE_LIMITS = {
|
|
15
|
+
maxEntries: 10_000,
|
|
16
|
+
maxFileBytes: 512 * 1024 * 1024,
|
|
17
|
+
maxTotalBytes: 2 * 1024 * 1024 * 1024,
|
|
18
|
+
maxPathDepth: 64,
|
|
19
|
+
maxPathLength: 4096,
|
|
20
|
+
};
|
|
21
|
+
const GBK_DECODER = new TextDecoder('gbk');
|
|
7
22
|
function toDisplayPath(targetPath) {
|
|
8
23
|
return String(targetPath || '').replace(/\\/g, '/');
|
|
9
24
|
}
|
|
@@ -17,6 +32,22 @@ function resolveWorkspaceRoot(workspacePath) {
|
|
|
17
32
|
}
|
|
18
33
|
return normalizedWorkspacePath;
|
|
19
34
|
}
|
|
35
|
+
async function resolveWorkspaceRootRealpath(workspacePath) {
|
|
36
|
+
const workspaceRoot = resolveWorkspaceRoot(workspacePath);
|
|
37
|
+
const realWorkspaceRoot = await fs.realpath(workspaceRoot);
|
|
38
|
+
const stat = await fs.stat(realWorkspaceRoot);
|
|
39
|
+
if (!stat.isDirectory()) {
|
|
40
|
+
throw new Error(`Workspace path is not a directory: ${workspaceRoot}`);
|
|
41
|
+
}
|
|
42
|
+
return realWorkspaceRoot;
|
|
43
|
+
}
|
|
44
|
+
async function resolveWorkspaceRoots(workspacePath) {
|
|
45
|
+
const requestedRoot = resolveWorkspaceRoot(workspacePath);
|
|
46
|
+
return {
|
|
47
|
+
requestedRoot,
|
|
48
|
+
workspaceRoot: await resolveWorkspaceRootRealpath(workspacePath),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
20
51
|
/**
|
|
21
52
|
* 判断目标路径是否位于工作区内。
|
|
22
53
|
*/
|
|
@@ -29,15 +60,44 @@ function ensureInsideWorkspace(workspaceRoot, targetPath) {
|
|
|
29
60
|
}
|
|
30
61
|
return resolvedTargetPath;
|
|
31
62
|
}
|
|
63
|
+
async function ensureRealPathInsideWorkspace(workspaceRoot, targetPath) {
|
|
64
|
+
const realTargetPath = await fs.realpath(targetPath);
|
|
65
|
+
return ensureInsideWorkspace(workspaceRoot, realTargetPath);
|
|
66
|
+
}
|
|
67
|
+
async function ensureWritableParentInsideWorkspace(workspaceRoot, targetPath) {
|
|
68
|
+
const realParentPath = await fs.realpath(path.dirname(targetPath));
|
|
69
|
+
ensureInsideWorkspace(workspaceRoot, realParentPath);
|
|
70
|
+
try {
|
|
71
|
+
const targetLinkStat = await fs.lstat(targetPath);
|
|
72
|
+
if (targetLinkStat.isSymbolicLink()) {
|
|
73
|
+
throw new Error(`Refusing to write through symbolic link: ${targetPath}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const errorCode = typeof error === 'object' && error !== null ? Reflect.get(error, 'code') : undefined;
|
|
78
|
+
if (errorCode !== 'ENOENT') {
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function resolveRequestedPathInsideWorkspace(requestedRoot, workspaceRoot, requestedPath) {
|
|
84
|
+
const resolvedRequestedPath = path.resolve(requestedPath);
|
|
85
|
+
const requestedRelativePath = path.relative(requestedRoot, resolvedRequestedPath);
|
|
86
|
+
if (requestedRelativePath.startsWith('..') || path.isAbsolute(requestedRelativePath)) {
|
|
87
|
+
throw new Error(`Target path is outside workspace: ${resolvedRequestedPath}`);
|
|
88
|
+
}
|
|
89
|
+
return ensureInsideWorkspace(workspaceRoot, path.join(workspaceRoot, requestedRelativePath));
|
|
90
|
+
}
|
|
32
91
|
/**
|
|
33
92
|
* 解析目录目标路径,空路径时回退到工作区根目录。
|
|
34
93
|
*/
|
|
35
|
-
function resolveDirectoryTarget({ workspacePath, targetPath = '' }) {
|
|
36
|
-
const workspaceRoot =
|
|
94
|
+
async function resolveDirectoryTarget({ workspacePath, targetPath = '' }) {
|
|
95
|
+
const { requestedRoot, workspaceRoot } = await resolveWorkspaceRoots(workspacePath);
|
|
37
96
|
const requestedPath = String(targetPath || '').trim();
|
|
38
|
-
const
|
|
39
|
-
?
|
|
97
|
+
const lexicalTargetPath = requestedPath
|
|
98
|
+
? resolveRequestedPathInsideWorkspace(requestedRoot, workspaceRoot, requestedPath)
|
|
40
99
|
: workspaceRoot;
|
|
100
|
+
const resolvedTargetPath = await ensureRealPathInsideWorkspace(workspaceRoot, lexicalTargetPath);
|
|
41
101
|
return { workspaceRoot, resolvedTargetPath };
|
|
42
102
|
}
|
|
43
103
|
/**
|
|
@@ -56,49 +116,235 @@ function normalizeEntryName(rawName) {
|
|
|
56
116
|
}
|
|
57
117
|
return entryName;
|
|
58
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* 修正 multipart 文件名在部分代理链路中被 latin1 解读造成的 UTF-8 中文乱码。
|
|
121
|
+
*/
|
|
122
|
+
function decodePossiblyLatin1Utf8FileName(rawName) {
|
|
123
|
+
const fileName = String(rawName || '');
|
|
124
|
+
if (!/[\u00c0-\u00ff]/.test(fileName)) {
|
|
125
|
+
return fileName;
|
|
126
|
+
}
|
|
127
|
+
const decodedFileName = Buffer.from(fileName, 'latin1').toString('utf8');
|
|
128
|
+
if (!decodedFileName || decodedFileName.includes('\uFFFD')) {
|
|
129
|
+
return fileName;
|
|
130
|
+
}
|
|
131
|
+
return decodedFileName;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 将上传文件名规范化为单级文件名,避免浏览器提交的路径穿透目录。
|
|
135
|
+
*/
|
|
136
|
+
function normalizeUploadedFileName(rawName) {
|
|
137
|
+
const decodedName = decodePossiblyLatin1Utf8FileName(rawName);
|
|
138
|
+
return normalizeEntryName(path.basename(decodedName.replace(/\\/g, '/')));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* 将浏览器传入的上传相对路径拆成安全路径片段,保留文件夹层级但拒绝穿透路径。
|
|
142
|
+
*/
|
|
143
|
+
function normalizeUploadedRelativePath(rawPath, fallbackName) {
|
|
144
|
+
if (!rawPath) {
|
|
145
|
+
return normalizeUploadedFileName(fallbackName);
|
|
146
|
+
}
|
|
147
|
+
const decodedPath = decodePossiblyLatin1Utf8FileName(String(rawPath || fallbackName || '')).replace(/\\/g, '/');
|
|
148
|
+
const segments = decodedPath.split('/').filter(Boolean).map((segment) => normalizeEntryName(segment));
|
|
149
|
+
if (segments.length === 0) {
|
|
150
|
+
return normalizeUploadedFileName(fallbackName);
|
|
151
|
+
}
|
|
152
|
+
return path.join(...segments);
|
|
153
|
+
}
|
|
154
|
+
function escapeHtml(rawText) {
|
|
155
|
+
return rawText
|
|
156
|
+
.replace(/&/g, '&')
|
|
157
|
+
.replace(/</g, '<')
|
|
158
|
+
.replace(/>/g, '>')
|
|
159
|
+
.replace(/"/g, '"')
|
|
160
|
+
.replace(/'/g, ''');
|
|
161
|
+
}
|
|
162
|
+
function decodeXmlText(rawText) {
|
|
163
|
+
return rawText
|
|
164
|
+
.replace(/</g, '<')
|
|
165
|
+
.replace(/>/g, '>')
|
|
166
|
+
.replace(/"/g, '"')
|
|
167
|
+
.replace(/'/g, "'")
|
|
168
|
+
.replace(/&/g, '&');
|
|
169
|
+
}
|
|
170
|
+
function docxTextToHtml(documentXml) {
|
|
171
|
+
const paragraphMatches = documentXml.match(/<w:p[\s\S]*?<\/w:p>/g) || [];
|
|
172
|
+
const paragraphs = paragraphMatches
|
|
173
|
+
.map((paragraphXml) => {
|
|
174
|
+
const textSegments = [...paragraphXml.matchAll(/<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>/g)]
|
|
175
|
+
.map((match) => decodeXmlText(match[1] || ''));
|
|
176
|
+
return textSegments.join('');
|
|
177
|
+
})
|
|
178
|
+
.map((paragraphText) => paragraphText.trim())
|
|
179
|
+
.filter(Boolean);
|
|
180
|
+
if (paragraphs.length === 0) {
|
|
181
|
+
return '<p class="workspace-document-preview__paragraph workspace-document-preview__paragraph--empty">该文档没有可预览的正文内容。</p>';
|
|
182
|
+
}
|
|
183
|
+
return paragraphs
|
|
184
|
+
.map((paragraphText) => `<p class="workspace-document-preview__paragraph">${escapeHtml(paragraphText)}</p>`)
|
|
185
|
+
.join('\n');
|
|
186
|
+
}
|
|
59
187
|
/**
|
|
60
188
|
* 解析并校验工作区内条目的目标路径。
|
|
61
189
|
*/
|
|
62
|
-
function resolveEntryTarget({ workspacePath, targetPath }) {
|
|
63
|
-
const workspaceRoot =
|
|
190
|
+
async function resolveEntryTarget({ workspacePath, targetPath }) {
|
|
191
|
+
const { requestedRoot, workspaceRoot } = await resolveWorkspaceRoots(workspacePath);
|
|
64
192
|
const normalizedTargetPath = String(targetPath || '').trim();
|
|
65
193
|
if (!normalizedTargetPath) {
|
|
66
194
|
throw new Error('targetPath is required');
|
|
67
195
|
}
|
|
68
|
-
const
|
|
196
|
+
const lexicalTargetPath = resolveRequestedPathInsideWorkspace(requestedRoot, workspaceRoot, normalizedTargetPath);
|
|
197
|
+
const resolvedTargetPath = await ensureRealPathInsideWorkspace(workspaceRoot, lexicalTargetPath);
|
|
69
198
|
if (resolvedTargetPath === workspaceRoot) {
|
|
70
199
|
throw new Error('Workspace root cannot be modified');
|
|
71
200
|
}
|
|
72
201
|
return { workspaceRoot, resolvedTargetPath };
|
|
73
202
|
}
|
|
203
|
+
async function resolveWritableDirectory(params) {
|
|
204
|
+
const { requestedRoot, workspaceRoot } = await resolveWorkspaceRoots(params.workspacePath);
|
|
205
|
+
const requestedPath = String(params.targetPath || '').trim();
|
|
206
|
+
if (!requestedPath) {
|
|
207
|
+
return { workspaceRoot, resolvedTargetPath: workspaceRoot };
|
|
208
|
+
}
|
|
209
|
+
const lexicalTargetPath = resolveRequestedPathInsideWorkspace(requestedRoot, workspaceRoot, requestedPath);
|
|
210
|
+
if (await pathExists(lexicalTargetPath)) {
|
|
211
|
+
return {
|
|
212
|
+
workspaceRoot,
|
|
213
|
+
resolvedTargetPath: await ensureRealPathInsideWorkspace(workspaceRoot, lexicalTargetPath),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const realParentPath = await fs.realpath(path.dirname(lexicalTargetPath));
|
|
217
|
+
ensureInsideWorkspace(workspaceRoot, realParentPath);
|
|
218
|
+
return { workspaceRoot, resolvedTargetPath: lexicalTargetPath };
|
|
219
|
+
}
|
|
220
|
+
function archiveKind(filePath) {
|
|
221
|
+
const lowerName = filePath.toLowerCase();
|
|
222
|
+
if (lowerName.endsWith('.zip'))
|
|
223
|
+
return 'zip';
|
|
224
|
+
if (lowerName.endsWith('.tar.gz') || lowerName.endsWith('.tgz'))
|
|
225
|
+
return 'tgz';
|
|
226
|
+
if (lowerName.endsWith('.tar'))
|
|
227
|
+
return 'tar';
|
|
228
|
+
if (lowerName.endsWith('.gz'))
|
|
229
|
+
return 'gz';
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
function stripArchiveExtension(fileName) {
|
|
233
|
+
return fileName.replace(/\.tar\.gz$/i, '').replace(/\.tgz$/i, '').replace(/\.zip$/i, '').replace(/\.tar$/i, '').replace(/\.gz$/i, '');
|
|
234
|
+
}
|
|
235
|
+
function resolveArchiveUploadOutputPath(uploadTargetDirectory, archiveFileName) {
|
|
236
|
+
const outputDirectoryName = normalizeEntryName(stripArchiveExtension(archiveFileName) || 'extracted');
|
|
237
|
+
return path.join(uploadTargetDirectory, outputDirectoryName);
|
|
238
|
+
}
|
|
239
|
+
function ensureArchiveEntryInside(targetRoot, entryPath) {
|
|
240
|
+
const normalizedEntryPath = String(entryPath || '').replace(/\\/g, '/');
|
|
241
|
+
const pathSegments = normalizedEntryPath.split('/').filter(Boolean);
|
|
242
|
+
if (!normalizedEntryPath || normalizedEntryPath.startsWith('/') || /^[A-Za-z]:/.test(normalizedEntryPath)) {
|
|
243
|
+
throw new Error(`Archive entry path is invalid: ${entryPath}`);
|
|
244
|
+
}
|
|
245
|
+
if (normalizedEntryPath.length > ARCHIVE_LIMITS.maxPathLength || pathSegments.length > ARCHIVE_LIMITS.maxPathDepth) {
|
|
246
|
+
throw new Error(`Archive entry path is too deep or too long: ${entryPath}`);
|
|
247
|
+
}
|
|
248
|
+
return ensureInsideWorkspace(targetRoot, path.join(targetRoot, normalizedEntryPath));
|
|
249
|
+
}
|
|
250
|
+
function decodeZipEntryPath(entry) {
|
|
251
|
+
const pathBuffer = entry.props?.pathBuffer;
|
|
252
|
+
if (!pathBuffer || entry.props?.flags?.isUnicode) {
|
|
253
|
+
return entry.path;
|
|
254
|
+
}
|
|
255
|
+
const decodedPath = GBK_DECODER.decode(pathBuffer);
|
|
256
|
+
if (!decodedPath || decodedPath.includes('\uFFFD')) {
|
|
257
|
+
return entry.path;
|
|
258
|
+
}
|
|
259
|
+
return decodedPath;
|
|
260
|
+
}
|
|
261
|
+
async function drainZipEntry(entry) {
|
|
262
|
+
const drainedStream = entry.autodrain?.();
|
|
263
|
+
if (!drainedStream) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
await new Promise((resolve, reject) => {
|
|
267
|
+
drainedStream.on('close', resolve);
|
|
268
|
+
drainedStream.on('end', resolve);
|
|
269
|
+
drainedStream.on('finish', resolve);
|
|
270
|
+
drainedStream.on('error', reject);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
function trackArchiveEntry(meter) {
|
|
274
|
+
meter.entries += 1;
|
|
275
|
+
if (meter.entries > ARCHIVE_LIMITS.maxEntries) {
|
|
276
|
+
throw new Error(`Archive contains too many entries. Limit: ${ARCHIVE_LIMITS.maxEntries}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function trackArchiveBytes(meter, bytes) {
|
|
280
|
+
meter.totalBytes += bytes;
|
|
281
|
+
if (bytes > ARCHIVE_LIMITS.maxFileBytes) {
|
|
282
|
+
throw new Error(`Archive entry exceeds max size. Limit: ${ARCHIVE_LIMITS.maxFileBytes} bytes`);
|
|
283
|
+
}
|
|
284
|
+
if (meter.totalBytes > ARCHIVE_LIMITS.maxTotalBytes) {
|
|
285
|
+
throw new Error(`Archive extraction exceeds total size. Limit: ${ARCHIVE_LIMITS.maxTotalBytes} bytes`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function pipelineWithArchiveLimits(input, destinationPath, meter) {
|
|
289
|
+
let fileBytes = 0;
|
|
290
|
+
input.on('data', (chunk) => {
|
|
291
|
+
const chunkBytes = Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
|
|
292
|
+
fileBytes += chunkBytes;
|
|
293
|
+
if (fileBytes > ARCHIVE_LIMITS.maxFileBytes || meter.totalBytes + fileBytes > ARCHIVE_LIMITS.maxTotalBytes) {
|
|
294
|
+
if (input instanceof Readable) {
|
|
295
|
+
input.destroy(new Error('Archive extraction size limit exceeded'));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
await pipeline(input, createWriteStream(destinationPath));
|
|
300
|
+
trackArchiveBytes(meter, fileBytes);
|
|
301
|
+
}
|
|
302
|
+
async function pathExists(targetPath) {
|
|
303
|
+
try {
|
|
304
|
+
await fs.access(targetPath);
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
const errorCode = typeof error === 'object' && error !== null ? Reflect.get(error, 'code') : undefined;
|
|
309
|
+
if (errorCode === 'ENOENT') {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
74
315
|
/**
|
|
75
316
|
* 解析文件目标路径,并确保文件位于工作区中。
|
|
76
317
|
*/
|
|
77
|
-
function resolveFileTarget({ workspacePath, filePath }) {
|
|
78
|
-
const workspaceRoot =
|
|
318
|
+
async function resolveFileTarget({ workspacePath, filePath }) {
|
|
319
|
+
const { requestedRoot, workspaceRoot } = await resolveWorkspaceRoots(workspacePath);
|
|
79
320
|
const normalizedFilePath = String(filePath || '').trim();
|
|
80
321
|
if (!normalizedFilePath) {
|
|
81
322
|
throw new Error('filePath is required');
|
|
82
323
|
}
|
|
83
324
|
return {
|
|
84
325
|
workspaceRoot,
|
|
85
|
-
resolvedFilePath:
|
|
326
|
+
resolvedFilePath: resolveRequestedPathInsideWorkspace(requestedRoot, workspaceRoot, normalizedFilePath)
|
|
86
327
|
};
|
|
87
328
|
}
|
|
88
329
|
/**
|
|
89
330
|
* 列出工作区目录内容。
|
|
90
331
|
*/
|
|
91
332
|
export async function listWorkspaceDirectory(params) {
|
|
92
|
-
const { workspaceRoot, resolvedTargetPath } = resolveDirectoryTarget(params);
|
|
333
|
+
const { workspaceRoot, resolvedTargetPath } = await resolveDirectoryTarget(params);
|
|
93
334
|
const stat = await fs.stat(resolvedTargetPath);
|
|
94
335
|
if (!stat.isDirectory()) {
|
|
95
336
|
throw new Error(`Path is not a directory: ${resolvedTargetPath}`);
|
|
96
337
|
}
|
|
97
338
|
const entries = await fs.readdir(resolvedTargetPath, { withFileTypes: true });
|
|
98
|
-
const items = entries.map((entry) =>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
339
|
+
const items = await Promise.all(entries.map(async (entry) => {
|
|
340
|
+
const entryPath = path.join(resolvedTargetPath, entry.name);
|
|
341
|
+
const entrySize = entry.isFile() ? (await fs.stat(entryPath)).size : undefined;
|
|
342
|
+
return {
|
|
343
|
+
name: entry.name,
|
|
344
|
+
path: toDisplayPath(entryPath),
|
|
345
|
+
isDirectory: entry.isDirectory(),
|
|
346
|
+
size: entrySize
|
|
347
|
+
};
|
|
102
348
|
}));
|
|
103
349
|
return {
|
|
104
350
|
currentPath: toDisplayPath(resolvedTargetPath),
|
|
@@ -110,7 +356,8 @@ export async function listWorkspaceDirectory(params) {
|
|
|
110
356
|
* 读取工作区中的文本文件。
|
|
111
357
|
*/
|
|
112
358
|
export async function readWorkspaceFile(params) {
|
|
113
|
-
const { workspaceRoot, resolvedFilePath } = resolveFileTarget(params);
|
|
359
|
+
const { workspaceRoot, resolvedFilePath: lexicalFilePath } = await resolveFileTarget(params);
|
|
360
|
+
const resolvedFilePath = await ensureRealPathInsideWorkspace(workspaceRoot, lexicalFilePath);
|
|
114
361
|
const stat = await fs.stat(resolvedFilePath);
|
|
115
362
|
if (!stat.isFile()) {
|
|
116
363
|
throw new Error(`Path is not a file: ${resolvedFilePath}`);
|
|
@@ -122,12 +369,47 @@ export async function readWorkspaceFile(params) {
|
|
|
122
369
|
content
|
|
123
370
|
};
|
|
124
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* 返回工作区内 Word 文档的预览元数据;docx 由前端下载原文件后渲染,旧版 doc 明确提示不支持。
|
|
374
|
+
*/
|
|
375
|
+
export async function previewWorkspaceDocument(params) {
|
|
376
|
+
const { workspaceRoot, resolvedFilePath: lexicalFilePath } = await resolveFileTarget(params);
|
|
377
|
+
const resolvedFilePath = await ensureRealPathInsideWorkspace(workspaceRoot, lexicalFilePath);
|
|
378
|
+
const stat = await fs.stat(resolvedFilePath);
|
|
379
|
+
if (!stat.isFile()) {
|
|
380
|
+
throw new Error(`Path is not a file: ${resolvedFilePath}`);
|
|
381
|
+
}
|
|
382
|
+
const extension = path.extname(resolvedFilePath).toLowerCase();
|
|
383
|
+
if (extension === '.doc') {
|
|
384
|
+
return {
|
|
385
|
+
workspacePath: toDisplayPath(workspaceRoot),
|
|
386
|
+
filePath: toDisplayPath(resolvedFilePath),
|
|
387
|
+
kind: 'unsupported',
|
|
388
|
+
content: '',
|
|
389
|
+
message: '暂不支持旧版 .doc 格式预览,请另存为 .docx 后再打开。',
|
|
390
|
+
size: stat.size,
|
|
391
|
+
mtimeMs: stat.mtimeMs,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (extension !== '.docx') {
|
|
395
|
+
throw new Error('Document preview only supports .docx files');
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
workspacePath: toDisplayPath(workspaceRoot),
|
|
399
|
+
filePath: toDisplayPath(resolvedFilePath),
|
|
400
|
+
kind: 'html',
|
|
401
|
+
content: '',
|
|
402
|
+
size: stat.size,
|
|
403
|
+
mtimeMs: stat.mtimeMs,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
125
406
|
/**
|
|
126
407
|
* 将文本内容保存到工作区文件中。
|
|
127
408
|
*/
|
|
128
409
|
export async function writeWorkspaceFile(params) {
|
|
129
|
-
const { workspaceRoot, resolvedFilePath } = resolveFileTarget(params);
|
|
410
|
+
const { workspaceRoot, resolvedFilePath } = await resolveFileTarget(params);
|
|
130
411
|
await fs.mkdir(path.dirname(resolvedFilePath), { recursive: true });
|
|
412
|
+
await ensureWritableParentInsideWorkspace(workspaceRoot, resolvedFilePath);
|
|
131
413
|
await fs.writeFile(resolvedFilePath, String(params.content ?? ''), 'utf-8');
|
|
132
414
|
return {
|
|
133
415
|
ok: true,
|
|
@@ -139,7 +421,7 @@ export async function writeWorkspaceFile(params) {
|
|
|
139
421
|
* 在指定工作区目录下创建文件或目录。
|
|
140
422
|
*/
|
|
141
423
|
export async function createWorkspaceEntry(params) {
|
|
142
|
-
const { workspaceRoot, resolvedTargetPath } = resolveDirectoryTarget(params);
|
|
424
|
+
const { workspaceRoot, resolvedTargetPath } = await resolveDirectoryTarget(params);
|
|
143
425
|
const entryName = normalizeEntryName(params.entryName);
|
|
144
426
|
const targetPath = ensureInsideWorkspace(workspaceRoot, path.join(resolvedTargetPath, entryName));
|
|
145
427
|
const parentStat = await fs.stat(resolvedTargetPath);
|
|
@@ -161,6 +443,7 @@ export async function createWorkspaceEntry(params) {
|
|
|
161
443
|
}
|
|
162
444
|
else {
|
|
163
445
|
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
446
|
+
await ensureWritableParentInsideWorkspace(workspaceRoot, targetPath);
|
|
164
447
|
await fs.writeFile(targetPath, '', 'utf-8');
|
|
165
448
|
}
|
|
166
449
|
return {
|
|
@@ -174,7 +457,7 @@ export async function createWorkspaceEntry(params) {
|
|
|
174
457
|
* 在工作区内重命名文件或目录,保持目标仍位于原父目录中。
|
|
175
458
|
*/
|
|
176
459
|
export async function renameWorkspaceEntry(params) {
|
|
177
|
-
const { workspaceRoot, resolvedTargetPath } = resolveEntryTarget(params);
|
|
460
|
+
const { workspaceRoot, resolvedTargetPath } = await resolveEntryTarget(params);
|
|
178
461
|
const newName = normalizeEntryName(params.newName);
|
|
179
462
|
const renamedPath = ensureInsideWorkspace(workspaceRoot, path.join(path.dirname(resolvedTargetPath), newName));
|
|
180
463
|
if (renamedPath === resolvedTargetPath) {
|
|
@@ -204,11 +487,60 @@ export async function renameWorkspaceEntry(params) {
|
|
|
204
487
|
targetPath: toDisplayPath(renamedPath)
|
|
205
488
|
};
|
|
206
489
|
}
|
|
490
|
+
/**
|
|
491
|
+
* 将工作区内文件或目录移动到另一个工作区目录,保持原文件名不变。
|
|
492
|
+
*/
|
|
493
|
+
export async function moveWorkspaceEntry(params) {
|
|
494
|
+
const { workspaceRoot, resolvedTargetPath } = await resolveEntryTarget(params);
|
|
495
|
+
const destination = await resolveDirectoryTarget({
|
|
496
|
+
workspacePath: params.workspacePath,
|
|
497
|
+
targetPath: params.destinationPath,
|
|
498
|
+
});
|
|
499
|
+
const destinationStat = await fs.stat(destination.resolvedTargetPath);
|
|
500
|
+
if (!destinationStat.isDirectory()) {
|
|
501
|
+
throw new Error(`Destination is not a directory: ${destination.resolvedTargetPath}`);
|
|
502
|
+
}
|
|
503
|
+
const targetStat = await fs.stat(resolvedTargetPath);
|
|
504
|
+
if (targetStat.isDirectory()) {
|
|
505
|
+
const relativeDestination = path.relative(resolvedTargetPath, destination.resolvedTargetPath);
|
|
506
|
+
if (relativeDestination === '' || (!relativeDestination.startsWith('..') && !path.isAbsolute(relativeDestination))) {
|
|
507
|
+
throw new Error('Cannot move a directory into itself or its descendant');
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const movedPath = ensureInsideWorkspace(workspaceRoot, path.join(destination.resolvedTargetPath, path.basename(resolvedTargetPath)));
|
|
511
|
+
if (movedPath === resolvedTargetPath) {
|
|
512
|
+
return {
|
|
513
|
+
ok: true,
|
|
514
|
+
workspacePath: toDisplayPath(workspaceRoot),
|
|
515
|
+
sourcePath: toDisplayPath(resolvedTargetPath),
|
|
516
|
+
targetPath: toDisplayPath(movedPath),
|
|
517
|
+
destinationPath: toDisplayPath(destination.resolvedTargetPath),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
await fs.access(movedPath);
|
|
522
|
+
throw new Error(`Path already exists: ${movedPath}`);
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
const errorCode = typeof error === 'object' && error !== null ? Reflect.get(error, 'code') : undefined;
|
|
526
|
+
if (errorCode !== 'ENOENT') {
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
await fs.rename(resolvedTargetPath, movedPath);
|
|
531
|
+
return {
|
|
532
|
+
ok: true,
|
|
533
|
+
workspacePath: toDisplayPath(workspaceRoot),
|
|
534
|
+
sourcePath: toDisplayPath(resolvedTargetPath),
|
|
535
|
+
targetPath: toDisplayPath(movedPath),
|
|
536
|
+
destinationPath: toDisplayPath(destination.resolvedTargetPath),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
207
539
|
/**
|
|
208
540
|
* 删除工作区内的文件或目录,并阻止误删工作区根目录。
|
|
209
541
|
*/
|
|
210
542
|
export async function deleteWorkspaceEntry(params) {
|
|
211
|
-
const { workspaceRoot, resolvedTargetPath } = resolveEntryTarget(params);
|
|
543
|
+
const { workspaceRoot, resolvedTargetPath } = await resolveEntryTarget(params);
|
|
212
544
|
const targetStat = await fs.stat(resolvedTargetPath);
|
|
213
545
|
if (targetStat.isDirectory()) {
|
|
214
546
|
await fs.rm(resolvedTargetPath, { recursive: true, force: false });
|
|
@@ -222,3 +554,221 @@ export async function deleteWorkspaceEntry(params) {
|
|
|
222
554
|
targetPath: toDisplayPath(resolvedTargetPath)
|
|
223
555
|
};
|
|
224
556
|
}
|
|
557
|
+
/**
|
|
558
|
+
* 上传文件到工作区指定目录,并可在上传后自动解压常见归档。
|
|
559
|
+
*/
|
|
560
|
+
export async function uploadWorkspaceFiles(params) {
|
|
561
|
+
if (!Array.isArray(params.files) || params.files.length === 0) {
|
|
562
|
+
throw new Error('files are required');
|
|
563
|
+
}
|
|
564
|
+
const { workspaceRoot, resolvedTargetPath } = await resolveWritableDirectory(params);
|
|
565
|
+
const targetStat = await fs.stat(resolvedTargetPath);
|
|
566
|
+
if (!targetStat.isDirectory()) {
|
|
567
|
+
throw new Error(`Path is not a directory: ${resolvedTargetPath}`);
|
|
568
|
+
}
|
|
569
|
+
const files = [];
|
|
570
|
+
for (const file of params.files) {
|
|
571
|
+
const fileName = normalizeUploadedFileName(file.originalname);
|
|
572
|
+
const uploadRelativePath = normalizeUploadedRelativePath(file.relativePath, file.originalname);
|
|
573
|
+
const destinationPath = ensureInsideWorkspace(workspaceRoot, path.join(resolvedTargetPath, uploadRelativePath));
|
|
574
|
+
await fs.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
575
|
+
await ensureWritableParentInsideWorkspace(workspaceRoot, destinationPath);
|
|
576
|
+
await fs.copyFile(file.path, destinationPath);
|
|
577
|
+
let extracted;
|
|
578
|
+
if (params.extractArchives && archiveKind(destinationPath)) {
|
|
579
|
+
extracted = await extractWorkspaceArchive({
|
|
580
|
+
workspacePath: workspaceRoot,
|
|
581
|
+
archivePath: destinationPath,
|
|
582
|
+
outputPath: resolveArchiveUploadOutputPath(resolvedTargetPath, fileName)
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
files.push({
|
|
586
|
+
fileName,
|
|
587
|
+
targetPath: toDisplayPath(destinationPath),
|
|
588
|
+
size: file.size,
|
|
589
|
+
extracted
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
ok: true,
|
|
594
|
+
workspacePath: toDisplayPath(workspaceRoot),
|
|
595
|
+
targetPath: toDisplayPath(resolvedTargetPath),
|
|
596
|
+
files
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* 解析工作区下载目标;目录由路由层打包为 zip,文件直接流式下载。
|
|
601
|
+
*/
|
|
602
|
+
export async function resolveWorkspaceDownloadTarget(params) {
|
|
603
|
+
const { workspaceRoot, resolvedTargetPath } = await resolveEntryTarget(params);
|
|
604
|
+
const targetStat = await fs.stat(resolvedTargetPath);
|
|
605
|
+
const baseName = path.basename(resolvedTargetPath);
|
|
606
|
+
return {
|
|
607
|
+
workspacePath: toDisplayPath(workspaceRoot),
|
|
608
|
+
targetPath: toDisplayPath(resolvedTargetPath),
|
|
609
|
+
resolvedTargetPath,
|
|
610
|
+
fileName: targetStat.isDirectory() ? `${baseName || 'workspace'}.zip` : baseName,
|
|
611
|
+
contentType: targetStat.isDirectory() ? 'application/zip' : 'application/octet-stream',
|
|
612
|
+
isDirectory: targetStat.isDirectory()
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* 将目录内容以 zip 格式写入输出流,避免在磁盘生成临时包。
|
|
617
|
+
*/
|
|
618
|
+
export async function streamWorkspaceDirectoryZip(directoryPath, output) {
|
|
619
|
+
const archive = new ZipArchive({ zlib: { level: 9 } });
|
|
620
|
+
const completion = new Promise((resolve, reject) => {
|
|
621
|
+
output.on('close', resolve);
|
|
622
|
+
output.on('finish', resolve);
|
|
623
|
+
archive.on('error', reject);
|
|
624
|
+
output.on('error', reject);
|
|
625
|
+
});
|
|
626
|
+
archive.pipe(output);
|
|
627
|
+
archive.directory(directoryPath, false);
|
|
628
|
+
await archive.finalize();
|
|
629
|
+
await completion;
|
|
630
|
+
}
|
|
631
|
+
async function extractZipArchive(archivePath, outputRoot, meter) {
|
|
632
|
+
let extractedEntries = 0;
|
|
633
|
+
let extractionError;
|
|
634
|
+
const sourceStream = createReadStream(archivePath);
|
|
635
|
+
const parsedStream = sourceStream.pipe(unzipper.Parse({ forceStream: true }));
|
|
636
|
+
try {
|
|
637
|
+
for await (const entry of parsedStream) {
|
|
638
|
+
if (extractionError) {
|
|
639
|
+
await drainZipEntry(entry);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
const entryPath = decodeZipEntryPath(entry);
|
|
643
|
+
try {
|
|
644
|
+
const destinationPath = ensureArchiveEntryInside(outputRoot, entryPath);
|
|
645
|
+
trackArchiveEntry(meter);
|
|
646
|
+
if (entry.type === 'Directory') {
|
|
647
|
+
await fs.mkdir(destinationPath, { recursive: true });
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (entry.type !== 'File') {
|
|
651
|
+
throw new Error(`Unsupported archive entry type: ${entryPath}`);
|
|
652
|
+
}
|
|
653
|
+
await fs.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
654
|
+
await ensureWritableParentInsideWorkspace(outputRoot, destinationPath);
|
|
655
|
+
await pipelineWithArchiveLimits(entry, destinationPath, meter);
|
|
656
|
+
extractedEntries += 1;
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
extractionError = error;
|
|
660
|
+
await drainZipEntry(entry);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
finally {
|
|
665
|
+
sourceStream.destroy();
|
|
666
|
+
parsedStream.destroy();
|
|
667
|
+
}
|
|
668
|
+
if (extractionError) {
|
|
669
|
+
throw extractionError;
|
|
670
|
+
}
|
|
671
|
+
return extractedEntries;
|
|
672
|
+
}
|
|
673
|
+
async function extractTarArchive(archivePath, outputRoot, gzip, meter) {
|
|
674
|
+
let extractedEntries = 0;
|
|
675
|
+
let extractionError;
|
|
676
|
+
/**
|
|
677
|
+
* 校验 tar 条目并通过 filter 返回值控制是否解压。
|
|
678
|
+
* tar 的 filter 回调位于流解析链路中,直接抛错会变成未处理异常;这里先记录错误,待流结束后统一 reject。
|
|
679
|
+
*/
|
|
680
|
+
const shouldExtractTarEntry = (entryPath, entry) => {
|
|
681
|
+
if (extractionError) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
ensureArchiveEntryInside(outputRoot, entryPath);
|
|
686
|
+
trackArchiveEntry(meter);
|
|
687
|
+
const entryType = String(entry.type || '').toLowerCase();
|
|
688
|
+
if (entryType === 'symboliclink' || entryType === 'link') {
|
|
689
|
+
throw new Error(`Archive links are not supported: ${entryPath}`);
|
|
690
|
+
}
|
|
691
|
+
if (entryType !== 'file' && entryType !== 'oldfile' && entryType !== 'contiguousfile' && entryType !== 'directory') {
|
|
692
|
+
throw new Error(`Unsupported archive entry type: ${entryPath}`);
|
|
693
|
+
}
|
|
694
|
+
if (typeof entry.size === 'number') {
|
|
695
|
+
trackArchiveBytes(meter, entry.size);
|
|
696
|
+
}
|
|
697
|
+
if (entryType !== 'directory') {
|
|
698
|
+
extractedEntries += 1;
|
|
699
|
+
}
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
extractionError = error;
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
await tar.x({
|
|
708
|
+
file: archivePath,
|
|
709
|
+
cwd: outputRoot,
|
|
710
|
+
gzip,
|
|
711
|
+
strict: true,
|
|
712
|
+
filter: (entryPath, entry) => {
|
|
713
|
+
return shouldExtractTarEntry(entryPath, entry);
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
if (extractionError) {
|
|
717
|
+
throw extractionError;
|
|
718
|
+
}
|
|
719
|
+
return extractedEntries;
|
|
720
|
+
}
|
|
721
|
+
async function extractGzipFile(archivePath, outputRoot, meter) {
|
|
722
|
+
const outputName = stripArchiveExtension(path.basename(archivePath));
|
|
723
|
+
const destinationPath = ensureArchiveEntryInside(outputRoot, outputName || 'decompressed');
|
|
724
|
+
trackArchiveEntry(meter);
|
|
725
|
+
await fs.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
726
|
+
await ensureWritableParentInsideWorkspace(outputRoot, destinationPath);
|
|
727
|
+
await pipelineWithArchiveLimits(createReadStream(archivePath).pipe(createGunzip()), destinationPath, meter);
|
|
728
|
+
return 1;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* 解压工作区中的归档文件到指定目录,支持 zip、tar、tar.gz/tgz 与单文件 gz。
|
|
732
|
+
*/
|
|
733
|
+
export async function extractWorkspaceArchive(params) {
|
|
734
|
+
const { requestedRoot, workspaceRoot } = await resolveWorkspaceRoots(params.workspacePath);
|
|
735
|
+
const { resolvedTargetPath: outputRoot } = await resolveWritableDirectory({
|
|
736
|
+
workspacePath: params.workspacePath,
|
|
737
|
+
targetPath: params.outputPath || ''
|
|
738
|
+
});
|
|
739
|
+
const lexicalArchivePath = resolveRequestedPathInsideWorkspace(requestedRoot, workspaceRoot, String(params.archivePath || '').trim());
|
|
740
|
+
const archivePath = await ensureRealPathInsideWorkspace(workspaceRoot, lexicalArchivePath);
|
|
741
|
+
const archiveStat = await fs.stat(archivePath);
|
|
742
|
+
if (!archiveStat.isFile()) {
|
|
743
|
+
throw new Error(`Archive path is not a file: ${archivePath}`);
|
|
744
|
+
}
|
|
745
|
+
if (!(await pathExists(outputRoot))) {
|
|
746
|
+
await fs.mkdir(outputRoot, { recursive: true });
|
|
747
|
+
}
|
|
748
|
+
const outputStat = await fs.stat(outputRoot);
|
|
749
|
+
if (!outputStat.isDirectory()) {
|
|
750
|
+
throw new Error(`Output path is not a directory: ${outputRoot}`);
|
|
751
|
+
}
|
|
752
|
+
const kind = archiveKind(archivePath);
|
|
753
|
+
if (!kind) {
|
|
754
|
+
throw new Error('Unsupported archive type. Supported: .zip, .tar, .tar.gz, .tgz, .gz');
|
|
755
|
+
}
|
|
756
|
+
let extractedEntries = 0;
|
|
757
|
+
const meter = { entries: 0, totalBytes: 0 };
|
|
758
|
+
if (kind === 'zip') {
|
|
759
|
+
extractedEntries = await extractZipArchive(archivePath, outputRoot, meter);
|
|
760
|
+
}
|
|
761
|
+
else if (kind === 'tar' || kind === 'tgz') {
|
|
762
|
+
extractedEntries = await extractTarArchive(archivePath, outputRoot, kind === 'tgz', meter);
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
extractedEntries = await extractGzipFile(archivePath, outputRoot, meter);
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
ok: true,
|
|
769
|
+
workspacePath: toDisplayPath(workspaceRoot),
|
|
770
|
+
archivePath: toDisplayPath(archivePath),
|
|
771
|
+
targetPath: toDisplayPath(outputRoot),
|
|
772
|
+
extractedEntries
|
|
773
|
+
};
|
|
774
|
+
}
|