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