7zip-native 0.0.1 → 0.0.5
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 +249 -59
- package/dist/index.cjs +130 -35
- package/dist/index.d.cts +11 -7
- package/dist/index.d.mts +11 -7
- package/dist/index.d.ts +11 -7
- package/dist/index.mjs +130 -35
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/7zip_native.node +0 -0
- package/prebuilds/darwin-arm64/lib7z.dylib +0 -0
- package/prebuilds/darwin-x64/7zip_native.node +0 -0
- package/prebuilds/darwin-x64/lib7z.dylib +0 -0
- package/prebuilds/linux-arm/7z.so +0 -0
- package/prebuilds/linux-arm/7zip_native.node +0 -0
- package/prebuilds/linux-arm64/7z.so +0 -0
- package/prebuilds/linux-arm64/7zip_native.node +0 -0
- package/prebuilds/linux-ia32/7z.so +0 -0
- package/prebuilds/linux-ia32/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/README.md
CHANGED
|
@@ -3,102 +3,292 @@
|
|
|
3
3
|
|
|
4
4
|
# 7zip-native
|
|
5
5
|
|
|
6
|
-
Native Node-API bindings for the 7-Zip archive engine.
|
|
6
|
+
Native Node-API bindings for the 7-Zip archive engine. Use a typed, Promise-based Node.js API to inspect archives, read entry data, stream large files, or extract entries without launching `7z`, `7zz`, or another archive executable.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
The package includes the native addon and the matching 7-Zip engine for each supported target. Compression and decompression are provided by the upstream 7-Zip implementation.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Installation
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
|
|
14
|
-
cd 7zip-native
|
|
15
|
-
npm ci
|
|
16
|
-
npm run build
|
|
17
|
-
npm test
|
|
13
|
+
npm install 7zip-native
|
|
18
14
|
```
|
|
19
15
|
|
|
20
|
-
|
|
16
|
+
The package requires Node.js 18 or newer. The release workflow builds and tests with Node.js 22.
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import {Archive} from '7zip-native';
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
const archive = await Archive.open('./comics.cbz');
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
try
|
|
26
|
+
{
|
|
27
|
+
const entries = await archive.entries();
|
|
28
|
+
const cover = await archive.getEntry('chapter/cover.jpg');
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
if(cover && !cover.isDirectory)
|
|
31
|
+
{
|
|
32
|
+
const preview = await archive.read(cover.path, {offset: 0, length: 64 * 1024});
|
|
33
|
+
const image = await archive.extract(cover.path);
|
|
34
|
+
console.log(entries.length, preview.length, image.length);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
finally
|
|
38
|
+
{
|
|
39
|
+
await archive.close();
|
|
40
|
+
}
|
|
30
41
|
```
|
|
31
42
|
|
|
32
|
-
|
|
43
|
+
The package supports both ES modules and CommonJS:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
// ES module
|
|
47
|
+
import {Archive} from '7zip-native';
|
|
48
|
+
|
|
49
|
+
// CommonJS
|
|
50
|
+
const {Archive} = require('7zip-native');
|
|
51
|
+
```
|
|
33
52
|
|
|
34
53
|
## API
|
|
35
54
|
|
|
55
|
+
### `Archive.open(path, options?)`
|
|
56
|
+
|
|
57
|
+
Opens an archive from a filesystem path and indexes its metadata asynchronously.
|
|
58
|
+
|
|
36
59
|
```ts
|
|
37
|
-
|
|
60
|
+
const archive = await Archive.open('./archive.7z', {password: 'secret'});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`options.password` is passed to the upstream handler when it requests a password. It is held for the lifetime of the archive handle, is not logged, and is limited to 1 MiB of UTF-8 data.
|
|
38
64
|
|
|
39
|
-
|
|
65
|
+
### Listing and looking up entries
|
|
66
|
+
|
|
67
|
+
```ts
|
|
40
68
|
const entries = await archive.entries();
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
69
|
+
const byPath = await archive.getEntry('folder/page.jpg');
|
|
70
|
+
const byId = await archive.getEntry(0);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`entries()` returns cached metadata. Entry IDs are zero-based and stable for the lifetime of the opened archive. `getEntry()` returns `undefined` if the ID or full path does not match.
|
|
74
|
+
|
|
75
|
+
All entry-targeted methods accept either the entry's numeric `id` or its exact archive-internal `path`: `extract()`, `read()`, `createReadStream()`, and `extractToStream()`. Batch `extract()` calls can mix IDs and paths. For folder extraction, use either the `ids` or `paths` option; omit both to extract every entry.
|
|
76
|
+
|
|
77
|
+
Each entry includes:
|
|
78
|
+
|
|
79
|
+
| Property | Type | Description |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| `id` | `number` | Stable ID within this opened archive |
|
|
82
|
+
| `path` | `string` | Entry path, using `/` separators |
|
|
83
|
+
| `size` | `bigint` | Uncompressed size |
|
|
84
|
+
| `compressedSize` | `bigint` | Compressed size |
|
|
85
|
+
| `isDirectory` | `boolean` | Whether the entry is a directory |
|
|
86
|
+
| `encrypted` | `boolean` | Whether the entry is encrypted |
|
|
87
|
+
| `attributes` | `number` | Format-specific attributes |
|
|
88
|
+
| `crc` | `number \| undefined` | CRC when supplied by the format |
|
|
89
|
+
|
|
90
|
+
### Extracting to memory
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const image = await archive.extract(entry.id); // Buffer
|
|
94
|
+
const sameImage = await archive.extract(entry.path); // Buffer
|
|
95
|
+
const pages = await archive.extract([page1.id, page2.path]); // Buffer[]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Paths use `/` separators. Batch results are returned in the same order as the requested selectors; duplicate entries are rejected. A missing path rejects with `ERR_INVALID_ENTRY`. A buffered extraction is limited to 256 MiB per entry, with a 512 MiB cap on native results pending transfer to JavaScript. For large entries, use a stream or extract to a folder instead.
|
|
99
|
+
|
|
100
|
+
### Reading a byte range
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const header = await archive.read(entry.path, {
|
|
104
|
+
offset: 0,
|
|
105
|
+
length: 64 * 1024,
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Offsets and lengths accept safe integers or `bigint`. A range read returns only the requested bytes and is limited to 64 MiB. It uses the handler's normal extraction path, however, so a sequential or solid format may still need to decompress preceding data or the whole entry. It is not guaranteed to be constant-time random access.
|
|
110
|
+
|
|
111
|
+
### Streams and output destinations
|
|
112
|
+
|
|
113
|
+
Create a standard Node.js `Readable` for a large entry:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
for await (const chunk of archive.createReadStream(entry.path, {
|
|
117
|
+
highWaterMark: 256 * 1024,
|
|
118
|
+
signal: abortController.signal,
|
|
119
|
+
}))
|
|
120
|
+
{
|
|
121
|
+
// Consume each Buffer chunk before requesting more data.
|
|
45
122
|
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The native engine emits bounded 64 KiB chunks and waits for stream demand, preserving backpressure. Destroying the stream or aborting its signal cancels extraction at the next output boundary. Streams default to a 4 GiB output limit; set `maxBytes` to another non-negative `number` or `bigint`, or to `null` to disable the limit.
|
|
126
|
+
|
|
127
|
+
To pipe directly to a Node.js `Writable`:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import {createWriteStream} from 'node:fs';
|
|
131
|
+
|
|
132
|
+
await archive.extractToStream(entry.path, createWriteStream('./page.jpg'), {
|
|
133
|
+
signal: abortController.signal,
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This respects backpressure and ends the destination when extraction succeeds. A destination error, early close, or abort rejects the Promise.
|
|
138
|
+
|
|
139
|
+
### Extracting to a folder
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
await archive.extractToFolder('./unpacked', {
|
|
143
|
+
paths: ['chapter/page1.jpg', 'chapter/page2.jpg'], // alternatively, use ids
|
|
144
|
+
maxBytes: 2n * 1024n * 1024n * 1024n,
|
|
145
|
+
signal: abortController.signal,
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Folder extraction accepts either `ids` or exact `paths`; omit both to extract every entry, and do not provide both together. Paths use archive-internal `/` separators. Extraction is sequential and uses bounded memory. Its default total output limit is 4 GiB; `maxBytes: null` disables that limit. Existing files are never overwritten. Unsafe paths, path collisions, and symlinked destination components are rejected. Archive links are written as regular file data; the API does not create symbolic or hard links. If extraction fails partway through, files already written (or partially written) may remain in the destination.
|
|
150
|
+
|
|
151
|
+
### Concurrency
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const current = Archive.concurrency();
|
|
155
|
+
Archive.concurrency(8);
|
|
156
|
+
Archive.concurrency(0); // reset to the default
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The setting controls the process-wide native worker pool used by extraction, streams, and range reads. The default is the lesser of 4 and the parallelism available to Node.js; the allowed range is 1–128 workers, with `0` restoring the default.
|
|
160
|
+
|
|
161
|
+
Concurrent operations on one archive use independent decoder contexts rather than calling one 7-Zip archive object simultaneously. This is safe for handlers that support independent contexts, but it does not guarantee a speedup. Solid archives may decompress shared data more than once; batching related entries may be faster.
|
|
162
|
+
|
|
163
|
+
### Closing an archive
|
|
164
|
+
|
|
165
|
+
Call `close()` when finished. It rejects future API calls; operations already submitted keep the native state alive until they finish.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
46
168
|
await archive.close();
|
|
47
169
|
```
|
|
48
170
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
171
|
+
## Errors and limits
|
|
172
|
+
|
|
173
|
+
Invalid argument types and values throw standard `TypeError` or `RangeError`. Archive and extraction failures use `ArchiveError`, which has a stable `code` property:
|
|
174
|
+
|
|
175
|
+
| Code | Meaning |
|
|
176
|
+
| --- | --- |
|
|
177
|
+
| `ERR_ARCHIVE_CLOSED` | Operation attempted after `close()` |
|
|
178
|
+
| `ERR_INVALID_ENTRY` | Entry ID is invalid or out of range |
|
|
179
|
+
| `ERR_INVALID_RANGE` | Byte range is invalid or exceeds the native offset limit |
|
|
180
|
+
| `ERR_RESOURCE_LIMIT` | Configured or default output limit was exceeded |
|
|
181
|
+
| `ERR_ARCHIVE_OPEN` | Archive could not be opened or indexed |
|
|
182
|
+
| `ERR_ARCHIVE_PASSWORD` | Password is missing or incorrect when requested by the handler |
|
|
183
|
+
| `ERR_ARCHIVE_PATH` | Unsafe or colliding path during folder extraction |
|
|
184
|
+
| `ERR_ARCHIVE_EXTRACT` | Entry extraction failed |
|
|
185
|
+
|
|
186
|
+
Buffered `extract()` and `read()` do not currently support cancellation. Streams and folder extraction accept an `AbortSignal`.
|
|
187
|
+
|
|
188
|
+
## Supported formats
|
|
189
|
+
|
|
190
|
+
The package uses the handlers registered in the pinned upstream `Format7zF` bundle, including 7z, ZIP, RAR, TAR, GZIP, BZIP2, XZ, WIM, ISO/UDF, CAB, ARJ, LZH, CPIO, Zstandard, and additional formats enabled by that upstream revision. Whether a particular archive can be extracted depends on its handler and compression method; unsupported methods, corrupt data, and integrity failures reject the operation. Standalone compression codecs are not necessarily archive formats.
|
|
191
|
+
|
|
192
|
+
### Format-specific notes
|
|
62
193
|
|
|
63
|
-
|
|
194
|
+
- **RAR:** extraction uses upstream code subject to its LGPL terms and the unRAR restriction. This package does not create RAR archives or implement the proprietary RAR compression algorithm. Review the upstream notices before redistribution.
|
|
195
|
+
- **ACE:** not supported by this 7-Zip revision.
|
|
196
|
+
- **Encryption:** password callbacks are connected to archive opening and extraction. AES-256 encrypted ZIP extraction and range reads are tested on Linux x64; encrypted-header behavior and password variants in other formats are not yet verified.
|
|
197
|
+
- **Partial reads:** memory use is limited to the requested range, but the decoder may still process more data internally, especially for solid or sequential formats.
|
|
64
198
|
|
|
65
|
-
|
|
199
|
+
See [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) for third-party redistribution terms.
|
|
66
200
|
|
|
67
|
-
|
|
201
|
+
## Platform support
|
|
68
202
|
|
|
69
|
-
|
|
70
|
-
- ACE is not supplied by this 7-Zip revision and is not supported.
|
|
71
|
-
- The upstream 7z handler is covered by a native open-and-extract integration test on Linux x64. Support for individual compression methods and encrypted variants still depends on the capabilities of the upstream handler.
|
|
72
|
-
- Password callbacks are wired for upstream archive opening and extraction. AES-256 encrypted ZIP entry extraction and ranged reads are tested on Linux x64. Wrong or missing passwords requested by a handler are reported with `ArchiveError.code === 'ERR_ARCHIVE_PASSWORD'`. Encrypted-header behavior and password variants in other formats are not yet verified.
|
|
73
|
-
- Partial reads bound memory to the selected range but use the normal `IInArchive::Extract` path. Sequential or solid decoders may decompress preceding data or the whole entry; this is not constant-time random access and does not promise early decoder termination.
|
|
74
|
-
- Opening and metadata queries use Node's libuv workers. Extraction and ranged reads use a persistent native worker pool, defaulting to the lesser of 4 and the processors available to Node. `Archive.concurrency(n)` sets the process-wide pool size; submitted native operations remain bounded.
|
|
75
|
-
- The addon never calls one `IInArchive` instance concurrently. It retains independent archive and input-stream contexts per handle and lazily creates more as concurrent work needs them. Context creation checks the archive's size and modification time; reopen the archive after changing it.
|
|
76
|
-
- Concurrent extraction from a solid archive is safe with independent contexts but may repeat decompression of shared solid blocks. There is no decompressed-block cache, so batching related entries can be faster.
|
|
77
|
-
- Cancellation of buffered `extract()`/`read()` requests and decompressed-block caching are not implemented. Streaming and folder extraction have bounded defaults that can be adjusted per operation; buffered methods retain their fixed in-memory limits.
|
|
78
|
-
- Cross-platform builds and runtime behavior are not claimed until the corresponding matrix jobs succeed. No performance claim is made without measurements on the target platform.
|
|
203
|
+
The release workflow builds eight targets:
|
|
79
204
|
|
|
80
|
-
|
|
205
|
+
| Platform | Architecture | CI verification |
|
|
206
|
+
| --- | --- | --- |
|
|
207
|
+
| Linux | x64, ARM64 | Build and runtime tests |
|
|
208
|
+
| Linux | x86 (ia32), ARM32 | Cross-compile and verify binary architecture; not runtime-tested in CI |
|
|
209
|
+
| macOS | x64, ARM64 | Build and runtime tests |
|
|
210
|
+
| Windows | x64, ARM64 | Build and runtime tests |
|
|
81
211
|
|
|
82
|
-
|
|
212
|
+
The published binaries must match the target operating system and architecture. Windows x86 (ia32) prebuilds are not published. The package declares Node.js `>=18`; the workflow currently builds and tests with Node.js 26. This checkout has been verified locally on Linux x64. Do not treat cross-compiled Linux 32-bit artifacts as runtime-tested.
|
|
83
213
|
|
|
84
|
-
|
|
214
|
+
## Electron
|
|
215
|
+
|
|
216
|
+
The addon uses Node-API, but the addon and 7-Zip engine still need to match the Electron application's operating system and architecture. Use a matching package prebuild when available, or rebuild for the Electron version and target architecture using your Electron build tooling.
|
|
217
|
+
|
|
218
|
+
When packaging with ASAR, ensure both `7zip_native.node` and the matching engine library (`7z.dll`, `7z.so`, or `lib7z.dylib`) are physically unpacked and that the runtime paths resolve to those unpacked files. For `electron-builder`, an `asarUnpack` configuration can include:
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{
|
|
222
|
+
"build": {
|
|
223
|
+
"asarUnpack": [
|
|
224
|
+
"node_modules/7zip-native/prebuilds/**/*.node",
|
|
225
|
+
"node_modules/7zip-native/prebuilds/**/*.dll",
|
|
226
|
+
"node_modules/7zip-native/prebuilds/**/*.so",
|
|
227
|
+
"node_modules/7zip-native/prebuilds/**/*.dylib"
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Test the packaged application on each target; ASAR unpacking does not replace checking that the addon and engine can both be loaded from the packaged runtime.
|
|
234
|
+
|
|
235
|
+
## Build from source
|
|
85
236
|
|
|
86
237
|
```sh
|
|
87
|
-
|
|
238
|
+
git clone --recurse-submodules https://github.com/ollm/7zip-native.git
|
|
239
|
+
cd 7zip-native
|
|
240
|
+
npm ci --ignore-scripts
|
|
241
|
+
npm run build
|
|
242
|
+
npm test
|
|
88
243
|
```
|
|
89
244
|
|
|
90
|
-
|
|
245
|
+
Source builds require a C++17 toolchain, Python 3, Node.js headers (downloaded by `node-gyp`), and the upstream submodule. Linux and macOS use GNU make; Windows requires Visual C++ build tools and `nmake`. `npm run build:native` compiles the upstream `Format7zF` bundle and Node-API addon, then stages them under `prebuilds/<platform>-<arch>/`.
|
|
91
246
|
|
|
92
|
-
|
|
247
|
+
To update the upstream submodule deliberately:
|
|
93
248
|
|
|
94
249
|
```sh
|
|
95
|
-
|
|
250
|
+
git submodule update --remote vendor/7zip
|
|
251
|
+
git -C vendor/7zip rev-parse HEAD
|
|
252
|
+
git add .gitmodules vendor/7zip
|
|
96
253
|
```
|
|
97
254
|
|
|
98
|
-
|
|
255
|
+
Review the upstream changes and commit the new submodule revision. The current submodule is pinned to 7-Zip 26.05 (`b88c6bc09339de32f7d18bbab900686b244f42ba`).
|
|
99
256
|
|
|
100
|
-
##
|
|
257
|
+
## Benchmarks
|
|
258
|
+
|
|
259
|
+
Run the native benchmark against a representative archive:
|
|
260
|
+
|
|
261
|
+
```sh
|
|
262
|
+
BENCH_ITERATIONS=20 npm run benchmark -- "./archive.zip"
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
It measures native open/list, prefix reads, full extraction, and reads across independent archive handles. `BENCH_ITERATIONS` defaults to 15 and must be at least 3.
|
|
266
|
+
|
|
267
|
+
Compare the native API with the `7zip-bin-full` command-line baseline:
|
|
268
|
+
|
|
269
|
+
```sh
|
|
270
|
+
npm run benchmark:compare -- \
|
|
271
|
+
--archive "./archive.zip" \
|
|
272
|
+
--entry "chapter/page.jpg" \
|
|
273
|
+
--iterations 20 \
|
|
274
|
+
--read-bytes 65536 \
|
|
275
|
+
--concurrency 4
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The comparison validates entry listings and output data before reporting median and p95 latency. CLI timings include process startup. Since the CLI has no ranged-read API, its range baseline extracts the entry to stdout and slices the requested bytes; this is not equivalent to native range reading. Captured output is capped at 256 MiB, and full-archive comparison is skipped if an entry exceeds that limit.
|
|
279
|
+
|
|
280
|
+
Compare extraction concurrency:
|
|
281
|
+
|
|
282
|
+
```sh
|
|
283
|
+
npm run benchmark:concurrency -- \
|
|
284
|
+
--archive "./archive.7z" \
|
|
285
|
+
--iterations 20 \
|
|
286
|
+
--concurrencies 1,2,3,4,5,8,16 \
|
|
287
|
+
--max-files 20
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The report includes median and p95 latency, throughput, speedup, and the number of entries that can overlap. `--max-files` selects evenly spaced entries, which is useful when sampling solid archives. Benchmarks report measurements only; results depend on the archive, machine, and workload and do not guarantee a speedup.
|
|
101
291
|
|
|
102
|
-
|
|
292
|
+
## License
|
|
103
293
|
|
|
104
|
-
|
|
294
|
+
This package is MIT-licensed. The integrated 7-Zip code and other bundled dependencies have their own licensing terms; see [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) before redistribution.
|
package/dist/index.cjs
CHANGED
|
@@ -167,6 +167,7 @@ class NativeArchiveReadable extends node_stream.Readable {
|
|
|
167
167
|
maxBytes;
|
|
168
168
|
streamId;
|
|
169
169
|
outputBytes = 0n;
|
|
170
|
+
starting = false;
|
|
170
171
|
constructor(start, resumeNative, cancel, options, maxBytes) {
|
|
171
172
|
super({ highWaterMark: options.highWaterMark, signal: options.signal });
|
|
172
173
|
this.start = start;
|
|
@@ -175,38 +176,63 @@ class NativeArchiveReadable extends node_stream.Readable {
|
|
|
175
176
|
this.maxBytes = maxBytes;
|
|
176
177
|
}
|
|
177
178
|
_read() {
|
|
179
|
+
if (this.streamId !== undefined) {
|
|
180
|
+
try {
|
|
181
|
+
this.resumeNative(this.streamId);
|
|
182
|
+
}
|
|
183
|
+
catch (cause) {
|
|
184
|
+
this.destroy(extractionError(cause));
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (this.starting) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
this.starting = true;
|
|
178
192
|
try {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
193
|
+
Promise.resolve(this.start((chunk, error, done) => {
|
|
194
|
+
if (this.destroyed) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (error) {
|
|
198
|
+
this.destroy(extractionError(error));
|
|
199
|
+
}
|
|
200
|
+
else if (done) {
|
|
201
|
+
this.push(null);
|
|
202
|
+
}
|
|
203
|
+
else if (chunk) {
|
|
204
|
+
if (this.maxBytes !== null
|
|
205
|
+
&& this.outputBytes + BigInt(chunk.length) > this.maxBytes) {
|
|
206
|
+
this.destroy(new ArchiveError('ERR_RESOURCE_LIMIT', `Stream output exceeds the ${this.maxBytes}-byte limit`));
|
|
182
207
|
return;
|
|
183
208
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
this.outputBytes += BigInt(chunk.length);
|
|
197
|
-
this.push(chunk);
|
|
209
|
+
this.outputBytes += BigInt(chunk.length);
|
|
210
|
+
this.push(chunk);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
this.destroy(new ArchiveError('ERR_ARCHIVE_EXTRACT', 'Native stream returned an empty chunk notification'));
|
|
214
|
+
}
|
|
215
|
+
})).then((streamId) => {
|
|
216
|
+
this.streamId = streamId;
|
|
217
|
+
this.starting = false;
|
|
218
|
+
try {
|
|
219
|
+
if (this.destroyed) {
|
|
220
|
+
this.cancel(streamId);
|
|
198
221
|
}
|
|
199
222
|
else {
|
|
200
|
-
this.
|
|
223
|
+
this.resumeNative(streamId);
|
|
201
224
|
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
225
|
+
}
|
|
226
|
+
catch (cause) {
|
|
227
|
+
this.destroy(extractionError(cause));
|
|
228
|
+
}
|
|
229
|
+
}, (cause) => {
|
|
230
|
+
this.starting = false;
|
|
231
|
+
this.destroy(extractionError(cause));
|
|
232
|
+
});
|
|
208
233
|
}
|
|
209
234
|
catch (cause) {
|
|
235
|
+
this.starting = false;
|
|
210
236
|
this.destroy(extractionError(cause));
|
|
211
237
|
}
|
|
212
238
|
}
|
|
@@ -272,21 +298,41 @@ class Archive {
|
|
|
272
298
|
}
|
|
273
299
|
return (await this.entries()).find(entry => typeof idOrPath === 'number' ? entry.id === idOrPath : entry.path === idOrPath);
|
|
274
300
|
}
|
|
275
|
-
async extract(
|
|
301
|
+
async extract(idOrPathOrPaths) {
|
|
276
302
|
this.assertOpen();
|
|
277
|
-
const
|
|
278
|
-
|
|
303
|
+
const isBatch = Array.isArray(idOrPathOrPaths);
|
|
304
|
+
let selectors;
|
|
305
|
+
if (isBatch) {
|
|
306
|
+
selectors = idOrPathOrPaths;
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
selectors = [idOrPathOrPaths];
|
|
310
|
+
}
|
|
311
|
+
const numericIds = selectors.filter((selector) => typeof selector === 'number');
|
|
312
|
+
let ids;
|
|
313
|
+
if (numericIds.length === selectors.length) {
|
|
314
|
+
this.validateIds(numericIds);
|
|
315
|
+
ids = numericIds;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
ids = await this.resolveEntryIds(selectors);
|
|
319
|
+
}
|
|
279
320
|
try {
|
|
280
321
|
const buffers = await native.extract(this.handle, ids);
|
|
281
|
-
return
|
|
322
|
+
return isBatch ? buffers : buffers[0];
|
|
282
323
|
}
|
|
283
324
|
catch (cause) {
|
|
284
325
|
throw extractionError(cause);
|
|
285
326
|
}
|
|
286
327
|
}
|
|
287
|
-
createReadStream(
|
|
328
|
+
createReadStream(idOrPath, options = {}) {
|
|
288
329
|
this.assertOpen();
|
|
289
|
-
|
|
330
|
+
if (typeof idOrPath === 'number') {
|
|
331
|
+
this.validateIds([idOrPath]);
|
|
332
|
+
}
|
|
333
|
+
else if (typeof idOrPath !== 'string') {
|
|
334
|
+
throw new TypeError('Entry selector must be an ID or archive path');
|
|
335
|
+
}
|
|
290
336
|
if (!options || typeof options !== 'object') {
|
|
291
337
|
throw new TypeError('Stream options must be an object');
|
|
292
338
|
}
|
|
@@ -298,13 +344,18 @@ class Archive {
|
|
|
298
344
|
}
|
|
299
345
|
options.signal?.throwIfAborted();
|
|
300
346
|
const maxBytes = normalizeMaxBytes(options.maxBytes, 'maxBytes');
|
|
301
|
-
return new NativeArchiveReadable(
|
|
347
|
+
return new NativeArchiveReadable((callback) => {
|
|
348
|
+
if (typeof idOrPath === 'number') {
|
|
349
|
+
return native.startStream(this.handle, idOrPath, callback);
|
|
350
|
+
}
|
|
351
|
+
return this.resolveEntryId(idOrPath).then(id => native.startStream(this.handle, id, callback));
|
|
352
|
+
}, streamId => native.resumeStream(streamId), streamId => native.cancelStream(streamId), options, maxBytes);
|
|
302
353
|
}
|
|
303
|
-
async extractToStream(
|
|
354
|
+
async extractToStream(idOrPath, destination, options = {}) {
|
|
304
355
|
if (!destination || typeof destination.write !== 'function') {
|
|
305
356
|
throw new TypeError('Destination must be a writable stream');
|
|
306
357
|
}
|
|
307
|
-
await promises.pipeline(this.createReadStream(
|
|
358
|
+
await promises.pipeline(this.createReadStream(idOrPath, options), destination, { signal: options.signal });
|
|
308
359
|
}
|
|
309
360
|
async extractToFolder(destination, options = {}) {
|
|
310
361
|
this.assertOpen();
|
|
@@ -318,6 +369,9 @@ class Archive {
|
|
|
318
369
|
const maxBytes = normalizeMaxBytes(options.maxBytes, 'maxBytes');
|
|
319
370
|
const entries = await this.entries();
|
|
320
371
|
let selectedEntries = entries;
|
|
372
|
+
if (options.ids !== undefined && options.paths !== undefined) {
|
|
373
|
+
throw new TypeError('Specify either folder extraction IDs or paths, not both');
|
|
374
|
+
}
|
|
321
375
|
if (options.ids !== undefined) {
|
|
322
376
|
if (!Array.isArray(options.ids)) {
|
|
323
377
|
throw new TypeError('Folder extraction IDs must be an array');
|
|
@@ -332,6 +386,21 @@ class Archive {
|
|
|
332
386
|
return entry;
|
|
333
387
|
});
|
|
334
388
|
}
|
|
389
|
+
else if (options.paths !== undefined) {
|
|
390
|
+
if (!Array.isArray(options.paths)
|
|
391
|
+
|| options.paths.some(entryPath => typeof entryPath !== 'string')) {
|
|
392
|
+
throw new TypeError('Folder extraction paths must be an array of strings');
|
|
393
|
+
}
|
|
394
|
+
const ids = await this.resolveEntryIds(options.paths);
|
|
395
|
+
const entriesById = new Map(entries.map(entry => [entry.id, entry]));
|
|
396
|
+
selectedEntries = ids.map((id) => {
|
|
397
|
+
const entry = entriesById.get(id);
|
|
398
|
+
if (!entry) {
|
|
399
|
+
throw new ArchiveError('ERR_INVALID_ENTRY', `Entry ID ${id} is out of range`);
|
|
400
|
+
}
|
|
401
|
+
return entry;
|
|
402
|
+
});
|
|
403
|
+
}
|
|
335
404
|
const root = node_path.resolve(destination);
|
|
336
405
|
const plans = makeOutputPlans(root, selectedEntries);
|
|
337
406
|
const expectedBytes = selectedEntries.reduce((total, entry) => total + (entry.isDirectory ? 0n : entry.size), 0n);
|
|
@@ -366,9 +435,16 @@ class Archive {
|
|
|
366
435
|
}), limit, output, { signal: options.signal });
|
|
367
436
|
}
|
|
368
437
|
}
|
|
369
|
-
async read(
|
|
438
|
+
async read(idOrPath, options) {
|
|
370
439
|
this.assertOpen();
|
|
371
|
-
|
|
440
|
+
let id;
|
|
441
|
+
if (typeof idOrPath === 'number') {
|
|
442
|
+
this.validateIds([idOrPath]);
|
|
443
|
+
id = idOrPath;
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
id = await this.resolveEntryId(idOrPath);
|
|
447
|
+
}
|
|
372
448
|
const offset = this.toUnsignedBigInt(options.offset ?? 0, 'offset');
|
|
373
449
|
const length = this.toUnsignedBigInt(options.length, 'length');
|
|
374
450
|
const maxUInt64 = (1n << 64n) - 1n;
|
|
@@ -399,6 +475,25 @@ class Archive {
|
|
|
399
475
|
throw new ArchiveError('ERR_INVALID_ENTRY', 'Entry IDs must be unique non-negative safe integers');
|
|
400
476
|
}
|
|
401
477
|
}
|
|
478
|
+
async resolveEntryId(selector) {
|
|
479
|
+
if (typeof selector === 'number') {
|
|
480
|
+
this.validateIds([selector]);
|
|
481
|
+
return selector;
|
|
482
|
+
}
|
|
483
|
+
if (typeof selector !== 'string') {
|
|
484
|
+
throw new TypeError('Entry selector must be an ID or archive path');
|
|
485
|
+
}
|
|
486
|
+
const entry = await this.getEntry(selector);
|
|
487
|
+
if (!entry) {
|
|
488
|
+
throw new ArchiveError('ERR_INVALID_ENTRY', `Archive entry "${selector}" was not found`);
|
|
489
|
+
}
|
|
490
|
+
return entry.id;
|
|
491
|
+
}
|
|
492
|
+
async resolveEntryIds(selectors) {
|
|
493
|
+
const ids = await Promise.all(selectors.map(selector => this.resolveEntryId(selector)));
|
|
494
|
+
this.validateIds(ids);
|
|
495
|
+
return ids;
|
|
496
|
+
}
|
|
402
497
|
toUnsignedBigInt(value, label) {
|
|
403
498
|
if (typeof value === 'bigint') {
|
|
404
499
|
if (value < 0n) {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Readable, Writable } from 'node:stream';
|
|
2
2
|
|
|
3
3
|
type EntryId = number;
|
|
4
|
+
type EntrySelector = EntryId | string;
|
|
4
5
|
interface ArchiveEntry {
|
|
5
6
|
readonly id: EntryId;
|
|
6
7
|
readonly path: string;
|
|
@@ -25,6 +26,7 @@ interface ReadStreamOptions {
|
|
|
25
26
|
}
|
|
26
27
|
interface ExtractToFolderOptions {
|
|
27
28
|
readonly ids?: readonly EntryId[];
|
|
29
|
+
readonly paths?: readonly string[];
|
|
28
30
|
readonly maxBytes?: number | bigint | null;
|
|
29
31
|
readonly signal?: AbortSignal;
|
|
30
32
|
}
|
|
@@ -42,18 +44,20 @@ declare class Archive {
|
|
|
42
44
|
static concurrency(value?: number): number;
|
|
43
45
|
static open(path: string, options?: OpenOptions): Promise<Archive>;
|
|
44
46
|
entries(): Promise<readonly ArchiveEntry[]>;
|
|
45
|
-
getEntry(idOrPath:
|
|
46
|
-
extract(
|
|
47
|
-
extract(
|
|
48
|
-
createReadStream(
|
|
49
|
-
extractToStream(
|
|
47
|
+
getEntry(idOrPath: EntrySelector): Promise<ArchiveEntry | undefined>;
|
|
48
|
+
extract(idOrPath: EntrySelector): Promise<Buffer>;
|
|
49
|
+
extract(idsOrPaths: readonly EntrySelector[]): Promise<Buffer[]>;
|
|
50
|
+
createReadStream(idOrPath: EntrySelector, options?: ReadStreamOptions): Readable;
|
|
51
|
+
extractToStream(idOrPath: EntrySelector, destination: Writable, options?: ReadStreamOptions): Promise<void>;
|
|
50
52
|
extractToFolder(destination: string, options?: ExtractToFolderOptions): Promise<void>;
|
|
51
|
-
read(
|
|
53
|
+
read(idOrPath: EntrySelector, options: ReadOptions): Promise<Buffer>;
|
|
52
54
|
close(): Promise<void>;
|
|
53
55
|
private assertOpen;
|
|
54
56
|
private validateIds;
|
|
57
|
+
private resolveEntryId;
|
|
58
|
+
private resolveEntryIds;
|
|
55
59
|
private toUnsignedBigInt;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
export { Archive, ArchiveError };
|
|
59
|
-
export type { ArchiveEntry, ArchiveErrorCode, EntryId, ExtractToFolderOptions, OpenOptions, ReadOptions, ReadStreamOptions };
|
|
63
|
+
export type { ArchiveEntry, ArchiveErrorCode, EntryId, EntrySelector, ExtractToFolderOptions, OpenOptions, ReadOptions, ReadStreamOptions };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Readable, Writable } from 'node:stream';
|
|
2
2
|
|
|
3
3
|
type EntryId = number;
|
|
4
|
+
type EntrySelector = EntryId | string;
|
|
4
5
|
interface ArchiveEntry {
|
|
5
6
|
readonly id: EntryId;
|
|
6
7
|
readonly path: string;
|
|
@@ -25,6 +26,7 @@ interface ReadStreamOptions {
|
|
|
25
26
|
}
|
|
26
27
|
interface ExtractToFolderOptions {
|
|
27
28
|
readonly ids?: readonly EntryId[];
|
|
29
|
+
readonly paths?: readonly string[];
|
|
28
30
|
readonly maxBytes?: number | bigint | null;
|
|
29
31
|
readonly signal?: AbortSignal;
|
|
30
32
|
}
|
|
@@ -42,18 +44,20 @@ declare class Archive {
|
|
|
42
44
|
static concurrency(value?: number): number;
|
|
43
45
|
static open(path: string, options?: OpenOptions): Promise<Archive>;
|
|
44
46
|
entries(): Promise<readonly ArchiveEntry[]>;
|
|
45
|
-
getEntry(idOrPath:
|
|
46
|
-
extract(
|
|
47
|
-
extract(
|
|
48
|
-
createReadStream(
|
|
49
|
-
extractToStream(
|
|
47
|
+
getEntry(idOrPath: EntrySelector): Promise<ArchiveEntry | undefined>;
|
|
48
|
+
extract(idOrPath: EntrySelector): Promise<Buffer>;
|
|
49
|
+
extract(idsOrPaths: readonly EntrySelector[]): Promise<Buffer[]>;
|
|
50
|
+
createReadStream(idOrPath: EntrySelector, options?: ReadStreamOptions): Readable;
|
|
51
|
+
extractToStream(idOrPath: EntrySelector, destination: Writable, options?: ReadStreamOptions): Promise<void>;
|
|
50
52
|
extractToFolder(destination: string, options?: ExtractToFolderOptions): Promise<void>;
|
|
51
|
-
read(
|
|
53
|
+
read(idOrPath: EntrySelector, options: ReadOptions): Promise<Buffer>;
|
|
52
54
|
close(): Promise<void>;
|
|
53
55
|
private assertOpen;
|
|
54
56
|
private validateIds;
|
|
57
|
+
private resolveEntryId;
|
|
58
|
+
private resolveEntryIds;
|
|
55
59
|
private toUnsignedBigInt;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
export { Archive, ArchiveError };
|
|
59
|
-
export type { ArchiveEntry, ArchiveErrorCode, EntryId, ExtractToFolderOptions, OpenOptions, ReadOptions, ReadStreamOptions };
|
|
63
|
+
export type { ArchiveEntry, ArchiveErrorCode, EntryId, EntrySelector, ExtractToFolderOptions, OpenOptions, ReadOptions, ReadStreamOptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Readable, Writable } from 'node:stream';
|
|
2
2
|
|
|
3
3
|
type EntryId = number;
|
|
4
|
+
type EntrySelector = EntryId | string;
|
|
4
5
|
interface ArchiveEntry {
|
|
5
6
|
readonly id: EntryId;
|
|
6
7
|
readonly path: string;
|
|
@@ -25,6 +26,7 @@ interface ReadStreamOptions {
|
|
|
25
26
|
}
|
|
26
27
|
interface ExtractToFolderOptions {
|
|
27
28
|
readonly ids?: readonly EntryId[];
|
|
29
|
+
readonly paths?: readonly string[];
|
|
28
30
|
readonly maxBytes?: number | bigint | null;
|
|
29
31
|
readonly signal?: AbortSignal;
|
|
30
32
|
}
|
|
@@ -42,18 +44,20 @@ declare class Archive {
|
|
|
42
44
|
static concurrency(value?: number): number;
|
|
43
45
|
static open(path: string, options?: OpenOptions): Promise<Archive>;
|
|
44
46
|
entries(): Promise<readonly ArchiveEntry[]>;
|
|
45
|
-
getEntry(idOrPath:
|
|
46
|
-
extract(
|
|
47
|
-
extract(
|
|
48
|
-
createReadStream(
|
|
49
|
-
extractToStream(
|
|
47
|
+
getEntry(idOrPath: EntrySelector): Promise<ArchiveEntry | undefined>;
|
|
48
|
+
extract(idOrPath: EntrySelector): Promise<Buffer>;
|
|
49
|
+
extract(idsOrPaths: readonly EntrySelector[]): Promise<Buffer[]>;
|
|
50
|
+
createReadStream(idOrPath: EntrySelector, options?: ReadStreamOptions): Readable;
|
|
51
|
+
extractToStream(idOrPath: EntrySelector, destination: Writable, options?: ReadStreamOptions): Promise<void>;
|
|
50
52
|
extractToFolder(destination: string, options?: ExtractToFolderOptions): Promise<void>;
|
|
51
|
-
read(
|
|
53
|
+
read(idOrPath: EntrySelector, options: ReadOptions): Promise<Buffer>;
|
|
52
54
|
close(): Promise<void>;
|
|
53
55
|
private assertOpen;
|
|
54
56
|
private validateIds;
|
|
57
|
+
private resolveEntryId;
|
|
58
|
+
private resolveEntryIds;
|
|
55
59
|
private toUnsignedBigInt;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
export { Archive, ArchiveError };
|
|
59
|
-
export type { ArchiveEntry, ArchiveErrorCode, EntryId, ExtractToFolderOptions, OpenOptions, ReadOptions, ReadStreamOptions };
|
|
63
|
+
export type { ArchiveEntry, ArchiveErrorCode, EntryId, EntrySelector, ExtractToFolderOptions, OpenOptions, ReadOptions, ReadStreamOptions };
|
package/dist/index.mjs
CHANGED
|
@@ -164,6 +164,7 @@ class NativeArchiveReadable extends Readable {
|
|
|
164
164
|
maxBytes;
|
|
165
165
|
streamId;
|
|
166
166
|
outputBytes = 0n;
|
|
167
|
+
starting = false;
|
|
167
168
|
constructor(start, resumeNative, cancel, options, maxBytes) {
|
|
168
169
|
super({ highWaterMark: options.highWaterMark, signal: options.signal });
|
|
169
170
|
this.start = start;
|
|
@@ -172,38 +173,63 @@ class NativeArchiveReadable extends Readable {
|
|
|
172
173
|
this.maxBytes = maxBytes;
|
|
173
174
|
}
|
|
174
175
|
_read() {
|
|
176
|
+
if (this.streamId !== undefined) {
|
|
177
|
+
try {
|
|
178
|
+
this.resumeNative(this.streamId);
|
|
179
|
+
}
|
|
180
|
+
catch (cause) {
|
|
181
|
+
this.destroy(extractionError(cause));
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (this.starting) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.starting = true;
|
|
175
189
|
try {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
190
|
+
Promise.resolve(this.start((chunk, error, done) => {
|
|
191
|
+
if (this.destroyed) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (error) {
|
|
195
|
+
this.destroy(extractionError(error));
|
|
196
|
+
}
|
|
197
|
+
else if (done) {
|
|
198
|
+
this.push(null);
|
|
199
|
+
}
|
|
200
|
+
else if (chunk) {
|
|
201
|
+
if (this.maxBytes !== null
|
|
202
|
+
&& this.outputBytes + BigInt(chunk.length) > this.maxBytes) {
|
|
203
|
+
this.destroy(new ArchiveError('ERR_RESOURCE_LIMIT', `Stream output exceeds the ${this.maxBytes}-byte limit`));
|
|
179
204
|
return;
|
|
180
205
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
this.outputBytes += BigInt(chunk.length);
|
|
194
|
-
this.push(chunk);
|
|
206
|
+
this.outputBytes += BigInt(chunk.length);
|
|
207
|
+
this.push(chunk);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
this.destroy(new ArchiveError('ERR_ARCHIVE_EXTRACT', 'Native stream returned an empty chunk notification'));
|
|
211
|
+
}
|
|
212
|
+
})).then((streamId) => {
|
|
213
|
+
this.streamId = streamId;
|
|
214
|
+
this.starting = false;
|
|
215
|
+
try {
|
|
216
|
+
if (this.destroyed) {
|
|
217
|
+
this.cancel(streamId);
|
|
195
218
|
}
|
|
196
219
|
else {
|
|
197
|
-
this.
|
|
220
|
+
this.resumeNative(streamId);
|
|
198
221
|
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
222
|
+
}
|
|
223
|
+
catch (cause) {
|
|
224
|
+
this.destroy(extractionError(cause));
|
|
225
|
+
}
|
|
226
|
+
}, (cause) => {
|
|
227
|
+
this.starting = false;
|
|
228
|
+
this.destroy(extractionError(cause));
|
|
229
|
+
});
|
|
205
230
|
}
|
|
206
231
|
catch (cause) {
|
|
232
|
+
this.starting = false;
|
|
207
233
|
this.destroy(extractionError(cause));
|
|
208
234
|
}
|
|
209
235
|
}
|
|
@@ -269,21 +295,41 @@ class Archive {
|
|
|
269
295
|
}
|
|
270
296
|
return (await this.entries()).find(entry => typeof idOrPath === 'number' ? entry.id === idOrPath : entry.path === idOrPath);
|
|
271
297
|
}
|
|
272
|
-
async extract(
|
|
298
|
+
async extract(idOrPathOrPaths) {
|
|
273
299
|
this.assertOpen();
|
|
274
|
-
const
|
|
275
|
-
|
|
300
|
+
const isBatch = Array.isArray(idOrPathOrPaths);
|
|
301
|
+
let selectors;
|
|
302
|
+
if (isBatch) {
|
|
303
|
+
selectors = idOrPathOrPaths;
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
selectors = [idOrPathOrPaths];
|
|
307
|
+
}
|
|
308
|
+
const numericIds = selectors.filter((selector) => typeof selector === 'number');
|
|
309
|
+
let ids;
|
|
310
|
+
if (numericIds.length === selectors.length) {
|
|
311
|
+
this.validateIds(numericIds);
|
|
312
|
+
ids = numericIds;
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
ids = await this.resolveEntryIds(selectors);
|
|
316
|
+
}
|
|
276
317
|
try {
|
|
277
318
|
const buffers = await native.extract(this.handle, ids);
|
|
278
|
-
return
|
|
319
|
+
return isBatch ? buffers : buffers[0];
|
|
279
320
|
}
|
|
280
321
|
catch (cause) {
|
|
281
322
|
throw extractionError(cause);
|
|
282
323
|
}
|
|
283
324
|
}
|
|
284
|
-
createReadStream(
|
|
325
|
+
createReadStream(idOrPath, options = {}) {
|
|
285
326
|
this.assertOpen();
|
|
286
|
-
|
|
327
|
+
if (typeof idOrPath === 'number') {
|
|
328
|
+
this.validateIds([idOrPath]);
|
|
329
|
+
}
|
|
330
|
+
else if (typeof idOrPath !== 'string') {
|
|
331
|
+
throw new TypeError('Entry selector must be an ID or archive path');
|
|
332
|
+
}
|
|
287
333
|
if (!options || typeof options !== 'object') {
|
|
288
334
|
throw new TypeError('Stream options must be an object');
|
|
289
335
|
}
|
|
@@ -295,13 +341,18 @@ class Archive {
|
|
|
295
341
|
}
|
|
296
342
|
options.signal?.throwIfAborted();
|
|
297
343
|
const maxBytes = normalizeMaxBytes(options.maxBytes, 'maxBytes');
|
|
298
|
-
return new NativeArchiveReadable(
|
|
344
|
+
return new NativeArchiveReadable((callback) => {
|
|
345
|
+
if (typeof idOrPath === 'number') {
|
|
346
|
+
return native.startStream(this.handle, idOrPath, callback);
|
|
347
|
+
}
|
|
348
|
+
return this.resolveEntryId(idOrPath).then(id => native.startStream(this.handle, id, callback));
|
|
349
|
+
}, streamId => native.resumeStream(streamId), streamId => native.cancelStream(streamId), options, maxBytes);
|
|
299
350
|
}
|
|
300
|
-
async extractToStream(
|
|
351
|
+
async extractToStream(idOrPath, destination, options = {}) {
|
|
301
352
|
if (!destination || typeof destination.write !== 'function') {
|
|
302
353
|
throw new TypeError('Destination must be a writable stream');
|
|
303
354
|
}
|
|
304
|
-
await pipeline(this.createReadStream(
|
|
355
|
+
await pipeline(this.createReadStream(idOrPath, options), destination, { signal: options.signal });
|
|
305
356
|
}
|
|
306
357
|
async extractToFolder(destination, options = {}) {
|
|
307
358
|
this.assertOpen();
|
|
@@ -315,6 +366,9 @@ class Archive {
|
|
|
315
366
|
const maxBytes = normalizeMaxBytes(options.maxBytes, 'maxBytes');
|
|
316
367
|
const entries = await this.entries();
|
|
317
368
|
let selectedEntries = entries;
|
|
369
|
+
if (options.ids !== undefined && options.paths !== undefined) {
|
|
370
|
+
throw new TypeError('Specify either folder extraction IDs or paths, not both');
|
|
371
|
+
}
|
|
318
372
|
if (options.ids !== undefined) {
|
|
319
373
|
if (!Array.isArray(options.ids)) {
|
|
320
374
|
throw new TypeError('Folder extraction IDs must be an array');
|
|
@@ -329,6 +383,21 @@ class Archive {
|
|
|
329
383
|
return entry;
|
|
330
384
|
});
|
|
331
385
|
}
|
|
386
|
+
else if (options.paths !== undefined) {
|
|
387
|
+
if (!Array.isArray(options.paths)
|
|
388
|
+
|| options.paths.some(entryPath => typeof entryPath !== 'string')) {
|
|
389
|
+
throw new TypeError('Folder extraction paths must be an array of strings');
|
|
390
|
+
}
|
|
391
|
+
const ids = await this.resolveEntryIds(options.paths);
|
|
392
|
+
const entriesById = new Map(entries.map(entry => [entry.id, entry]));
|
|
393
|
+
selectedEntries = ids.map((id) => {
|
|
394
|
+
const entry = entriesById.get(id);
|
|
395
|
+
if (!entry) {
|
|
396
|
+
throw new ArchiveError('ERR_INVALID_ENTRY', `Entry ID ${id} is out of range`);
|
|
397
|
+
}
|
|
398
|
+
return entry;
|
|
399
|
+
});
|
|
400
|
+
}
|
|
332
401
|
const root = resolve(destination);
|
|
333
402
|
const plans = makeOutputPlans(root, selectedEntries);
|
|
334
403
|
const expectedBytes = selectedEntries.reduce((total, entry) => total + (entry.isDirectory ? 0n : entry.size), 0n);
|
|
@@ -363,9 +432,16 @@ class Archive {
|
|
|
363
432
|
}), limit, output, { signal: options.signal });
|
|
364
433
|
}
|
|
365
434
|
}
|
|
366
|
-
async read(
|
|
435
|
+
async read(idOrPath, options) {
|
|
367
436
|
this.assertOpen();
|
|
368
|
-
|
|
437
|
+
let id;
|
|
438
|
+
if (typeof idOrPath === 'number') {
|
|
439
|
+
this.validateIds([idOrPath]);
|
|
440
|
+
id = idOrPath;
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
id = await this.resolveEntryId(idOrPath);
|
|
444
|
+
}
|
|
369
445
|
const offset = this.toUnsignedBigInt(options.offset ?? 0, 'offset');
|
|
370
446
|
const length = this.toUnsignedBigInt(options.length, 'length');
|
|
371
447
|
const maxUInt64 = (1n << 64n) - 1n;
|
|
@@ -396,6 +472,25 @@ class Archive {
|
|
|
396
472
|
throw new ArchiveError('ERR_INVALID_ENTRY', 'Entry IDs must be unique non-negative safe integers');
|
|
397
473
|
}
|
|
398
474
|
}
|
|
475
|
+
async resolveEntryId(selector) {
|
|
476
|
+
if (typeof selector === 'number') {
|
|
477
|
+
this.validateIds([selector]);
|
|
478
|
+
return selector;
|
|
479
|
+
}
|
|
480
|
+
if (typeof selector !== 'string') {
|
|
481
|
+
throw new TypeError('Entry selector must be an ID or archive path');
|
|
482
|
+
}
|
|
483
|
+
const entry = await this.getEntry(selector);
|
|
484
|
+
if (!entry) {
|
|
485
|
+
throw new ArchiveError('ERR_INVALID_ENTRY', `Archive entry "${selector}" was not found`);
|
|
486
|
+
}
|
|
487
|
+
return entry.id;
|
|
488
|
+
}
|
|
489
|
+
async resolveEntryIds(selectors) {
|
|
490
|
+
const ids = await Promise.all(selectors.map(selector => this.resolveEntryId(selector)));
|
|
491
|
+
this.validateIds(ids);
|
|
492
|
+
return ids;
|
|
493
|
+
}
|
|
399
494
|
toUnsignedBigInt(value, label) {
|
|
400
495
|
if (typeof value === 'bigint') {
|
|
401
496
|
if (value < 0n) {
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|