Skip to content

Commit bd2b810

Browse files
committed
feat: zip reader file path glob filter and helpers (VF-000) (#77)
<!-- You can erase any parts of this template not applicable to your Pull Request. --> **Fixes or implements VF-000** ### Brief description. What is this change? - Adds a glob filter to file reader - Adds helper methods to iterator items to access file contents Co-authored-by: Tyler Stewart <[email protected]>
1 parent 99fd059 commit bd2b810

File tree

5 files changed

+52
-9
lines changed

5 files changed

+52
-9
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"jszip": "3.7.1",
2323
"lodash": "^4.17.11",
2424
"luxon": "^1.21.3",
25+
"minimatch": "5.0.1",
2526
"rate-limiter-flexible": "^2.2.2",
2627
"sinon": "^10.0.0"
2728
},

src/common/zip/reader.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { AnyRecord } from '@voiceflow/common';
12
import JSZip from 'jszip';
3+
import minimatch from 'minimatch';
24

35
import { isZipFile } from './guard';
46

@@ -34,6 +36,12 @@ export interface ZipReaderConfig {
3436
export interface ZipEntry {
3537
name: string;
3638
content: Uint8Array;
39+
toString: (decoder?: TextDecoder) => string;
40+
toJSON: <T = AnyRecord>(decoder?: TextDecoder) => T;
41+
}
42+
43+
export interface ZipGetFilesOptions {
44+
path: string;
3745
}
3846

3947
export class ZipReader {
@@ -48,12 +56,15 @@ export class ZipReader {
4856
};
4957
}
5058

51-
public async *getFiles(): AsyncGenerator<ZipEntry> {
52-
yield* this.getFilesRecursively(this.zip, this.config.maxZipRecursionDepth);
59+
public async *getFiles(options?: Partial<ZipGetFilesOptions>): AsyncGenerator<ZipEntry> {
60+
yield* this.getFilesRecursively(this.zip, {
61+
path: '**/*',
62+
...(options ?? {}),
63+
});
5364
}
5465

55-
private async *getFilesRecursively(zip: JSZip, maxDepth: number, currentDepth = 0): AsyncGenerator<ZipEntry> {
56-
const objects = zip.filter(() => true);
66+
private async *getFilesRecursively(zip: JSZip, config: ZipGetFilesOptions, currentDepth = 0): AsyncGenerator<ZipEntry> {
67+
const objects = zip.filter((_, file) => minimatch(file.name, config.path));
5768

5869
let fileCount = 0;
5970
let totalSize = 0;
@@ -75,14 +86,16 @@ export class ZipReader {
7586
throw new Error(`Total file size exceeded maximum (${this.config.maxUnzipSizeBytes} bytes)`);
7687
}
7788

78-
if (currentDepth < maxDepth && isZipFile(content)) {
89+
if (currentDepth < this.config.maxZipRecursionDepth && isZipFile(content)) {
7990
// eslint-disable-next-line no-await-in-loop
8091
const zip = await JSZip.loadAsync(content);
81-
yield* this.getFilesRecursively(zip, maxDepth, currentDepth + 1);
92+
yield* this.getFilesRecursively(zip, config, currentDepth + 1);
8293
} else {
8394
yield {
8495
name: obj.name,
8596
content,
97+
toString: (decoder = new TextDecoder()) => decoder.decode(content),
98+
toJSON: <T = AnyRecord>(decoder = new TextDecoder()) => JSON.parse(decoder.decode(content)) as T,
8699
};
87100
}
88101
}

tests/common/zip/reader.unit.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ describe('ZipReader', () => {
1919
zip = new JSZip();
2020
zip.file('file.json', JSON.stringify({ hello: 'world' }));
2121
zip.file('nested/file.zip', new JSZip().file('inside.json', JSON.stringify({ a: 1 })).generateNodeStream());
22+
zip.file('nested/other.json', JSON.stringify({ key: true }));
2223
});
2324

2425
afterEach(() => {
@@ -29,12 +30,16 @@ describe('ZipReader', () => {
2930
const reader = new ZipReader(zip);
3031
const files = await asyncArrayFrom(reader.getFiles());
3132

32-
expect(files.length).to.equal(2);
33+
expect(files.length).to.equal(3);
34+
3335
expect(files[0].name).to.equal('file.json');
3436
expect(new TextDecoder().decode(files[0].content)).to.equal(JSON.stringify({ hello: 'world' }));
3537

3638
expect(files[1].name).to.equal('inside.json');
3739
expect(new TextDecoder().decode(files[1].content)).to.equal(JSON.stringify({ a: 1 }));
40+
41+
expect(files[2].name).to.equal('nested/other.json');
42+
expect(new TextDecoder().decode(files[2].content)).to.equal(JSON.stringify({ key: true }));
3843
});
3944

4045
it('throws if total size exceeds limit', async () => {
@@ -55,8 +60,17 @@ describe('ZipReader', () => {
5560
const reader = new ZipReader(zip, { maxZipRecursionDepth: 0 });
5661
const files = await asyncArrayFrom(reader.getFiles());
5762

58-
expect(files.length).to.equal(2);
63+
expect(files.length).to.equal(3);
5964
expect(files[0].name).to.equal('file.json');
6065
expect(files[1].name).to.equal('nested/file.zip');
66+
expect(files[2].name).to.equal('nested/other.json');
67+
});
68+
69+
it('filters by path glob', async () => {
70+
const reader = new ZipReader(zip);
71+
const files = await asyncArrayFrom(reader.getFiles({ path: 'nested/*.json' }));
72+
73+
expect(files.length).to.equal(1);
74+
expect(files[0].name).to.equal('nested/other.json');
6175
});
6276
});

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"extends": "@voiceflow/tsconfig",
33
"compilerOptions": {
4-
"baseUrl": "./src"
4+
"baseUrl": "./src",
5+
"lib": ["DOM", "ES2020"]
56
},
67
"compileOnSave": false,
78
"include": [

yarn.lock

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1782,6 +1782,13 @@ brace-expansion@^1.1.7:
17821782
balanced-match "^1.0.0"
17831783
concat-map "0.0.1"
17841784

1785+
brace-expansion@^2.0.1:
1786+
version "2.0.1"
1787+
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
1788+
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
1789+
dependencies:
1790+
balanced-match "^1.0.0"
1791+
17851792
braces@^2.3.1:
17861793
version "2.3.2"
17871794
resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
@@ -5212,6 +5219,13 @@ [email protected], minimatch@^3.0.4:
52125219
dependencies:
52135220
brace-expansion "^1.1.7"
52145221

5222+
5223+
version "5.0.1"
5224+
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b"
5225+
integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==
5226+
dependencies:
5227+
brace-expansion "^2.0.1"
5228+
52155229
52165230
version "4.1.0"
52175231
resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"

0 commit comments

Comments
 (0)