zip now include empty folders

Massimo Melina committed May 11, 2023 at 16:27 UTC 07cec4d0a12ae44f33e3d74e2f0d1c163512a29d
2 files changed +21 -11
src/QuickZipStream.ts
+14 -9
@@ -19,9 +19,9 @@ const FLAGS = 0x0808 // bit3 = no crc in local header + bit11 = utf8
19 interface ZipSource {
20 path: string
21 sourcePath?: string
22 - getData: () => Readable // deferred stream, so that we don't keep many open files because of calculateSize()
23 - size: number
24 - ts: Date
22 + getData?: () => Readable // deferred stream, so that we don't keep many open files because of calculateSize()
23 + size?: number
24 + ts?: Date
25 mode?: number
26 }
27 export class QuickZipStream extends Readable {
@@ -32,6 +32,7 @@ export class QuickZipStream extends Readable {
32 private consumedCalculating: ZipSource[] = []
33 private skip: number = 0
34 private limit?: number
35 + private now = new Date()
36
37 constructor(private readonly walker: AsyncIterableIterator<ZipSource>) {
38 super({})
@@ -85,9 +86,10 @@ export class QuickZipStream extends Readable {
86 let centralDirSize = 0
87 for (const file of this.consumedCalculating) {
88 const pathSize = Buffer.from(file.path, 'utf8').length
88 - const extraLength = (file.size > ZIP64_SIZE_LIMIT ? 2 : 0) + (offset > ZIP64_SIZE_LIMIT ? 1 : 0)
89 + const { size=0 } = file
90 + const extraLength = (size > ZIP64_SIZE_LIMIT ? 2 : 0) + (offset > ZIP64_SIZE_LIMIT ? 1 : 0)
91 const extraDataSize = extraLength && (2+2 + extraLength*8)
90 - offset += 4+2+2+2+ 4+4+4+4+ 2+2+ pathSize + file.size
92 + offset += 4+2+2+2+ 4+4+4+4+ 2+2+ pathSize + size
93 centralDirSize += 4+2+2+2+2+ 4+4+4+4+ 2+2+2+2+2+ 4+4 + pathSize + extraDataSize
94 }
95 const n = this.consumedCalculating.length
@@ -107,7 +109,7 @@ export class QuickZipStream extends Readable {
109 const file = this.consumedCalculating.shift() || (await this.walker.next()).value as ZipSource
110 if (!file)
111 return this.closeArchive()
110 - let { path, sourcePath, getData, size, ts, mode } = file
112 + let { path, sourcePath, getData, size=0, ts=this.now, mode=0o40775 } = file
113 const pathAsBuffer = Buffer.from(path, 'utf8')
114 const offset = this.dataWritten
115 const version = 20
@@ -116,7 +118,7 @@ export class QuickZipStream extends Readable {
118 2, version,
119 2, FLAGS,
120 2, 0, // compression = store
119 - ...ts2buf(ts),
121 + ...ts2buf(ts || this.now),
122 4, 0, // crc
123 4, 0, // size
124 4, 0, // size
@@ -128,7 +130,7 @@ export class QuickZipStream extends Readable {
130
131 const cache = sourcePath ? crcCache[sourcePath] : undefined
132 const cacheHit = Number(cache?.ts) === Number(ts)
131 - let crc = cacheHit ? cache!.crc : crc32function('')
133 + let crc = cacheHit ? cache!.crc : getData ? crc32function('') : 0
134 const extAttr = !mode ? 0 : (mode | 0x8000) * 0x10000 // it's like <<16 but doesn't overflow so easily
135 const entry = { size, crc, pathAsBuffer, ts, offset, version, extAttr }
136 if (this.skip >= size && cacheHit) {
@@ -138,11 +140,14 @@ export class QuickZipStream extends Readable {
140 setTimeout(() => this.push('')) // this "signal" works only after _read() is done
141 return
142 }
143 + if (!getData) {
144 + this.entries.push(entry)
145 + return
146 + }
147 const data = getData()
148 data.on('error', (err) => console.error(err))
149 data.on('end', ()=>{
150 this.workingFile = undefined
145 - entry.crc = crc
151 if (sourcePath)
152 crcCache[sourcePath] = { ts, crc }
153 this.entries.push(entry)
src/zip.ts
+7 -2
@@ -27,8 +27,10 @@ export async function zipStreamFromFolder(node: VfsNode, ctx: Koa.Context) {
27 if (!subNode)
28 continue
29 if (await nodeIsDirectory(subNode)) { // a directory needs to walked
30 - if (hasPermission(subNode, 'can_list',ctx))
30 + if (hasPermission(subNode, 'can_list',ctx)) {
31 + yield subNode // it could be empty
32 yield* walkNode(subNode, ctx, Infinity, uri + '/', 'can_read')
33 + }
34 continue
35 }
36 let folder = dirname(decodeURIComponent(uri)) // decodeURI() won't account for %23=#
@@ -40,9 +42,12 @@ export async function zipStreamFromFolder(node: VfsNode, ctx: Koa.Context) {
42 if (!hasPermission(el, 'can_read', ctx)) return // the fact you see it doesn't mean you can read it
43 const { source } = el
44 const name = getNodeName(el)
43 - if (!source || ctx.req.aborted || !filter(name))
45 + if (ctx.req.aborted || !filter(name))
46 return
47 try {
48 + if (el.isFolder)
49 + return { path: name + '/' }
50 + if (!source) return
51 const st = await fs.stat(source)
52 if (!st || !st.isFile())
53 return