diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts index 6505ef4d20..50972a52a4 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts @@ -24,6 +24,7 @@ import { ReadTreeResponseDirOptions, ReadTreeResponseFile, } from '../types'; +import { streamToTimeoutPromise } from './util'; /** * Wraps a zip archive stream into a tree response reader. @@ -86,27 +87,9 @@ export class ZipArchiveResponse implements ReadTreeResponse { const files = Array(); - // Some corrupted ZIP files cause the zlib inflater to hang indefinitely. - // This is a workaround to bail on stuck streams after 3 seconds. - const piped = this.stream.pipe(unzipper.Parse()); - let lastEntryTimeout: NodeJS.Timeout | undefined; - - await piped + const parseStream = this.stream + .pipe(unzipper.Parse()) .on('entry', (entry: Entry) => { - clearTimeout(lastEntryTimeout); - - lastEntryTimeout = setTimeout(() => { - piped.emit( - 'error', - new Error(`Timed out unzipping file ${entry.path}`), - ); - }, 3000); - - if (entry.type === 'Directory') { - entry.resume(); - return; - } - if (this.shouldBeIncluded(entry)) { files.push({ path: this.getInnerPath(entry.path), @@ -115,10 +98,14 @@ export class ZipArchiveResponse implements ReadTreeResponse { } else { entry.autodrain(); } - }) - .promise(); + }); - clearTimeout(lastEntryTimeout); + await streamToTimeoutPromise(parseStream, { + eventName: 'entry', + timeoutMs: 3000, + getError: (entry: Entry) => + new Error(`Timed out while unzipping ${entry.type}: ${entry.path}`), + }); return files; } diff --git a/packages/backend-common/src/reading/tree/util.ts b/packages/backend-common/src/reading/tree/util.ts index cfb986a0b0..51f904f313 100644 --- a/packages/backend-common/src/reading/tree/util.ts +++ b/packages/backend-common/src/reading/tree/util.ts @@ -23,3 +23,30 @@ const directoryNameRegex = /^[^\/]+\//; export function stripFirstDirectoryFromPath(path: string): string { return path.replace(directoryNameRegex, ''); } + +// Some corrupted ZIP files cause the zlib inflater to hang indefinitely. +// This is a workaround to bail on stuck streams after 3 seconds. +export async function streamToTimeoutPromise( + stream: NodeJS.ReadableStream, + options: { + timeoutMs: number; + eventName: string; + getError: (data: T) => Error; + }, +) { + let lastEntryTimeout: NodeJS.Timeout | undefined; + stream.on(options.eventName, (data: T) => { + clearTimeout(lastEntryTimeout); + + lastEntryTimeout = setTimeout(() => { + stream.emit('error', options.getError(data)); + }, options.timeoutMs); + }); + + await new Promise(function (resolve, reject) { + stream.on('finish', resolve); + stream.on('error', reject); + }); + + clearTimeout(lastEntryTimeout); +}