UNPKG

gzipper

Version:

CLI for compressing files.

166 lines 15.6 kB
import { parentPort, workerData } from 'node:worker_threads'; import { createReadStream, createWriteStream } from 'node:fs'; import { lstat, unlink } from 'node:fs/promises'; import { pipeline } from 'node:stream/promises'; import path from 'node:path'; import crypto from 'node:crypto'; import { OUTPUT_FILE_FORMAT_REGEXP } from './constants.js'; import { createFolders, checkFileExists, readableSize, readableHrtime, } from './helpers.js'; import { Logger } from './logger/Logger.js'; import { CompressService } from './Compress.service.js'; import { Incremental } from './Incremental.js'; class CompressWorker { options = workerData.options; chunk = workerData.chunk; target = workerData.target; outputPath = workerData.outputPath; incrementalFilePaths = workerData.incrementalFilePaths; incremental; service; compressionInstances; logger; constructor() { if (this.options.incremental) { this.incremental = new Incremental(); this.incremental.filePaths = this.incrementalFilePaths; } this.logger = new Logger(); this.logger.initialize({ verbose: this.options.verbose, color: this.options.color, }); this.service = new CompressService(this.options); this.compressionInstances = this.service.getCompressionInstances(); } /** * Compress files list and returns files and incremental data. */ async compressFiles() { const filesList = []; for (const compressionInstance of this.compressionInstances) { for (const filePath of this.chunk) { const hrtimeStart = process.hrtime(); const fileInfo = await this.compressFile(path.basename(filePath), path.dirname(filePath), compressionInstance); if (!fileInfo.removeCompressed && !fileInfo.isSkipped) { filesList.push(filePath); } if (this.options.verbose) { const hrTimeEnd = process.hrtime(hrtimeStart); this.logger.log(this.getCompressedFileMsg(compressionInstance, filePath, fileInfo, hrTimeEnd)); } } } return { files: filesList, filePaths: this.incremental?.filePaths, }; } /** * File compression. */ async compressFile(filename, target, compressionInstance) { const createCompression = await compressionInstance.getCompression(); let isCached = false; let isSkipped = false; const inputPath = path.join(target, filename); if (this.outputPath) { const isFileTarget = (await lstat(this.target)).isFile(); target = isFileTarget ? this.outputPath : path.join(this.outputPath, path.relative(this.target, target)); await createFolders(target); } const outputPath = this.getOutputPath(target, filename, compressionInstance.ext); if (this.options.skipCompressed) { if (await checkFileExists(outputPath)) { isSkipped = true; return { isCached, isSkipped }; } } if (this.options.incremental) { const checksum = await this.incremental.getFileChecksum(inputPath); const { isChanged, fileId } = this.incremental.setFile(inputPath, checksum, compressionInstance.compressionName, compressionInstance.compressionOptions); const cachedFile = path.resolve(this.incremental.cacheFolder, fileId); if (isChanged) { await pipeline(createReadStream(inputPath), createCompression, createWriteStream(outputPath)); await pipeline(createReadStream(outputPath), createWriteStream(cachedFile)); } else { await pipeline(createReadStream(cachedFile), createWriteStream(outputPath)); isCached = true; } } else { await pipeline(createReadStream(inputPath), createCompression, createWriteStream(outputPath)); } if (this.options.verbose || this.options.removeLarger) { const beforeSize = (await lstat(inputPath)).size; const afterSize = (await lstat(outputPath)).size; const removeCompressed = this.options.removeLarger && beforeSize < afterSize; if (removeCompressed) { await unlink(outputPath); } return { beforeSize, afterSize, isCached, isSkipped, removeCompressed, }; } return { isCached, isSkipped }; } /** * Get output path which is based on [outputFileFormat]. */ getOutputPath(target, file, ext) { const artifactsMap = new Map([ ['[filename]', path.parse(file).name], ['[ext]', path.extname(file).slice(1)], ['[compressExt]', ext], ]); let filename = `${artifactsMap.get('[filename]')}.${artifactsMap.get('[ext]')}.${artifactsMap.get('[compressExt]')}`; if (this.options.outputFileFormat) { artifactsMap.set('[hash]', null); filename = this.options.outputFileFormat.replace(OUTPUT_FILE_FORMAT_REGEXP, (artifact) => { if (artifactsMap.has(artifact)) { // Need to generate hash only if we have appropriate param if (artifact === '[hash]') { artifactsMap.set('[hash]', crypto.randomUUID()); } return artifactsMap.get(artifact); } else { return artifact; } }); } filename = filename.replaceAll(/\.+/g, (match, offset, value) => match.length + offset === value.length ? '' : '.'); return path.join(target, filename); } /** * Returns information message about compressed file (size, time, cache, etc.) */ getCompressedFileMsg(compressionInstance, file, fileInfo, hrtime) { const fileRelative = path.relative(this.target, file); const compressionName = compressionInstance.compressionName; if (fileInfo.isSkipped) { return `File ${fileRelative} has been skipped.`; } const getSize = `${readableSize(fileInfo.beforeSize)} -> ${readableSize(fileInfo.afterSize)}`; const getTime = readableHrtime(hrtime); const fileMessage = fileInfo.isCached ? `File ${fileRelative} has been retrieved from the cache.` : `File ${fileRelative} has been compressed.`; return `${fileMessage} \n Algorithm: ${compressionName} \n Size: ${getSize} \n Time: ${getTime}`; } } const compressWorker = new CompressWorker(); (async function () { const { files, filePaths } = await compressWorker.compressFiles(); parentPort?.postMessage({ files, filePaths }); })(); //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiQ29tcHJlc3Mud29ya2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL0NvbXByZXNzLndvcmtlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsVUFBVSxFQUFFLFVBQVUsRUFBRSxNQUFNLHFCQUFxQixDQUFDO0FBQzdELE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxpQkFBaUIsRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUM5RCxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0sRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ2pELE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUNoRCxPQUFPLElBQUksTUFBTSxXQUFXLENBQUM7QUFDN0IsT0FBTyxNQUFNLE1BQU0sYUFBYSxDQUFDO0FBU2pDLE9BQU8sRUFBRSx5QkFBeUIsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBQzNELE9BQU8sRUFDTCxhQUFhLEVBQ2IsZUFBZSxFQUNmLFlBQVksRUFDWixjQUFjLEdBQ2YsTUFBTSxjQUFjLENBQUM7QUFDdEIsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQzVDLE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUN4RCxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFFL0MsTUFBTSxjQUFjO0lBQ0QsT0FBTyxHQUFvQixVQUFVLENBQUMsT0FBTyxDQUFDO0lBQzlDLEtBQUssR0FBYSxVQUFVLENBQUMsS0FBSyxDQUFDO0lBQ25DLE1BQU0sR0FBVyxVQUFVLENBQUMsTUFBTSxDQUFDO0lBQ25DLFVBQVUsR0FBVyxVQUFVLENBQUMsVUFBVSxDQUFDO0lBQzNDLG9CQUFvQixHQUNuQyxVQUFVLENBQUMsb0JBQW9CLENBQUM7SUFDakIsV0FBVyxDQUFlO0lBQzFCLE9BQU8sQ0FBa0I7SUFDekIsb0JBQW9CLENBQW9CO0lBQ3hDLE1BQU0sQ0FB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