import * as plugins from './plugins.js'; import type { IArchiveEntry, TCompressionLevel } from './interfaces.js'; /** * Transform stream for ZIP decompression using fflate * Emits StreamFile objects for each file in the archive */ export class ZipDecompressionTransform extends plugins.smartstream.SmartDuplex { private streamtools!: plugins.smartstream.IStreamTools; private unzipper = new plugins.fflate.Unzip(async (fileArg) => { let resultBuffer: Buffer; fileArg.ondata = async (_flateError, dat, final) => { resultBuffer ? (resultBuffer = Buffer.concat([resultBuffer, Buffer.from(dat)])) : (resultBuffer = Buffer.from(dat)); if (final) { const streamFile = plugins.smartfile.StreamFile.fromBuffer(resultBuffer); streamFile.relativeFilePath = fileArg.name; this.streamtools.push(streamFile); } }; fileArg.start(); }); constructor() { super({ objectMode: true, writeFunction: async (chunkArg, streamtoolsArg) => { this.streamtools ? null : (this.streamtools = streamtoolsArg); this.unzipper.push( Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg as unknown as ArrayBuffer), false ); return null; }, finalFunction: async () => { this.unzipper.push(Buffer.from(''), true); await plugins.smartdelay.delayFor(0); await this.streamtools.push(null); return null; }, }); this.unzipper.register(plugins.fflate.UnzipInflate); } } /** * Streaming ZIP compression using fflate * Allows adding multiple entries before finalizing */ export class ZipCompressionStream extends plugins.stream.Duplex { private files: Map = new Map(); private finalized = false; constructor() { super(); } /** * Add a file entry to the ZIP archive */ public async addEntry( fileName: string, content: Buffer | plugins.stream.Readable, options?: { compressionLevel?: TCompressionLevel } ): Promise { if (this.finalized) { throw new Error('Cannot add entries to a finalized ZIP archive'); } let data: Buffer; if (Buffer.isBuffer(content)) { data = content; } else { // Collect stream to buffer const chunks: Buffer[] = []; for await (const chunk of content) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } data = Buffer.concat(chunks); } this.files.set(fileName, { data: new Uint8Array(data), options: options?.compressionLevel !== undefined ? { level: options.compressionLevel } : undefined, }); } /** * Finalize the ZIP archive and emit the compressed data */ public async finalize(): Promise { if (this.finalized) { return; } this.finalized = true; const filesObj: plugins.fflate.Zippable = {}; for (const [name, { data, options }] of this.files) { filesObj[name] = options ? [data, options] : data; } // Use sync version for Deno compatibility (fflate async uses Web Workers) try { const result = plugins.fflate.zipSync(filesObj); this.push(Buffer.from(result)); this.push(null); } catch (err) { throw err; } } _read(): void { // No-op: data is pushed when finalize() is called } _write( _chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null) => void ): void { // Not used for ZIP creation - use addEntry() instead callback(new Error('Use addEntry() to add files to the ZIP archive')); } } /** * ZIP compression and decompression utilities */ export class ZipTools { /** * Get a streaming compression object for creating ZIP archives */ public getCompressionStream(): ZipCompressionStream { return new ZipCompressionStream(); } /** * Get a streaming decompression transform for extracting ZIP archives */ public getDecompressionStream(): ZipDecompressionTransform { return new ZipDecompressionTransform(); } /** * Create a ZIP archive from an array of entries */ public async createZip(entries: IArchiveEntry[], compressionLevel?: TCompressionLevel): Promise { const filesObj: plugins.fflate.Zippable = {}; for (const entry of entries) { let data: Uint8Array; if (typeof entry.content === 'string') { data = new TextEncoder().encode(entry.content); } else if (Buffer.isBuffer(entry.content)) { data = new Uint8Array(entry.content); } else if (entry.content instanceof plugins.smartfile.SmartFile) { data = new Uint8Array(entry.content.contents); } else if (entry.content instanceof plugins.smartfile.StreamFile) { const buffer = await entry.content.getContentAsBuffer(); data = new Uint8Array(buffer); } else { // Readable stream const chunks: Buffer[] = []; for await (const chunk of entry.content as plugins.stream.Readable) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } data = new Uint8Array(Buffer.concat(chunks)); } if (compressionLevel !== undefined) { filesObj[entry.archivePath] = [data, { level: compressionLevel }]; } else { filesObj[entry.archivePath] = data; } } // Use sync version for Deno compatibility (fflate async uses Web Workers) const result = plugins.fflate.zipSync(filesObj); return Buffer.from(result); } /** * Extract a ZIP buffer to an array of entries */ public async extractZip(data: Buffer): Promise> { // Use sync version for Deno compatibility (fflate async uses Web Workers) const result = plugins.fflate.unzipSync(data); const entries: Array<{ path: string; content: Buffer }> = []; for (const [path, content] of Object.entries(result)) { entries.push({ path, content: Buffer.from(content) }); } return entries; } }