Files
smartarchive/ts/classes.ziptools.ts

197 lines
6.0 KiB
TypeScript
Raw Permalink Normal View History

2024-03-17 00:29:42 +01:00
import * as plugins from './plugins.js';
import type { IArchiveEntry, TCompressionLevel } from './interfaces.js';
2024-03-17 00:29:42 +01: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;
fileArg.ondata = async (_flateError, dat, final) => {
resultBuffer
? (resultBuffer = Buffer.concat([resultBuffer, Buffer.from(dat)]))
: (resultBuffer = Buffer.from(dat));
2024-03-17 00:29:42 +01:00
if (final) {
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
}
};
2024-03-17 00:29:42 +01:00
fileArg.start();
});
2024-03-17 00:29:42 +01:00
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;
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);
return null;
},
2024-03-17 00:29:42 +01:00
});
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<string, { data: Uint8Array; options?: plugins.fflate.ZipOptions }> = new Map();
private finalized = false;
2024-03-17 00:29:42 +01:00
constructor() {
super();
}
/**
* 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
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
}
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;
}
// 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
}
_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
}
/**
* ZIP compression and decompression utilities
*/
2024-03-17 00:29:42 +01:00
export class ZipTools {
/**
* Get a streaming compression object for creating ZIP archives
*/
public getCompressionStream(): ZipCompressionStream {
return new ZipCompressionStream();
}
2024-03-17 00:29:42 +01:00
/**
* Get a streaming decompression transform for extracting ZIP archives
*/
public getDecompressionStream(): ZipDecompressionTransform {
return new ZipDecompressionTransform();
2024-03-17 00:29:42 +01: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;
}
}
// 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<Array<{ path: string; content: Buffer }>> {
// 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
}
}