66import { WritableStreamBuffer } from 'stream-buffers'
77import crypto from 'crypto'
88import { readFileAsString } from '../filesystemUtilities'
9- // Use require instead of import since this package doesn't support commonjs
10- const { ZipWriter, TextReader } = require ( '@zip.js/zip.js' )
9+ // // Use require instead of import since this package doesn't support commonjs
10+ // const { ZipWriter, TextReader } = require('@zip.js/zip.js')
11+ // @ts -ignore
12+ import { ZipWriter , TextReader } from '@zip.js/zip.js'
13+ import { getLogger } from '../logger/logger'
1114
1215export interface ZipStreamResult {
1316 sizeInBytes : number
14- md5 : string
17+ hash : string
1518 streamBuffer : WritableStreamBuffer
1619}
1720
21+ export type ZipStreamProps = {
22+ hashAlgorithm : 'md5' | 'sha256'
23+ maxNumberOfFileStreams : number
24+ compressionLevel : number
25+ }
26+
27+ const defaultProps : ZipStreamProps = {
28+ hashAlgorithm : 'sha256' ,
29+ maxNumberOfFileStreams : 100 ,
30+ compressionLevel : 1 ,
31+ }
32+
1833/**
1934 * Creates in-memory zip archives that output to a stream buffer.
2035 *
2136 * Example usage:
2237 * ```ts
23- * const zipStream = new ZipStream()
38+ * const zipStream = new ZipStream({
39+ hashAlgorithm: 'sha256',
40+ maxNumberOfFileStreams: 150,
41+ compressionLevel: 1,
42+ memLevel: 9,
43+ })
2444 * zipStream.writeString('Hello World', 'file1.txt')
2545 * zipStream.writeFile('/path/to/some/file.txt', 'file2.txt')
26- * const result = await zipStream.finalize()
27- * console.log(result) // { sizeInBytes: ..., md5 : ..., streamBuffer: ... }
46+ * const result = await zipStream.finalize([optional onProgress handler, called 1x per sec] )
47+ * console.log(result) // { sizeInBytes: ..., hash : ..., streamBuffer: ... }
2848 * ```
2949 */
3050export class ZipStream {
@@ -33,35 +53,86 @@ export class ZipStream {
3353 private _zipWriter : ZipWriter < WritableStream >
3454 private _streamBuffer : WritableStreamBuffer
3555 private _hasher : crypto . Hash
56+ private _numberOfFilesToStream : number = 0
57+ private _numberOfFilesSucceeded : number = 0
58+ private _filesToZip : [ string , string ] [ ] = [ ]
59+ private _filesBeingZipped : number = 0
60+ private _maxNumberOfFileStreams : number
3661
37- constructor ( ) {
38- this . _streamBuffer = new WritableStreamBuffer ( )
39- this . _hasher = crypto . createHash ( 'md5' )
62+ constructor ( props : Partial < ZipStreamProps > = { } ) {
63+ // Allow any user-provided values to override default values
64+ const mergedProps = { ...defaultProps , ...props }
65+ const { hashAlgorithm, compressionLevel, maxNumberOfFileStreams } = mergedProps
4066
4167 this . _zipWriter = new ZipWriter (
4268 new WritableStream ( {
4369 write : chunk => {
4470 this . _streamBuffer . write ( chunk )
4571 this . _hasher . update ( chunk )
72+ this . _numberOfFilesSucceeded ++
73+ this . _filesBeingZipped --
74+
75+ if ( this . _filesToZip . length > 0 && this . _filesBeingZipped < maxNumberOfFileStreams ) {
76+ this . _filesBeingZipped ++
77+ const [ fileToZip , path ] = this . _filesToZip . shift ( ) !
78+ void readFileAsString ( fileToZip ) . then ( content => {
79+ return this . _zipWriter . add ( path , new TextReader ( content ) )
80+ } )
81+ }
4682 } ,
47- } )
83+ } ) ,
84+ { level : compressionLevel }
4885 )
86+ this . _maxNumberOfFileStreams = maxNumberOfFileStreams
87+
88+ this . _streamBuffer = new WritableStreamBuffer ( )
89+
90+ this . _hasher = crypto . createHash ( hashAlgorithm )
4991 }
5092
51- public async writeString ( data : string , path : string ) {
93+ public writeString ( data : string , path : string ) {
5294 return this . _zipWriter . add ( path , new TextReader ( data ) )
5395 }
5496
55- public async writeFile ( file : string , path : string ) {
56- const content = await readFileAsString ( file )
57- return this . _zipWriter . add ( path , new TextReader ( content ) )
97+ public writeFile ( file : string , path : string ) {
98+ // We use _numberOfFilesToStream to make sure we don't finalize too soon
99+ // (before the progress event has been fired for the last file)
100+ // The problem is that we can't rely on progress.entries.total,
101+ // because files can be added to the queue faster
102+ // than the progress event is fired
103+ this . _numberOfFilesToStream ++
104+ // We only start zipping another file if we're under our limit
105+ // of concurrent file streams
106+ if ( this . _filesBeingZipped < this . _maxNumberOfFileStreams ) {
107+ this . _filesBeingZipped ++
108+ void readFileAsString ( file ) . then ( content => {
109+ return this . _zipWriter . add ( path , new TextReader ( content ) )
110+ } )
111+ } else {
112+ // Queue it for later (see "write" event)
113+ this . _filesToZip . push ( [ file , path ] )
114+ }
58115 }
59116
60- public async finalize ( ) : Promise < ZipStreamResult > {
117+ public async finalize ( onProgress ?: ( percentComplete : number ) => void ) : Promise < ZipStreamResult > {
118+ let finished = false
119+ // We need to poll to check for all the file streams to be completely processed
120+ // -- we are keeping track of this via the "progress" event handler
121+ while ( ! finished ) {
122+ finished = await new Promise ( resolve => {
123+ setTimeout ( ( ) => {
124+ getLogger ( ) . verbose ( 'success is' , this . _numberOfFilesSucceeded , '/' , this . _numberOfFilesToStream )
125+ onProgress ?.( Math . floor ( ( 100 * this . _numberOfFilesSucceeded ) / this . _numberOfFilesToStream ) )
126+ resolve ( this . _numberOfFilesToStream <= this . _numberOfFilesSucceeded )
127+ } , 1000 )
128+ } )
129+ }
130+ // We're done streaming all files, so we can close the zip stream
131+
61132 await this . _zipWriter . close ( )
62133 return {
63134 sizeInBytes : this . _streamBuffer . size ( ) ,
64- md5 : this . _hasher . digest ( 'base64' ) ,
135+ hash : this . _hasher . digest ( 'base64' ) ,
65136 streamBuffer : this . _streamBuffer ,
66137 }
67138 }
0 commit comments