11import { Entry as ZipEntry , open , Options as ZipOptions , ZipFile } from "yauzl" ;
2- import { Readable } from "stream" ;
2+ import { Readable , Transform } from "stream" ;
33import { dirname , join } from "path" ;
44import { WriteStream } from "fs" ;
55import { createWriteStream , ensureDir } from "fs-extra" ;
@@ -25,6 +25,10 @@ export function excludeDirectories(entries: ZipEntry[]): ZipEntry[] {
2525 return entries . filter ( ( entry ) => ! / \/ $ / . test ( entry . fileName ) ) ;
2626}
2727
28+ function calculateTotalUncompressedByteSize ( entries : ZipEntry [ ] ) : number {
29+ return entries . reduce ( ( total , entry ) => total + entry . uncompressedSize , 0 ) ;
30+ }
31+
2832export function readZipEntries ( zipFile : ZipFile ) : Promise < ZipEntry [ ] > {
2933 return new Promise ( ( resolve , reject ) => {
3034 const files : ZipEntry [ ] = [ ] ;
@@ -84,6 +88,7 @@ export async function openZipBuffer(
8488async function copyStream (
8589 readable : Readable ,
8690 writeStream : WriteStream ,
91+ bytesExtractedCallback ?: ( bytesExtracted : number ) => void ,
8792) : Promise < void > {
8893 return new Promise ( ( resolve , reject ) => {
8994 readable . on ( "error" , ( err ) => {
@@ -93,28 +98,53 @@ async function copyStream(
9398 resolve ( ) ;
9499 } ) ;
95100
96- readable . pipe ( writeStream ) ;
101+ readable
102+ . pipe (
103+ new Transform ( {
104+ transform ( chunk , _encoding , callback ) {
105+ bytesExtractedCallback ?.( chunk . length ) ;
106+ this . push ( chunk ) ;
107+ callback ( ) ;
108+ } ,
109+ } ) ,
110+ )
111+ . pipe ( writeStream ) ;
97112 } ) ;
98113}
99114
115+ type UnzipProgress = {
116+ filesExtracted : number ;
117+ totalFiles : number ;
118+
119+ bytesExtracted : number ;
120+ totalBytes : number ;
121+ } ;
122+
123+ export type UnzipProgressCallback = ( progress : UnzipProgress ) => void ;
124+
100125/**
101126 * Unzips a single file from a zip archive.
102127 *
103128 * @param zipFile
104129 * @param entry
105130 * @param rootDestinationPath
131+ * @param bytesExtractedCallback Called when bytes are extracted.
132+ * @return The number of bytes extracted.
106133 */
107134async function unzipFile (
108135 zipFile : ZipFile ,
109136 entry : ZipEntry ,
110137 rootDestinationPath : string ,
111- ) : Promise < void > {
138+ bytesExtractedCallback ?: ( bytesExtracted : number ) => void ,
139+ ) : Promise < number > {
112140 const path = join ( rootDestinationPath , entry . fileName ) ;
113141
114142 if ( / \/ $ / . test ( entry . fileName ) ) {
115143 // Directory file names end with '/'
116144
117145 await ensureDir ( path ) ;
146+
147+ return 0 ;
118148 } else {
119149 // Ensure the directory exists
120150 await ensureDir ( dirname ( path ) ) ;
@@ -131,7 +161,9 @@ async function unzipFile(
131161 mode,
132162 } ) ;
133163
134- await copyStream ( readable , writeStream ) ;
164+ await copyStream ( readable , writeStream , bytesExtractedCallback ) ;
165+
166+ return entry . uncompressedSize ;
135167 }
136168}
137169
@@ -143,10 +175,12 @@ async function unzipFile(
143175 * @param archivePath
144176 * @param destinationPath
145177 * @param taskRunner A function that runs the tasks (either sequentially or concurrently).
178+ * @param progress
146179 */
147180export async function unzipToDirectory (
148181 archivePath : string ,
149182 destinationPath : string ,
183+ progress : UnzipProgressCallback | undefined ,
150184 taskRunner : ( tasks : Array < ( ) => Promise < void > > ) => Promise < void > ,
151185) : Promise < void > {
152186 const zipFile = await openZip ( archivePath , {
@@ -158,8 +192,43 @@ export async function unzipToDirectory(
158192 try {
159193 const entries = await readZipEntries ( zipFile ) ;
160194
195+ let filesExtracted = 0 ;
196+ const totalFiles = entries . length ;
197+ let bytesExtracted = 0 ;
198+ const totalBytes = calculateTotalUncompressedByteSize ( entries ) ;
199+
200+ const reportProgress = ( ) => {
201+ progress ?.( {
202+ filesExtracted,
203+ totalFiles,
204+ bytesExtracted,
205+ totalBytes,
206+ } ) ;
207+ } ;
208+
209+ reportProgress ( ) ;
210+
161211 await taskRunner (
162- entries . map ( ( entry ) => ( ) => unzipFile ( zipFile , entry , destinationPath ) ) ,
212+ entries . map ( ( entry ) => async ( ) => {
213+ let entryBytesExtracted = 0 ;
214+
215+ const totalEntryBytesExtracted = await unzipFile (
216+ zipFile ,
217+ entry ,
218+ destinationPath ,
219+ ( thisBytesExtracted ) => {
220+ entryBytesExtracted += thisBytesExtracted ;
221+ bytesExtracted += thisBytesExtracted ;
222+ reportProgress ( ) ;
223+ } ,
224+ ) ;
225+
226+ // Should be 0, but just in case.
227+ bytesExtracted += - entryBytesExtracted + totalEntryBytesExtracted ;
228+
229+ filesExtracted ++ ;
230+ reportProgress ( ) ;
231+ } ) ,
163232 ) ;
164233 } finally {
165234 zipFile . close ( ) ;
@@ -173,14 +242,21 @@ export async function unzipToDirectory(
173242 *
174243 * @param archivePath
175244 * @param destinationPath
245+ * @param progress
176246 */
177247export async function unzipToDirectorySequentially (
178248 archivePath : string ,
179249 destinationPath : string ,
250+ progress ?: UnzipProgressCallback ,
180251) : Promise < void > {
181- return unzipToDirectory ( archivePath , destinationPath , async ( tasks ) => {
182- for ( const task of tasks ) {
183- await task ( ) ;
184- }
185- } ) ;
252+ return unzipToDirectory (
253+ archivePath ,
254+ destinationPath ,
255+ progress ,
256+ async ( tasks ) => {
257+ for ( const task of tasks ) {
258+ await task ( ) ;
259+ }
260+ } ,
261+ ) ;
186262}
0 commit comments