7zip-native 0.0.5 → 0.0.7
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 +47 -10
- package/bench/folder-concurrency.mts +143 -0
- package/dist/index.cjs +290 -15
- package/dist/index.d.cts +28 -2
- package/dist/index.d.mts +28 -2
- package/dist/index.d.ts +28 -2
- package/dist/index.mjs +285 -10
- package/native/addon.cpp +470 -0
- package/package.json +5 -2
- package/prebuilds/darwin-arm64/7zip_native.node +0 -0
- package/prebuilds/darwin-x64/7zip_native.node +0 -0
- package/prebuilds/linux-arm/7zip_native.node +0 -0
- package/prebuilds/linux-arm64/7zip_native.node +0 -0
- package/prebuilds/linux-ia32/7zip_native.node +0 -0
- package/prebuilds/linux-x64/7zip_native.node +0 -0
- package/prebuilds/win32-arm64/7z.dll +0 -0
- package/prebuilds/win32-arm64/7zip_native.node +0 -0
- package/prebuilds/win32-x64/7z.dll +0 -0
- package/prebuilds/win32-x64/7zip_native.node +0 -0
- package/scripts/build-native.mjs +119 -0
- package/scripts/clean-upstream-build.mjs +9 -0
- package/scripts/verify-prebuild.mjs +29 -0
package/dist/index.d.cts
CHANGED
|
@@ -19,6 +19,27 @@ interface ReadOptions {
|
|
|
19
19
|
interface OpenOptions {
|
|
20
20
|
readonly password?: string;
|
|
21
21
|
}
|
|
22
|
+
type ArchiveFormat = '7z' | 'zip' | 'tar' | 'wim';
|
|
23
|
+
interface ArchiveCreateEntry {
|
|
24
|
+
readonly sourcePath: string;
|
|
25
|
+
readonly path: string;
|
|
26
|
+
}
|
|
27
|
+
interface CreateArchiveOptions {
|
|
28
|
+
readonly format?: ArchiveFormat;
|
|
29
|
+
readonly compressionLevel?: number;
|
|
30
|
+
readonly overwrite?: boolean;
|
|
31
|
+
}
|
|
32
|
+
interface ExtractToFolderProgress {
|
|
33
|
+
readonly filesExtracted: number;
|
|
34
|
+
readonly totalFiles: number;
|
|
35
|
+
readonly bytesProcessed: bigint;
|
|
36
|
+
readonly totalBytes: bigint;
|
|
37
|
+
readonly percent: number;
|
|
38
|
+
}
|
|
39
|
+
interface ExtractedArchiveFile {
|
|
40
|
+
readonly entry: ArchiveEntry;
|
|
41
|
+
readonly outputPath: string;
|
|
42
|
+
}
|
|
22
43
|
interface ReadStreamOptions {
|
|
23
44
|
readonly highWaterMark?: number;
|
|
24
45
|
readonly maxBytes?: number | bigint | null;
|
|
@@ -27,10 +48,14 @@ interface ReadStreamOptions {
|
|
|
27
48
|
interface ExtractToFolderOptions {
|
|
28
49
|
readonly ids?: readonly EntryId[];
|
|
29
50
|
readonly paths?: readonly string[];
|
|
51
|
+
readonly concurrency?: number;
|
|
52
|
+
readonly overwrite?: boolean;
|
|
53
|
+
readonly onProgress?: (progress: ExtractToFolderProgress) => void;
|
|
54
|
+
readonly onExtract?: (result: ExtractedArchiveFile) => void;
|
|
30
55
|
readonly maxBytes?: number | bigint | null;
|
|
31
56
|
readonly signal?: AbortSignal;
|
|
32
57
|
}
|
|
33
|
-
type ArchiveErrorCode = 'ERR_ARCHIVE_CLOSED' | 'ERR_INVALID_ENTRY' | 'ERR_INVALID_RANGE' | 'ERR_RESOURCE_LIMIT' | 'ERR_ARCHIVE_OPEN' | 'ERR_ARCHIVE_PASSWORD' | 'ERR_ARCHIVE_PATH' | 'ERR_ARCHIVE_EXTRACT';
|
|
58
|
+
type ArchiveErrorCode = 'ERR_ARCHIVE_CLOSED' | 'ERR_INVALID_ENTRY' | 'ERR_INVALID_RANGE' | 'ERR_RESOURCE_LIMIT' | 'ERR_ARCHIVE_OPEN' | 'ERR_ARCHIVE_PASSWORD' | 'ERR_ARCHIVE_PATH' | 'ERR_ARCHIVE_EXTRACT' | 'ERR_ARCHIVE_CREATE';
|
|
34
59
|
declare class ArchiveError extends Error {
|
|
35
60
|
readonly code: ArchiveErrorCode;
|
|
36
61
|
constructor(code: ArchiveErrorCode, message: string, options?: ErrorOptions);
|
|
@@ -43,6 +68,7 @@ declare class Archive {
|
|
|
43
68
|
private constructor();
|
|
44
69
|
static concurrency(value?: number): number;
|
|
45
70
|
static open(path: string, options?: OpenOptions): Promise<Archive>;
|
|
71
|
+
static create(outputPath: string, entries: readonly ArchiveCreateEntry[], options?: CreateArchiveOptions): Promise<void>;
|
|
46
72
|
entries(): Promise<readonly ArchiveEntry[]>;
|
|
47
73
|
getEntry(idOrPath: EntrySelector): Promise<ArchiveEntry | undefined>;
|
|
48
74
|
extract(idOrPath: EntrySelector): Promise<Buffer>;
|
|
@@ -60,4 +86,4 @@ declare class Archive {
|
|
|
60
86
|
}
|
|
61
87
|
|
|
62
88
|
export { Archive, ArchiveError };
|
|
63
|
-
export type { ArchiveEntry, ArchiveErrorCode, EntryId, EntrySelector, ExtractToFolderOptions, OpenOptions, ReadOptions, ReadStreamOptions };
|
|
89
|
+
export type { ArchiveCreateEntry, ArchiveEntry, ArchiveErrorCode, ArchiveFormat, CreateArchiveOptions, EntryId, EntrySelector, ExtractToFolderOptions, ExtractToFolderProgress, ExtractedArchiveFile, OpenOptions, ReadOptions, ReadStreamOptions };
|
package/dist/index.d.mts
CHANGED
|
@@ -19,6 +19,27 @@ interface ReadOptions {
|
|
|
19
19
|
interface OpenOptions {
|
|
20
20
|
readonly password?: string;
|
|
21
21
|
}
|
|
22
|
+
type ArchiveFormat = '7z' | 'zip' | 'tar' | 'wim';
|
|
23
|
+
interface ArchiveCreateEntry {
|
|
24
|
+
readonly sourcePath: string;
|
|
25
|
+
readonly path: string;
|
|
26
|
+
}
|
|
27
|
+
interface CreateArchiveOptions {
|
|
28
|
+
readonly format?: ArchiveFormat;
|
|
29
|
+
readonly compressionLevel?: number;
|
|
30
|
+
readonly overwrite?: boolean;
|
|
31
|
+
}
|
|
32
|
+
interface ExtractToFolderProgress {
|
|
33
|
+
readonly filesExtracted: number;
|
|
34
|
+
readonly totalFiles: number;
|
|
35
|
+
readonly bytesProcessed: bigint;
|
|
36
|
+
readonly totalBytes: bigint;
|
|
37
|
+
readonly percent: number;
|
|
38
|
+
}
|
|
39
|
+
interface ExtractedArchiveFile {
|
|
40
|
+
readonly entry: ArchiveEntry;
|
|
41
|
+
readonly outputPath: string;
|
|
42
|
+
}
|
|
22
43
|
interface ReadStreamOptions {
|
|
23
44
|
readonly highWaterMark?: number;
|
|
24
45
|
readonly maxBytes?: number | bigint | null;
|
|
@@ -27,10 +48,14 @@ interface ReadStreamOptions {
|
|
|
27
48
|
interface ExtractToFolderOptions {
|
|
28
49
|
readonly ids?: readonly EntryId[];
|
|
29
50
|
readonly paths?: readonly string[];
|
|
51
|
+
readonly concurrency?: number;
|
|
52
|
+
readonly overwrite?: boolean;
|
|
53
|
+
readonly onProgress?: (progress: ExtractToFolderProgress) => void;
|
|
54
|
+
readonly onExtract?: (result: ExtractedArchiveFile) => void;
|
|
30
55
|
readonly maxBytes?: number | bigint | null;
|
|
31
56
|
readonly signal?: AbortSignal;
|
|
32
57
|
}
|
|
33
|
-
type ArchiveErrorCode = 'ERR_ARCHIVE_CLOSED' | 'ERR_INVALID_ENTRY' | 'ERR_INVALID_RANGE' | 'ERR_RESOURCE_LIMIT' | 'ERR_ARCHIVE_OPEN' | 'ERR_ARCHIVE_PASSWORD' | 'ERR_ARCHIVE_PATH' | 'ERR_ARCHIVE_EXTRACT';
|
|
58
|
+
type ArchiveErrorCode = 'ERR_ARCHIVE_CLOSED' | 'ERR_INVALID_ENTRY' | 'ERR_INVALID_RANGE' | 'ERR_RESOURCE_LIMIT' | 'ERR_ARCHIVE_OPEN' | 'ERR_ARCHIVE_PASSWORD' | 'ERR_ARCHIVE_PATH' | 'ERR_ARCHIVE_EXTRACT' | 'ERR_ARCHIVE_CREATE';
|
|
34
59
|
declare class ArchiveError extends Error {
|
|
35
60
|
readonly code: ArchiveErrorCode;
|
|
36
61
|
constructor(code: ArchiveErrorCode, message: string, options?: ErrorOptions);
|
|
@@ -43,6 +68,7 @@ declare class Archive {
|
|
|
43
68
|
private constructor();
|
|
44
69
|
static concurrency(value?: number): number;
|
|
45
70
|
static open(path: string, options?: OpenOptions): Promise<Archive>;
|
|
71
|
+
static create(outputPath: string, entries: readonly ArchiveCreateEntry[], options?: CreateArchiveOptions): Promise<void>;
|
|
46
72
|
entries(): Promise<readonly ArchiveEntry[]>;
|
|
47
73
|
getEntry(idOrPath: EntrySelector): Promise<ArchiveEntry | undefined>;
|
|
48
74
|
extract(idOrPath: EntrySelector): Promise<Buffer>;
|
|
@@ -60,4 +86,4 @@ declare class Archive {
|
|
|
60
86
|
}
|
|
61
87
|
|
|
62
88
|
export { Archive, ArchiveError };
|
|
63
|
-
export type { ArchiveEntry, ArchiveErrorCode, EntryId, EntrySelector, ExtractToFolderOptions, OpenOptions, ReadOptions, ReadStreamOptions };
|
|
89
|
+
export type { ArchiveCreateEntry, ArchiveEntry, ArchiveErrorCode, ArchiveFormat, CreateArchiveOptions, EntryId, EntrySelector, ExtractToFolderOptions, ExtractToFolderProgress, ExtractedArchiveFile, OpenOptions, ReadOptions, ReadStreamOptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,27 @@ interface ReadOptions {
|
|
|
19
19
|
interface OpenOptions {
|
|
20
20
|
readonly password?: string;
|
|
21
21
|
}
|
|
22
|
+
type ArchiveFormat = '7z' | 'zip' | 'tar' | 'wim';
|
|
23
|
+
interface ArchiveCreateEntry {
|
|
24
|
+
readonly sourcePath: string;
|
|
25
|
+
readonly path: string;
|
|
26
|
+
}
|
|
27
|
+
interface CreateArchiveOptions {
|
|
28
|
+
readonly format?: ArchiveFormat;
|
|
29
|
+
readonly compressionLevel?: number;
|
|
30
|
+
readonly overwrite?: boolean;
|
|
31
|
+
}
|
|
32
|
+
interface ExtractToFolderProgress {
|
|
33
|
+
readonly filesExtracted: number;
|
|
34
|
+
readonly totalFiles: number;
|
|
35
|
+
readonly bytesProcessed: bigint;
|
|
36
|
+
readonly totalBytes: bigint;
|
|
37
|
+
readonly percent: number;
|
|
38
|
+
}
|
|
39
|
+
interface ExtractedArchiveFile {
|
|
40
|
+
readonly entry: ArchiveEntry;
|
|
41
|
+
readonly outputPath: string;
|
|
42
|
+
}
|
|
22
43
|
interface ReadStreamOptions {
|
|
23
44
|
readonly highWaterMark?: number;
|
|
24
45
|
readonly maxBytes?: number | bigint | null;
|
|
@@ -27,10 +48,14 @@ interface ReadStreamOptions {
|
|
|
27
48
|
interface ExtractToFolderOptions {
|
|
28
49
|
readonly ids?: readonly EntryId[];
|
|
29
50
|
readonly paths?: readonly string[];
|
|
51
|
+
readonly concurrency?: number;
|
|
52
|
+
readonly overwrite?: boolean;
|
|
53
|
+
readonly onProgress?: (progress: ExtractToFolderProgress) => void;
|
|
54
|
+
readonly onExtract?: (result: ExtractedArchiveFile) => void;
|
|
30
55
|
readonly maxBytes?: number | bigint | null;
|
|
31
56
|
readonly signal?: AbortSignal;
|
|
32
57
|
}
|
|
33
|
-
type ArchiveErrorCode = 'ERR_ARCHIVE_CLOSED' | 'ERR_INVALID_ENTRY' | 'ERR_INVALID_RANGE' | 'ERR_RESOURCE_LIMIT' | 'ERR_ARCHIVE_OPEN' | 'ERR_ARCHIVE_PASSWORD' | 'ERR_ARCHIVE_PATH' | 'ERR_ARCHIVE_EXTRACT';
|
|
58
|
+
type ArchiveErrorCode = 'ERR_ARCHIVE_CLOSED' | 'ERR_INVALID_ENTRY' | 'ERR_INVALID_RANGE' | 'ERR_RESOURCE_LIMIT' | 'ERR_ARCHIVE_OPEN' | 'ERR_ARCHIVE_PASSWORD' | 'ERR_ARCHIVE_PATH' | 'ERR_ARCHIVE_EXTRACT' | 'ERR_ARCHIVE_CREATE';
|
|
34
59
|
declare class ArchiveError extends Error {
|
|
35
60
|
readonly code: ArchiveErrorCode;
|
|
36
61
|
constructor(code: ArchiveErrorCode, message: string, options?: ErrorOptions);
|
|
@@ -43,6 +68,7 @@ declare class Archive {
|
|
|
43
68
|
private constructor();
|
|
44
69
|
static concurrency(value?: number): number;
|
|
45
70
|
static open(path: string, options?: OpenOptions): Promise<Archive>;
|
|
71
|
+
static create(outputPath: string, entries: readonly ArchiveCreateEntry[], options?: CreateArchiveOptions): Promise<void>;
|
|
46
72
|
entries(): Promise<readonly ArchiveEntry[]>;
|
|
47
73
|
getEntry(idOrPath: EntrySelector): Promise<ArchiveEntry | undefined>;
|
|
48
74
|
extract(idOrPath: EntrySelector): Promise<Buffer>;
|
|
@@ -60,4 +86,4 @@ declare class Archive {
|
|
|
60
86
|
}
|
|
61
87
|
|
|
62
88
|
export { Archive, ArchiveError };
|
|
63
|
-
export type { ArchiveEntry, ArchiveErrorCode, EntryId, EntrySelector, ExtractToFolderOptions, OpenOptions, ReadOptions, ReadStreamOptions };
|
|
89
|
+
export type { ArchiveCreateEntry, ArchiveEntry, ArchiveErrorCode, ArchiveFormat, CreateArchiveOptions, EntryId, EntrySelector, ExtractToFolderOptions, ExtractToFolderProgress, ExtractedArchiveFile, OpenOptions, ReadOptions, ReadStreamOptions };
|
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { existsSync, createWriteStream } from 'node:fs';
|
|
2
|
-
import { lstat, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { rename, link, rm, lstat, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import { createRequire } from 'node:module';
|
|
4
5
|
import { availableParallelism } from 'node:os';
|
|
5
|
-
import { dirname, resolve, join, relative, sep } from 'node:path';
|
|
6
|
-
import {
|
|
6
|
+
import { dirname, resolve, join, extname, relative, sep } from 'node:path';
|
|
7
|
+
import { Readable, Transform } from 'node:stream';
|
|
7
8
|
import { pipeline } from 'node:stream/promises';
|
|
8
9
|
import { fileURLToPath } from 'node:url';
|
|
9
10
|
|
|
@@ -72,10 +73,32 @@ function safeEntrySegments(entryPath) {
|
|
|
72
73
|
}
|
|
73
74
|
return segments;
|
|
74
75
|
}
|
|
76
|
+
function sanitizeOutputPath(entryPath) {
|
|
77
|
+
if (process.platform !== 'win32') {
|
|
78
|
+
return entryPath;
|
|
79
|
+
}
|
|
80
|
+
return entryPath
|
|
81
|
+
.replace(/([. ]+)([\\/]|$)/g, (_match, unsupported, separator) => '_'.repeat(unsupported.length) + separator)
|
|
82
|
+
.replace(/[<>:|?*"]/g, '_');
|
|
83
|
+
}
|
|
84
|
+
function assertSafeRelativeArchivePath(entryPath) {
|
|
85
|
+
const normalized = entryPath.replace(/\\/g, '/');
|
|
86
|
+
const segments = normalized.split('/').filter(segment => segment !== '' && segment !== '.');
|
|
87
|
+
if (entryPath.includes('\0')
|
|
88
|
+
|| normalized.startsWith('/')
|
|
89
|
+
|| /^[a-zA-Z]:/.test(normalized)
|
|
90
|
+
|| segments.length === 0
|
|
91
|
+
|| segments.some(segment => segment === '..')) {
|
|
92
|
+
throw new ArchiveError('ERR_ARCHIVE_PATH', `Unsafe archive entry path: ${entryPath}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
75
95
|
function makeOutputPlans(root, entries) {
|
|
76
96
|
const caseInsensitivePaths = process.platform === 'win32' || process.platform === 'darwin';
|
|
77
97
|
const plans = entries.map((entry) => {
|
|
78
|
-
|
|
98
|
+
if (process.platform === 'win32') {
|
|
99
|
+
assertSafeRelativeArchivePath(entry.path);
|
|
100
|
+
}
|
|
101
|
+
const segments = safeEntrySegments(sanitizeOutputPath(entry.path));
|
|
79
102
|
const targetPath = resolve(root, ...segments);
|
|
80
103
|
const relative$1 = relative(root, targetPath);
|
|
81
104
|
if (relative$1 === ''
|
|
@@ -121,6 +144,50 @@ async function ensureDirectory(directoryPath) {
|
|
|
121
144
|
throw new ArchiveError('ERR_ARCHIVE_PATH', `Destination path is not a real directory: ${directoryPath}`);
|
|
122
145
|
}
|
|
123
146
|
}
|
|
147
|
+
async function assertReplaceableOutput(filePath) {
|
|
148
|
+
try {
|
|
149
|
+
const details = await lstat(filePath);
|
|
150
|
+
if (!details.isFile()) {
|
|
151
|
+
throw new ArchiveError('ERR_ARCHIVE_PATH', `Cannot replace a non-file extraction target: ${filePath}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (error instanceof ArchiveError || error.code !== 'ENOENT') {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function assertOutputDoesNotExist(filePath) {
|
|
161
|
+
try {
|
|
162
|
+
await lstat(filePath);
|
|
163
|
+
throw new ArchiveError('ERR_ARCHIVE_CREATE', `Output archive already exists: ${filePath}`);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (error instanceof ArchiveError || error.code !== 'ENOENT') {
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function normalizeCreateEntry(entry) {
|
|
172
|
+
if (entry === null || typeof entry !== 'object'
|
|
173
|
+
|| typeof entry.sourcePath !== 'string' || entry.sourcePath.length === 0
|
|
174
|
+
|| typeof entry.path !== 'string' || entry.path.length === 0) {
|
|
175
|
+
throw new TypeError('Each archive creation entry must have non-empty sourcePath and path strings');
|
|
176
|
+
}
|
|
177
|
+
const internalPath = safeEntrySegments(entry.path).join('/');
|
|
178
|
+
const sourcePath = resolve(entry.sourcePath);
|
|
179
|
+
let details;
|
|
180
|
+
try {
|
|
181
|
+
details = await lstat(sourcePath);
|
|
182
|
+
}
|
|
183
|
+
catch (cause) {
|
|
184
|
+
throw new ArchiveError('ERR_ARCHIVE_CREATE', `Unable to access archive creation source: ${sourcePath}`, { cause });
|
|
185
|
+
}
|
|
186
|
+
if (details.isSymbolicLink() || !details.isFile()) {
|
|
187
|
+
throw new ArchiveError('ERR_ARCHIVE_CREATE', `Archive creation source is not a regular file: ${sourcePath}`);
|
|
188
|
+
}
|
|
189
|
+
return { sourcePath, path: internalPath };
|
|
190
|
+
}
|
|
124
191
|
async function ensureExtractionRoot(root) {
|
|
125
192
|
try {
|
|
126
193
|
const details = await lstat(root);
|
|
@@ -282,6 +349,99 @@ class Archive {
|
|
|
282
349
|
throw new ArchiveError(/password/i.test(message) ? 'ERR_ARCHIVE_PASSWORD' : 'ERR_ARCHIVE_OPEN', message, { cause });
|
|
283
350
|
}
|
|
284
351
|
}
|
|
352
|
+
static async create(outputPath, entries, options = {}) {
|
|
353
|
+
if (typeof outputPath !== 'string' || outputPath.length === 0) {
|
|
354
|
+
throw new TypeError('Output archive path must be a non-empty string');
|
|
355
|
+
}
|
|
356
|
+
if (!Array.isArray(entries)) {
|
|
357
|
+
throw new TypeError('Archive creation entries must be an array');
|
|
358
|
+
}
|
|
359
|
+
if (entries.length > 1_000_000) {
|
|
360
|
+
throw new RangeError('Archive creation exceeds the 1,000,000-entry limit');
|
|
361
|
+
}
|
|
362
|
+
if (options === null || typeof options !== 'object') {
|
|
363
|
+
throw new TypeError('Archive creation options must be an object');
|
|
364
|
+
}
|
|
365
|
+
if (options.overwrite !== undefined && typeof options.overwrite !== 'boolean') {
|
|
366
|
+
throw new TypeError('Archive creation overwrite option must be a boolean');
|
|
367
|
+
}
|
|
368
|
+
const compressionLevel = options.compressionLevel ?? 5;
|
|
369
|
+
if (!Number.isSafeInteger(compressionLevel) || compressionLevel < 0 || compressionLevel > 9) {
|
|
370
|
+
throw new RangeError('Compression level must be an integer between 0 and 9');
|
|
371
|
+
}
|
|
372
|
+
const targetPath = resolve(outputPath);
|
|
373
|
+
let format;
|
|
374
|
+
if (options.format !== undefined) {
|
|
375
|
+
if (options.format !== '7z' && options.format !== 'zip'
|
|
376
|
+
&& options.format !== 'tar' && options.format !== 'wim') {
|
|
377
|
+
throw new TypeError('Archive format must be "7z", "zip", "tar", or "wim"');
|
|
378
|
+
}
|
|
379
|
+
format = options.format;
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
const extension = extname(targetPath).toLowerCase();
|
|
383
|
+
if (extension === '.7z') {
|
|
384
|
+
format = '7z';
|
|
385
|
+
}
|
|
386
|
+
else if (extension === '.zip' || extension === '.cbz') {
|
|
387
|
+
format = 'zip';
|
|
388
|
+
}
|
|
389
|
+
else if (extension === '.tar') {
|
|
390
|
+
format = 'tar';
|
|
391
|
+
}
|
|
392
|
+
else if (extension === '.wim') {
|
|
393
|
+
format = 'wim';
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
throw new TypeError('Specify format as "7z", "zip", "tar", or "wim" for an unrecognized output extension');
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (options.compressionLevel !== undefined && format !== '7z' && format !== 'zip') {
|
|
400
|
+
throw new TypeError('compressionLevel is only supported for ZIP and 7z archives');
|
|
401
|
+
}
|
|
402
|
+
const normalizedEntries = await Promise.all(entries.map(normalizeCreateEntry));
|
|
403
|
+
const caseInsensitivePaths = process.platform === 'win32' || process.platform === 'darwin';
|
|
404
|
+
const pathKeys = normalizedEntries.map(entry => caseInsensitivePaths ? entry.path.toLowerCase() : entry.path);
|
|
405
|
+
const archivePaths = new Set(pathKeys);
|
|
406
|
+
if (archivePaths.size !== normalizedEntries.length) {
|
|
407
|
+
throw new ArchiveError('ERR_ARCHIVE_PATH', 'Archive creation paths must be unique');
|
|
408
|
+
}
|
|
409
|
+
for (const entry of normalizedEntries) {
|
|
410
|
+
const segments = entry.path.split('/');
|
|
411
|
+
for (let index = 1; index < segments.length; index++) {
|
|
412
|
+
const parent = segments.slice(0, index).join('/');
|
|
413
|
+
const parentKey = caseInsensitivePaths ? parent.toLowerCase() : parent;
|
|
414
|
+
if (archivePaths.has(parentKey)) {
|
|
415
|
+
throw new ArchiveError('ERR_ARCHIVE_PATH', `Archive creation file path is a parent of another entry: ${parent}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (options.overwrite) {
|
|
420
|
+
await assertReplaceableOutput(targetPath);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
await assertOutputDoesNotExist(targetPath);
|
|
424
|
+
}
|
|
425
|
+
const temporaryPath = join(dirname(targetPath), `.7zip-native-${randomUUID()}.tmp`);
|
|
426
|
+
try {
|
|
427
|
+
await native.create(temporaryPath, format, normalizedEntries, compressionLevel, enginePath);
|
|
428
|
+
if (options.overwrite) {
|
|
429
|
+
await assertReplaceableOutput(targetPath);
|
|
430
|
+
await rename(temporaryPath, targetPath);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
await link(temporaryPath, targetPath);
|
|
434
|
+
await rm(temporaryPath);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
catch (cause) {
|
|
438
|
+
await rm(temporaryPath, { force: true });
|
|
439
|
+
if (cause instanceof ArchiveError) {
|
|
440
|
+
throw cause;
|
|
441
|
+
}
|
|
442
|
+
throw new ArchiveError('ERR_ARCHIVE_CREATE', errorMessage(cause), { cause });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
285
445
|
async entries() {
|
|
286
446
|
this.assertOpen();
|
|
287
447
|
this.entriesPromise ??= native.entries(this.handle).catch((cause) => {
|
|
@@ -362,6 +522,22 @@ class Archive {
|
|
|
362
522
|
if (!options || typeof options !== 'object') {
|
|
363
523
|
throw new TypeError('Folder extraction options must be an object');
|
|
364
524
|
}
|
|
525
|
+
if (options.overwrite !== undefined && typeof options.overwrite !== 'boolean') {
|
|
526
|
+
throw new TypeError('Folder extraction overwrite option must be a boolean');
|
|
527
|
+
}
|
|
528
|
+
if (options.concurrency !== undefined
|
|
529
|
+
&& (!Number.isSafeInteger(options.concurrency)
|
|
530
|
+
|| options.concurrency < 1
|
|
531
|
+
|| options.concurrency > 128)) {
|
|
532
|
+
throw new RangeError('Folder extraction concurrency must be an integer between 1 and 128');
|
|
533
|
+
}
|
|
534
|
+
if (options.onProgress !== undefined && typeof options.onProgress !== 'function') {
|
|
535
|
+
throw new TypeError('Folder extraction onProgress option must be a function');
|
|
536
|
+
}
|
|
537
|
+
if (options.onExtract !== undefined && typeof options.onExtract !== 'function') {
|
|
538
|
+
throw new TypeError('Folder extraction onExtract option must be a function');
|
|
539
|
+
}
|
|
540
|
+
const concurrency = Math.min(options.concurrency ?? 1, Archive.concurrency());
|
|
365
541
|
options.signal?.throwIfAborted();
|
|
366
542
|
const maxBytes = normalizeMaxBytes(options.maxBytes, 'maxBytes');
|
|
367
543
|
const entries = await this.entries();
|
|
@@ -407,14 +583,53 @@ class Archive {
|
|
|
407
583
|
options.signal?.throwIfAborted();
|
|
408
584
|
await ensureExtractionRoot(root);
|
|
409
585
|
let extractedBytes = 0n;
|
|
586
|
+
let filesExtracted = 0;
|
|
587
|
+
const totalFiles = plans.filter(plan => !plan.isDirectory).length;
|
|
588
|
+
const reportProgress = () => {
|
|
589
|
+
let percent = 0;
|
|
590
|
+
if (expectedBytes > 0n) {
|
|
591
|
+
const scaledPercent = extractedBytes * 10000n / expectedBytes;
|
|
592
|
+
percent = Number(scaledPercent > 9900n ? 9900n : scaledPercent) / 100;
|
|
593
|
+
}
|
|
594
|
+
if (filesExtracted === totalFiles) {
|
|
595
|
+
percent = 100;
|
|
596
|
+
}
|
|
597
|
+
options.onProgress?.({
|
|
598
|
+
filesExtracted,
|
|
599
|
+
totalFiles,
|
|
600
|
+
bytesProcessed: extractedBytes,
|
|
601
|
+
totalBytes: expectedBytes,
|
|
602
|
+
percent,
|
|
603
|
+
});
|
|
604
|
+
};
|
|
605
|
+
reportProgress();
|
|
410
606
|
for (const plan of plans) {
|
|
411
607
|
options.signal?.throwIfAborted();
|
|
412
608
|
if (plan.isDirectory) {
|
|
413
609
|
await ensureOutputDirectoryTree(root, plan.targetPath);
|
|
414
|
-
continue;
|
|
415
610
|
}
|
|
611
|
+
}
|
|
612
|
+
const filePlans = plans.filter(plan => !plan.isDirectory);
|
|
613
|
+
const controller = new AbortController();
|
|
614
|
+
const extractionSignal = controller.signal;
|
|
615
|
+
let hasFailure = false;
|
|
616
|
+
let failure;
|
|
617
|
+
let nextFileIndex = 0;
|
|
618
|
+
const stop = (error) => {
|
|
619
|
+
if (!hasFailure) {
|
|
620
|
+
hasFailure = true;
|
|
621
|
+
failure = error;
|
|
622
|
+
}
|
|
623
|
+
if (!extractionSignal.aborted) {
|
|
624
|
+
controller.abort(error);
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
const abortFromCaller = () => {
|
|
628
|
+
stop(options.signal?.reason);
|
|
629
|
+
};
|
|
630
|
+
options.signal?.addEventListener('abort', abortFromCaller, { once: true });
|
|
631
|
+
const extractFile = async (plan) => {
|
|
416
632
|
await ensureOutputParents(root, plan.targetPath);
|
|
417
|
-
const output = createWriteStream(plan.targetPath, { flags: 'wx', mode: 0o600 });
|
|
418
633
|
const limit = new Transform({
|
|
419
634
|
transform(chunk, _encoding, callback) {
|
|
420
635
|
const size = BigInt(chunk.length);
|
|
@@ -423,13 +638,73 @@ class Archive {
|
|
|
423
638
|
return;
|
|
424
639
|
}
|
|
425
640
|
extractedBytes += size;
|
|
641
|
+
try {
|
|
642
|
+
reportProgress();
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
callback(error instanceof Error ? error : new Error(String(error)));
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
426
648
|
callback(null, chunk);
|
|
427
649
|
},
|
|
428
650
|
});
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
651
|
+
if (options.overwrite) {
|
|
652
|
+
await assertReplaceableOutput(plan.targetPath);
|
|
653
|
+
const temporaryPath = join(dirname(plan.targetPath), `.7zip-native-${randomUUID()}.tmp`);
|
|
654
|
+
try {
|
|
655
|
+
await pipeline(this.createReadStream(plan.entry.id, {
|
|
656
|
+
signal: extractionSignal,
|
|
657
|
+
maxBytes: null,
|
|
658
|
+
}), limit, createWriteStream(temporaryPath, { flags: 'wx', mode: 0o600 }), { signal: extractionSignal });
|
|
659
|
+
extractionSignal.throwIfAborted();
|
|
660
|
+
await assertReplaceableOutput(plan.targetPath);
|
|
661
|
+
extractionSignal.throwIfAborted();
|
|
662
|
+
await rename(temporaryPath, plan.targetPath);
|
|
663
|
+
}
|
|
664
|
+
catch (error) {
|
|
665
|
+
await rm(temporaryPath, { force: true });
|
|
666
|
+
throw error;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
await pipeline(this.createReadStream(plan.entry.id, {
|
|
671
|
+
signal: extractionSignal,
|
|
672
|
+
maxBytes: null,
|
|
673
|
+
}), limit, createWriteStream(plan.targetPath, { flags: 'wx', mode: 0o600 }), { signal: extractionSignal });
|
|
674
|
+
}
|
|
675
|
+
filesExtracted++;
|
|
676
|
+
reportProgress();
|
|
677
|
+
options.onExtract?.({ entry: plan.entry, outputPath: plan.targetPath });
|
|
678
|
+
};
|
|
679
|
+
const runWorker = async () => {
|
|
680
|
+
while (!hasFailure) {
|
|
681
|
+
if (options.signal?.aborted) {
|
|
682
|
+
stop(options.signal.reason);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const plan = filePlans[nextFileIndex++];
|
|
686
|
+
if (!plan) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
await extractFile(plan);
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
stop(error);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
try {
|
|
699
|
+
options.signal?.throwIfAborted();
|
|
700
|
+
const workers = Array.from({ length: Math.min(concurrency, filePlans.length) }, () => runWorker());
|
|
701
|
+
await Promise.all(workers);
|
|
702
|
+
if (hasFailure) {
|
|
703
|
+
throw failure;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
finally {
|
|
707
|
+
options.signal?.removeEventListener('abort', abortFromCaller);
|
|
433
708
|
}
|
|
434
709
|
}
|
|
435
710
|
async read(idOrPath, options) {
|