@actiondock/builder 2.0.11 → 2.2.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 +46 -30
- package/dist/archive.d.ts +47 -0
- package/dist/archive.js +519 -0
- package/dist/build.d.ts +9 -0
- package/dist/build.js +576 -0
- package/dist/dependency-check.d.ts +14 -0
- package/dist/dependency-check.js +246 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +22 -0
- package/dist/exporter.d.ts +69 -0
- package/dist/exporter.js +761 -0
- package/dist/fs-utils.d.ts +16 -0
- package/dist/fs-utils.js +146 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +21 -0
- package/dist/manifest.d.ts +75 -0
- package/dist/manifest.js +213 -0
- package/dist/pack.d.ts +13 -0
- package/dist/pack.js +339 -0
- package/dist/planner.d.ts +68 -0
- package/dist/planner.js +681 -0
- package/dist/types.d.ts +442 -0
- package/dist/types.js +1 -0
- package/package.json +15 -17
- package/src/archive.ts +0 -248
- package/src/build.ts +0 -106
- package/src/compiler.ts +0 -303
- package/src/errors.ts +0 -67
- package/src/exporter.ts +0 -680
- package/src/index.ts +0 -6
- package/src/planner.ts +0 -731
- package/src/types.ts +0 -337
package/dist/archive.js
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import { closeSync, createReadStream, createWriteStream, existsSync, openSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
2
|
+
import { createDeflateRaw, deflateRawSync, gzipSync, createGzip } from "node:zlib";
|
|
3
|
+
import { basename, join, relative, sep } from "node:path";
|
|
4
|
+
import { Transform, Writable } from "node:stream";
|
|
5
|
+
import { pipeline } from "node:stream/promises";
|
|
6
|
+
import { assertPathWithinRoot } from "@actiondock/core";
|
|
7
|
+
/** 递归收集目录内全部条目,执行安全路径边界校验与符号链接循环拦截 */
|
|
8
|
+
function collectEntries(dir, baseDir = dir, visited = new Set()) {
|
|
9
|
+
if (!existsSync(dir))
|
|
10
|
+
return [];
|
|
11
|
+
if (visited.size === 0) {
|
|
12
|
+
try {
|
|
13
|
+
visited.add(existsSync(baseDir) ? realpathSync(baseDir) : baseDir);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// 忽略
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const entries = [];
|
|
20
|
+
let names;
|
|
21
|
+
try {
|
|
22
|
+
names = readdirSync(dir).sort();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
for (const name of names) {
|
|
28
|
+
const fullPath = join(dir, name);
|
|
29
|
+
try {
|
|
30
|
+
assertPathWithinRoot(baseDir, fullPath, "archive path");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
let real;
|
|
36
|
+
try {
|
|
37
|
+
real = existsSync(fullPath) ? realpathSync(fullPath) : fullPath;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
assertPathWithinRoot(baseDir, real, "archive path");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// 忽略并跳过指向 baseDir 外部的软链接
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (visited.has(real)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
visited.add(real);
|
|
53
|
+
try {
|
|
54
|
+
const stat = statSync(fullPath);
|
|
55
|
+
if (stat.isDirectory()) {
|
|
56
|
+
entries.push({ relPath: relative(baseDir, fullPath).split(sep).join("/"), isDir: true });
|
|
57
|
+
entries.push(...collectEntries(fullPath, baseDir, visited));
|
|
58
|
+
}
|
|
59
|
+
else if (stat.isFile()) {
|
|
60
|
+
entries.push({ relPath: relative(baseDir, fullPath).split(sep).join("/"), isDir: false });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// 忽略无法访问或损坏的文件/符号链接
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return entries;
|
|
69
|
+
}
|
|
70
|
+
/* ------------------------------------------------------------------ */
|
|
71
|
+
/* zip 格式写入 */
|
|
72
|
+
/* ------------------------------------------------------------------ */
|
|
73
|
+
/** CRC32 查表(IEEE 802.3 多项式) */
|
|
74
|
+
const CRC32_TABLE = (() => {
|
|
75
|
+
const table = new Uint32Array(256);
|
|
76
|
+
for (let n = 0; n < 256; n++) {
|
|
77
|
+
let c = n;
|
|
78
|
+
for (let k = 0; k < 8; k++) {
|
|
79
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
80
|
+
}
|
|
81
|
+
table[n] = c >>> 0;
|
|
82
|
+
}
|
|
83
|
+
return table;
|
|
84
|
+
})();
|
|
85
|
+
function updateCrc32(crc, buf) {
|
|
86
|
+
for (let i = 0; i < buf.length; i++) {
|
|
87
|
+
crc = CRC32_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
|
|
88
|
+
}
|
|
89
|
+
return crc;
|
|
90
|
+
}
|
|
91
|
+
function crc32(buf) {
|
|
92
|
+
return (updateCrc32(0xffffffff, buf) ^ 0xffffffff) >>> 0;
|
|
93
|
+
}
|
|
94
|
+
/** Unix 毫秒时间戳转 DOS 时间格式(date/time 各 16 位) */
|
|
95
|
+
function dosDateTime(ms) {
|
|
96
|
+
const d = new Date(ms);
|
|
97
|
+
const year = Math.max(d.getFullYear(), 1980);
|
|
98
|
+
const time = (d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1);
|
|
99
|
+
const date = ((year - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate();
|
|
100
|
+
return { date, time };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 将目录打包为标准 zip 归档(PKZIP APPNOTE 结构)。
|
|
104
|
+
* 直接流式分块写入磁盘,避免在内存中积压全量归档内容。
|
|
105
|
+
*
|
|
106
|
+
* @param dir 待打包目录(目录名即归档内根文件夹名)
|
|
107
|
+
* @param outPath 输出 zip 文件路径
|
|
108
|
+
*/
|
|
109
|
+
export function createZipArchive(dir, outPath) {
|
|
110
|
+
const rootName = basename(dir);
|
|
111
|
+
const entries = collectEntries(dir);
|
|
112
|
+
const fd = openSync(outPath, "w");
|
|
113
|
+
const centralRecords = [];
|
|
114
|
+
let offset = 0;
|
|
115
|
+
try {
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
// 归档内路径带根目录前缀;目录条目名以 / 结尾
|
|
118
|
+
const name = `${rootName}/${entry.relPath}${entry.isDir ? "/" : ""}`;
|
|
119
|
+
const nameBuf = Buffer.from(name, "utf8");
|
|
120
|
+
const stat = statSync(join(dir, entry.relPath));
|
|
121
|
+
const { date, time } = dosDateTime(stat.mtimeMs);
|
|
122
|
+
const content = entry.isDir ? Buffer.alloc(0) : readFileSync(join(dir, entry.relPath));
|
|
123
|
+
let method = 0;
|
|
124
|
+
let payload = content;
|
|
125
|
+
if (!entry.isDir && content.length > 0) {
|
|
126
|
+
const deflated = deflateRawSync(content, { level: 9 });
|
|
127
|
+
if (deflated.length < content.length) {
|
|
128
|
+
method = 8;
|
|
129
|
+
payload = deflated;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const crc = crc32(content);
|
|
133
|
+
// Local File Header(30 字节 + 文件名)
|
|
134
|
+
const local = Buffer.alloc(30);
|
|
135
|
+
local.writeUInt32LE(0x04034b50, 0);
|
|
136
|
+
local.writeUInt16LE(20, 4);
|
|
137
|
+
local.writeUInt16LE(0x0800, 6); // UTF-8 文件名标志
|
|
138
|
+
local.writeUInt16LE(method, 8);
|
|
139
|
+
local.writeUInt16LE(time, 10);
|
|
140
|
+
local.writeUInt16LE(date, 12);
|
|
141
|
+
local.writeUInt32LE(crc, 14);
|
|
142
|
+
local.writeUInt32LE(payload.length, 18);
|
|
143
|
+
local.writeUInt32LE(content.length, 22);
|
|
144
|
+
local.writeUInt16LE(nameBuf.length, 26);
|
|
145
|
+
local.writeUInt16LE(0, 28);
|
|
146
|
+
writeSync(fd, local);
|
|
147
|
+
writeSync(fd, nameBuf);
|
|
148
|
+
if (payload.length > 0) {
|
|
149
|
+
writeSync(fd, payload);
|
|
150
|
+
}
|
|
151
|
+
// Central Directory Header(46 字节 + 文件名)
|
|
152
|
+
const central = Buffer.alloc(46);
|
|
153
|
+
central.writeUInt32LE(0x02014b50, 0);
|
|
154
|
+
central.writeUInt16LE((3 << 8) | 20, 4); // version made by: Unix + ZIP 2.0
|
|
155
|
+
central.writeUInt16LE(20, 6); // version needed
|
|
156
|
+
central.writeUInt16LE(0x0800, 8);
|
|
157
|
+
central.writeUInt16LE(method, 10);
|
|
158
|
+
central.writeUInt16LE(time, 12);
|
|
159
|
+
central.writeUInt16LE(date, 14);
|
|
160
|
+
central.writeUInt32LE(crc, 16);
|
|
161
|
+
central.writeUInt32LE(payload.length, 20);
|
|
162
|
+
central.writeUInt32LE(content.length, 24);
|
|
163
|
+
central.writeUInt16LE(nameBuf.length, 28);
|
|
164
|
+
central.writeUInt16LE(0, 30); // extra len
|
|
165
|
+
central.writeUInt16LE(0, 32); // comment len
|
|
166
|
+
central.writeUInt16LE(0, 34); // disk start
|
|
167
|
+
central.writeUInt16LE(0, 36); // internal attrs
|
|
168
|
+
// 外部属性:Unix 权限左移 16 位,目录附加 MS-DOS 目录位
|
|
169
|
+
const isExec = !entry.isDir &&
|
|
170
|
+
(Boolean(stat.mode & 0o111) || entry.relPath.includes("bin/") || entry.relPath.startsWith("bin/"));
|
|
171
|
+
const fileMode = isExec ? 0o100755 : 0o100644;
|
|
172
|
+
const extAttrs = ((entry.isDir ? 0o40755 : fileMode) << 16) | (entry.isDir ? 0x10 : 0);
|
|
173
|
+
central.writeUInt32LE(extAttrs >>> 0, 38);
|
|
174
|
+
central.writeUInt32LE(offset, 42);
|
|
175
|
+
centralRecords.push({ header: Buffer.concat([central, nameBuf]), localOffset: offset });
|
|
176
|
+
offset += local.length + nameBuf.length + payload.length;
|
|
177
|
+
}
|
|
178
|
+
const centralDirOffset = offset;
|
|
179
|
+
let centralDirSize = 0;
|
|
180
|
+
for (const record of centralRecords) {
|
|
181
|
+
writeSync(fd, record.header);
|
|
182
|
+
centralDirSize += record.header.length;
|
|
183
|
+
}
|
|
184
|
+
// End of Central Directory Record(22 字节)
|
|
185
|
+
const eocd = Buffer.alloc(22);
|
|
186
|
+
eocd.writeUInt32LE(0x06054b50, 0);
|
|
187
|
+
eocd.writeUInt16LE(0, 4);
|
|
188
|
+
eocd.writeUInt16LE(0, 6);
|
|
189
|
+
eocd.writeUInt16LE(centralRecords.length, 8);
|
|
190
|
+
eocd.writeUInt16LE(centralRecords.length, 10);
|
|
191
|
+
eocd.writeUInt32LE(centralDirSize, 12);
|
|
192
|
+
eocd.writeUInt32LE(centralDirOffset, 16);
|
|
193
|
+
eocd.writeUInt16LE(0, 20); // comment len
|
|
194
|
+
writeSync(fd, eocd);
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
closeSync(fd);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* 向可写流安全写入数据块,严格处理背压与流异常。
|
|
202
|
+
* 当 stream.write 返回 false 时挂起等待 drain 事件触发;
|
|
203
|
+
* 若流销毁或出错则安全拒绝。
|
|
204
|
+
*/
|
|
205
|
+
export async function writeToStream(stream, chunk) {
|
|
206
|
+
if (stream.destroyed || stream.errored) {
|
|
207
|
+
throw stream.errored ?? new Error("Target stream has been destroyed");
|
|
208
|
+
}
|
|
209
|
+
const canWrite = stream.write(chunk);
|
|
210
|
+
if (!canWrite) {
|
|
211
|
+
if (stream.destroyed || stream.errored) {
|
|
212
|
+
throw stream.errored ?? new Error("Target stream has been destroyed");
|
|
213
|
+
}
|
|
214
|
+
await new Promise((resolve, reject) => {
|
|
215
|
+
const onDrain = () => {
|
|
216
|
+
cleanup();
|
|
217
|
+
resolve();
|
|
218
|
+
};
|
|
219
|
+
const onError = (err) => {
|
|
220
|
+
cleanup();
|
|
221
|
+
reject(err);
|
|
222
|
+
};
|
|
223
|
+
const onClose = () => {
|
|
224
|
+
cleanup();
|
|
225
|
+
if (stream.destroyed && !stream.writableEnded) {
|
|
226
|
+
reject(stream.errored ?? new Error("Target stream closed prematurely"));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
resolve();
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
const cleanup = () => {
|
|
233
|
+
stream.off("drain", onDrain);
|
|
234
|
+
stream.off("error", onError);
|
|
235
|
+
stream.off("close", onClose);
|
|
236
|
+
};
|
|
237
|
+
stream.once("drain", onDrain);
|
|
238
|
+
stream.once("error", onError);
|
|
239
|
+
stream.once("close", onClose);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* 异步流式 zip 归档:基于 PKZIP Data Descriptor 规范实现真正流式打包,
|
|
245
|
+
* 逐文件流式读取与增量压缩写入磁盘,避免在内存中积压全量原文件与压缩内容。
|
|
246
|
+
*/
|
|
247
|
+
export async function createZipArchiveAsync(dir, outPath) {
|
|
248
|
+
const rootName = basename(dir);
|
|
249
|
+
const entries = collectEntries(dir);
|
|
250
|
+
const out = createWriteStream(outPath, { highWaterMark: 1024 * 1024 });
|
|
251
|
+
const centralRecords = [];
|
|
252
|
+
let offset = 0;
|
|
253
|
+
const writeChunk = async (chunk) => {
|
|
254
|
+
offset += chunk.length;
|
|
255
|
+
await writeToStream(out, chunk);
|
|
256
|
+
};
|
|
257
|
+
try {
|
|
258
|
+
for (const entry of entries) {
|
|
259
|
+
const name = `${rootName}/${entry.relPath}${entry.isDir ? "/" : ""}`;
|
|
260
|
+
const nameBuf = Buffer.from(name, "utf8");
|
|
261
|
+
const fullPath = join(dir, entry.relPath);
|
|
262
|
+
const stat = statSync(fullPath);
|
|
263
|
+
const { date, time } = dosDateTime(stat.mtimeMs);
|
|
264
|
+
// 外部属性:Unix 权限左移 16 位,目录附加 MS-DOS 目录位
|
|
265
|
+
const isExec = !entry.isDir &&
|
|
266
|
+
(Boolean(stat.mode & 0o111) || entry.relPath.includes("bin/") || entry.relPath.startsWith("bin/"));
|
|
267
|
+
const fileMode = isExec ? 0o100755 : 0o100644;
|
|
268
|
+
const extAttrs = ((entry.isDir ? 0o40755 : fileMode) << 16) | (entry.isDir ? 0x10 : 0);
|
|
269
|
+
const localOffset = offset;
|
|
270
|
+
const isDeflatedFile = !entry.isDir && stat.size > 0;
|
|
271
|
+
const method = isDeflatedFile ? 8 : 0;
|
|
272
|
+
const flag = isDeflatedFile ? 0x0808 : 0x0800;
|
|
273
|
+
// Local File Header(30 字节 + 文件名)
|
|
274
|
+
const local = Buffer.alloc(30);
|
|
275
|
+
local.writeUInt32LE(0x04034b50, 0);
|
|
276
|
+
local.writeUInt16LE(20, 4);
|
|
277
|
+
local.writeUInt16LE(flag, 6);
|
|
278
|
+
local.writeUInt16LE(method, 8);
|
|
279
|
+
local.writeUInt16LE(time, 10);
|
|
280
|
+
local.writeUInt16LE(date, 12);
|
|
281
|
+
local.writeUInt32LE(0, 14); // bit 3 开启或空文件/目录置 0
|
|
282
|
+
local.writeUInt32LE(0, 18); // bit 3 开启或空文件/目录置 0
|
|
283
|
+
local.writeUInt32LE(0, 22); // bit 3 开启或空文件/目录置 0
|
|
284
|
+
local.writeUInt16LE(nameBuf.length, 26);
|
|
285
|
+
local.writeUInt16LE(0, 28);
|
|
286
|
+
await writeChunk(local);
|
|
287
|
+
await writeChunk(nameBuf);
|
|
288
|
+
let fileCrc = 0;
|
|
289
|
+
let fileCompressedSize = 0;
|
|
290
|
+
let fileUncompressedSize = 0;
|
|
291
|
+
if (isDeflatedFile) {
|
|
292
|
+
let crcState = 0xffffffff;
|
|
293
|
+
let uncompressedSize = 0;
|
|
294
|
+
let compressedSize = 0;
|
|
295
|
+
const crcTransform = new Transform({
|
|
296
|
+
transform(chunk, _encoding, callback) {
|
|
297
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
298
|
+
crcState = updateCrc32(crcState, buf);
|
|
299
|
+
uncompressedSize += buf.length;
|
|
300
|
+
callback(null, buf);
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
const deflater = createDeflateRaw({ level: 9 });
|
|
304
|
+
const outWritable = new Writable({
|
|
305
|
+
async write(chunk, _encoding, callback) {
|
|
306
|
+
try {
|
|
307
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
308
|
+
compressedSize += buf.length;
|
|
309
|
+
await writeChunk(buf);
|
|
310
|
+
callback();
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
callback(err);
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
const fileStream = createReadStream(fullPath);
|
|
318
|
+
await pipeline(fileStream, crcTransform, deflater, outWritable);
|
|
319
|
+
fileCrc = (crcState ^ 0xffffffff) >>> 0;
|
|
320
|
+
fileCompressedSize = compressedSize;
|
|
321
|
+
fileUncompressedSize = uncompressedSize;
|
|
322
|
+
// Data Descriptor(16 字节):0x08074b50 + CRC32 + compressedSize + uncompressedSize
|
|
323
|
+
const dd = Buffer.alloc(16);
|
|
324
|
+
dd.writeUInt32LE(0x08074b50, 0);
|
|
325
|
+
dd.writeUInt32LE(fileCrc, 4);
|
|
326
|
+
dd.writeUInt32LE(fileCompressedSize, 8);
|
|
327
|
+
dd.writeUInt32LE(fileUncompressedSize, 12);
|
|
328
|
+
await writeChunk(dd);
|
|
329
|
+
}
|
|
330
|
+
// Central Directory Header(46 字节 + 文件名)
|
|
331
|
+
const central = Buffer.alloc(46);
|
|
332
|
+
central.writeUInt32LE(0x02014b50, 0);
|
|
333
|
+
central.writeUInt16LE((3 << 8) | 20, 4); // version made by: Unix + ZIP 2.0
|
|
334
|
+
central.writeUInt16LE(20, 6); // version needed
|
|
335
|
+
central.writeUInt16LE(flag, 8);
|
|
336
|
+
central.writeUInt16LE(method, 10);
|
|
337
|
+
central.writeUInt16LE(time, 12);
|
|
338
|
+
central.writeUInt16LE(date, 14);
|
|
339
|
+
central.writeUInt32LE(fileCrc, 16);
|
|
340
|
+
central.writeUInt32LE(fileCompressedSize, 20);
|
|
341
|
+
central.writeUInt32LE(fileUncompressedSize, 24);
|
|
342
|
+
central.writeUInt16LE(nameBuf.length, 28);
|
|
343
|
+
central.writeUInt16LE(0, 30); // extra len
|
|
344
|
+
central.writeUInt16LE(0, 32); // comment len
|
|
345
|
+
central.writeUInt16LE(0, 34); // disk start
|
|
346
|
+
central.writeUInt16LE(0, 36); // internal attrs
|
|
347
|
+
central.writeUInt32LE(extAttrs >>> 0, 38);
|
|
348
|
+
central.writeUInt32LE(localOffset, 42);
|
|
349
|
+
centralRecords.push({ header: Buffer.concat([central, nameBuf]) });
|
|
350
|
+
}
|
|
351
|
+
const centralDirOffset = offset;
|
|
352
|
+
let centralDirSize = 0;
|
|
353
|
+
for (const record of centralRecords) {
|
|
354
|
+
await writeChunk(record.header);
|
|
355
|
+
centralDirSize += record.header.length;
|
|
356
|
+
}
|
|
357
|
+
// End of Central Directory Record(22 字节)
|
|
358
|
+
const eocd = Buffer.alloc(22);
|
|
359
|
+
eocd.writeUInt32LE(0x06054b50, 0);
|
|
360
|
+
eocd.writeUInt16LE(0, 4);
|
|
361
|
+
eocd.writeUInt16LE(0, 6);
|
|
362
|
+
eocd.writeUInt16LE(centralRecords.length, 8);
|
|
363
|
+
eocd.writeUInt16LE(centralRecords.length, 10);
|
|
364
|
+
eocd.writeUInt32LE(centralDirSize, 12);
|
|
365
|
+
eocd.writeUInt32LE(centralDirOffset, 16);
|
|
366
|
+
eocd.writeUInt16LE(0, 20); // comment len
|
|
367
|
+
await writeChunk(eocd);
|
|
368
|
+
await new Promise((resolve, reject) => {
|
|
369
|
+
out.end(() => resolve());
|
|
370
|
+
out.once("error", reject);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
out.destroy();
|
|
375
|
+
throw err;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/* ------------------------------------------------------------------ */
|
|
379
|
+
/* tar.gz 格式写入 */
|
|
380
|
+
/* ------------------------------------------------------------------ */
|
|
381
|
+
/**
|
|
382
|
+
* 构造单个条目的 USTAR 头块(512 字节)。
|
|
383
|
+
* 路径超长时按 USTAR prefix 字段拆分,仍放不下则抛错。
|
|
384
|
+
*/
|
|
385
|
+
function tarHeader(path, size, mtimeSec, isDir, modeOrExec = false) {
|
|
386
|
+
let name = path;
|
|
387
|
+
let prefix = "";
|
|
388
|
+
if (Buffer.byteLength(name, "utf8") > 100) {
|
|
389
|
+
const trailingSlash = name.endsWith("/");
|
|
390
|
+
const cleanPath = trailingSlash ? name.slice(0, -1) : name;
|
|
391
|
+
const split = cleanPath.lastIndexOf("/");
|
|
392
|
+
prefix = cleanPath.slice(0, split);
|
|
393
|
+
name = cleanPath.slice(split + 1) + (trailingSlash ? "/" : "");
|
|
394
|
+
if (split <= 0 || Buffer.byteLength(prefix, "utf8") > 155 || Buffer.byteLength(name, "utf8") > 100) {
|
|
395
|
+
throw new Error(`归档路径超出 USTAR 字段容量: ${path}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const buf = Buffer.alloc(512);
|
|
399
|
+
buf.write(name, 0, 100, "utf8");
|
|
400
|
+
let mode;
|
|
401
|
+
if (typeof modeOrExec === "string") {
|
|
402
|
+
mode = modeOrExec;
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
mode = isDir ? "0000755" : (modeOrExec ? "0000755" : "0000644");
|
|
406
|
+
}
|
|
407
|
+
buf.write(mode, 100, 8, "ascii"); // mode
|
|
408
|
+
buf.write("0000000", 108, 8, "ascii"); // uid
|
|
409
|
+
buf.write("0000000", 116, 8, "ascii"); // gid
|
|
410
|
+
buf.write(size.toString(8).padStart(11, "0"), 124, 12, "ascii"); // size
|
|
411
|
+
buf.write(Math.floor(mtimeSec).toString(8).padStart(11, "0"), 136, 12, "ascii"); // mtime
|
|
412
|
+
buf.fill(" ", 148, 156); // chksum 占位(计算时视为空格)
|
|
413
|
+
buf.write(isDir ? "5" : "0", 156, 1, "ascii"); // typeflag
|
|
414
|
+
buf.write("ustar\0", 257, 6, "ascii"); // magic
|
|
415
|
+
buf.write("00", 263, 2, "ascii"); // version
|
|
416
|
+
if (prefix)
|
|
417
|
+
buf.write(prefix, 345, 155, "utf8");
|
|
418
|
+
// 校验和:整块字节的无符号累加和,写作 6 位八进制 + NUL + 空格
|
|
419
|
+
let sum = 0;
|
|
420
|
+
for (let i = 0; i < 512; i++)
|
|
421
|
+
sum += buf[i];
|
|
422
|
+
buf.write(sum.toString(8).padStart(6, "0"), 148, 7, "ascii");
|
|
423
|
+
buf.write("\0", 154, 1, "ascii");
|
|
424
|
+
buf.write(" ", 155, 1, "ascii");
|
|
425
|
+
return buf;
|
|
426
|
+
}
|
|
427
|
+
/** 将字节数补齐到 512 的整数倍 */
|
|
428
|
+
function pad512(length) {
|
|
429
|
+
const rem = length % 512;
|
|
430
|
+
return rem === 0 ? Buffer.alloc(0) : Buffer.alloc(512 - rem);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* 将目录打包为标准 tar.gz 归档(USTAR + gzip)。
|
|
434
|
+
*
|
|
435
|
+
* @param dir 待打包目录(目录名即归档内根文件夹名)
|
|
436
|
+
* @param outPath 输出 tar.gz 文件路径
|
|
437
|
+
*/
|
|
438
|
+
export function createTarGzArchive(dir, outPath) {
|
|
439
|
+
const rootName = basename(dir);
|
|
440
|
+
const entries = collectEntries(dir);
|
|
441
|
+
const chunks = [];
|
|
442
|
+
for (const entry of entries) {
|
|
443
|
+
const path = `${rootName}/${entry.relPath}${entry.isDir ? "/" : ""}`;
|
|
444
|
+
const fullPath = join(dir, entry.relPath);
|
|
445
|
+
const stat = statSync(fullPath);
|
|
446
|
+
const content = entry.isDir ? Buffer.alloc(0) : readFileSync(fullPath);
|
|
447
|
+
const isExec = !entry.isDir &&
|
|
448
|
+
(Boolean(stat.mode & 0o111) || entry.relPath.includes("bin/") || entry.relPath.startsWith("bin/"));
|
|
449
|
+
chunks.push(tarHeader(path, content.length, Math.floor(stat.mtimeMs / 1000), entry.isDir, isExec));
|
|
450
|
+
if (!entry.isDir) {
|
|
451
|
+
chunks.push(content, pad512(content.length));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// 归档结尾:两个全零块
|
|
455
|
+
chunks.push(Buffer.alloc(1024));
|
|
456
|
+
writeFileSync(outPath, gzipSync(Buffer.concat(chunks), { level: 9 }));
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* 将目录异步流式打包为标准 tar.gz 归档(USTAR 与 gzip 流),避免在内存中积压全量原文件与压缩内容。
|
|
460
|
+
* 严格基于 Node.js 异步流背压机制与错误传播链路实现。
|
|
461
|
+
*
|
|
462
|
+
* @param dir 待打包目录
|
|
463
|
+
* @param outPath 输出 tar.gz 文件路径
|
|
464
|
+
* @param options 归档配置选项
|
|
465
|
+
*/
|
|
466
|
+
export async function createTarGzArchiveAsync(dir, outPath, options) {
|
|
467
|
+
const rootName = basename(dir);
|
|
468
|
+
const entries = collectEntries(dir);
|
|
469
|
+
const gzip = options?.createGzip ? options.createGzip() : createGzip({ level: options?.level ?? 9 });
|
|
470
|
+
const getWriteStream = options?.createWriteStream ?? createWriteStream;
|
|
471
|
+
const getReadStream = options?.createReadStream ?? createReadStream;
|
|
472
|
+
const writeStream = getWriteStream(outPath);
|
|
473
|
+
const pipePromise = pipeline(gzip, writeStream);
|
|
474
|
+
pipePromise.catch(() => { });
|
|
475
|
+
try {
|
|
476
|
+
for (const entry of entries) {
|
|
477
|
+
const path = `${rootName}/${entry.relPath}${entry.isDir ? "/" : ""}`;
|
|
478
|
+
const fullPath = join(dir, entry.relPath);
|
|
479
|
+
const stat = statSync(fullPath);
|
|
480
|
+
const isExec = !entry.isDir &&
|
|
481
|
+
(Boolean(stat.mode & 0o111) || entry.relPath.includes("bin/") || entry.relPath.startsWith("bin/"));
|
|
482
|
+
const header = tarHeader(path, entry.isDir ? 0 : stat.size, Math.floor(stat.mtimeMs / 1000), entry.isDir, isExec);
|
|
483
|
+
await writeToStream(gzip, header);
|
|
484
|
+
if (!entry.isDir && stat.size > 0) {
|
|
485
|
+
const fileStream = getReadStream(fullPath);
|
|
486
|
+
const onGzipError = (err) => {
|
|
487
|
+
if ("destroy" in fileStream && typeof fileStream.destroy === "function") {
|
|
488
|
+
fileStream.destroy(err);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
gzip.once("error", onGzipError);
|
|
492
|
+
try {
|
|
493
|
+
for await (const chunk of fileStream) {
|
|
494
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
495
|
+
await writeToStream(gzip, buf);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
gzip.off("error", onGzipError);
|
|
500
|
+
}
|
|
501
|
+
const rem = stat.size % 512;
|
|
502
|
+
if (rem !== 0) {
|
|
503
|
+
await writeToStream(gzip, Buffer.alloc(512 - rem));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
await writeToStream(gzip, Buffer.alloc(1024));
|
|
508
|
+
gzip.end();
|
|
509
|
+
await pipePromise;
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
gzip.destroy(err);
|
|
513
|
+
if ("destroy" in writeStream && typeof writeStream.destroy === "function") {
|
|
514
|
+
writeStream.destroy(err);
|
|
515
|
+
}
|
|
516
|
+
await pipePromise.catch(() => { });
|
|
517
|
+
throw err;
|
|
518
|
+
}
|
|
519
|
+
}
|
package/dist/build.d.ts
ADDED