@@ -8,23 +8,41 @@ import crypto from 'crypto'
88import { readFileAsString } from '../filesystemUtilities'
99// Use require instead of import since this package doesn't support commonjs
1010const { ZipWriter, TextReader } = require ( '@zip.js/zip.js' )
11+ import { getLogger } from '../logger/logger'
1112
1213export interface ZipStreamResult {
1314 sizeInBytes : number
14- md5 : string
15+ hash : string
1516 streamBuffer : WritableStreamBuffer
1617}
1718
19+ export type ZipStreamProps = {
20+ hashAlgorithm : 'md5' | 'sha256'
21+ maxNumberOfFileStreams : number
22+ compressionLevel : number
23+ }
24+
25+ const defaultProps : ZipStreamProps = {
26+ hashAlgorithm : 'sha256' ,
27+ maxNumberOfFileStreams : 100 ,
28+ compressionLevel : 1 ,
29+ }
30+
1831/**
1932 * Creates in-memory zip archives that output to a stream buffer.
2033 *
2134 * Example usage:
2235 * ```ts
23- * const zipStream = new ZipStream()
36+ * const zipStream = new ZipStream({
37+ hashAlgorithm: 'sha256',
38+ maxNumberOfFileStreams: 150,
39+ compressionLevel: 1,
40+ memLevel: 9,
41+ })
2442 * zipStream.writeString('Hello World', 'file1.txt')
2543 * zipStream.writeFile('/path/to/some/file.txt', 'file2.txt')
26- * const result = await zipStream.finalize()
27- * console.log(result) // { sizeInBytes: ..., md5 : ..., streamBuffer: ... }
44+ * const result = await zipStream.finalize([optional onProgress handler, called 1x per sec] )
45+ * console.log(result) // { sizeInBytes: ..., hash : ..., streamBuffer: ... }
2846 * ```
2947 */
3048export class ZipStream {
@@ -33,35 +51,86 @@ export class ZipStream {
3351 private _zipWriter : ZipWriter < WritableStream >
3452 private _streamBuffer : WritableStreamBuffer
3553 private _hasher : crypto . Hash
54+ private _numberOfFilesToStream : number = 0
55+ private _numberOfFilesSucceeded : number = 0
56+ private _filesToZip : [ string , string ] [ ] = [ ]
57+ private _filesBeingZipped : number = 0
58+ private _maxNumberOfFileStreams : number
3659
37- constructor ( ) {
38- this . _streamBuffer = new WritableStreamBuffer ( )
39- this . _hasher = crypto . createHash ( 'md5' )
60+ constructor ( props : Partial < ZipStreamProps > = { } ) {
61+ // Allow any user-provided values to override default values
62+ const mergedProps = { ...defaultProps , ...props }
63+ const { hashAlgorithm, compressionLevel, maxNumberOfFileStreams } = mergedProps
4064
4165 this . _zipWriter = new ZipWriter (
4266 new WritableStream ( {
4367 write : chunk => {
4468 this . _streamBuffer . write ( chunk )
4569 this . _hasher . update ( chunk )
70+ this . _numberOfFilesSucceeded ++
71+ this . _filesBeingZipped --
72+
73+ if ( this . _filesToZip . length > 0 && this . _filesBeingZipped < maxNumberOfFileStreams ) {
74+ this . _filesBeingZipped ++
75+ const [ fileToZip , path ] = this . _filesToZip . shift ( ) !
76+ void readFileAsString ( fileToZip ) . then ( content => {
77+ return this . _zipWriter . add ( path , new TextReader ( content ) )
78+ } )
79+ }
4680 } ,
47- } )
81+ } ) ,
82+ { level : compressionLevel }
4883 )
84+ this . _maxNumberOfFileStreams = maxNumberOfFileStreams
85+
86+ this . _streamBuffer = new WritableStreamBuffer ( )
87+
88+ this . _hasher = crypto . createHash ( hashAlgorithm )
4989 }
5090
51- public async writeString ( data : string , path : string ) {
91+ public writeString ( data : string , path : string ) {
5292 return this . _zipWriter . add ( path , new TextReader ( data ) )
5393 }
5494
55- public async writeFile ( file : string , path : string ) {
56- const content = await readFileAsString ( file )
57- return this . _zipWriter . add ( path , new TextReader ( content ) )
95+ public writeFile ( file : string , path : string ) {
96+ // We use _numberOfFilesToStream to make sure we don't finalize too soon
97+ // (before the progress event has been fired for the last file)
98+ // The problem is that we can't rely on progress.entries.total,
99+ // because files can be added to the queue faster
100+ // than the progress event is fired
101+ this . _numberOfFilesToStream ++
102+ // We only start zipping another file if we're under our limit
103+ // of concurrent file streams
104+ if ( this . _filesBeingZipped < this . _maxNumberOfFileStreams ) {
105+ this . _filesBeingZipped ++
106+ void readFileAsString ( file ) . then ( content => {
107+ return this . _zipWriter . add ( path , new TextReader ( content ) )
108+ } )
109+ } else {
110+ // Queue it for later (see "write" event)
111+ this . _filesToZip . push ( [ file , path ] )
112+ }
58113 }
59114
60- public async finalize ( ) : Promise < ZipStreamResult > {
115+ public async finalize ( onProgress ?: ( percentComplete : number ) => void ) : Promise < ZipStreamResult > {
116+ let finished = false
117+ // We need to poll to check for all the file streams to be completely processed
118+ // -- we are keeping track of this via the "progress" event handler
119+ while ( ! finished ) {
120+ finished = await new Promise ( resolve => {
121+ setTimeout ( ( ) => {
122+ getLogger ( ) . verbose ( 'success is' , this . _numberOfFilesSucceeded , '/' , this . _numberOfFilesToStream )
123+ onProgress ?.( Math . floor ( ( 100 * this . _numberOfFilesSucceeded ) / this . _numberOfFilesToStream ) )
124+ resolve ( this . _numberOfFilesToStream <= this . _numberOfFilesSucceeded )
125+ } , 1000 )
126+ } )
127+ }
128+ // We're done streaming all files, so we can close the zip stream
129+
61130 await this . _zipWriter . close ( )
62131 return {
63132 sizeInBytes : this . _streamBuffer . size ( ) ,
64- md5 : this . _hasher . digest ( 'base64' ) ,
133+ hash : this . _hasher . digest ( 'base64' ) ,
65134 streamBuffer : this . _streamBuffer ,
66135 }
67136 }
0 commit comments