下载文件支持zip64
continuous-integration/drone/push Build is passing Details

main
wangxiaoshuang 2026-01-19 15:53:11 +08:00
parent a5824cecac
commit ec83609139
2 changed files with 380 additions and 1 deletions

View File

@ -1,5 +1,5 @@
import streamSaver from "streamsaver"; import streamSaver from "streamsaver";
import "streamsaver/examples/zip-stream.js"; import "./zip-stream.js";
import store from '@/store' import store from '@/store'
import dcmjs from './dcmUpload/dcmjs' import dcmjs from './dcmUpload/dcmjs'
streamSaver.mitm = `${window.location.origin}/mitm.html?version=2.0.0` streamSaver.mitm = `${window.location.origin}/mitm.html?version=2.0.0`

379
src/utils/zip-stream.js Normal file
View File

@ -0,0 +1,379 @@
class Crc32 {
constructor() {
this.crc = -1
}
append(data) {
var crc = this.crc | 0; var table = this.table
for (var offset = 0, len = data.length | 0; offset < len; offset++) {
crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF]
}
this.crc = crc
}
get() {
return ~this.crc
}
}
Crc32.prototype.table = (() => {
var i; var j; var t; var table = []
for (i = 0; i < 256; i++) {
t = i
for (j = 0; j < 8; j++) {
t = (t & 1)
? (t >>> 1) ^ 0xEDB88320
: t >>> 1
}
table[i] = t
}
return table
})()
const getDataHelper = byteLength => {
var uint8 = new Uint8Array(byteLength)
return {
array: uint8,
view: new DataView(uint8.buffer)
}
}
const ZIP_SIGNATURE_LOCAL = 0x04034b50
const ZIP_SIGNATURE_CENTRAL = 0x02014b50
const ZIP_SIGNATURE_EOCD = 0x06054b50
const ZIP_SIGNATURE_ZIP64_EOCD = 0x06064b50
const ZIP_SIGNATURE_ZIP64_LOCATOR = 0x07064b50
const ZIP64_MAGIC = 0xFFFFFFFF
function u16(view, offset, value) { view.setUint16(offset, value, true) }
function u32(view, offset, value) { view.setUint32(offset, value >>> 0, true) }
function concatUint8(chunks, total) {
const out = new Uint8Array(total)
let off = 0
for (const c of chunks) {
out.set(c, off)
off += c.length
}
return out
}
function createWriter(underlyingSource) {
const files = Object.create(null)
const filenames = []
const encoder = new TextEncoder()
let offset = 0 // bytes written to output so far
let activeZipIndex = 0
let ctrl
let activeZipObject
let closed = false
function processNextChunk() {
if (!activeZipObject) return
// directory entry: just local header + immediate footer
if (activeZipObject.directory) {
if (!activeZipObject.headerWritten) {
activeZipObject.writeLocalHeader()
activeZipObject.headerWritten = true
}
if (!activeZipObject.footerWritten) {
activeZipObject.writeDataDescriptor() // will be zeros
activeZipObject.footerWritten = true
next()
}
return
}
if (!activeZipObject.reader) {
if (!activeZipObject.fileLike || !activeZipObject.fileLike.stream) {
next()
return
}
activeZipObject.crc = new Crc32()
activeZipObject.reader = activeZipObject.fileLike.stream().getReader()
activeZipObject.writeLocalHeader()
activeZipObject.headerWritten = true
return
}
return activeZipObject.reader.read().then(({ done, value }) => {
if (done) {
activeZipObject.writeDataDescriptor()
activeZipObject.footerWritten = true
next()
return
}
const chunk = value instanceof Uint8Array ? value : new Uint8Array(value)
activeZipObject.crc.append(chunk)
activeZipObject.uncompressedLength += chunk.length
activeZipObject.compressedLength += chunk.length
ctrl.enqueue(chunk)
offset += chunk.length
})
}
function next() {
activeZipIndex++
activeZipObject = files[filenames[activeZipIndex]]
if (activeZipObject) {
processNextChunk()
} else if (closed) {
closeZip()
}
}
function dosDateTime(date) {
const dt = new Date(date)
const time = ((dt.getHours() << 11) | (dt.getMinutes() << 5) | (dt.getSeconds() / 2)) & 0xFFFF
const d = (((dt.getFullYear() - 1980) << 9) | ((dt.getMonth() + 1) << 5) | dt.getDate()) & 0xFFFF
return { time, date: d }
}
const zipWriter = {
enqueue(fileLike) {
if (closed) throw new TypeError('Cannot enqueue after close()')
let name = String(fileLike.name || '').trim()
if (!name) throw new Error('Missing file name')
if (fileLike.directory && !name.endsWith('/')) name += '/'
if (files[name]) throw new Error('File already exists.')
const nameBuf = encoder.encode(name)
const commentBuf = encoder.encode(fileLike.comment || '')
const { time, date } = dosDateTime(typeof fileLike.lastModified === 'undefined' ? Date.now() : fileLike.lastModified)
const zipObject = (files[name] = {
fileLike,
directory: !!fileLike.directory,
nameBuf,
comment: commentBuf,
time,
date,
// tracked
offset: 0,
crc: null,
compressedLength: 0,
uncompressedLength: 0,
headerWritten: false,
footerWritten: false,
reader: null,
// whether sizes/offset require ZIP64
needsZip64() {
return (
this.uncompressedLength > ZIP64_MAGIC ||
this.compressedLength > ZIP64_MAGIC ||
this.offset > ZIP64_MAGIC
)
},
writeLocalHeader() {
this.offset = offset
const generalPurposeBitFlag = 0x0008 // data descriptor present
const compressionMethod = 0 // store
const versionNeeded = 20 // 2.0 (ZIP64 switches this to 45)
// We always use data descriptor; write zeros for crc/sizes in local header
const local = getDataHelper(30 + this.nameBuf.length)
u32(local.view, 0, ZIP_SIGNATURE_LOCAL)
u16(local.view, 4, versionNeeded)
u16(local.view, 6, generalPurposeBitFlag)
u16(local.view, 8, compressionMethod)
u16(local.view, 10, this.time)
u16(local.view, 12, this.date)
u32(local.view, 14, 0)
u32(local.view, 18, 0)
u32(local.view, 22, 0)
u16(local.view, 26, this.nameBuf.length)
u16(local.view, 28, 0) // extra length
local.array.set(this.nameBuf, 30)
ctrl.enqueue(local.array)
offset += local.array.length
},
writeDataDescriptor() {
// data descriptor: optional signature + crc32 + sizes (32-bit or 64-bit)
const crc = this.crc ? this.crc.get() >>> 0 : 0
const useZip64 = this.needsZip64()
if (!useZip64) {
const dd = getDataHelper(16)
u32(dd.view, 0, 0x08074b50)
u32(dd.view, 4, crc)
u32(dd.view, 8, this.compressedLength)
u32(dd.view, 12, this.uncompressedLength)
ctrl.enqueue(dd.array)
offset += dd.array.length
} else {
// 24 bytes: sig + crc32 + compSize(8) + uncompSize(8)
const dd = getDataHelper(24)
u32(dd.view, 0, 0x08074b50)
u32(dd.view, 4, crc)
// BigInt writes
dd.view.setBigUint64(8, BigInt(this.compressedLength), true)
dd.view.setBigUint64(16, BigInt(this.uncompressedLength), true)
ctrl.enqueue(dd.array)
offset += dd.array.length
}
}
})
filenames.push(name)
if (!activeZipObject) {
activeZipObject = zipObject
processNextChunk()
}
},
close() {
if (closed) throw new TypeError('Cannot close twice')
closed = true
if (!activeZipObject) closeZip()
}
}
function closeZip() {
// Build central directory in memory then enqueue once.
const cdChunks = []
let cdSize = 0
for (const name of filenames) {
const file = files[name]
const useZip64 = file.needsZip64()
const versionMadeBy = useZip64 ? 45 : 20
const versionNeeded = useZip64 ? 45 : 20
const generalPurposeBitFlag = 0x0008
const compressionMethod = 0
const crc = file.crc ? file.crc.get() >>> 0 : 0
const compressed32 = useZip64 ? ZIP64_MAGIC : file.compressedLength
const uncompressed32 = useZip64 ? ZIP64_MAGIC : file.uncompressedLength
const offset32 = useZip64 ? ZIP64_MAGIC : file.offset
// ZIP64 extra field if needed
let extra = new Uint8Array(0)
if (useZip64) {
// headerId(2)=0x0001, dataSize(2)=24, uncompressed(8), compressed(8), offset(8)
extra = new Uint8Array(4 + 24)
const ev = new DataView(extra.buffer)
u16(ev, 0, 0x0001)
u16(ev, 2, 24)
ev.setBigUint64(4, BigInt(file.uncompressedLength), true)
ev.setBigUint64(12, BigInt(file.compressedLength), true)
ev.setBigUint64(20, BigInt(file.offset), true)
}
const headerLen = 46 + file.nameBuf.length + extra.length + file.comment.length
const cd = getDataHelper(headerLen)
u32(cd.view, 0, ZIP_SIGNATURE_CENTRAL)
u16(cd.view, 4, versionMadeBy)
u16(cd.view, 6, versionNeeded)
u16(cd.view, 8, generalPurposeBitFlag)
u16(cd.view, 10, compressionMethod)
u16(cd.view, 12, file.time)
u16(cd.view, 14, file.date)
u32(cd.view, 16, crc)
u32(cd.view, 20, compressed32)
u32(cd.view, 24, uncompressed32)
u16(cd.view, 28, file.nameBuf.length)
u16(cd.view, 30, extra.length)
u16(cd.view, 32, file.comment.length)
u16(cd.view, 34, 0) // disk number start
u16(cd.view, 36, 0) // internal attrs
// external file attrs: mark directory
u32(cd.view, 38, file.directory ? 0x10 : 0)
u32(cd.view, 42, offset32)
cd.array.set(file.nameBuf, 46)
if (extra.length) cd.array.set(extra, 46 + file.nameBuf.length)
if (file.comment.length) cd.array.set(file.comment, 46 + file.nameBuf.length + extra.length)
cdChunks.push(cd.array)
cdSize += cd.array.length
}
const centralDirectoryOffset = offset
const centralDirectory = concatUint8(cdChunks, cdSize)
ctrl.enqueue(centralDirectory)
offset += centralDirectory.length
const entries = filenames.length
const needsZip64ForArchive =
entries > 0xFFFF ||
centralDirectoryOffset > ZIP64_MAGIC ||
cdSize > ZIP64_MAGIC ||
offset > ZIP64_MAGIC
const tailChunks = []
let tailSize = 0
if (needsZip64ForArchive) {
// ZIP64 EOCD
const zip64eocd = getDataHelper(56)
u32(zip64eocd.view, 0, ZIP_SIGNATURE_ZIP64_EOCD)
zip64eocd.view.setBigUint64(4, BigInt(44), true) // remaining size
u16(zip64eocd.view, 12, 45)
u16(zip64eocd.view, 14, 45)
u32(zip64eocd.view, 16, 0)
u32(zip64eocd.view, 20, 0)
zip64eocd.view.setBigUint64(24, BigInt(entries), true)
zip64eocd.view.setBigUint64(32, BigInt(entries), true)
zip64eocd.view.setBigUint64(40, BigInt(cdSize), true)
zip64eocd.view.setBigUint64(48, BigInt(centralDirectoryOffset), true)
// ZIP64 locator
const locator = getDataHelper(20)
u32(locator.view, 0, ZIP_SIGNATURE_ZIP64_LOCATOR)
u32(locator.view, 4, 0)
locator.view.setBigUint64(8, BigInt(centralDirectoryOffset + cdSize), true) // offset of zip64eocd
u32(locator.view, 16, 1)
tailChunks.push(zip64eocd.array, locator.array)
tailSize += zip64eocd.array.length + locator.array.length
}
// EOCD
const eocd = getDataHelper(22)
u32(eocd.view, 0, ZIP_SIGNATURE_EOCD)
u16(eocd.view, 4, 0)
u16(eocd.view, 6, 0)
u16(eocd.view, 8, Math.min(entries, 0xFFFF))
u16(eocd.view, 10, Math.min(entries, 0xFFFF))
u32(eocd.view, 12, needsZip64ForArchive ? ZIP64_MAGIC : cdSize)
u32(eocd.view, 16, needsZip64ForArchive ? ZIP64_MAGIC : centralDirectoryOffset)
u16(eocd.view, 20, 0) // comment length
tailChunks.push(eocd.array)
tailSize += eocd.array.length
ctrl.enqueue(concatUint8(tailChunks, tailSize))
ctrl.close()
}
return new ReadableStream({
start: c => {
ctrl = c
underlyingSource.start && Promise.resolve(underlyingSource.start(zipWriter))
},
pull() {
return processNextChunk() || (
underlyingSource.pull &&
Promise.resolve(underlyingSource.pull(zipWriter))
)
}
})
}
window.ZIP = createWriter