modern-tar 0.0.0 → 0.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 +73 -0
- package/dist/fs/index.d.ts +75 -20
- package/dist/fs/index.js +186 -53
- package/dist/{index-BLp7i3zL.d.ts → index-Dx4tbuJh.d.ts} +70 -70
- package/dist/web/index.d.ts +1 -1
- package/dist/web/index.js +1 -1
- package/dist/{web-BsjPG7md.js → web-C1b5fAZt.js} +182 -152
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -173,6 +173,25 @@ const extractStream = unpackTar('./output', {
|
|
|
173
173
|
await pipeline(sourceStream, extractStream);
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
+
#### Archive Creation
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { packTarSources, type TarSource } from 'modern-tar/fs';
|
|
180
|
+
import { createWriteStream } from 'node:fs';
|
|
181
|
+
import { pipeline } from 'node:stream/promises';
|
|
182
|
+
|
|
183
|
+
// Pack multiple sources
|
|
184
|
+
const sources: TarSource[] = [
|
|
185
|
+
{ type: 'file', source: './package.json', target: 'project/package.json' },
|
|
186
|
+
{ type: 'directory', source: './src', target: 'project/src' },
|
|
187
|
+
{ type: 'content', content: 'Hello World!', target: 'project/hello.txt' },
|
|
188
|
+
{ type: 'content', content: '#!/bin/bash\necho "Executable"', target: 'bin/script.sh', mode: 0o755 }
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
const archiveStream = packTarSources(sources);
|
|
192
|
+
await pipeline(archiveStream, createWriteStream('project.tar'));
|
|
193
|
+
```
|
|
194
|
+
|
|
176
195
|
## API Reference
|
|
177
196
|
|
|
178
197
|
### Core API (`modern-tar`)
|
|
@@ -339,6 +358,31 @@ const extractStream = unpackTar('/restore/location', {
|
|
|
339
358
|
await pipeline(tarStream, extractStream);
|
|
340
359
|
```
|
|
341
360
|
|
|
361
|
+
#### `packTarSources(sources: TarSource[]): Readable`
|
|
362
|
+
|
|
363
|
+
Pack multiple sources (files, directories, or raw content) into a tar archive stream.
|
|
364
|
+
|
|
365
|
+
- **`sources`**: Array of `TarSource` objects describing what to include in the archive.
|
|
366
|
+
- **Returns**: Node.js `Readable` stream of tar archive bytes.
|
|
367
|
+
|
|
368
|
+
**Example:**
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
import { packTarSources, type TarSource } from 'modern-tar/fs';
|
|
372
|
+
import { createWriteStream } from 'node:fs';
|
|
373
|
+
import { pipeline } from 'node:stream/promises';
|
|
374
|
+
|
|
375
|
+
const sources: TarSource[] = [
|
|
376
|
+
{ type: 'file', source: './README.md', target: 'docs/readme.txt' },
|
|
377
|
+
{ type: 'directory', source: './src', target: 'app/src' },
|
|
378
|
+
{ type: 'content', content: '{"version": "1.0.0"}', target: 'app/config.json' },
|
|
379
|
+
{ type: 'content', content: Buffer.from('binary data'), target: 'data/binary.dat' }
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
const archiveStream = packTarSources(sources);
|
|
383
|
+
await pipeline(archiveStream, createWriteStream('app.tar'));
|
|
384
|
+
```
|
|
385
|
+
|
|
342
386
|
## Types
|
|
343
387
|
|
|
344
388
|
### Core Types
|
|
@@ -400,6 +444,35 @@ interface PackOptionsFS {
|
|
|
400
444
|
map?: (header: TarHeader) => TarHeader;
|
|
401
445
|
}
|
|
402
446
|
|
|
447
|
+
// Source types for packTarSources function
|
|
448
|
+
interface FileSource {
|
|
449
|
+
type: "file";
|
|
450
|
+
/** Path to the source file on the local filesystem */
|
|
451
|
+
source: string;
|
|
452
|
+
/** Destination path inside the tar archive */
|
|
453
|
+
target: string;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
interface DirectorySource {
|
|
457
|
+
type: "directory";
|
|
458
|
+
/** Path to the source directory on the local filesystem */
|
|
459
|
+
source: string;
|
|
460
|
+
/** Destination path inside the tar archive */
|
|
461
|
+
target: string;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
interface ContentSource {
|
|
465
|
+
type: "content";
|
|
466
|
+
/** Raw content to add. Supports string, Uint8Array, ArrayBuffer, ReadableStream, Blob, or null. */
|
|
467
|
+
content: TarEntryData;
|
|
468
|
+
/** Destination path inside the tar archive */
|
|
469
|
+
target: string;
|
|
470
|
+
/** Optional Unix file permissions (e.g., 0o644, 0o755) */
|
|
471
|
+
mode?: number;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
type TarSource = FileSource | DirectorySource | ContentSource;
|
|
475
|
+
|
|
403
476
|
interface UnpackOptionsFS extends UnpackOptions {
|
|
404
477
|
// Inherited from UnpackOptions (platform-neutral):
|
|
405
478
|
/** Number of leading path components to strip from entry names */
|
package/dist/fs/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { TarHeader, UnpackOptions } from "../index-
|
|
1
|
+
import { TarEntryData, TarHeader, UnpackOptions } from "../index-Dx4tbuJh.js";
|
|
2
2
|
import { Stats } from "node:fs";
|
|
3
3
|
import { Readable, Writable } from "node:stream";
|
|
4
4
|
|
|
5
|
-
//#region src/fs/
|
|
5
|
+
//#region src/fs/types.d.ts
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Filesystem-specific configuration options for packing directories into tar archives.
|
|
@@ -18,6 +18,78 @@ interface PackOptionsFS {
|
|
|
18
18
|
/** Transform function to modify tar headers before packing */
|
|
19
19
|
map?: (header: TarHeader) => TarHeader;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Filesystem-specific configuration options for extracting tar archives to the filesystem.
|
|
23
|
+
*
|
|
24
|
+
* Extends the core {@link UnpackOptions} with Node.js filesystem-specific settings
|
|
25
|
+
* for controlling file permissions and other filesystem behaviors.
|
|
26
|
+
*/
|
|
27
|
+
interface UnpackOptionsFS extends UnpackOptions {
|
|
28
|
+
/** Default mode for created directories (e.g., 0o755). If not specified, uses mode from tar header or system default */
|
|
29
|
+
dmode?: number;
|
|
30
|
+
/** Default mode for created files (e.g., 0o644). If not specified, uses mode from tar header or system default */
|
|
31
|
+
fmode?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Prevent symlinks from pointing outside the extraction directory.
|
|
34
|
+
* @default true
|
|
35
|
+
*/
|
|
36
|
+
validateSymlinks?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** Describes a file on the local filesystem to be added to the archive. */
|
|
39
|
+
interface FileSource {
|
|
40
|
+
type: "file";
|
|
41
|
+
/** Path to the source file on the local filesystem. */
|
|
42
|
+
source: string;
|
|
43
|
+
/** Destination path for the file inside the tar archive. */
|
|
44
|
+
target: string;
|
|
45
|
+
}
|
|
46
|
+
/** Describes a directory on the local filesystem to be added to the archive. */
|
|
47
|
+
interface DirectorySource {
|
|
48
|
+
type: "directory";
|
|
49
|
+
/** Path to the source directory on the local filesystem. */
|
|
50
|
+
source: string;
|
|
51
|
+
/** Destination path for the directory inside the tar archive. */
|
|
52
|
+
target: string;
|
|
53
|
+
}
|
|
54
|
+
/** Describes raw content to be added to the archive. Supports all TarEntryData types including strings, buffers, streams, blobs, and null. */
|
|
55
|
+
interface ContentSource {
|
|
56
|
+
type: "content";
|
|
57
|
+
/** Raw content to add. Supports string, Uint8Array, ArrayBuffer, ReadableStream, Blob, or null. */
|
|
58
|
+
content: TarEntryData;
|
|
59
|
+
/** Destination path for the content inside the tar archive. */
|
|
60
|
+
target: string;
|
|
61
|
+
/** Optional Unix file permissions for the entry (e.g., 0o644). */
|
|
62
|
+
mode?: number;
|
|
63
|
+
}
|
|
64
|
+
/** A union of all possible source types for creating a tar archive. */
|
|
65
|
+
type TarSource = FileSource | DirectorySource | ContentSource;
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/fs/archive.d.ts
|
|
68
|
+
/**
|
|
69
|
+
* Packs multiple sources into a tar archive as a Node.js Readable stream from an
|
|
70
|
+
* array of sources (files, directories, or raw content).
|
|
71
|
+
*
|
|
72
|
+
* @param sources - An array of {@link TarSource} objects describing what to include.
|
|
73
|
+
* @returns A Node.js [`Readable`](https://nodejs.org/api/stream.html#class-streamreadable)
|
|
74
|
+
* stream that outputs the tar archive bytes.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* import { packTarSources, TarSource } from 'modern-tar/fs';
|
|
79
|
+
*
|
|
80
|
+
* const sources: TarSource[] = [
|
|
81
|
+
* { type: 'file', source: './package.json', target: 'project/package.json' },
|
|
82
|
+
* { type: 'directory', source: './src', target: 'project/src' },
|
|
83
|
+
* { type: 'content', content: 'hello world', target: 'project/hello.txt' }
|
|
84
|
+
* ];
|
|
85
|
+
*
|
|
86
|
+
* const archiveStream = packTarSources(sources);
|
|
87
|
+
* await pipeline(archiveStream, createWriteStream('project.tar'));
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
declare function packTarSources(sources: TarSource[]): Readable;
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/fs/pack.d.ts
|
|
21
93
|
/**
|
|
22
94
|
* Pack a directory into a Node.js [`Readable`](https://nodejs.org/api/stream.html#class-streamreadable) stream containing tar archive bytes.
|
|
23
95
|
*
|
|
@@ -49,23 +121,6 @@ interface PackOptionsFS {
|
|
|
49
121
|
declare function packTar(directoryPath: string, options?: PackOptionsFS): Readable;
|
|
50
122
|
//#endregion
|
|
51
123
|
//#region src/fs/unpack.d.ts
|
|
52
|
-
/**
|
|
53
|
-
* Filesystem-specific configuration options for extracting tar archives to the filesystem.
|
|
54
|
-
*
|
|
55
|
-
* Extends the core {@link UnpackOptions} with Node.js filesystem-specific settings
|
|
56
|
-
* for controlling file permissions and other filesystem behaviors.
|
|
57
|
-
*/
|
|
58
|
-
interface UnpackOptionsFS extends UnpackOptions {
|
|
59
|
-
/** Default mode for created directories (e.g., 0o755). If not specified, uses mode from tar header or system default */
|
|
60
|
-
dmode?: number;
|
|
61
|
-
/** Default mode for created files (e.g., 0o644). If not specified, uses mode from tar header or system default */
|
|
62
|
-
fmode?: number;
|
|
63
|
-
/**
|
|
64
|
-
* Prevent symlinks from pointing outside the extraction directory.
|
|
65
|
-
* @default true
|
|
66
|
-
*/
|
|
67
|
-
validateSymlinks?: boolean;
|
|
68
|
-
}
|
|
69
124
|
/**
|
|
70
125
|
* Extract a tar archive to a directory.
|
|
71
126
|
*
|
|
@@ -99,4 +154,4 @@ interface UnpackOptionsFS extends UnpackOptions {
|
|
|
99
154
|
*/
|
|
100
155
|
declare function unpackTar(directoryPath: string, options?: UnpackOptionsFS): Writable;
|
|
101
156
|
//#endregion
|
|
102
|
-
export { type PackOptionsFS, type UnpackOptionsFS, packTar, unpackTar };
|
|
157
|
+
export { type ContentSource, type DirectorySource, type FileSource, type PackOptionsFS, type TarSource, type UnpackOptionsFS, packTar, packTarSources, unpackTar };
|
package/dist/fs/index.js
CHANGED
|
@@ -1,10 +1,121 @@
|
|
|
1
|
-
import { BLOCK_SIZE, createTarDecoder, createTarHeader, createTarOptionsTransformer } from "../web-
|
|
1
|
+
import { BLOCK_SIZE, createTarDecoder, createTarHeader, createTarOptionsTransformer, createTarPacker } from "../web-C1b5fAZt.js";
|
|
2
2
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { PassThrough, Readable, Writable } from "node:stream";
|
|
6
6
|
import { pipeline } from "node:stream/promises";
|
|
7
7
|
|
|
8
|
+
//#region src/fs/archive.ts
|
|
9
|
+
async function addFileToPacker(controller, sourcePath, targetPath) {
|
|
10
|
+
const stat = await fs.stat(sourcePath);
|
|
11
|
+
const entryStream = controller.add({
|
|
12
|
+
name: targetPath,
|
|
13
|
+
size: stat.size,
|
|
14
|
+
mode: stat.mode,
|
|
15
|
+
mtime: stat.mtime,
|
|
16
|
+
type: "file"
|
|
17
|
+
});
|
|
18
|
+
await pipeline(createReadStream(sourcePath), Writable.fromWeb(entryStream));
|
|
19
|
+
}
|
|
20
|
+
async function addDirectoryToPacker(controller, sourcePath, targetPathInArchive) {
|
|
21
|
+
const sourceStat = await fs.stat(sourcePath);
|
|
22
|
+
controller.add({
|
|
23
|
+
name: `${targetPathInArchive}/`,
|
|
24
|
+
type: "directory",
|
|
25
|
+
mode: sourceStat.mode,
|
|
26
|
+
mtime: sourceStat.mtime,
|
|
27
|
+
size: 0
|
|
28
|
+
}).close();
|
|
29
|
+
const dirents = await fs.readdir(sourcePath, { withFileTypes: true });
|
|
30
|
+
for (const dirent of dirents) {
|
|
31
|
+
const fullSourcePath = path.join(sourcePath, dirent.name);
|
|
32
|
+
const archiveEntryPath = path.join(targetPathInArchive, dirent.name).replace(/\\/g, "/");
|
|
33
|
+
if (dirent.isDirectory()) await addDirectoryToPacker(controller, fullSourcePath, archiveEntryPath);
|
|
34
|
+
else if (dirent.isFile()) await addFileToPacker(controller, fullSourcePath, archiveEntryPath);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Packs multiple sources into a tar archive as a Node.js Readable stream from an
|
|
39
|
+
* array of sources (files, directories, or raw content).
|
|
40
|
+
*
|
|
41
|
+
* @param sources - An array of {@link TarSource} objects describing what to include.
|
|
42
|
+
* @returns A Node.js [`Readable`](https://nodejs.org/api/stream.html#class-streamreadable)
|
|
43
|
+
* stream that outputs the tar archive bytes.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* import { packTarSources, TarSource } from 'modern-tar/fs';
|
|
48
|
+
*
|
|
49
|
+
* const sources: TarSource[] = [
|
|
50
|
+
* { type: 'file', source: './package.json', target: 'project/package.json' },
|
|
51
|
+
* { type: 'directory', source: './src', target: 'project/src' },
|
|
52
|
+
* { type: 'content', content: 'hello world', target: 'project/hello.txt' }
|
|
53
|
+
* ];
|
|
54
|
+
*
|
|
55
|
+
* const archiveStream = packTarSources(sources);
|
|
56
|
+
* await pipeline(archiveStream, createWriteStream('project.tar'));
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
function packTarSources(sources) {
|
|
60
|
+
const { readable, controller } = createTarPacker();
|
|
61
|
+
(async () => {
|
|
62
|
+
for (const source of sources) {
|
|
63
|
+
const targetPath = source.target.replace(/\\/g, "/");
|
|
64
|
+
switch (source.type) {
|
|
65
|
+
case "file":
|
|
66
|
+
await addFileToPacker(controller, source.source, targetPath);
|
|
67
|
+
break;
|
|
68
|
+
case "directory":
|
|
69
|
+
await addDirectoryToPacker(controller, source.source, targetPath);
|
|
70
|
+
break;
|
|
71
|
+
case "content": {
|
|
72
|
+
const { content, mode } = source;
|
|
73
|
+
if (content instanceof Blob) {
|
|
74
|
+
const entryStream = controller.add({
|
|
75
|
+
name: targetPath,
|
|
76
|
+
size: content.size,
|
|
77
|
+
mode,
|
|
78
|
+
type: "file"
|
|
79
|
+
});
|
|
80
|
+
await content.stream().pipeTo(entryStream);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
if (content instanceof ReadableStream) {
|
|
84
|
+
const chunks = [];
|
|
85
|
+
for await (const chunk of Readable.fromWeb(content)) chunks.push(chunk);
|
|
86
|
+
const buffer = Buffer.concat(chunks);
|
|
87
|
+
const writer$1 = controller.add({
|
|
88
|
+
name: targetPath,
|
|
89
|
+
size: buffer.length,
|
|
90
|
+
mode,
|
|
91
|
+
type: "file"
|
|
92
|
+
}).getWriter();
|
|
93
|
+
await writer$1.write(buffer);
|
|
94
|
+
await writer$1.close();
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
let data;
|
|
98
|
+
if (content === null || content === void 0) data = new Uint8Array(0);
|
|
99
|
+
else if (typeof content === "string") data = Buffer.from(content);
|
|
100
|
+
else if (content instanceof ArrayBuffer) data = new Uint8Array(content);
|
|
101
|
+
else data = content;
|
|
102
|
+
const writer = controller.add({
|
|
103
|
+
name: targetPath,
|
|
104
|
+
size: data.length,
|
|
105
|
+
mode,
|
|
106
|
+
type: "file"
|
|
107
|
+
}).getWriter();
|
|
108
|
+
await writer.write(data);
|
|
109
|
+
await writer.close();
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
})().then(() => controller.finalize()).catch((err) => controller.error(err));
|
|
115
|
+
return Readable.fromWeb(readable);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
8
119
|
//#region src/fs/pack.ts
|
|
9
120
|
/**
|
|
10
121
|
* Pack a directory into a Node.js [`Readable`](https://nodejs.org/api/stream.html#class-streamreadable) stream containing tar archive bytes.
|
|
@@ -119,58 +230,17 @@ function packTar(directoryPath, options = {}) {
|
|
|
119
230
|
*/
|
|
120
231
|
function unpackTar(directoryPath, options = {}) {
|
|
121
232
|
const bridge = new PassThrough();
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
for await (const entry of entryStream) {
|
|
131
|
-
const header = entry.header;
|
|
132
|
-
const outPath = path.join(directoryPath, header.name);
|
|
133
|
-
const parentDir = path.dirname(outPath);
|
|
134
|
-
if (!createdDirs.has(parentDir)) {
|
|
135
|
-
await fs.mkdir(parentDir, { recursive: true });
|
|
136
|
-
createdDirs.add(parentDir);
|
|
137
|
-
}
|
|
138
|
-
switch (header.type) {
|
|
139
|
-
case "directory": {
|
|
140
|
-
const mode = options.dmode ?? header.mode;
|
|
141
|
-
if (createdDirs.has(outPath) && mode) await fs.chmod(outPath, mode);
|
|
142
|
-
else {
|
|
143
|
-
await fs.mkdir(outPath, {
|
|
144
|
-
recursive: true,
|
|
145
|
-
mode
|
|
146
|
-
});
|
|
147
|
-
createdDirs.add(outPath);
|
|
148
|
-
}
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
case "file":
|
|
152
|
-
await pipeline(Readable.fromWeb(entry.body), createWriteStream(outPath, { mode: options.fmode ?? header.mode }));
|
|
153
|
-
break;
|
|
154
|
-
case "symlink":
|
|
155
|
-
if (header.linkname) {
|
|
156
|
-
if (validateSymlinks) {
|
|
157
|
-
const symlinkDir = path.dirname(outPath);
|
|
158
|
-
const resolvedTarget = path.resolve(symlinkDir, header.linkname);
|
|
159
|
-
if (!resolvedTarget.startsWith(resolvedDestDir + path.sep) && resolvedTarget !== resolvedDestDir) throw new Error(`Symlink target "${header.linkname}" points outside of the extraction directory.`);
|
|
160
|
-
}
|
|
161
|
-
await fs.symlink(header.linkname, outPath);
|
|
162
|
-
}
|
|
163
|
-
break;
|
|
164
|
-
case "link":
|
|
165
|
-
if (header.linkname) await fs.link(path.join(directoryPath, header.linkname), outPath);
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
if (header.mtime) try {
|
|
169
|
-
await (header.type === "symlink" ? fs.lutimes : fs.utimes)(outPath, header.mtime, header.mtime);
|
|
170
|
-
} catch {}
|
|
233
|
+
const readable = new ReadableStream({
|
|
234
|
+
start(controller) {
|
|
235
|
+
bridge.on("data", (chunk) => controller.enqueue(chunk));
|
|
236
|
+
bridge.on("end", () => controller.close());
|
|
237
|
+
bridge.on("error", (err) => controller.error(err));
|
|
238
|
+
},
|
|
239
|
+
cancel(reason) {
|
|
240
|
+
bridge.destroy(reason instanceof Error ? reason : new Error(String(reason)));
|
|
171
241
|
}
|
|
172
|
-
})
|
|
173
|
-
|
|
242
|
+
});
|
|
243
|
+
const writable = new Writable({
|
|
174
244
|
write(chunk, encoding, callback) {
|
|
175
245
|
if (!bridge.write(chunk, encoding)) {
|
|
176
246
|
bridge.once("drain", callback);
|
|
@@ -187,7 +257,70 @@ function unpackTar(directoryPath, options = {}) {
|
|
|
187
257
|
callback(err);
|
|
188
258
|
}
|
|
189
259
|
});
|
|
260
|
+
const processingPromise = (async () => {
|
|
261
|
+
const resolvedDestDir = path.resolve(directoryPath);
|
|
262
|
+
const createdDirs = /* @__PURE__ */ new Set();
|
|
263
|
+
await fs.mkdir(resolvedDestDir, { recursive: true });
|
|
264
|
+
createdDirs.add(resolvedDestDir);
|
|
265
|
+
const reader = readable.pipeThrough(createTarDecoder()).pipeThrough(createTarOptionsTransformer(options)).getReader();
|
|
266
|
+
try {
|
|
267
|
+
while (true) {
|
|
268
|
+
const { done, value: entry } = await reader.read();
|
|
269
|
+
if (done) break;
|
|
270
|
+
const { header } = entry;
|
|
271
|
+
if (path.isAbsolute(header.name)) throw new Error(`Path traversal attempt detected for entry "${header.name}".`);
|
|
272
|
+
const outPath = path.join(resolvedDestDir, header.name);
|
|
273
|
+
if (!outPath.startsWith(resolvedDestDir)) throw new Error(`Path traversal attempt detected for entry "${header.name}".`);
|
|
274
|
+
const parentDir = path.dirname(outPath);
|
|
275
|
+
if (!createdDirs.has(parentDir)) {
|
|
276
|
+
await fs.mkdir(parentDir, { recursive: true });
|
|
277
|
+
createdDirs.add(parentDir);
|
|
278
|
+
}
|
|
279
|
+
switch (header.type) {
|
|
280
|
+
case "directory": {
|
|
281
|
+
const mode = options.dmode ?? header.mode;
|
|
282
|
+
if (createdDirs.has(outPath) && mode) await fs.chmod(outPath, mode);
|
|
283
|
+
else {
|
|
284
|
+
await fs.mkdir(outPath, {
|
|
285
|
+
recursive: true,
|
|
286
|
+
mode
|
|
287
|
+
});
|
|
288
|
+
createdDirs.add(outPath);
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case "file":
|
|
293
|
+
await pipeline(Readable.fromWeb(entry.body), createWriteStream(outPath, { mode: options.fmode ?? header.mode }));
|
|
294
|
+
break;
|
|
295
|
+
case "symlink":
|
|
296
|
+
if (!header.linkname) break;
|
|
297
|
+
if (options.validateSymlinks ?? true) {
|
|
298
|
+
const symlinkDir = path.dirname(outPath);
|
|
299
|
+
if (!path.resolve(symlinkDir, header.linkname).startsWith(resolvedDestDir)) throw new Error(`Symlink target "${header.linkname}" points outside the extraction directory.`);
|
|
300
|
+
}
|
|
301
|
+
await fs.symlink(header.linkname, outPath);
|
|
302
|
+
break;
|
|
303
|
+
case "link": {
|
|
304
|
+
if (!header.linkname) break;
|
|
305
|
+
const resolvedLinkTarget = path.resolve(resolvedDestDir, header.linkname);
|
|
306
|
+
if (!resolvedLinkTarget.startsWith(resolvedDestDir)) throw new Error(`Hardlink target "${header.linkname}" points outside the extraction directory.`);
|
|
307
|
+
await fs.link(resolvedLinkTarget, outPath);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (header.mtime) try {
|
|
312
|
+
await (header.type === "symlink" ? fs.lutimes : fs.utimes)(outPath, header.mtime, header.mtime);
|
|
313
|
+
} catch {}
|
|
314
|
+
}
|
|
315
|
+
} finally {
|
|
316
|
+
reader.releaseLock();
|
|
317
|
+
}
|
|
318
|
+
})();
|
|
319
|
+
processingPromise.catch((err) => {
|
|
320
|
+
writable.destroy(err);
|
|
321
|
+
});
|
|
322
|
+
return writable;
|
|
190
323
|
}
|
|
191
324
|
|
|
192
325
|
//#endregion
|
|
193
|
-
export { packTar, unpackTar };
|
|
326
|
+
export { packTar, packTarSources, unpackTar };
|
|
@@ -1,3 +1,73 @@
|
|
|
1
|
+
//#region src/web/compression.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Creates a gzip compression stream using the native
|
|
4
|
+
* [`CompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream) API.
|
|
5
|
+
*
|
|
6
|
+
* @returns A [`CompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream) configured for gzip compression
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createGzipEncoder, createTarPacker } from '@modern-tar/core';
|
|
10
|
+
*
|
|
11
|
+
* // Create and compress a tar archive
|
|
12
|
+
* const { readable, controller } = createTarPacker();
|
|
13
|
+
* const compressedStream = readable.pipeThrough(createGzipEncoder());
|
|
14
|
+
*
|
|
15
|
+
* // Add entries...
|
|
16
|
+
* const fileStream = controller.add({ name: "file.txt", size: 5, type: "file" });
|
|
17
|
+
* const writer = fileStream.getWriter();
|
|
18
|
+
* await writer.write(new TextEncoder().encode("hello"));
|
|
19
|
+
* await writer.close();
|
|
20
|
+
* controller.finalize();
|
|
21
|
+
*
|
|
22
|
+
* // Upload compressed .tar.gz
|
|
23
|
+
* await fetch('/api/upload', {
|
|
24
|
+
* method: 'POST',
|
|
25
|
+
* body: compressedStream,
|
|
26
|
+
* headers: { 'Content-Type': 'application/gzip' }
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
declare function createGzipEncoder(): CompressionStream;
|
|
31
|
+
/**
|
|
32
|
+
* Creates a gzip decompression stream using the native
|
|
33
|
+
* [`DecompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) API.
|
|
34
|
+
*
|
|
35
|
+
* @returns A [`DecompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) configured for gzip decompression
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* import { createGzipDecoder, createTarDecoder } from '@modern-tar/core';
|
|
39
|
+
*
|
|
40
|
+
* // Download and process a .tar.gz file
|
|
41
|
+
* const response = await fetch('https://api.example.com/archive.tar.gz');
|
|
42
|
+
* if (!response.body) throw new Error('No response body');
|
|
43
|
+
*
|
|
44
|
+
* // Chain decompression and tar parsing
|
|
45
|
+
* const entries = response.body
|
|
46
|
+
* .pipeThrough(createGzipDecoder())
|
|
47
|
+
* .pipeThrough(createTarDecoder());
|
|
48
|
+
*
|
|
49
|
+
* for await (const entry of entries) {
|
|
50
|
+
* console.log(`Extracted: ${entry.header.name}`);
|
|
51
|
+
* // Process entry.body ReadableStream as needed
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* // Decompress local .tar.gz data
|
|
57
|
+
* const gzippedData = new Uint8Array([...]); // your gzipped tar data
|
|
58
|
+
* const stream = new ReadableStream({
|
|
59
|
+
* start(controller) {
|
|
60
|
+
* controller.enqueue(gzippedData);
|
|
61
|
+
* controller.close();
|
|
62
|
+
* }
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* const tarStream = stream.pipeThrough(createGzipDecoder());
|
|
66
|
+
* // Now process tarStream with createTarDecoder()...
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
declare function createGzipDecoder(): DecompressionStream;
|
|
70
|
+
//#endregion
|
|
1
71
|
//#region src/web/types.d.ts
|
|
2
72
|
/**
|
|
3
73
|
* Header information for a tar entry in USTAR format.
|
|
@@ -78,76 +148,6 @@ interface UnpackOptions {
|
|
|
78
148
|
map?: (header: TarHeader) => TarHeader;
|
|
79
149
|
}
|
|
80
150
|
//#endregion
|
|
81
|
-
//#region src/web/compression.d.ts
|
|
82
|
-
/**
|
|
83
|
-
* Creates a gzip compression stream using the native
|
|
84
|
-
* [`CompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream) API.
|
|
85
|
-
*
|
|
86
|
-
* @returns A [`CompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream) configured for gzip compression
|
|
87
|
-
* @example
|
|
88
|
-
* ```typescript
|
|
89
|
-
* import { createGzipEncoder, createTarPacker } from '@modern-tar/core';
|
|
90
|
-
*
|
|
91
|
-
* // Create and compress a tar archive
|
|
92
|
-
* const { readable, controller } = createTarPacker();
|
|
93
|
-
* const compressedStream = readable.pipeThrough(createGzipEncoder());
|
|
94
|
-
*
|
|
95
|
-
* // Add entries...
|
|
96
|
-
* const fileStream = controller.add({ name: "file.txt", size: 5, type: "file" });
|
|
97
|
-
* const writer = fileStream.getWriter();
|
|
98
|
-
* await writer.write(new TextEncoder().encode("hello"));
|
|
99
|
-
* await writer.close();
|
|
100
|
-
* controller.finalize();
|
|
101
|
-
*
|
|
102
|
-
* // Upload compressed .tar.gz
|
|
103
|
-
* await fetch('/api/upload', {
|
|
104
|
-
* method: 'POST',
|
|
105
|
-
* body: compressedStream,
|
|
106
|
-
* headers: { 'Content-Type': 'application/gzip' }
|
|
107
|
-
* });
|
|
108
|
-
* ```
|
|
109
|
-
*/
|
|
110
|
-
declare function createGzipEncoder(): CompressionStream;
|
|
111
|
-
/**
|
|
112
|
-
* Creates a gzip decompression stream using the native
|
|
113
|
-
* [`DecompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) API.
|
|
114
|
-
*
|
|
115
|
-
* @returns A [`DecompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) configured for gzip decompression
|
|
116
|
-
* @example
|
|
117
|
-
* ```typescript
|
|
118
|
-
* import { createGzipDecoder, createTarDecoder } from '@modern-tar/core';
|
|
119
|
-
*
|
|
120
|
-
* // Download and process a .tar.gz file
|
|
121
|
-
* const response = await fetch('https://api.example.com/archive.tar.gz');
|
|
122
|
-
* if (!response.body) throw new Error('No response body');
|
|
123
|
-
*
|
|
124
|
-
* // Chain decompression and tar parsing
|
|
125
|
-
* const entries = response.body
|
|
126
|
-
* .pipeThrough(createGzipDecoder())
|
|
127
|
-
* .pipeThrough(createTarDecoder());
|
|
128
|
-
*
|
|
129
|
-
* for await (const entry of entries) {
|
|
130
|
-
* console.log(`Extracted: ${entry.header.name}`);
|
|
131
|
-
* // Process entry.body ReadableStream as needed
|
|
132
|
-
* }
|
|
133
|
-
* ```
|
|
134
|
-
* @example
|
|
135
|
-
* ```typescript
|
|
136
|
-
* // Decompress local .tar.gz data
|
|
137
|
-
* const gzippedData = new Uint8Array([...]); // your gzipped tar data
|
|
138
|
-
* const stream = new ReadableStream({
|
|
139
|
-
* start(controller) {
|
|
140
|
-
* controller.enqueue(gzippedData);
|
|
141
|
-
* controller.close();
|
|
142
|
-
* }
|
|
143
|
-
* });
|
|
144
|
-
*
|
|
145
|
-
* const tarStream = stream.pipeThrough(createGzipDecoder());
|
|
146
|
-
* // Now process tarStream with createTarDecoder()...
|
|
147
|
-
* ```
|
|
148
|
-
*/
|
|
149
|
-
declare function createGzipDecoder(): DecompressionStream;
|
|
150
|
-
//#endregion
|
|
151
151
|
//#region src/web/helpers.d.ts
|
|
152
152
|
/**
|
|
153
153
|
* Packs an array of tar entries into a single `Uint8Array` buffer.
|
package/dist/web/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { ParsedTarEntry, ParsedTarEntryWithData, TarEntry, TarEntryData, TarHeader, TarPackController, UnpackOptions, createGzipDecoder, createGzipEncoder, createTarDecoder, createTarOptionsTransformer, createTarPacker, packTar, unpackTar } from "../index-
|
|
1
|
+
import { ParsedTarEntry, ParsedTarEntryWithData, TarEntry, TarEntryData, TarHeader, TarPackController, UnpackOptions, createGzipDecoder, createGzipEncoder, createTarDecoder, createTarOptionsTransformer, createTarPacker, packTar, unpackTar } from "../index-Dx4tbuJh.js";
|
|
2
2
|
export { ParsedTarEntry, ParsedTarEntryWithData, TarEntry, TarEntryData, TarHeader, TarPackController, UnpackOptions, createGzipDecoder, createGzipEncoder, createTarDecoder, createTarOptionsTransformer, createTarPacker, packTar, unpackTar };
|
package/dist/web/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { createGzipDecoder, createGzipEncoder, createTarDecoder, createTarOptionsTransformer, createTarPacker, packTar, unpackTar } from "../web-
|
|
1
|
+
import { createGzipDecoder, createGzipEncoder, createTarDecoder, createTarOptionsTransformer, createTarPacker, packTar, unpackTar } from "../web-C1b5fAZt.js";
|
|
2
2
|
|
|
3
3
|
export { createGzipDecoder, createGzipEncoder, createTarDecoder, createTarOptionsTransformer, createTarPacker, packTar, unpackTar };
|
|
@@ -1,3 +1,154 @@
|
|
|
1
|
+
//#region src/web/compression.ts
|
|
2
|
+
/**
|
|
3
|
+
* Creates a gzip compression stream using the native
|
|
4
|
+
* [`CompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream) API.
|
|
5
|
+
*
|
|
6
|
+
* @returns A [`CompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream) configured for gzip compression
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createGzipEncoder, createTarPacker } from '@modern-tar/core';
|
|
10
|
+
*
|
|
11
|
+
* // Create and compress a tar archive
|
|
12
|
+
* const { readable, controller } = createTarPacker();
|
|
13
|
+
* const compressedStream = readable.pipeThrough(createGzipEncoder());
|
|
14
|
+
*
|
|
15
|
+
* // Add entries...
|
|
16
|
+
* const fileStream = controller.add({ name: "file.txt", size: 5, type: "file" });
|
|
17
|
+
* const writer = fileStream.getWriter();
|
|
18
|
+
* await writer.write(new TextEncoder().encode("hello"));
|
|
19
|
+
* await writer.close();
|
|
20
|
+
* controller.finalize();
|
|
21
|
+
*
|
|
22
|
+
* // Upload compressed .tar.gz
|
|
23
|
+
* await fetch('/api/upload', {
|
|
24
|
+
* method: 'POST',
|
|
25
|
+
* body: compressedStream,
|
|
26
|
+
* headers: { 'Content-Type': 'application/gzip' }
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
function createGzipEncoder() {
|
|
31
|
+
return new CompressionStream("gzip");
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Creates a gzip decompression stream using the native
|
|
35
|
+
* [`DecompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) API.
|
|
36
|
+
*
|
|
37
|
+
* @returns A [`DecompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) configured for gzip decompression
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* import { createGzipDecoder, createTarDecoder } from '@modern-tar/core';
|
|
41
|
+
*
|
|
42
|
+
* // Download and process a .tar.gz file
|
|
43
|
+
* const response = await fetch('https://api.example.com/archive.tar.gz');
|
|
44
|
+
* if (!response.body) throw new Error('No response body');
|
|
45
|
+
*
|
|
46
|
+
* // Chain decompression and tar parsing
|
|
47
|
+
* const entries = response.body
|
|
48
|
+
* .pipeThrough(createGzipDecoder())
|
|
49
|
+
* .pipeThrough(createTarDecoder());
|
|
50
|
+
*
|
|
51
|
+
* for await (const entry of entries) {
|
|
52
|
+
* console.log(`Extracted: ${entry.header.name}`);
|
|
53
|
+
* // Process entry.body ReadableStream as needed
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* // Decompress local .tar.gz data
|
|
59
|
+
* const gzippedData = new Uint8Array([...]); // your gzipped tar data
|
|
60
|
+
* const stream = new ReadableStream({
|
|
61
|
+
* start(controller) {
|
|
62
|
+
* controller.enqueue(gzippedData);
|
|
63
|
+
* controller.close();
|
|
64
|
+
* }
|
|
65
|
+
* });
|
|
66
|
+
*
|
|
67
|
+
* const tarStream = stream.pipeThrough(createGzipDecoder());
|
|
68
|
+
* // Now process tarStream with createTarDecoder()...
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
function createGzipDecoder() {
|
|
72
|
+
return new DecompressionStream("gzip");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/web/options.ts
|
|
77
|
+
/**
|
|
78
|
+
* Creates a transform stream that applies {@link UnpackOptions} to tar entries.
|
|
79
|
+
*
|
|
80
|
+
* @param options - The unpacking options to apply
|
|
81
|
+
* @returns A TransformStream that processes {@link ParsedTarEntry} objects
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* import { createTarDecoder, createTarOptionsTransformer } from '@modern-tar/core';
|
|
86
|
+
*
|
|
87
|
+
* const transformedStream = sourceStream
|
|
88
|
+
* .pipeThrough(createTarDecoder())
|
|
89
|
+
* .pipeThrough(createTarOptionsTransformer({
|
|
90
|
+
* strip: 1,
|
|
91
|
+
* filter: (header) => header.name.endsWith('.txt'),
|
|
92
|
+
* map: (header) => ({ ...header, mode: 0o644 })
|
|
93
|
+
* }));
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
function createTarOptionsTransformer(options = {}) {
|
|
97
|
+
return new TransformStream({ async transform(entry, controller) {
|
|
98
|
+
let header = entry.header;
|
|
99
|
+
if (options.strip !== void 0) {
|
|
100
|
+
if (options.strip < 0) {
|
|
101
|
+
drainStream(entry.body);
|
|
102
|
+
throw new Error(`Invalid strip value: ${options.strip}. Must be non-negative.`);
|
|
103
|
+
}
|
|
104
|
+
if (options.strip > 0) {
|
|
105
|
+
const strippedComponents = header.name.split("/").filter((component) => component.length > 0).slice(options.strip);
|
|
106
|
+
if (strippedComponents.length === 0) {
|
|
107
|
+
drainStream(entry.body);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const strippedName = strippedComponents.join("/");
|
|
111
|
+
if (header.type === "directory" && !strippedName.endsWith("/")) header = {
|
|
112
|
+
...header,
|
|
113
|
+
name: `${strippedName}/`
|
|
114
|
+
};
|
|
115
|
+
else header = {
|
|
116
|
+
...header,
|
|
117
|
+
name: strippedName
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (options.filter && options.filter(header) === false) {
|
|
122
|
+
drainStream(entry.body);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (options.map) header = options.map(header);
|
|
126
|
+
controller.enqueue({
|
|
127
|
+
header,
|
|
128
|
+
body: entry.body
|
|
129
|
+
});
|
|
130
|
+
} });
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Drains the stream asynchronously without blocking the transform stream.
|
|
134
|
+
*/
|
|
135
|
+
function drainStream(stream) {
|
|
136
|
+
(async () => {
|
|
137
|
+
const reader = stream.getReader();
|
|
138
|
+
try {
|
|
139
|
+
while (true) {
|
|
140
|
+
const { done } = await reader.read();
|
|
141
|
+
if (done) break;
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.debug("Stream drain error (non-critical):", error);
|
|
145
|
+
} finally {
|
|
146
|
+
reader.releaseLock();
|
|
147
|
+
}
|
|
148
|
+
})();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
1
152
|
//#region src/web/constants.ts
|
|
2
153
|
/** Size of a TAR block in bytes. */
|
|
3
154
|
const BLOCK_SIZE = 512;
|
|
@@ -227,6 +378,32 @@ function createTarPacker() {
|
|
|
227
378
|
controller: {
|
|
228
379
|
add(header) {
|
|
229
380
|
const size = header.type === "directory" || header.type === "symlink" || header.type === "link" ? 0 : header.size ?? 0;
|
|
381
|
+
if (header.pax) {
|
|
382
|
+
let paxRecords = "";
|
|
383
|
+
for (const [key, value] of Object.entries(header.pax)) {
|
|
384
|
+
const record = `${key}=${value}\n`;
|
|
385
|
+
let length = record.length + 1;
|
|
386
|
+
const lengthStr = String(length);
|
|
387
|
+
length += lengthStr.length;
|
|
388
|
+
const finalLengthStr = String(length);
|
|
389
|
+
if (finalLengthStr.length !== lengthStr.length) length += finalLengthStr.length - lengthStr.length;
|
|
390
|
+
paxRecords += `${length} ${record}`;
|
|
391
|
+
}
|
|
392
|
+
if (paxRecords) {
|
|
393
|
+
const paxBytes = encoder.encode(paxRecords);
|
|
394
|
+
const paxHeader = createTarHeader({
|
|
395
|
+
name: `PaxHeader/${header.name}`,
|
|
396
|
+
size: paxBytes.length,
|
|
397
|
+
type: "pax-header",
|
|
398
|
+
mode: 420,
|
|
399
|
+
mtime: header.mtime
|
|
400
|
+
});
|
|
401
|
+
streamController.enqueue(paxHeader);
|
|
402
|
+
streamController.enqueue(paxBytes);
|
|
403
|
+
const paxPadding = (BLOCK_SIZE - paxBytes.length % BLOCK_SIZE) % BLOCK_SIZE;
|
|
404
|
+
if (paxPadding > 0) streamController.enqueue(new Uint8Array(paxPadding));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
230
407
|
const headerBlock = createTarHeader({
|
|
231
408
|
...header,
|
|
232
409
|
size
|
|
@@ -268,157 +445,6 @@ function createTarPacker() {
|
|
|
268
445
|
};
|
|
269
446
|
}
|
|
270
447
|
|
|
271
|
-
//#endregion
|
|
272
|
-
//#region src/web/compression.ts
|
|
273
|
-
/**
|
|
274
|
-
* Creates a gzip compression stream using the native
|
|
275
|
-
* [`CompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream) API.
|
|
276
|
-
*
|
|
277
|
-
* @returns A [`CompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream) configured for gzip compression
|
|
278
|
-
* @example
|
|
279
|
-
* ```typescript
|
|
280
|
-
* import { createGzipEncoder, createTarPacker } from '@modern-tar/core';
|
|
281
|
-
*
|
|
282
|
-
* // Create and compress a tar archive
|
|
283
|
-
* const { readable, controller } = createTarPacker();
|
|
284
|
-
* const compressedStream = readable.pipeThrough(createGzipEncoder());
|
|
285
|
-
*
|
|
286
|
-
* // Add entries...
|
|
287
|
-
* const fileStream = controller.add({ name: "file.txt", size: 5, type: "file" });
|
|
288
|
-
* const writer = fileStream.getWriter();
|
|
289
|
-
* await writer.write(new TextEncoder().encode("hello"));
|
|
290
|
-
* await writer.close();
|
|
291
|
-
* controller.finalize();
|
|
292
|
-
*
|
|
293
|
-
* // Upload compressed .tar.gz
|
|
294
|
-
* await fetch('/api/upload', {
|
|
295
|
-
* method: 'POST',
|
|
296
|
-
* body: compressedStream,
|
|
297
|
-
* headers: { 'Content-Type': 'application/gzip' }
|
|
298
|
-
* });
|
|
299
|
-
* ```
|
|
300
|
-
*/
|
|
301
|
-
function createGzipEncoder() {
|
|
302
|
-
return new CompressionStream("gzip");
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Creates a gzip decompression stream using the native
|
|
306
|
-
* [`DecompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) API.
|
|
307
|
-
*
|
|
308
|
-
* @returns A [`DecompressionStream`](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) configured for gzip decompression
|
|
309
|
-
* @example
|
|
310
|
-
* ```typescript
|
|
311
|
-
* import { createGzipDecoder, createTarDecoder } from '@modern-tar/core';
|
|
312
|
-
*
|
|
313
|
-
* // Download and process a .tar.gz file
|
|
314
|
-
* const response = await fetch('https://api.example.com/archive.tar.gz');
|
|
315
|
-
* if (!response.body) throw new Error('No response body');
|
|
316
|
-
*
|
|
317
|
-
* // Chain decompression and tar parsing
|
|
318
|
-
* const entries = response.body
|
|
319
|
-
* .pipeThrough(createGzipDecoder())
|
|
320
|
-
* .pipeThrough(createTarDecoder());
|
|
321
|
-
*
|
|
322
|
-
* for await (const entry of entries) {
|
|
323
|
-
* console.log(`Extracted: ${entry.header.name}`);
|
|
324
|
-
* // Process entry.body ReadableStream as needed
|
|
325
|
-
* }
|
|
326
|
-
* ```
|
|
327
|
-
* @example
|
|
328
|
-
* ```typescript
|
|
329
|
-
* // Decompress local .tar.gz data
|
|
330
|
-
* const gzippedData = new Uint8Array([...]); // your gzipped tar data
|
|
331
|
-
* const stream = new ReadableStream({
|
|
332
|
-
* start(controller) {
|
|
333
|
-
* controller.enqueue(gzippedData);
|
|
334
|
-
* controller.close();
|
|
335
|
-
* }
|
|
336
|
-
* });
|
|
337
|
-
*
|
|
338
|
-
* const tarStream = stream.pipeThrough(createGzipDecoder());
|
|
339
|
-
* // Now process tarStream with createTarDecoder()...
|
|
340
|
-
* ```
|
|
341
|
-
*/
|
|
342
|
-
function createGzipDecoder() {
|
|
343
|
-
return new DecompressionStream("gzip");
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
//#endregion
|
|
347
|
-
//#region src/web/options.ts
|
|
348
|
-
/**
|
|
349
|
-
* Creates a transform stream that applies {@link UnpackOptions} to tar entries.
|
|
350
|
-
*
|
|
351
|
-
* @param options - The unpacking options to apply
|
|
352
|
-
* @returns A TransformStream that processes {@link ParsedTarEntry} objects
|
|
353
|
-
*
|
|
354
|
-
* @example
|
|
355
|
-
* ```typescript
|
|
356
|
-
* import { createTarDecoder, createTarOptionsTransformer } from '@modern-tar/core';
|
|
357
|
-
*
|
|
358
|
-
* const transformedStream = sourceStream
|
|
359
|
-
* .pipeThrough(createTarDecoder())
|
|
360
|
-
* .pipeThrough(createTarOptionsTransformer({
|
|
361
|
-
* strip: 1,
|
|
362
|
-
* filter: (header) => header.name.endsWith('.txt'),
|
|
363
|
-
* map: (header) => ({ ...header, mode: 0o644 })
|
|
364
|
-
* }));
|
|
365
|
-
* ```
|
|
366
|
-
*/
|
|
367
|
-
function createTarOptionsTransformer(options = {}) {
|
|
368
|
-
return new TransformStream({ async transform(entry, controller) {
|
|
369
|
-
let header = entry.header;
|
|
370
|
-
if (options.strip !== void 0) {
|
|
371
|
-
if (options.strip < 0) {
|
|
372
|
-
drainStream(entry.body);
|
|
373
|
-
throw new Error(`Invalid strip value: ${options.strip}. Must be non-negative.`);
|
|
374
|
-
}
|
|
375
|
-
if (options.strip > 0) {
|
|
376
|
-
const strippedComponents = header.name.split("/").filter((component) => component.length > 0).slice(options.strip);
|
|
377
|
-
if (strippedComponents.length === 0) {
|
|
378
|
-
drainStream(entry.body);
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
const strippedName = strippedComponents.join("/");
|
|
382
|
-
if (header.type === "directory" && !strippedName.endsWith("/")) header = {
|
|
383
|
-
...header,
|
|
384
|
-
name: `${strippedName}/`
|
|
385
|
-
};
|
|
386
|
-
else header = {
|
|
387
|
-
...header,
|
|
388
|
-
name: strippedName
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
if (options.filter && options.filter(header) === false) {
|
|
393
|
-
drainStream(entry.body);
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
if (options.map) header = options.map(header);
|
|
397
|
-
controller.enqueue({
|
|
398
|
-
header,
|
|
399
|
-
body: entry.body
|
|
400
|
-
});
|
|
401
|
-
} });
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Drains the stream asynchronously without blocking the transform stream.
|
|
405
|
-
*/
|
|
406
|
-
function drainStream(stream) {
|
|
407
|
-
(async () => {
|
|
408
|
-
const reader = stream.getReader();
|
|
409
|
-
try {
|
|
410
|
-
while (true) {
|
|
411
|
-
const { done } = await reader.read();
|
|
412
|
-
if (done) break;
|
|
413
|
-
}
|
|
414
|
-
} catch (error) {
|
|
415
|
-
console.debug("Stream drain error (non-critical):", error);
|
|
416
|
-
} finally {
|
|
417
|
-
reader.releaseLock();
|
|
418
|
-
}
|
|
419
|
-
})();
|
|
420
|
-
}
|
|
421
|
-
|
|
422
448
|
//#endregion
|
|
423
449
|
//#region src/web/stream.ts
|
|
424
450
|
function parseHeader(block) {
|
|
@@ -551,7 +577,11 @@ function createTarDecoder() {
|
|
|
551
577
|
buffer = combined.subarray(offset);
|
|
552
578
|
},
|
|
553
579
|
flush(controller) {
|
|
554
|
-
if (currentEntry)
|
|
580
|
+
if (currentEntry) {
|
|
581
|
+
const error = /* @__PURE__ */ new Error("Tar archive is truncated.");
|
|
582
|
+
currentEntry.controller.error(error);
|
|
583
|
+
controller.error(error);
|
|
584
|
+
}
|
|
555
585
|
if (buffer.some((b) => b !== 0)) controller.error(/* @__PURE__ */ new Error("Unexpected data at end of archive."));
|
|
556
586
|
}
|
|
557
587
|
});
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modern-tar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Zero dependency streaming tar parser and writer for JavaScript.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
8
|
+
"main": "./dist/web/index.js",
|
|
9
|
+
"module": "./dist/web/index.js",
|
|
10
|
+
"types": "./dist/web/index.d.ts",
|
|
8
11
|
"exports": {
|
|
9
12
|
"./package.json": "./package.json",
|
|
10
13
|
".": {
|