66 lines
1.7 KiB
TypeScript
66 lines
1.7 KiB
TypeScript
import type { Archiver } from 'archiver'
|
||
import { Readable } from 'node:stream'
|
||
|
||
import { nodeReadableToWebStream } from '@/lib/node-readable-stream'
|
||
|
||
/**
|
||
* 管理动态 ZIP 的输出与外部输入流。
|
||
*
|
||
* Archiver 仅销毁输出流时不会保证取消排队任务或销毁已 append 的远程流,
|
||
* 因此下载取消必须同时 abort 压缩队列并销毁所有仍存活的显式输入流。
|
||
*/
|
||
export function createArchiveDownloadStream(archive: Archiver) {
|
||
const trackedSources = new Set<Readable>()
|
||
let stopped = false
|
||
let finalized = false
|
||
|
||
const destroyTrackedSources = () => {
|
||
trackedSources.forEach((source) => {
|
||
if (!source.destroyed) {
|
||
source.destroy()
|
||
}
|
||
})
|
||
trackedSources.clear()
|
||
}
|
||
|
||
const abort = () => {
|
||
if (stopped) {
|
||
return
|
||
}
|
||
stopped = true
|
||
destroyTrackedSources()
|
||
try {
|
||
archive.abort()
|
||
} catch {
|
||
// 已结束或已销毁的 archive 仍继续执行下方 destroy,确保输出关闭。
|
||
}
|
||
if (!archive.destroyed) {
|
||
archive.destroy()
|
||
}
|
||
}
|
||
|
||
archive.once('error', destroyTrackedSources)
|
||
archive.once('end', () => trackedSources.clear())
|
||
|
||
return {
|
||
trackSource<T extends Readable>(source: T): T {
|
||
if (!source.destroyed) {
|
||
trackedSources.add(source)
|
||
source.once('close', () => trackedSources.delete(source))
|
||
}
|
||
return source
|
||
},
|
||
abort,
|
||
finalize(): ReadableStream<Uint8Array> {
|
||
if (finalized) {
|
||
throw new Error('ZIP 下载流已经开始生成')
|
||
}
|
||
finalized = true
|
||
|
||
const stream = nodeReadableToWebStream(archive, { onCancel: abort })
|
||
void archive.finalize().catch(() => abort())
|
||
return stream
|
||
},
|
||
}
|
||
}
|