import * as plugins from './plugins.js'; import type { IArchiveEntry, TCompressionLevel } from './interfaces.js'; import { GzipTools } from './classes.gziptools.js'; /** * TAR archive creation and extraction utilities */ export class TarTools { /** * Add a file to a TAR pack stream */ public async addFileToPack( pack: plugins.tarStream.Pack, optionsArg: { fileName?: string; content?: | string | Buffer | plugins.stream.Readable | plugins.smartfile.SmartFile | plugins.smartfile.StreamFile; byteLength?: number; filePath?: string; } ): Promise { return new Promise(async (resolve, reject) => { let fileName: string | null = null; if (optionsArg.fileName) { fileName = optionsArg.fileName; } else if (optionsArg.content instanceof plugins.smartfile.SmartFile) { fileName = optionsArg.content.relative; } else if (optionsArg.content instanceof plugins.smartfile.StreamFile) { fileName = optionsArg.content.relativeFilePath; } else if (optionsArg.filePath) { fileName = optionsArg.filePath; } if (!fileName) { reject(new Error('No filename specified for TAR entry')); return; } // Determine content byte length let contentByteLength: number | undefined; if (optionsArg.byteLength) { contentByteLength = optionsArg.byteLength; } else if (typeof optionsArg.content === 'string') { contentByteLength = Buffer.byteLength(optionsArg.content, 'utf8'); } else if (Buffer.isBuffer(optionsArg.content)) { contentByteLength = optionsArg.content.length; } else if (optionsArg.content instanceof plugins.smartfile.SmartFile) { contentByteLength = await optionsArg.content.getSize(); } else if (optionsArg.content instanceof plugins.smartfile.StreamFile) { contentByteLength = await optionsArg.content.getSize(); } else if (optionsArg.filePath) { const fileStat = await plugins.fsPromises.stat(optionsArg.filePath); contentByteLength = fileStat.size; } // Convert all content types to Readable stream let content: plugins.stream.Readable; if (Buffer.isBuffer(optionsArg.content)) { content = plugins.stream.Readable.from(optionsArg.content); } else if (typeof optionsArg.content === 'string') { content = plugins.stream.Readable.from(Buffer.from(optionsArg.content)); } else if (optionsArg.content instanceof plugins.smartfile.SmartFile) { content = plugins.stream.Readable.from(optionsArg.content.contents); } else if (optionsArg.content instanceof plugins.smartfile.StreamFile) { content = await optionsArg.content.createReadStream(); } else if (optionsArg.content instanceof plugins.stream.Readable) { content = optionsArg.content; } else if (optionsArg.filePath) { content = plugins.fs.createReadStream(optionsArg.filePath); } else { reject(new Error('No content or filePath specified for TAR entry')); return; } const entry = pack.entry( { name: fileName, ...(contentByteLength !== undefined ? { size: contentByteLength } : {}), }, (err: Error | null) => { if (err) { reject(err); } else { resolve(); } } ); content.pipe(entry); // Note: resolve() is called in the callback above when pipe completes }); } /** * Pack a directory into a TAR stream */ public async packDirectory(directoryPath: string): Promise { const fileTree = await plugins.listFileTree(directoryPath, '**/*'); const pack = await this.getPackStream(); for (const filePath of fileTree) { const absolutePath = plugins.path.join(directoryPath, filePath); const fileStat = await plugins.fsPromises.stat(absolutePath); await this.addFileToPack(pack, { byteLength: fileStat.size, filePath: absolutePath, fileName: filePath, content: plugins.fs.createReadStream(absolutePath), }); } return pack; } /** * Get a new TAR pack stream */ public async getPackStream(): Promise { return plugins.tarStream.pack(); } /** * Get a TAR extraction stream */ public getDecompressionStream(): plugins.tarStream.Extract { return plugins.tarStream.extract(); } /** * Pack files into a TAR buffer */ public async packFiles(files: IArchiveEntry[]): Promise { const pack = await this.getPackStream(); for (const file of files) { await this.addFileToPack(pack, { fileName: file.archivePath, content: file.content as string | Buffer | plugins.stream.Readable | plugins.smartfile.SmartFile | plugins.smartfile.StreamFile, byteLength: file.size, }); } pack.finalize(); const chunks: Buffer[] = []; return new Promise((resolve, reject) => { pack.on('data', (chunk: Buffer) => chunks.push(chunk)); pack.on('end', () => resolve(Buffer.concat(chunks))); pack.on('error', reject); }); } /** * Pack a directory into a TAR.GZ buffer */ public async packDirectoryToTarGz( directoryPath: string, compressionLevel?: TCompressionLevel ): Promise { const pack = await this.packDirectory(directoryPath); pack.finalize(); const gzipTools = new GzipTools(); const gzipStream = gzipTools.getCompressionStream(compressionLevel); const chunks: Buffer[] = []; return new Promise((resolve, reject) => { pack .pipe(gzipStream) .on('data', (chunk: Buffer) => chunks.push(chunk)) .on('end', () => resolve(Buffer.concat(chunks))) .on('error', reject); }); } /** * Pack a directory into a TAR.GZ stream */ public async packDirectoryToTarGzStream( directoryPath: string, compressionLevel?: TCompressionLevel ): Promise { const pack = await this.packDirectory(directoryPath); pack.finalize(); const gzipTools = new GzipTools(); const gzipStream = gzipTools.getCompressionStream(compressionLevel); return pack.pipe(gzipStream); } /** * Pack files into a TAR.GZ buffer */ public async packFilesToTarGz( files: IArchiveEntry[], compressionLevel?: TCompressionLevel ): Promise { const tarBuffer = await this.packFiles(files); const gzipTools = new GzipTools(); return gzipTools.compress(tarBuffer, compressionLevel); } }