@samitouri / QOSami-HFS / commits / 83b890c6

fix: partial zip requests could be left hanging

Massimo Melina committed Sep 13, 2025 at 00:23 UTC 83b890c6587f2c712c370f365f657202dc0a1c7c
3 files changed +14 -10
src/QuickZipStream.ts
+8 -5
@@ -57,6 +57,10 @@ export class QuickZipStream extends Readable {
57 this.push(null) // EOF
58 }
59
60 + continuePiping() {
61 + setImmediate(() => this.push('')) // stimulate the pipe. In some situations this needs to be done at next tick; not sure when, but maybe it's when we are inside a _read call that writes nothing
62 + }
63 +
64 applyRange(start: number, end: number) {
65 if (end < start)
66 return this.closeStream()
@@ -153,12 +157,11 @@ export class QuickZipStream extends Readable {
157 this.skip -= size
158 this.dataWritten += size
159 this.entries.push(entry)
156 - setTimeout(() => this.push('')) // this "signal" works only after _read() is done
157 - return
160 + return this.continuePiping()
161 }
162 if (!getData) {
163 this.entries.push(entry)
161 - return
164 + return this.continuePiping()
165 }
166 const data = getData()
167 data.on('error', (err) => {
@@ -166,7 +169,7 @@ export class QuickZipStream extends Readable {
169 console.error('zipping:', String(err))
170 data.destroy(err)
171 this.workingFile = undefined
169 - this.push('') // continue piping
172 + this.continuePiping()
173 })
174 data.on('end', ()=>{
175 entry.crc = crc
@@ -174,7 +177,7 @@ export class QuickZipStream extends Readable {
177 crcCache[sourcePath] = { ts, crc }
178 this.entries.push(entry)
179 this.workingFile = undefined
177 - this.push('') // continue piping
180 + this.continuePiping()
181 })
182 this.workingFile = data
183 data.on('data', chunk => {
src/zip.ts
+1 -1
@@ -53,7 +53,7 @@ export async function zipStreamFromFolder(node: VfsNode, ctx: Koa.Context) {
53 || filterComment && !filterComment(await getCommentFor(source) || ''))
54 return
55 try {
56 - if (el.isFolder)
56 + if (nodeIsFolder(el))
57 return { path: name + '/' }
58 if (!source) return
59 const st = el.stats || await fs.stat(source)
tests/test.ts
+5 -4
@@ -117,11 +117,12 @@ describe('basics', () => {
117 test('protectFromAbove.list', reqList('/protectFromAbove/child/', { inList:['alfa.txt'] }))
118 test('inheritNegativeMask', reqList('/tests/page', { outList: ['index.html'] }))
119
120 - const zipSize = 13010
121 - const zipOfs = 0x1359
120 + const zipSize = 13178
121 + const zipOfs = 0x32AF
122 + const zipLength = 4
123 test('zip.head', req('/f1/?get=zip', { empty:true, length:zipSize }, { method:'HEAD' }) )
123 - test('zip.partial', req('/f1/?get=zip', { re:/^C3$/, length: 2 }, { headers: { Range: `bytes=${zipOfs}-${zipOfs+1}` } }) )
124 - test('zip.partial.resume', req('/f1/?get=zip', { re:/^C3/, length:zipSize-zipOfs }, { headers: { Range: `bytes=${zipOfs}-` } }) )
124 + test('zip.partial', req('/f1/?get=zip', { re:/^page$/, length: zipLength }, { headers: { Range: `bytes=${zipOfs}-${zipOfs+zipLength-1}` } }) )
125 + test('zip.partial.resume', req('/f1/?get=zip', { re:/^page/, length:zipSize-zipOfs }, { headers: { Range: `bytes=${zipOfs}-` } }) )
126 test('zip.partial.end', req('/f1/f2/?get=zip', { re:/^6/, length:10 }, { headers: { Range: 'bytes=-10' } }) )
127 test('zip.alfa is forbidden', req('/protectFromAbove/child/?get=zip&list=alfa.txt//renamed', { empty: true, length:118 }, { method:'HEAD' }))
128 test('zip.cantReadPage', req('/cantReadPage/?get=zip', { length: 4800 }, { method:'HEAD' }))