-
Notifications
You must be signed in to change notification settings - Fork 27
/
index.ts
182 lines (171 loc) · 5.02 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
/**
* @module @xmcl/unzip
*/
import { Readable } from 'stream'
import { Entry, fromBuffer, fromFd, open as yopen, ZipFile, ZipFileOptions, Options } from 'yauzl'
export type OpenTarget = string | Buffer | number
/**
* Open a yauzl zip
* @param target The zip path or buffer or file descriptor
* @param options The option to open
*/
export async function open(target: OpenTarget, options: Options = { lazyEntries: true, autoClose: false }) {
return new Promise<ZipFile>((resolve, reject) => {
function handleZip(err: Error | null, zipfile: ZipFile | null) {
if (err || !zipfile) {
reject(err ?? new Error('Cannot open zip!'))
} else {
resolve(zipfile)
}
}
if (typeof target === 'string') {
yopen(target, options, handleZip)
} else if (target instanceof Buffer) {
fromBuffer(target, options, handleZip)
} else {
fromFd(target, options, handleZip)
}
})
}
/**
* Open the entry readstream for the zip file
* @param zip The zip file object
* @param entry The entry to open
* @param options The options to open stream
*/
export function openEntryReadStream(zip: ZipFile, entry: Entry, options?: ZipFileOptions) {
return new Promise<Readable>((resolve, reject) => {
function handleStream(err: Error | null, stream: Readable | null) {
if (err || !stream) { reject(err) } else { resolve(stream) }
}
if (options) { zip.openReadStream(entry, options, handleStream) } else { zip.openReadStream(entry, handleStream) }
})
}
/**
* Read the entry to buffer
* @param zip The zip file object
* @param entry The entry to open
* @param options The options to open stream
*/
export async function readEntry(zip: ZipFile, entry: Entry, options?: ZipFileOptions) {
const stream = await openEntryReadStream(zip, entry, options)
const buffers: Buffer[] = []
await new Promise((resolve, reject) => {
stream.on('data', (chunk) => { buffers.push(chunk) })
stream.on('end', resolve)
stream.on('error', reject)
})
return Buffer.concat(buffers)
}
/**
* Get the async entry generator for the zip file
* @param zip The zip file
*/
export async function * walkEntriesGenerator(zip: ZipFile): AsyncGenerator<Entry, void, boolean | undefined> {
let ended = false
let error: any
let resume: (v?: any) => void = () => { }
let wait = new Promise<void>((resolve) => {
resume = resolve
})
const entries: Entry[] = []
const onEntry = (e: Entry) => {
entries.push(e)
resume()
}
const onEnd = () => {
ended = true
resume()
}
const onError = (e: any) => {
error = e
resume()
}
zip.addListener('entry', onEntry)
.addListener('end', onEnd)
.addListener('error', onError)
try {
while (!ended) {
if (zip.lazyEntries) {
zip.readEntry()
}
await wait
// if error, throw error
if (error) {
throw error
}
// if entries read, yield entries
while (entries.length > 0 && !ended) {
ended = !!(yield entries.pop()!)
}
// reset wait
wait = new Promise<void>((resolve) => {
resume = resolve
})
}
} finally {
zip.removeListener('entry', onEntry)
.removeListener('end', onEnd)
.removeListener('error', onError)
}
}
/**
* Walk all the entries of the zip and once provided entries are all found, then terminate the walk process
* @param zip The zip file
* @param entries The entry to read
*/
export async function filterEntries(zip: ZipFile, entries: Array<string | ((entry: Entry) => boolean)>): Promise<(Entry | undefined)[]> {
const bags = entries.map(e => [e, undefined as undefined | Entry] as const)
let remaining = entries.length
for await (const entry of walkEntriesGenerator(zip)) {
for (const bag of bags) {
if (typeof bag[0] === 'string') {
if (bag[0] === entry.fileName) {
// @ts-ignore
bag[1] = entry
remaining -= 1
}
} else {
if (bag[0](entry)) {
// @ts-ignore
bag[1] = entry
remaining -= 1
}
}
if (remaining === 0) break
}
}
return bags.map(b => b[1])
}
/**
* Walk all the entries of a unread zip file
* @param zip The unread zip file
* @param entryHandler The handler to recieve entries. Return true or Promise<true> to stop the walk
*/
export async function walkEntries(zip: ZipFile, entryHandler: (entry: Entry) => Promise<boolean> | boolean | void) {
const itr = walkEntriesGenerator(zip)
for await (const entry of itr) {
const result = await entryHandler(entry)
if (result) {
break
}
}
}
export function getEntriesRecord(entries: Entry[]): Record<string, Entry> {
const record: Record<string, Entry> = {}
for (const entry of entries) {
record[entry.fileName] = entry
}
return record
}
/**
* Walk all entries of the zip file
* @param zipFile The zip file object
*/
export async function readAllEntries(zipFile: ZipFile) {
const entries: Entry[] = []
for await (const entry of walkEntriesGenerator(zipFile)) {
entries.push(entry)
}
return entries
}