@gyng/remote-zip 0.2.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/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2021 AIcadium
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # remote-zip
2
+
3
+ [API documentation](https://gyng.github.io/remote-zip)
4
+
5
+ Fetch file listings and individual files from a remote ZIP file.
6
+
7
+ ## Features
8
+
9
+ Without downloading the entire ZIP:
10
+
11
+ - Fetch individual files in a remote ZIP
12
+ - Fetch file listings
13
+
14
+ The gist of what the library does is:
15
+
16
+ 1. Get the content size of the ZIP file using a HTTP HEAD request.
17
+ 2. Get the last 100 bytes using HTTP Range queries (hoping it's enough) to read the end-of-central-directory (EOCD) section.
18
+ 3. Using the EOCD, read the central directory (CD) section with a Range request. This section contains a complete file listing and file byte offsets in the ZIP.
19
+ 4. To get individual files, use another Range request with an offset to get the local file header + compressed data.
20
+
21
+ ## Limitations
22
+
23
+ - No ZIP64 support
24
+ - No encrypted ZIP support
25
+ - No stream support via `ReadableStream` due to testing/dev difficulties
26
+ - Long comments in a ZIP file might cause `populate()` to fail
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ yarn add @gyng/remote-zip
32
+ ```
33
+
34
+ ```bash
35
+ npm install --save @gyng/remote-zip
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ See the [generated API documentation](https://gyng.github.io/remote-zip/).
41
+
42
+ If using in the browser, the server will need to whitelist CORS for `GET`, `HEAD`, and the `Range` header.
43
+
44
+ ### Basic
45
+
46
+ ```ts
47
+ const url = new URL("http://www.example.com/test.zip");
48
+ const remoteZip = await new RemoteZipPointer({ url }).populate();
49
+ const fileListing = remoteZip.files(); // RemoteZipFile[]
50
+ const uncompressedBytes = await remoteZip.fetch("test.txt"); // ArrayBuffer
51
+ ```
52
+
53
+ ### With more features
54
+
55
+ ```ts
56
+ const method = "POST";
57
+ const additonalHeaders = new Headers();
58
+ additonalHeaders.append("X-Example", "foobar");
59
+ const url = new URL("http://www.example.com/test.zip");
60
+ const remoteZip = await new RemoteZipPointer({
61
+ url,
62
+ additionalHeaders,
63
+ method,
64
+ credentials: "include",
65
+ }).populate();
66
+ const uncompressedBytes = await remoteZip.fetch("test.txt", additionalHeaders);
67
+ ```
68
+
69
+ ## Dev
70
+
71
+ <details>
72
+ <summary>Dev instructions</summary>
73
+ See `scripts` in `package.json` for more scripts.
74
+
75
+ - `yarn d` watch and build
76
+ - `yarn t:watch` watch and test
77
+ - `yarn lint`
78
+ - `yarn build`
79
+
80
+ Run tests and checks with Docker
81
+
82
+ ```
83
+ docker-compose -f docker-compose.test.yml up --build
84
+ ```
85
+
86
+ ### Publish
87
+
88
+ #### Setup
89
+
90
+ 1. Get an automation token from npm under settings
91
+
92
+ ```
93
+ https://www.npmjs.com/settings/aicadium/tokens/
94
+ ```
95
+
96
+ 2. Add the token to your repository secrets.
97
+
98
+ ```
99
+ https://github.com/$YOUR_USERNAME/$YOUR_REPO_NAME/settings/secrets/actions/new
100
+ ```
101
+
102
+ - Name: `NPM_TOKEN`
103
+ - Value: The automation token you got from the previous step
104
+
105
+ #### Run
106
+
107
+ 1. Create a new release.
108
+
109
+ ```
110
+ https://github.com/$YOUR_USERNAME/$YOUR_REPO_NAME/releases
111
+ ```
112
+
113
+ The workflow at `./github/workflows/publish.yml` should run and publish your packages to both NPM and GitHub Packages.
114
+
115
+ Don't forget to bump your version number in `package.json` before this.
116
+ </details>
@@ -0,0 +1 @@
1
+ export * from "./zip";
@@ -0,0 +1,330 @@
1
+ export declare class RemoteZipError extends Error {
2
+ constructor(message: any);
3
+ }
4
+ export interface EndOfCentralDirectory {
5
+ meta: Record<string, unknown>;
6
+ data: {
7
+ /** End of central directory signature = 0x06054b50 */
8
+ signature: ArrayBuffer;
9
+ /** Number of this disk (or 0xffff for ZIP64) */
10
+ diskNumber: number;
11
+ /** Disk where central directory starts (or 0xffff for ZIP64) */
12
+ cdDisk: number;
13
+ /** Number of central directory records on this disk (or 0xffff for ZIP64) */
14
+ centralDirectoryDiskNumber: number;
15
+ /** Number of central directory records on this disk (or 0xffff for ZIP64) */
16
+ centralDirectoryRecordCount: number;
17
+ /** Size of central directory (bytes) (or 0xffffffff for ZIP64) */
18
+ centralDirectoryByteSize: number;
19
+ /** Offset of start of central directory, relative to start of archive (or 0xffffffff for ZIP64) */
20
+ centralDirectoryByteOffset: number;
21
+ /** Comment length (n) */
22
+ comment: string;
23
+ /** Comment */
24
+ commentLength: number;
25
+ };
26
+ }
27
+ export interface CentralDirectoryRecord {
28
+ meta: {
29
+ /** Length of the entire record */
30
+ length: number;
31
+ };
32
+ data: {
33
+ /** Central directory file header signature = 0x02014b50 */
34
+ signature: ArrayBuffer;
35
+ /** Version of the program this ZIP was made by.
36
+ *
37
+ * Upper byte:
38
+ * - 0 - MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems)
39
+ * - 1 - Amiga
40
+ * - 2 - OpenVMS
41
+ * - 3 - UNIX
42
+ * - 4 - VM/CMS
43
+ * - 5 - Atari ST
44
+ * - 6 - OS/2 H.P.F.S.
45
+ * - 7 - Macintosh
46
+ * - 8 - Z-System
47
+ * - 9 - CP/M
48
+ * - 10 - Windows NTFS
49
+ * - 11 - MVS (OS/390 - Z/OS)
50
+ * - 12 - VSE
51
+ * - 13 - Acorn Risc
52
+ * - 14 - VFAT
53
+ * - 15 - alternate MVS
54
+ * - 16 - BeOS
55
+ * - 17 - Tandem
56
+ * - 18 - OS/400
57
+ * - 19 - OS/X (Darwin)
58
+ * - 20 - 255: Unused
59
+ */
60
+ versionMadeBy: number;
61
+ /** Version needed to extract (minimum) */
62
+ versionToExtract: number;
63
+ /** General purpose bit flag
64
+ *
65
+ * - Bit 00: encrypted file
66
+ * - Bit 01: compression option
67
+ * - Bit 02: compression option
68
+ * - Bit 03: data descriptor
69
+ * - Bit 04: enhanced deflation
70
+ * - Bit 05: compressed patched data
71
+ * - Bit 06: strong encryption
72
+ * - Bit 07-10: unused
73
+ * - Bit 11: language encoding
74
+ * - Bit 12: reserved
75
+ * - Bit 13: mask header values
76
+ * - Bit 14-15: reserved
77
+ */
78
+ generalPurposeBitFlag: number;
79
+ /** Compression method; e.g. none = 0, DEFLATE = 8 (or "\0x08\0x00")
80
+ *
81
+ * - 00: no compression
82
+ * - 01: shrunk
83
+ * - 02: reduced with compression factor 1
84
+ * - 03: reduced with compression factor 2
85
+ * - 04: reduced with compression factor 3
86
+ * - 05: reduced with compression factor 4
87
+ * - 06: imploded
88
+ * - 07: reserved
89
+ * - 08: deflated
90
+ * - 09: enhanced deflated
91
+ * - 10: PKWare DCL imploded
92
+ * - 11: reserved
93
+ * - 12: compressed using BZIP2
94
+ * - 13: reserved
95
+ * - 14: LZMA
96
+ * - 15-17: reserved
97
+ * - 18: compressed using IBM TERSE
98
+ * - 19: IBM LZ77 z
99
+ * - 98: PPMd version I, Rev 1
100
+ */
101
+ compressionMethod: number;
102
+ /** File last modification time (DOS) */
103
+ lastModifiedTime: number;
104
+ /** File last modification date (DOS) */
105
+ lastModifiedDate: number;
106
+ /** CRC-32 of uncompressed data
107
+ * Value computed over file data by CRC-32 algorithm with
108
+ * 'magic number' 0xdebb20e3 (little endian)
109
+ */
110
+ crc32: number;
111
+ /** Compressed size (or 0xffffffff for ZIP64) */
112
+ compressedSize: number;
113
+ /** Uncompressed size (or 0xffffffff for ZIP64) */
114
+ uncompressedSize: number;
115
+ /** File name length (n) */
116
+ filenameLength: number;
117
+ /** Extra field length (m) */
118
+ extraFieldLength: number;
119
+ /** File comment length (k) */
120
+ fileCommentLength: number;
121
+ /** Disk number where file starts */
122
+ startingDiskNumber: number;
123
+ /** Internal file attributes
124
+ *
125
+ * Bit 0: apparent ASCII/text file
126
+ * Bit 1: reserved
127
+ * Bit 2: control field records precede logical records
128
+ * Bits 3-16: unused
129
+ */
130
+ internalFileAttributes: number;
131
+ /** External file attributes (host-system dependent) */
132
+ externalFileAttributes: number;
133
+ /** Relative offset of local file header.
134
+ * This is the number of bytes between the start of the first
135
+ * disk on which the file occurs, and the start of the local file
136
+ * header. This allows software reading the central directory to
137
+ * locate the position of the file inside the ZIP file. */
138
+ localFileHeaderRelativeOffset: number;
139
+ /** File name */
140
+ filename: string;
141
+ /** Extra field
142
+ *
143
+ * Used to store additional information. The field consistes of a sequence of
144
+ * header and data pairs, where the header has a 2 byte identifier and a 2 byte data size field.
145
+ */
146
+ extraField: ArrayBuffer;
147
+ /** File comment */
148
+ fileComment: string;
149
+ };
150
+ }
151
+ export interface LocalFileHeader {
152
+ meta: {
153
+ compressedData: ArrayBuffer;
154
+ /** If the bit at offset 3 (0x08) of the general-purpose flags field is set,
155
+ * then the CRC-32 and file sizes are not known when the header is written.
156
+ * The fields in the local header are filled with zero, and the CRC-32 and size are
157
+ * appended in a 12-byte structure (optionally preceded by a 4-byte signature) immediately
158
+ * after the compressed data */
159
+ dataDescriptor?: {
160
+ /** Optional data descriptor signature = 0x08074b50 */
161
+ optionalSignature?: ArrayBuffer;
162
+ /** CRC-32 of uncompressed data */
163
+ crc32: number;
164
+ /** Compressed size */
165
+ compressedSize: number;
166
+ /** Uncompressed size */
167
+ uncompressedSize: number;
168
+ };
169
+ };
170
+ data: {
171
+ /** Local file header signature = 0x04034b50 (PK♥♦ or "PK\3\4") */
172
+ signature: ArrayBuffer;
173
+ /** Version needed to extract (minimum) */
174
+ versionToExtract: number;
175
+ /** General purpose bit flag */
176
+ generalPurposeBitFlag: number;
177
+ /** Compression method; e.g. none = 0, DEFLATE = 8 (or "\0x08\0x00") */
178
+ compressionMethod: number;
179
+ /** File last modification time (DOS format) */
180
+ lastModifiedTime: number;
181
+ /** File last modification date (DOS format) */
182
+ lastModifiedDate: number;
183
+ /** CRC-32 of uncompressed data
184
+ * Value computed over file data by CRC-32 algorithm with
185
+ * 'magic number' 0xdebb20e3 (little endian)
186
+ */
187
+ crc32: number;
188
+ /** Compressed size (or 0xffffffff for ZIP64) */
189
+ compressedSize: number;
190
+ /** Uncompressed size (or 0xffffffff for ZIP64) */
191
+ uncompressedSize: number;
192
+ /** File name length (n) */
193
+ filenameLength: number;
194
+ /** Extra field length (m) */
195
+ extraFieldLength: number;
196
+ /** File name */
197
+ filename: string;
198
+ /** Extra field */
199
+ extraField: ArrayBuffer;
200
+ };
201
+ }
202
+ /**
203
+ * A friendly representation of a file inside a ZIP archive
204
+ */
205
+ export interface RemoteZipFile {
206
+ /** Full path of the file inside the archive */
207
+ filename: string;
208
+ /** Size in bytes */
209
+ size: number;
210
+ /** ISO timestamp without timezone (ZIP/DOS does not preserve timezones) */
211
+ modified: string;
212
+ /** File attributes of host system */
213
+ attributes: number;
214
+ }
215
+ /**
216
+ * An initialised object representating a remote ZIP archive.
217
+ *
218
+ * Best constructed from a `RemoteZipPointer`.
219
+ *
220
+ * ```ts
221
+ * import { RemoteZipPointer } from "remote-zip";
222
+ *
223
+ * const url = new URL("http://www.example.com/test.zip");
224
+ * const remoteZip = await new RemoteZipPointer({ url }).populate();
225
+ * ```
226
+ */
227
+ export declare class RemoteZip {
228
+ /** Size of the remote ZIP archive in bytes */
229
+ contentLength: number;
230
+ /** URL of the remote ZIP archive */
231
+ url: URL;
232
+ /** Records representing the files in the remote ZIP archive */
233
+ centralDirectoryRecords: CentralDirectoryRecord[];
234
+ /** Metadata of the remote ZIP archive */
235
+ endOfCentralDirectory: EndOfCentralDirectory | null;
236
+ /** HTTP method used to fetch files from the remote ZIP archive */
237
+ method: string;
238
+ /** Credentials passed to `fetch` when retrieving files. Defaults to `same-origin`. */
239
+ credentials: "include" | "omit" | "same-origin";
240
+ constructor({ contentLength, url, centralDirectoryRecords, endOfCentralDirectory, method, credentials, }: {
241
+ /** Length of the remote ZIP archive in bytes */
242
+ contentLength: number;
243
+ /** Passed to fetch when performing a HTTP GET request for the file */
244
+ url: URL;
245
+ centralDirectoryRecords: CentralDirectoryRecord[];
246
+ endOfCentralDirectory: EndOfCentralDirectory | null;
247
+ /** Passed to fetch when performing a HTTP GET request for the file */
248
+ method: string;
249
+ /** Passed to fetch when performing a HTTP GET request for the file. */
250
+ credentials: "include" | "omit" | "same-origin";
251
+ });
252
+ /**
253
+ * Get a formatted file listing of the remote ZIP archive.
254
+ *
255
+ * @returns List of files in the remote ZIP archive.
256
+ *
257
+ * ```ts
258
+ * import { RemoteZipPointer } from "remote-zip";
259
+ *
260
+ * const url = new URL("http://www.example.com/test.zip");
261
+ * const remoteZip = await new RemoteZipPointer({ url }).populate();
262
+ * const files = remoteZip.files();
263
+ * // files = [{ attributes: 1107099648, filename: "text.txt", modified: "2021-06-17T12:28:02", size: 14 }]
264
+ * ```
265
+ */
266
+ files(): RemoteZipFile[];
267
+ /**
268
+ * Gets a single uncompressed file in the remote ZIP archive.
269
+ *
270
+ * @param path Path of the file in the remote ZIP archive
271
+ * @param additionalHeaders Additional headers, if any, to be passed to the `fetch` request
272
+ * @returns Inflated (uncompressed) bytes of the requested file
273
+ * @throws [RemoteZipError](RemoteZipError) if it fails to parse or fetch
274
+ */
275
+ fetch(path: string, additionalHeaders?: Headers): Promise<ArrayBuffer>;
276
+ }
277
+ /**
278
+ * An uninitialised pointer to a remote ZIP file.
279
+ *
280
+ * No network requests are sent until `populate()` is called.
281
+ *
282
+ * ```ts
283
+ * const url = new URL("http://www.example.com/test.zip");
284
+ * const remoteZip = await new RemoteZipPointer({ url }).populate();
285
+ * const fileListing = remoteZip.files(); // RemoteZipFile[]
286
+ * const uncompressedBytes = await remoteZip.fetch("test.txt"); // ArrayBuffer
287
+ * ```
288
+ */
289
+ export declare class RemoteZipPointer {
290
+ /** URL of the remote ZIP archive */
291
+ url: URL;
292
+ /** URL used when performing the HTTP HEAD request to fetch ZIP metadata */
293
+ headUrl: URL;
294
+ /** Additional headers, if any, passed to `fetch` when calling `url` or `headUrl` */
295
+ additionalHeaders?: Headers;
296
+ /** HTTP method used to fetch ZIP metadata (the initial HEAD request is always sent) */
297
+ method: string;
298
+ /** Credentials passed to `fetch` when retrieving files. Defaults to `same-origin`. */
299
+ credentials: "include" | "omit" | "same-origin";
300
+ constructor({ url, headUrl, additionalHeaders, method, credentials, }: {
301
+ /** URL for GET requests */
302
+ url: URL;
303
+ /** Passed to fetch when performing a HTTP GET request for the file */
304
+ additionalHeaders?: Headers;
305
+ /** Passed to fetch when performing a HTTP GET request for the file */
306
+ method?: string;
307
+ /** Passed to fetch when performing a HTTP GET request for the file */
308
+ credentials?: "include" | "omit" | "same-origin";
309
+ /** URL for HEAD request. Defaults to `url`. This can, for example, differ from `url` if you are using a signed URL for S3. */
310
+ headUrl?: URL;
311
+ });
312
+ /**
313
+ * Gets metadata about the ZIP file and constructs an initialised `RemoteZip`.
314
+ *
315
+ * @returns An initialised [RemoteZip](RemoteZip)
316
+ * @throws [RemoteZipError](RemoteZipError) if it fails to parse or fetch
317
+ */
318
+ populate(): Promise<RemoteZip>;
319
+ private fetchEndOfCentralDirectory;
320
+ private fetchCentralDirectoryRecords;
321
+ }
322
+ /**
323
+ * Parses DOS datetime into an ISO string without timezone.
324
+ *
325
+ * @param zipDate DOS date format
326
+ * @param zipTime DOS time format
327
+ * @returns An ISO datetime without timezone. Defaults to `"1980-01-01T00:00:00"` if the datetime is invalid
328
+ * @see https://github.com/Stuk/jszip/blob/112fcdb9953c6b9a2744afee451d73029f7cd2f8/lib/reader/DataReader.js#L105
329
+ */
330
+ export declare const parseZipDatetime: (zipDate: number, zipTime: number) => string;
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@gyng/remote-zip",
3
+ "version": "0.2.5",
4
+ "main": "lib/cjs/index.js",
5
+ "module": "lib/esm/index.js",
6
+ "types": "lib/types/index.d.ts",
7
+ "author": "AIcadium, Ng Guoyou <gyng@users.noreply.github.com>",
8
+ "license": "MIT",
9
+ "files": [
10
+ "LICENSE",
11
+ "package.json",
12
+ "lib/**"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git://github.com/gyng/remote-zip.git"
17
+ },
18
+ "scripts": {
19
+ "build": "rm -rf lib/* && yarn ts-types && node ./esbuild.mjs",
20
+ "d:server": "http-server fixtures",
21
+ "d": "WATCH=1 yarn build",
22
+ "doc:gen": "yarn typedoc src/index.ts --excludePrivate --cleanOutputDir --logLevel Verbose --out ./docs/api",
23
+ "lint:eslint:fix": "yarn lint:eslint --fix",
24
+ "lint:eslint": "eslint . --ext .js --ext .jsx --ext .ts --ext .tsx",
25
+ "lint:fix": "yarn lint:prettier:fix && yarn lint:eslint:fix",
26
+ "lint:prettier:fix": "prettier --write .",
27
+ "lint:prettier": "prettier --check .",
28
+ "lint": "yarn lint:prettier && yarn lint:eslint",
29
+ "t:watch": "yarn test --watch",
30
+ "t": "yarn test",
31
+ "test:coverage": "yarn test --coverage",
32
+ "test": "jest",
33
+ "ts-types": " tsc --emitDeclarationOnly --outDir lib/types"
34
+ },
35
+ "devDependencies": {
36
+ "@types/http-server": "^0.12.1",
37
+ "@types/jest": "^29.5.2",
38
+ "@types/node": "^20.3.1",
39
+ "@types/pako": "^2.0.0",
40
+ "@typescript-eslint/eslint-plugin": "^5.60.0",
41
+ "@typescript-eslint/parser": "^5.60.0",
42
+ "esbuild": "^0.18.7",
43
+ "esbuild-jest": "^0.5.0",
44
+ "eslint": "^8.43.0",
45
+ "eslint-config-prettier": "^8.8.0",
46
+ "eslint-plugin-jest": "^27.2.2",
47
+ "http-server": "^14.1.1",
48
+ "jest": "^29.5.0",
49
+ "prettier": "^2.8.8",
50
+ "typedoc": "^0.24.8",
51
+ "typescript": "^5.1.3"
52
+ },
53
+ "dependencies": {
54
+ "cross-fetch": "^3.1.6",
55
+ "pako": "^2.1.0"
56
+ }
57
+ }