2024-03-17 00:29:42 +01:00
|
|
|
import * as plugins from './plugins.js';
|
2025-11-25 12:32:13 +00:00
|
|
|
import type { IArchiveEntry, TCompressionLevel } from './interfaces.js';
|
2024-03-17 00:29:42 +01:00
|
|
|
|
2025-11-25 12:32:13 +00:00
|
|
|
/**
|
|
|
|
|
* Transform stream for ZIP decompression using fflate
|
|
|
|
|
* Emits StreamFile objects for each file in the archive
|
|
|
|
|
*/
|
|
|
|
|
export class ZipDecompressionTransform extends plugins.smartstream.SmartDuplex<Buffer, plugins.smartfile.StreamFile> {
|
|
|
|
|
private streamtools!: plugins.smartstream.IStreamTools;
|
2024-03-17 00:29:42 +01:00
|
|
|
private unzipper = new plugins.fflate.Unzip(async (fileArg) => {
|
|
|
|
|
let resultBuffer: Buffer;
|
2025-11-25 12:32:13 +00:00
|
|
|
fileArg.ondata = async (_flateError, dat, final) => {
|
2025-08-18 01:29:06 +00:00
|
|
|
resultBuffer
|
|
|
|
|
? (resultBuffer = Buffer.concat([resultBuffer, Buffer.from(dat)]))
|
|
|
|
|
: (resultBuffer = Buffer.from(dat));
|
2024-03-17 00:29:42 +01:00
|
|
|
if (final) {
|
2025-11-25 12:32:13 +00:00
|
|
|
const streamFile = plugins.smartfile.StreamFile.fromBuffer(resultBuffer);
|
2024-03-17 00:29:42 +01:00
|
|
|
streamFile.relativeFilePath = fileArg.name;
|
2024-03-17 00:35:17 +01:00
|
|
|
this.streamtools.push(streamFile);
|
2024-03-17 00:29:42 +01:00
|
|
|
}
|
2025-08-18 01:29:06 +00:00
|
|
|
};
|
2024-03-17 00:29:42 +01:00
|
|
|
fileArg.start();
|
|
|
|
|
});
|
2025-11-25 12:32:13 +00:00
|
|
|
|
2024-03-17 00:29:42 +01:00
|
|
|
constructor() {
|
|
|
|
|
super({
|
|
|
|
|
objectMode: true,
|
2025-08-18 01:01:02 +00:00
|
|
|
writeFunction: async (chunkArg, streamtoolsArg) => {
|
2025-08-18 01:29:06 +00:00
|
|
|
this.streamtools ? null : (this.streamtools = streamtoolsArg);
|
|
|
|
|
this.unzipper.push(
|
2025-11-25 12:32:13 +00:00
|
|
|
Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg as unknown as ArrayBuffer),
|
|
|
|
|
false
|
2025-08-18 01:29:06 +00:00
|
|
|
);
|
2025-11-25 12:32:13 +00:00
|
|
|
return null;
|
2024-03-17 00:29:42 +01:00
|
|
|
},
|
|
|
|
|
finalFunction: async () => {
|
|
|
|
|
this.unzipper.push(Buffer.from(''), true);
|
|
|
|
|
await plugins.smartdelay.delayFor(0);
|
2024-03-17 00:42:19 +01:00
|
|
|
await this.streamtools.push(null);
|
2025-11-25 12:32:13 +00:00
|
|
|
return null;
|
2025-08-18 01:29:06 +00:00
|
|
|
},
|
2024-03-17 00:29:42 +01:00
|
|
|
});
|
|
|
|
|
this.unzipper.register(plugins.fflate.UnzipInflate);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-25 12:32:13 +00:00
|
|
|
/**
|
|
|
|
|
* Streaming ZIP compression using fflate
|
|
|
|
|
* Allows adding multiple entries before finalizing
|
|
|
|
|
*/
|
|
|
|
|
export class ZipCompressionStream extends plugins.stream.Duplex {
|
|
|
|
|
private files: Map<string, { data: Uint8Array; options?: plugins.fflate.ZipOptions }> = new Map();
|
|
|
|
|
private finalized = false;
|
2024-03-17 00:29:42 +01:00
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-25 12:32:13 +00:00
|
|
|
/**
|
|
|
|
|
* Add a file entry to the ZIP archive
|
|
|
|
|
*/
|
|
|
|
|
public async addEntry(
|
|
|
|
|
fileName: string,
|
|
|
|
|
content: Buffer | plugins.stream.Readable,
|
|
|
|
|
options?: { compressionLevel?: TCompressionLevel }
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
if (this.finalized) {
|
|
|
|
|
throw new Error('Cannot add entries to a finalized ZIP archive');
|
|
|
|
|
}
|
2024-03-17 00:29:42 +01:00
|
|
|
|
2025-11-25 12:32:13 +00:00
|
|
|
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));
|
2024-03-17 00:29:42 +01:00
|
|
|
}
|
2025-11-25 12:32:13 +00:00
|
|
|
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<void> {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-25 13:37:27 +00:00
|
|
|
// 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;
|
|
|
|
|
}
|
2024-03-17 00:29:42 +01:00
|
|
|
}
|
2025-11-25 12:32:13 +00:00
|
|
|
|
|
|
|
|
_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'));
|
|
|
|
|
}
|
2024-03-17 00:29:42 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-25 12:32:13 +00:00
|
|
|
/**
|
|
|
|
|
* ZIP compression and decompression utilities
|
|
|
|
|
*/
|
2024-03-17 00:29:42 +01:00
|
|
|
export class ZipTools {
|
2025-11-25 12:32:13 +00:00
|
|
|
/**
|
|
|
|
|
* Get a streaming compression object for creating ZIP archives
|
|
|
|
|
*/
|
|
|
|
|
public getCompressionStream(): ZipCompressionStream {
|
|
|
|
|
return new ZipCompressionStream();
|
|
|
|
|
}
|
2024-03-17 00:29:42 +01:00
|
|
|
|
2025-11-25 12:32:13 +00:00
|
|
|
/**
|
|
|
|
|
* Get a streaming decompression transform for extracting ZIP archives
|
|
|
|
|
*/
|
|
|
|
|
public getDecompressionStream(): ZipDecompressionTransform {
|
|
|
|
|
return new ZipDecompressionTransform();
|
2024-03-17 00:29:42 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-25 12:32:13 +00:00
|
|
|
/**
|
|
|
|
|
* Create a ZIP archive from an array of entries
|
|
|
|
|
*/
|
|
|
|
|
public async createZip(entries: IArchiveEntry[], compressionLevel?: TCompressionLevel): Promise<Buffer> {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-25 13:37:27 +00:00
|
|
|
// Use sync version for Deno compatibility (fflate async uses Web Workers)
|
|
|
|
|
const result = plugins.fflate.zipSync(filesObj);
|
|
|
|
|
return Buffer.from(result);
|
2025-11-25 12:32:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extract a ZIP buffer to an array of entries
|
|
|
|
|
*/
|
|
|
|
|
public async extractZip(data: Buffer): Promise<Array<{ path: string; content: Buffer }>> {
|
2025-11-25 13:37:27 +00:00
|
|
|
// 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;
|
2024-03-17 00:29:42 +01:00
|
|
|
}
|
|
|
|
|
}
|