From bf7a442864397d47d0ec4c513503617bdc858b22 Mon Sep 17 00:00:00 2001 From: Lukasz Siatka Date: Wed, 28 Jan 2026 11:37:55 +0100 Subject: [PATCH 1/8] Chore: removes lib/ folder --- lib/core.js | 852 -------------------------------------------- lib/error.js | 37 -- lib/plugins/json.js | 79 ---- lib/plugins/tar.js | 118 ------ lib/plugins/zip.js | 72 ---- lib/utils.js | 66 ---- 6 files changed, 1224 deletions(-) delete mode 100644 lib/core.js delete mode 100644 lib/error.js delete mode 100644 lib/plugins/json.js delete mode 100644 lib/plugins/tar.js delete mode 100644 lib/plugins/zip.js delete mode 100644 lib/utils.js diff --git a/lib/core.js b/lib/core.js deleted file mode 100644 index f38bb5eaf..000000000 --- a/lib/core.js +++ /dev/null @@ -1,852 +0,0 @@ -import { createReadStream, lstat, readlinkSync, Stats } from "node:fs"; -import { isStream } from "is-stream"; -import { readdirGlob } from "readdir-glob"; -import { Readable } from "lazystream"; -import { queue } from "async"; -import { - dirname, - relative as relativePath, - resolve as resolvePath, -} from "node:path"; -import { ArchiverError } from "./error.js"; -import { Transform } from "readable-stream"; -import { - dateify, - normalizeInputSource, - sanitizePath, - trailingSlashIt, -} from "./utils.js"; -const { ReaddirGlob } = readdirGlob; -const win32 = process.platform === "win32"; - -const abortedError = new ArchiverError("ABORTED"); -const finalizingError = new ArchiverError("FINALIZING"); - -export default class Archiver extends Transform { - _supportsDirectory = false; - _supportsSymlink = false; - - /** - * @constructor - * @param {String} format The archive format to use. - * @param {(CoreOptions|TransformOptions)} options See also {@link ZipOptions} and {@link TarOptions}. - */ - constructor(options) { - options = { - highWaterMark: 1024 * 1024, - statConcurrency: 4, - ...options, - }; - super(options); - this.options = options; - this._format = false; - this._module = false; - this._pending = 0; - this._pointer = 0; - this._entriesCount = 0; - this._entriesProcessedCount = 0; - this._fsEntriesTotalBytes = 0; - this._fsEntriesProcessedBytes = 0; - this._queue = queue(this._onQueueTask.bind(this), 1); - this._queue.drain(this._onQueueDrain.bind(this)); - this._statQueue = queue( - this._onStatQueueTask.bind(this), - options.statConcurrency, - ); - this._statQueue.drain(this._onQueueDrain.bind(this)); - this._state = { - aborted: false, - finalize: false, - finalizing: false, - finalized: false, - modulePiped: false, - }; - this._streams = []; - } - - /** - * Internal logic for `abort`. - * - * @private - * @return void - */ - _abort() { - this._state.aborted = true; - this._queue.kill(); - this._statQueue.kill(); - if (this._queue.idle()) { - this._shutdown(); - } - } - /** - * Internal helper for appending files. - * - * @private - * @param {String} filepath The source filepath. - * @param {EntryData} data The entry data. - * @return void - */ - _append(filepath, data) { - data = data || {}; - let task = { - source: null, - filepath: filepath, - }; - if (!data.name) { - data.name = filepath; - } - data.sourcePath = filepath; - task.data = data; - this._entriesCount++; - if (data.stats && data.stats instanceof Stats) { - task = this._updateQueueTaskWithStats(task, data.stats); - if (task) { - if (data.stats.size) { - this._fsEntriesTotalBytes += data.stats.size; - } - this._queue.push(task); - } - } else { - this._statQueue.push(task); - } - } - /** - * Internal logic for `finalize`. - * - * @private - * @return void - */ - _finalize() { - if ( - this._state.finalizing || - this._state.finalized || - this._state.aborted - ) { - return; - } - this._state.finalizing = true; - this._moduleFinalize(); - this._state.finalizing = false; - this._state.finalized = true; - } - /** - * Checks the various state variables to determine if we can `finalize`. - * - * @private - * @return {Boolean} - */ - _maybeFinalize() { - if ( - this._state.finalizing || - this._state.finalized || - this._state.aborted - ) { - return false; - } - if ( - this._state.finalize && - this._pending === 0 && - this._queue.idle() && - this._statQueue.idle() - ) { - this._finalize(); - return true; - } - return false; - } - /** - * Appends an entry to the module. - * - * @private - * @fires Archiver#entry - * @param {(Buffer|Stream)} source - * @param {EntryData} data - * @param {Function} callback - * @return void - */ - _moduleAppend(source, data, callback) { - if (this._state.aborted) { - callback(); - return; - } - this._module.append( - source, - data, - function (err) { - this._task = null; - if (this._state.aborted) { - this._shutdown(); - return; - } - if (err) { - this.emit("error", err); - setImmediate(callback); - return; - } - /** - * Fires when the entry's input has been processed and appended to the archive. - * - * @event Archiver#entry - * @type {EntryData} - */ - this.emit("entry", data); - this._entriesProcessedCount++; - if (data.stats?.size) { - this._fsEntriesProcessedBytes += data.stats.size; - } - /** - * @event Archiver#progress - * @type {ProgressData} - */ - this.emit("progress", { - entries: { - total: this._entriesCount, - processed: this._entriesProcessedCount, - }, - fs: { - totalBytes: this._fsEntriesTotalBytes, - processedBytes: this._fsEntriesProcessedBytes, - }, - }); - setImmediate(callback); - }.bind(this), - ); - } - /** - * Finalizes the module. - * - * @private - * @return void - */ - _moduleFinalize() { - if (typeof this._module.finalize === "function") { - this._module.finalize(); - } else if (typeof this._module.end === "function") { - this._module.end(); - } else { - this.emit("error", new ArchiverError("NOENDMETHOD")); - } - } - /** - * Pipes the module to our internal stream with error bubbling. - * - * @private - * @return void - */ - _modulePipe() { - this._module.on("error", this._onModuleError.bind(this)); - this._module.pipe(this); - this._state.modulePiped = true; - } - /** - * Unpipes the module from our internal stream. - * - * @private - * @return void - */ - _moduleUnpipe() { - this._module.unpipe(this); - this._state.modulePiped = false; - } - /** - * Normalizes entry data with fallbacks for key properties. - * - * @private - * @param {Object} data - * @param {fs.Stats} stats - * @return {Object} - */ - _normalizeEntryData(data, stats) { - data = { - type: "file", - name: null, - date: null, - mode: null, - prefix: null, - sourcePath: null, - stats: false, - ...data, - }; - if (stats && data.stats === false) { - data.stats = stats; - } - let isDir = data.type === "directory"; - if (data.name) { - if (typeof data.prefix === "string" && "" !== data.prefix) { - data.name = data.prefix + "/" + data.name; - data.prefix = null; - } - data.name = sanitizePath(data.name); - if (data.type !== "symlink" && data.name.slice(-1) === "/") { - isDir = true; - data.type = "directory"; - } else if (isDir) { - data.name += "/"; - } - } - // 511 === 0777; 493 === 0755; 438 === 0666; 420 === 0644 - if (typeof data.mode === "number") { - if (win32) { - data.mode &= 511; - } else { - data.mode &= 4095; - } - } else if (data.stats && data.mode === null) { - if (win32) { - data.mode = data.stats.mode & 511; - } else { - data.mode = data.stats.mode & 4095; - } - // stat isn't reliable on windows; force 0755 for dir - if (win32 && isDir) { - data.mode = 493; - } - } else if (data.mode === null) { - data.mode = isDir ? 493 : 420; - } - if (data.stats && data.date === null) { - data.date = data.stats.mtime; - } else { - data.date = dateify(data.date); - } - return data; - } - /** - * Error listener that re-emits error on to our internal stream. - * - * @private - * @param {Error} err - * @return void - */ - _onModuleError(err) { - /** - * @event Archiver#error - * @type {ErrorData} - */ - this.emit("error", err); - } - /** - * Checks the various state variables after queue has drained to determine if - * we need to `finalize`. - * - * @private - * @return void - */ - _onQueueDrain() { - if ( - this._state.finalizing || - this._state.finalized || - this._state.aborted - ) { - return; - } - if ( - this._state.finalize && - this._pending === 0 && - this._queue.idle() && - this._statQueue.idle() - ) { - this._finalize(); - } - } - /** - * Appends each queue task to the module. - * - * @private - * @param {Object} task - * @param {Function} callback - * @return void - */ - _onQueueTask(task, callback) { - const fullCallback = () => { - if (task.data.callback) { - task.data.callback(); - } - callback(); - }; - if ( - this._state.finalizing || - this._state.finalized || - this._state.aborted - ) { - fullCallback(); - return; - } - this._task = task; - this._moduleAppend(task.source, task.data, fullCallback); - } - /** - * Performs a file stat and reinjects the task back into the queue. - * - * @private - * @param {Object} task - * @param {Function} callback - * @return void - */ - _onStatQueueTask(task, callback) { - if ( - this._state.finalizing || - this._state.finalized || - this._state.aborted - ) { - callback(); - return; - } - lstat( - task.filepath, - function (err, stats) { - if (this._state.aborted) { - setImmediate(callback); - return; - } - if (err) { - this._entriesCount--; - /** - * @event Archiver#warning - * @type {ErrorData} - */ - this.emit("warning", err); - setImmediate(callback); - return; - } - task = this._updateQueueTaskWithStats(task, stats); - if (task) { - if (stats.size) { - this._fsEntriesTotalBytes += stats.size; - } - this._queue.push(task); - } - setImmediate(callback); - }.bind(this), - ); - } - /** - * Unpipes the module and ends our internal stream. - * - * @private - * @return void - */ - _shutdown() { - this._moduleUnpipe(); - this.end(); - } - /** - * Tracks the bytes emitted by our internal stream. - * - * @private - * @param {Buffer} chunk - * @param {String} encoding - * @param {Function} callback - * @return void - */ - _transform(chunk, encoding, callback) { - if (chunk) { - this._pointer += chunk.length; - } - callback(null, chunk); - } - /** - * Updates and normalizes a queue task using stats data. - * - * @private - * @param {Object} task - * @param {Stats} stats - * @return {Object} - */ - _updateQueueTaskWithStats(task, stats) { - if (stats.isFile()) { - task.data.type = "file"; - task.data.sourceType = "stream"; - task.source = new Readable(() => { - return createReadStream(task.filepath); - }); - } else if (stats.isDirectory() && this._supportsDirectory) { - task.data.name = trailingSlashIt(task.data.name); - task.data.type = "directory"; - task.data.sourcePath = trailingSlashIt(task.filepath); - task.data.sourceType = "buffer"; - task.source = Buffer.concat([]); - } else if (stats.isSymbolicLink() && this._supportsSymlink) { - const linkPath = readlinkSync(task.filepath); - const dirName = dirname(task.filepath); - task.data.type = "symlink"; - task.data.linkname = relativePath( - dirName, - resolvePath(dirName, linkPath), - ); - task.data.sourceType = "buffer"; - task.source = Buffer.concat([]); - } else { - if (stats.isDirectory()) { - this.emit( - "warning", - new ArchiverError("DIRECTORYNOTSUPPORTED", task.data), - ); - } else if (stats.isSymbolicLink()) { - this.emit( - "warning", - new ArchiverError("SYMLINKNOTSUPPORTED", task.data), - ); - } else { - this.emit("warning", new ArchiverError("ENTRYNOTSUPPORTED", task.data)); - } - return null; - } - task.data = this._normalizeEntryData(task.data, stats); - return task; - } - /** - * Aborts the archiving process, taking a best-effort approach, by: - * - * - removing any pending queue tasks - * - allowing any active queue workers to finish - * - detaching internal module pipes - * - ending both sides of the Transform stream - * - * It will NOT drain any remaining sources. - * - * @return {this} - */ - abort() { - if (this._state.aborted || this._state.finalized) { - return this; - } - this._abort(); - return this; - } - /** - * Appends an input source (text string, buffer, or stream) to the instance. - * - * When the instance has received, processed, and emitted the input, the `entry` - * event is fired. - * - * @fires Archiver#entry - * @param {(Buffer|Stream|String)} source The input source. - * @param {EntryData} data See also {@link ZipEntryData} and {@link TarEntryData}. - * @return {this} - */ - append(source, data) { - if (this._state.finalize || this._state.aborted) { - this.emit("error", new ArchiverError("QUEUECLOSED")); - return this; - } - data = this._normalizeEntryData(data); - if (typeof data.name !== "string" || data.name.length === 0) { - this.emit("error", new ArchiverError("ENTRYNAMEREQUIRED")); - return this; - } - if (data.type === "directory" && !this._supportsDirectory) { - this.emit( - "error", - new ArchiverError("DIRECTORYNOTSUPPORTED", { name: data.name }), - ); - return this; - } - source = normalizeInputSource(source); - if (Buffer.isBuffer(source)) { - data.sourceType = "buffer"; - } else if (isStream(source)) { - data.sourceType = "stream"; - } else { - this.emit( - "error", - new ArchiverError("INPUTSTEAMBUFFERREQUIRED", { name: data.name }), - ); - return this; - } - this._entriesCount++; - this._queue.push({ - data: data, - source: source, - }); - return this; - } - /** - * Appends a directory and its files, recursively, given its dirpath. - * - * @param {String} dirpath The source directory path. - * @param {String} destpath The destination path within the archive. - * @param {(EntryData|Function)} data See also [ZipEntryData]{@link ZipEntryData} and - * [TarEntryData]{@link TarEntryData}. - * @return {this} - */ - directory(dirpath, destpath, data) { - if (this._state.finalize || this._state.aborted) { - this.emit("error", new ArchiverError("QUEUECLOSED")); - return this; - } - if (typeof dirpath !== "string" || dirpath.length === 0) { - this.emit("error", new ArchiverError("DIRECTORYDIRPATHREQUIRED")); - return this; - } - this._pending++; - if (destpath === false) { - destpath = ""; - } else if (typeof destpath !== "string") { - destpath = dirpath; - } - let dataFunction = false; - if (typeof data === "function") { - dataFunction = data; - data = {}; - } else if (typeof data !== "object") { - data = {}; - } - const globOptions = { - stat: true, - dot: true, - }; - function onGlobEnd() { - this._pending--; - this._maybeFinalize(); - } - function onGlobError(err) { - this.emit("error", err); - } - function onGlobMatch(match) { - globber.pause(); - let ignoreMatch = false; - let entryData = Object.assign({}, data); - entryData.name = match.relative; - entryData.prefix = destpath; - entryData.stats = match.stat; - entryData.callback = globber.resume.bind(globber); - try { - if (dataFunction) { - entryData = dataFunction(entryData); - if (entryData === false) { - ignoreMatch = true; - } else if (typeof entryData !== "object") { - throw new ArchiverError("DIRECTORYFUNCTIONINVALIDDATA", { - dirpath: dirpath, - }); - } - } - } catch (e) { - this.emit("error", e); - return; - } - if (ignoreMatch) { - globber.resume(); - return; - } - this._append(match.absolute, entryData); - } - const globber = readdirGlob(dirpath, globOptions); - globber.on("error", onGlobError.bind(this)); - globber.on("match", onGlobMatch.bind(this)); - globber.on("end", onGlobEnd.bind(this)); - return this; - } - /** - * Appends a file given its filepath using a - * [lazystream]{@link https://github.com/jpommerening/node-lazystream} wrapper to - * prevent issues with open file limits. - * - * When the instance has received, processed, and emitted the file, the `entry` - * event is fired. - * - * @param {String} filepath The source filepath. - * @param {EntryData} data See also [ZipEntryData]{@link ZipEntryData} and - * [TarEntryData]{@link TarEntryData}. - * @return {this} - */ - file(filepath, data) { - if (this._state.finalize || this._state.aborted) { - this.emit("error", new ArchiverError("QUEUECLOSED")); - return this; - } - if (typeof filepath !== "string" || filepath.length === 0) { - this.emit("error", new ArchiverError("FILEFILEPATHREQUIRED")); - return this; - } - this._append(filepath, data); - return this; - } - /** - * Appends multiple files that match a glob pattern. - * - * @param {String} pattern The [glob pattern]{@link https://github.com/isaacs/minimatch} to match. - * @param {Object} options See [node-readdir-glob]{@link https://github.com/yqnn/node-readdir-glob#options}. - * @param {EntryData} data See also [ZipEntryData]{@link ZipEntryData} and - * [TarEntryData]{@link TarEntryData}. - * @return {this} - */ - glob(pattern, options, data) { - this._pending++; - options = { - stat: true, - pattern: pattern, - ...options, - }; - function onGlobEnd() { - this._pending--; - this._maybeFinalize(); - } - function onGlobError(err) { - this.emit("error", err); - } - function onGlobMatch(match) { - globber.pause(); - const entryData = Object.assign({}, data); - entryData.callback = globber.resume.bind(globber); - entryData.stats = match.stat; - entryData.name = match.relative; - this._append(match.absolute, entryData); - } - const globber = new ReaddirGlob(options.cwd || ".", options); - globber.on("error", onGlobError.bind(this)); - globber.on("match", onGlobMatch.bind(this)); - globber.on("end", onGlobEnd.bind(this)); - return this; - } - /** - * Finalizes the instance and prevents further appending to the archive - * structure (queue will continue til drained). - * - * The `end`, `close` or `finish` events on the destination stream may fire - * right after calling this method so you should set listeners beforehand to - * properly detect stream completion. - * - * @return {Promise} - */ - finalize() { - if (this._state.aborted) { - this.emit("error", abortedError); - return Promise.reject(abortedError); - } - if (this._state.finalize) { - this.emit("error", finalizingError); - return Promise.reject(finalizingError); - } - this._state.finalize = true; - if (this._pending === 0 && this._queue.idle() && this._statQueue.idle()) { - this._finalize(); - } - const self = this; - return new Promise((resolve, reject) => { - let errored; - self._module.on("end", () => { - if (!errored) { - resolve(); - } - }); - self._module.on("error", (err) => { - errored = true; - reject(err); - }); - }); - } - /** - * Appends a symlink to the instance. - * - * This does NOT interact with filesystem and is used for programmatically creating symlinks. - * - * @param {String} filepath The symlink path (within archive). - * @param {String} target The target path (within archive). - * @param {Number} mode Sets the entry permissions. - * @return {this} - */ - symlink(filepath, target, mode) { - if (this._state.finalize || this._state.aborted) { - this.emit("error", new ArchiverError("QUEUECLOSED")); - return this; - } - if (typeof filepath !== "string" || filepath.length === 0) { - this.emit("error", new ArchiverError("SYMLINKFILEPATHREQUIRED")); - return this; - } - if (typeof target !== "string" || target.length === 0) { - this.emit( - "error", - new ArchiverError("SYMLINKTARGETREQUIRED", { filepath: filepath }), - ); - return this; - } - if (!this._supportsSymlink) { - this.emit( - "error", - new ArchiverError("SYMLINKNOTSUPPORTED", { filepath: filepath }), - ); - return this; - } - const data = {}; - data.type = "symlink"; - data.name = filepath.replace(/\\/g, "/"); - data.linkname = target.replace(/\\/g, "/"); - data.sourceType = "buffer"; - if (typeof mode === "number") { - data.mode = mode; - } - this._entriesCount++; - this._queue.push({ - data: data, - source: Buffer.concat([]), - }); - return this; - } - /** - * Returns the current length (in bytes) that has been emitted. - * - * @return {Number} - */ - pointer() { - return this._pointer; - } -} - -/** - * @typedef {Object} CoreOptions - * @global - * @property {Number} [statConcurrency=4] Sets the number of workers used to - * process the internal fs stat queue. - */ - -/** - * @typedef {Object} TransformOptions - * @property {Boolean} [allowHalfOpen=true] If set to false, then the stream - * will automatically end the readable side when the writable side ends and vice - * versa. - * @property {Boolean} [readableObjectMode=false] Sets objectMode for readable - * side of the stream. Has no effect if objectMode is true. - * @property {Boolean} [writableObjectMode=false] Sets objectMode for writable - * side of the stream. Has no effect if objectMode is true. - * @property {Boolean} [decodeStrings=true] Whether or not to decode strings - * into Buffers before passing them to _write(). `Writable` - * @property {String} [encoding=NULL] If specified, then buffers will be decoded - * to strings using the specified encoding. `Readable` - * @property {Number} [highWaterMark=16kb] The maximum number of bytes to store - * in the internal buffer before ceasing to read from the underlying resource. - * `Readable` `Writable` - * @property {Boolean} [objectMode=false] Whether this stream should behave as a - * stream of objects. Meaning that stream.read(n) returns a single value instead - * of a Buffer of size n. `Readable` `Writable` - */ - -/** - * @typedef {Object} EntryData - * @property {String} name Sets the entry name including internal path. - * @property {(String|Date)} [date=NOW()] Sets the entry date. - * @property {Number} [mode=D:0755/F:0644] Sets the entry permissions. - * @property {String} [prefix] Sets a path prefix for the entry name. Useful - * when working with methods like `directory` or `glob`. - * @property {fs.Stats} [stats] Sets the fs stat data for this entry allowing - * for reduction of fs stat calls when stat data is already known. - */ - -/** - * @typedef {Object} ErrorData - * @property {String} message The message of the error. - * @property {String} code The error code assigned to this error. - * @property {String} data Additional data provided for reporting or debugging (where available). - */ - -/** - * @typedef {Object} ProgressData - * @property {Object} entries - * @property {Number} entries.total Number of entries that have been appended. - * @property {Number} entries.processed Number of entries that have been processed. - * @property {Object} fs - * @property {Number} fs.totalBytes Number of bytes that have been appended. Calculated asynchronously and might not be accurate: it growth while entries are added. (based on fs.Stats) - * @property {Number} fs.processedBytes Number of bytes that have been processed. (based on fs.Stats) - */ diff --git a/lib/error.js b/lib/error.js deleted file mode 100644 index 1bb99a7a3..000000000 --- a/lib/error.js +++ /dev/null @@ -1,37 +0,0 @@ -import util from "util"; - -const ERROR_CODES = { - ABORTED: "archive was aborted", - DIRECTORYDIRPATHREQUIRED: - "diretory dirpath argument must be a non-empty string value", - DIRECTORYFUNCTIONINVALIDDATA: - "invalid data returned by directory custom data function", - ENTRYNAMEREQUIRED: "entry name must be a non-empty string value", - FILEFILEPATHREQUIRED: - "file filepath argument must be a non-empty string value", - FINALIZING: "archive already finalizing", - QUEUECLOSED: "queue closed", - NOENDMETHOD: "no suitable finalize/end method defined by module", - DIRECTORYNOTSUPPORTED: "support for directory entries not defined by module", - FORMATSET: "archive format already set", - INPUTSTEAMBUFFERREQUIRED: - "input source must be valid Stream or Buffer instance", - MODULESET: "module already set", - SYMLINKNOTSUPPORTED: "support for symlink entries not defined by module", - SYMLINKFILEPATHREQUIRED: - "symlink filepath argument must be a non-empty string value", - SYMLINKTARGETREQUIRED: - "symlink target argument must be a non-empty string value", - ENTRYNOTSUPPORTED: "entry not supported", -}; - -function ArchiverError(code, data) { - Error.captureStackTrace(this, this.constructor); - //this.name = this.constructor.name; - this.message = ERROR_CODES[code] || code; - this.code = code; - this.data = data; -} -util.inherits(ArchiverError, Error); - -export { ArchiverError }; diff --git a/lib/plugins/json.js b/lib/plugins/json.js deleted file mode 100644 index b377c90ed..000000000 --- a/lib/plugins/json.js +++ /dev/null @@ -1,79 +0,0 @@ -import { Transform } from "readable-stream"; -import crc32 from "buffer-crc32"; -import { collectStream } from "../utils.js"; - -/** - * JSON Format Plugin - * - * @module plugins/json - * @license [MIT]{@link https://github.com/archiverjs/node-archiver/blob/master/LICENSE} - * @copyright (c) 2012-2014 Chris Talkington, contributors. - */ -export default class Json extends Transform { - /** - * @constructor - * @param {(JsonOptions|TransformOptions)} options - */ - constructor(options) { - super({ ...options }); - this.files = []; - } - /** - * [_transform description] - * - * @private - * @param {Buffer} chunk - * @param {String} encoding - * @param {Function} callback - * @return void - */ - _transform(chunk, encoding, callback) { - callback(null, chunk); - } - /** - * [_writeStringified description] - * - * @private - * @return void - */ - _writeStringified() { - var fileString = JSON.stringify(this.files); - this.write(fileString); - } - /** - * [append description] - * - * @param {(Buffer|Stream)} source - * @param {EntryData} data - * @param {Function} callback - * @return void - */ - append(source, data, callback) { - var self = this; - data.crc32 = 0; - function onend(err, sourceBuffer) { - if (err) { - callback(err); - return; - } - data.size = sourceBuffer.length || 0; - data.crc32 = crc32.unsigned(sourceBuffer); - self.files.push(data); - callback(null, data); - } - if (data.sourceType === "buffer") { - onend(null, source); - } else if (data.sourceType === "stream") { - collectStream(source, onend); - } - } - /** - * [finalize description] - * - * @return void - */ - finalize() { - this._writeStringified(); - this.end(); - } -} diff --git a/lib/plugins/tar.js b/lib/plugins/tar.js deleted file mode 100644 index 1a4e1d043..000000000 --- a/lib/plugins/tar.js +++ /dev/null @@ -1,118 +0,0 @@ -import zlib from "zlib"; -import engine from "tar-stream"; -import { collectStream } from "../utils.js"; - -/** - * TAR Format Plugin - * - * @module plugins/tar - * @license [MIT]{@link https://github.com/archiverjs/node-archiver/blob/master/LICENSE} - * @copyright (c) 2012-2014 Chris Talkington, contributors. - */ -export default class Tar { - /** - * @constructor - * @param {TarOptions} options - */ - constructor(options) { - options = this.options = { - gzip: false, - ...options, - }; - if (typeof options.gzipOptions !== "object") { - options.gzipOptions = {}; - } - this.engine = engine.pack(options); - this.compressor = false; - if (options.gzip) { - this.compressor = zlib.createGzip(options.gzipOptions); - this.compressor.on("error", this._onCompressorError.bind(this)); - } - } - /** - * [_onCompressorError description] - * - * @private - * @param {Error} err - * @return void - */ - _onCompressorError(err) { - this.engine.emit("error", err); - } - /** - * [append description] - * - * @param {(Buffer|Stream)} source - * @param {TarEntryData} data - * @param {Function} callback - * @return void - */ - append(source, data, callback) { - var self = this; - data.mtime = data.date; - function append(err, sourceBuffer) { - if (err) { - callback(err); - return; - } - self.engine.entry(data, sourceBuffer, function (err) { - callback(err, data); - }); - } - if (data.sourceType === "buffer") { - append(null, source); - } else if (data.sourceType === "stream" && data.stats) { - data.size = data.stats.size; - var entry = self.engine.entry(data, function (err) { - callback(err, data); - }); - source.pipe(entry); - } else if (data.sourceType === "stream") { - collectStream(source, append); - } - } - /** - * [finalize description] - * - * @return void - */ - finalize() { - this.engine.finalize(); - } - /** - * [on description] - * - * @return this.engine - */ - on() { - return this.engine.on.apply(this.engine, arguments); - } - /** - * [pipe description] - * - * @param {String} destination - * @param {Object} options - * @return this.engine - */ - pipe(destination, options) { - if (this.compressor) { - return this.engine.pipe - .apply(this.engine, [this.compressor]) - .pipe(destination, options); - } else { - return this.engine.pipe.apply(this.engine, arguments); - } - } - /** - * [unpipe description] - * - * @return this.engine - */ - unpipe() { - if (this.compressor) { - return this.compressor.unpipe.apply(this.compressor, arguments); - } else { - return this.engine.unpipe.apply(this.engine, arguments); - } - } -} diff --git a/lib/plugins/zip.js b/lib/plugins/zip.js deleted file mode 100644 index 59bf5bb37..000000000 --- a/lib/plugins/zip.js +++ /dev/null @@ -1,72 +0,0 @@ -import engine from "zip-stream"; - -/** - * ZIP Format Plugin - * - * @module plugins/zip - * @license [MIT]{@link https://github.com/archiverjs/node-archiver/blob/master/LICENSE} - * @copyright (c) 2012-2014 Chris Talkington, contributors. - */ -export default class Zip { - /** - * @constructor - * @param {ZipOptions} [options] - * @param {String} [options.comment] Sets the zip archive comment. - * @param {Boolean} [options.forceLocalTime=false] Forces the archive to contain local file times instead of UTC. - * @param {Boolean} [options.forceZip64=false] Forces the archive to contain ZIP64 headers. - * @param {Boolean} [options.namePrependSlash=false] Prepends a forward slash to archive file paths. - * @param {Boolean} [options.store=false] Sets the compression method to STORE. - * @param {Object} [options.zlib] Passed to [zlib]{@link https://nodejs.org/api/zlib.html#zlib_class_options} - */ - constructor(options) { - options = this.options = { - comment: "", - forceUTC: false, - namePrependSlash: false, - store: false, - ...options, - }; - this.engine = new engine(options); - } - /** - * @param {(Buffer|Stream)} source - * @param {ZipEntryData} data - * @param {String} data.name Sets the entry name including internal path. - * @param {(String|Date)} [data.date=NOW()] Sets the entry date. - * @param {Number} [data.mode=D:0755/F:0644] Sets the entry permissions. - * @param {String} [data.prefix] Sets a path prefix for the entry name. Useful - * when working with methods like `directory` or `glob`. - * @param {fs.Stats} [data.stats] Sets the fs stat data for this entry allowing - * for reduction of fs stat calls when stat data is already known. - * @param {Boolean} [data.store=ZipOptions.store] Sets the compression method to STORE. - * @param {Function} callback - * @return void - */ - append(source, data, callback) { - this.engine.entry(source, data, callback); - } - /** - * @return void - */ - finalize() { - this.engine.finalize(); - } - /** - * @return this.engine - */ - on() { - return this.engine.on.apply(this.engine, arguments); - } - /** - * @return this.engine - */ - pipe() { - return this.engine.pipe.apply(this.engine, arguments); - } - /** - * @return this.engine - */ - unpipe() { - return this.engine.unpipe.apply(this.engine, arguments); - } -} diff --git a/lib/utils.js b/lib/utils.js deleted file mode 100644 index b8e7e1802..000000000 --- a/lib/utils.js +++ /dev/null @@ -1,66 +0,0 @@ -import normalizePath from "normalize-path"; -import { PassThrough } from "readable-stream"; -import { isStream } from "is-stream"; - -export function collectStream(source, callback) { - var collection = []; - var size = 0; - - source.on("error", callback); - - source.on("data", function (chunk) { - collection.push(chunk); - size += chunk.length; - }); - - source.on("end", function () { - var buf = Buffer.alloc(size); - var offset = 0; - - collection.forEach(function (data) { - data.copy(buf, offset); - offset += data.length; - }); - - callback(null, buf); - }); -} - -export function dateify(dateish) { - dateish = dateish || new Date(); - - if (dateish instanceof Date) { - dateish = dateish; - } else if (typeof dateish === "string") { - dateish = new Date(dateish); - } else { - dateish = new Date(); - } - - return dateish; -} - -export function normalizeInputSource(source) { - if (source === null) { - return Buffer.alloc(0); - } else if (typeof source === "string") { - return Buffer.from(source); - } else if (isStream(source)) { - // Always pipe through a PassThrough stream to guarantee pausing the stream if it's already flowing, - // since it will only be processed in a (distant) future iteration of the event loop, and will lose - // data if already flowing now. - return source.pipe(new PassThrough()); - } - - return source; -} - -export function sanitizePath(filepath) { - return normalizePath(filepath, false) - .replace(/^\w+:/, "") - .replace(/^(\.\.\/|\/)+/, ""); -} - -export function trailingSlashIt(str) { - return str.slice(-1) !== "/" ? str + "/" : str; -} From cff2e2587c53bb5f670e8a92bb9a31f8d013a052 Mon Sep 17 00:00:00 2001 From: Lukasz Siatka Date: Wed, 28 Jan 2026 11:38:24 +0100 Subject: [PATCH 2/8] Chore: removes index.js --- index.js | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 index.js diff --git a/index.js b/index.js deleted file mode 100644 index 4990640cc..000000000 --- a/index.js +++ /dev/null @@ -1,39 +0,0 @@ -import Archiver from "./lib/core.js"; -import Json from "./lib/plugins/json.js"; -import Tar from "./lib/plugins/tar.js"; -import Zip from "./lib/plugins/zip.js"; - -export { Archiver }; - -export class ZipArchive extends Archiver { - constructor(options) { - super(options); - this._format = "zip"; - this._module = new Zip(options); - this._supportsDirectory = true; - this._supportsSymlink = true; - this._modulePipe(); - } -} - -export class TarArchive extends Archiver { - constructor(options) { - super(options); - this._format = "tar"; - this._module = new Tar(options); - this._supportsDirectory = true; - this._supportsSymlink = true; - this._modulePipe(); - } -} - -export class JsonArchive extends Archiver { - constructor(options) { - super(options); - this._format = "json"; - this._module = new Json(options); - this._supportsDirectory = true; - this._supportsSymlink = true; - this._modulePipe(); - } -} From 45ff28d160ca2afa47097a0a0e044d313a3561d6 Mon Sep 17 00:00:00 2001 From: Lukasz Siatka Date: Wed, 28 Jan 2026 11:40:27 +0100 Subject: [PATCH 3/8] Chore: installs tpescript and required type libraries; adds build script; adjusts exports --- package.json | 12 +++++++---- tsconfig.json | 19 +++++++++++++++++ yarn.lock | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 tsconfig.json diff --git a/package.json b/package.json index a57954abc..05d1ac260 100644 --- a/package.json +++ b/package.json @@ -16,16 +16,16 @@ }, "license": "MIT", "type": "module", - "exports": "./index.js", + "exports": "./dist/index.js", + "types": "./dist/index.d.ts", "files": [ - "index.js", - "lib" + "dist" ], "engines": { "node": ">=18" }, "scripts": { - "build": "echo 'No build step required.'", + "build": "tsc -p tsconfig.json", "test": "mocha --reporter dot", "bench": "node benchmark/simple/pack-zip.js", "release": "npm run build && npm publish" @@ -43,6 +43,9 @@ }, "devDependencies": { "@biomejs/biome": "1.9.4", + "@types/async": "3.2.25", + "@types/node": "25.0.10", + "@types/readable-stream": "4.0.23", "archiver-jsdoc-theme": "1.1.3", "chai": "6.2.2", "jsdoc": "4.0.5", @@ -51,6 +54,7 @@ "rimraf": "6.1.2", "stream-bench": "0.1.2", "tar": "7.5.6", + "typescript": "5.9.3", "yauzl": "3.2.0" }, "keywords": [ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..d7017f3d0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": false, + "typeRoots": ["./src/types", "./node_modules/@types"] + }, + "include": ["src/**/*", "src/types/**/*"] +} diff --git a/yarn.lock b/yarn.lock index 0c8d301df..72e87bd26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -136,6 +136,9 @@ __metadata: resolution: "@evidenceprime/archiver@workspace:." dependencies: "@biomejs/biome": "npm:1.9.4" + "@types/async": "npm:3.2.25" + "@types/node": "npm:25.0.10" + "@types/readable-stream": "npm:4.0.23" archiver-jsdoc-theme: "npm:1.1.3" async: "npm:3.2.6" buffer-crc32: "npm:1.0.0" @@ -152,6 +155,7 @@ __metadata: stream-bench: "npm:0.1.2" tar: "npm:7.5.6" tar-stream: "npm:3.1.7" + typescript: "npm:5.9.3" yauzl: "npm:3.2.0" zip-stream: "npm:7.0.2" languageName: unknown @@ -212,6 +216,13 @@ __metadata: languageName: node linkType: hard +"@types/async@npm:3.2.25": + version: 3.2.25 + resolution: "@types/async@npm:3.2.25" + checksum: 10c0/a71010702d991facb51cea59521268aa22c873a9c8f2db12b72261a4c44fccca9a7916b9b9449428aa2a1aad69d56313abdf4303b55e7f4b93f4b9b71b9beb3c + languageName: node + linkType: hard + "@types/linkify-it@npm:^5": version: 5.0.0 resolution: "@types/linkify-it@npm:5.0.0" @@ -236,6 +247,24 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:*, @types/node@npm:25.0.10": + version: 25.0.10 + resolution: "@types/node@npm:25.0.10" + dependencies: + undici-types: "npm:~7.16.0" + checksum: 10c0/9edc3c812b487c32c76eebac7c87acae1f69515a0bc3f6b545806d513eb9e918c3217bf751dc93da39f60e06bf1b0caa92258ef3a6dd6457124b2e761e54f61f + languageName: node + linkType: hard + +"@types/readable-stream@npm:4.0.23": + version: 4.0.23 + resolution: "@types/readable-stream@npm:4.0.23" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/89ae3f6a53d186252c4c957b715c8dc12b318be30aeb3546f6513163572e5eebe0f61261e70c6d3f7496d63741ed6f92fbc5d17bfe9a72b0abfe720ee8fa471a + languageName: node + linkType: hard + "abort-controller@npm:^3.0.0": version: 3.0.0 resolution: "abort-controller@npm:3.0.0" @@ -1392,6 +1421,26 @@ __metadata: languageName: node linkType: hard +"typescript@npm:5.9.3": + version: 5.9.3 + resolution: "typescript@npm:5.9.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/6bd7552ce39f97e711db5aa048f6f9995b53f1c52f7d8667c1abdc1700c68a76a308f579cd309ce6b53646deb4e9a1be7c813a93baaf0a28ccd536a30270e1c5 + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A5.9.3#optional!builtin": + version: 5.9.3 + resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/ad09fdf7a756814dce65bc60c1657b40d44451346858eea230e10f2e95a289d9183b6e32e5c11e95acc0ccc214b4f36289dcad4bf1886b0adb84d711d336a430 + languageName: node + linkType: hard + "uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0": version: 2.1.0 resolution: "uc.micro@npm:2.1.0" @@ -1406,6 +1455,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~7.16.0": + version: 7.16.0 + resolution: "undici-types@npm:7.16.0" + checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a + languageName: node + linkType: hard + "util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" From 2c62dec8183c4027830dcc78598b48c3bb908696 Mon Sep 17 00:00:00 2001 From: Lukasz Siatka Date: Wed, 28 Jan 2026 11:41:47 +0100 Subject: [PATCH 4/8] Chore: updates tests imports --- test/archiver.js | 7 +------ test/plugins.js | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/test/archiver.js b/test/archiver.js index 20de9ad19..c38130121 100644 --- a/test/archiver.js +++ b/test/archiver.js @@ -2,27 +2,22 @@ import { WriteStream, chmodSync, createReadStream, - createWriteStream, statSync, symlinkSync, unlinkSync, writeFileSync, } from "fs"; -import { PassThrough } from "readable-stream"; import { Readable } from "readable-stream"; import { assert } from "chai"; import { mkdirp } from "mkdirp"; import { binaryBuffer, readJSON, - UnBufferedStream, - WriteHashStream, } from "./helpers/index.js"; -import { JsonArchive } from "../index.js"; +import { JsonArchive } from "../dist/index.js"; var testBuffer = binaryBuffer(1024 * 16); var testDate = new Date("Jan 03 2013 14:26:38 GMT"); -var testDate2 = new Date("Feb 10 2013 10:24:42 GMT"); var win32 = process.platform === "win32"; describe("archiver", function () { diff --git a/test/plugins.js b/test/plugins.js index 0a4f13bb3..4e3914808 100644 --- a/test/plugins.js +++ b/test/plugins.js @@ -10,7 +10,7 @@ import { assert } from "chai"; import { mkdirp } from "mkdirp"; import * as tar from "tar"; import yauzl from "yauzl"; -import { TarArchive, ZipArchive } from "../index.js"; +import { TarArchive, ZipArchive } from "../dist/index.js"; import { binaryBuffer } from "./helpers/index.js"; const testBuffer = binaryBuffer(1024 * 16); From 32d70579326323a34cce86aa931372be151dbeef Mon Sep 17 00:00:00 2001 From: Lukasz Siatka Date: Wed, 28 Jan 2026 11:42:02 +0100 Subject: [PATCH 5/8] Chore: adds typescript lib source --- src/core.ts | 572 ++++++++++++++++++++++++++++++++++ src/error.ts | 34 ++ src/index.ts | 39 +++ src/plugins/json.ts | 56 ++++ src/plugins/tar.ts | 81 +++++ src/plugins/zip.ts | 38 +++ src/types.ts | 43 +++ src/types/lazystream.d.ts | 11 + src/types/normalize-path.d.ts | 4 + src/types/readdir-glob.d.ts | 6 + src/types/tar-stream.d.ts | 6 + src/types/zip-stream.d.ts | 14 + src/utils.ts | 67 ++++ 13 files changed, 971 insertions(+) create mode 100644 src/core.ts create mode 100644 src/error.ts create mode 100644 src/index.ts create mode 100644 src/plugins/json.ts create mode 100644 src/plugins/tar.ts create mode 100644 src/plugins/zip.ts create mode 100644 src/types.ts create mode 100644 src/types/lazystream.d.ts create mode 100644 src/types/normalize-path.d.ts create mode 100644 src/types/readdir-glob.d.ts create mode 100644 src/types/tar-stream.d.ts create mode 100644 src/types/zip-stream.d.ts create mode 100644 src/utils.ts diff --git a/src/core.ts b/src/core.ts new file mode 100644 index 000000000..8ea30e3de --- /dev/null +++ b/src/core.ts @@ -0,0 +1,572 @@ +import { Stats, createReadStream, lstat, readlinkSync } from 'node:fs'; +import { dirname, relative as relativePath, resolve as resolvePath } from 'node:path'; +import { queue } from 'async'; +import { isStream } from 'is-stream'; +import { Readable } from 'lazystream'; +import { Transform } from 'readable-stream'; +import { readdirGlob } from 'readdir-glob'; +import { ArchiverError } from './error.js'; +import type { ArchiveModule, CoreOptions } from './types.js'; +import { dateify, normalizeInputSource, sanitizePath, trailingSlashIt } from './utils.js'; + +const { ReaddirGlob } = readdirGlob as any; +const win32 = process.platform === 'win32'; + +const abortedError = new ArchiverError('ABORTED'); +const finalizingError = new ArchiverError('FINALIZING'); + +export default class Archiver extends Transform { + _supportsDirectory = false; + _supportsSymlink = false; + + options: CoreOptions | any; + _format: string | false = false; + _module: ArchiveModule | any = false; + _pending = 0; + _pointer = 0; + _entriesCount = 0; + _entriesProcessedCount = 0; + _fsEntriesTotalBytes = 0; + _fsEntriesProcessedBytes = 0; + _queue: import('async').QueueObject; + _statQueue: import('async').QueueObject; + _state: { + aborted: boolean; + finalize: boolean; + finalizing: boolean; + finalized: boolean; + modulePiped: boolean; + }; + _streams: any[]; + _task: any; + + constructor(options?: CoreOptions) { + const opts: CoreOptions = { + highWaterMark: 1024 * 1024, + statConcurrency: 4, + ...options, + }; + super(opts as any); + this.options = opts; + this._format = false; + this._module = false; + this._pending = 0; + this._pointer = 0; + this._entriesCount = 0; + this._entriesProcessedCount = 0; + this._fsEntriesTotalBytes = 0; + this._fsEntriesProcessedBytes = 0; + this._queue = queue(this._onQueueTask.bind(this), 1); + this._queue.drain(this._onQueueDrain.bind(this)); + this._statQueue = queue(this._onStatQueueTask.bind(this), opts.statConcurrency); + this._statQueue.drain(this._onQueueDrain.bind(this)); + this._state = { + aborted: false, + finalize: false, + finalizing: false, + finalized: false, + modulePiped: false, + }; + this._streams = []; + } + + _abort(): void { + this._state.aborted = true; + this._queue.kill(); + this._statQueue.kill(); + if (this._queue.idle()) { + this._shutdown(); + } + } + + _append(filepath: string, data?: any): void { + data = data || {}; + let task: any = { + source: null, + filepath: filepath, + }; + if (!data.name) { + data.name = filepath; + } + data.sourcePath = filepath; + task.data = data; + this._entriesCount++; + if (data.stats && (data.stats as Stats) instanceof Stats) { + task = this._updateQueueTaskWithStats(task, data.stats as Stats); + if (task) { + if (data.stats.size) { + this._fsEntriesTotalBytes += data.stats.size; + } + this._queue.push(task); + } + } else { + this._statQueue.push(task); + } + } + + _finalize(): void { + if (this._state.finalizing || this._state.finalized || this._state.aborted) { + return; + } + this._state.finalizing = true; + this._moduleFinalize(); + this._state.finalizing = false; + this._state.finalized = true; + } + + _maybeFinalize(): boolean { + if (this._state.finalizing || this._state.finalized || this._state.aborted) { + return false; + } + if ( + this._state.finalize && + this._pending === 0 && + this._queue.idle() && + this._statQueue.idle() + ) { + this._finalize(); + return true; + } + return false; + } + + _moduleAppend(source: any, data: any, callback: () => void): void { + if (this._state.aborted) { + callback(); + return; + } + this._module.append( + source, + data, + function (this: Archiver, err: any) { + this._task = null; + if (this._state.aborted) { + this._shutdown(); + return; + } + if (err) { + this.emit('error', err); + setImmediate(callback); + return; + } + this.emit('entry', data); + this._entriesProcessedCount++; + if (data.stats?.size) { + this._fsEntriesProcessedBytes += data.stats.size; + } + this.emit('progress', { + entries: { + total: this._entriesCount, + processed: this._entriesProcessedCount, + }, + fs: { + totalBytes: this._fsEntriesTotalBytes, + processedBytes: this._fsEntriesProcessedBytes, + }, + }); + setImmediate(callback); + }.bind(this), + ); + } + + _moduleFinalize(): void { + if (typeof this._module.finalize === 'function') { + this._module.finalize(); + } else if (typeof this._module.end === 'function') { + this._module.end(); + } else { + this.emit('error', new ArchiverError('NOENDMETHOD')); + } + } + + _modulePipe(): void { + this._module.on('error', this._onModuleError.bind(this)); + this._module.pipe(this); + this._state.modulePiped = true; + } + + _moduleUnpipe(): void { + this._module.unpipe(this); + this._state.modulePiped = false; + } + + _normalizeEntryData(data: any, stats?: Stats): any { + data = { + type: 'file', + name: null, + date: null, + mode: null, + prefix: null, + sourcePath: null, + stats: false, + ...data, + }; + if (stats && data.stats === false) { + data.stats = stats; + } + let isDir = data.type === 'directory'; + if (data.name) { + if (typeof data.prefix === 'string' && '' !== data.prefix) { + data.name = data.prefix + '/' + data.name; + data.prefix = null; + } + data.name = sanitizePath(data.name); + if (data.type !== 'symlink' && data.name.slice(-1) === '/') { + isDir = true; + data.type = 'directory'; + } else if (isDir) { + data.name += '/'; + } + } + if (typeof data.mode === 'number') { + if (win32) { + data.mode &= 511; + } else { + data.mode &= 4095; + } + } else if (data.stats && data.mode === null) { + if (win32) { + data.mode = data.stats.mode & 511; + } else { + data.mode = data.stats.mode & 4095; + } + if (win32 && isDir) { + data.mode = 493; + } + } else if (data.mode === null) { + data.mode = isDir ? 493 : 420; + } + if (data.stats && data.date === null) { + data.date = data.stats.mtime; + } else { + data.date = dateify(data.date); + } + return data; + } + + _onModuleError(err: any): void { + this.emit('error', err); + } + + _onQueueDrain(): void { + if (this._state.finalizing || this._state.finalized || this._state.aborted) { + return; + } + if ( + this._state.finalize && + this._pending === 0 && + this._queue.idle() && + this._statQueue.idle() + ) { + this._finalize(); + } + } + + _onQueueTask(task: any, callback: () => void): void { + const fullCallback = () => { + if (task.data.callback) { + task.data.callback(); + } + callback(); + }; + if (this._state.finalizing || this._state.finalized || this._state.aborted) { + fullCallback(); + return; + } + this._task = task; + this._moduleAppend(task.source, task.data, fullCallback); + } + + _onStatQueueTask(task: any, callback: () => void): void { + if (this._state.finalizing || this._state.finalized || this._state.aborted) { + callback(); + return; + } + lstat( + task.filepath, + function (this: Archiver, err: NodeJS.ErrnoException | null, stats: Stats) { + if (this._state.aborted) { + setImmediate(callback); + return; + } + if (err) { + this._entriesCount--; + this.emit('warning', err); + setImmediate(callback); + return; + } + task = this._updateQueueTaskWithStats(task, stats as Stats); + if (task) { + if (stats.size) { + this._fsEntriesTotalBytes += stats.size; + } + this._queue.push(task); + } + setImmediate(callback); + }.bind(this), + ); + } + + _shutdown(): void { + this._moduleUnpipe(); + this.end(); + } + + _transform( + chunk: Buffer, + encoding: string, + callback: (err?: Error | null, data?: Buffer) => void, + ) { + if (chunk) { + this._pointer += chunk.length; + } + callback(null, chunk); + } + + _updateQueueTaskWithStats(task: any, stats: Stats): any { + if (stats.isFile()) { + task.data.type = 'file'; + task.data.sourceType = 'stream'; + task.source = new Readable(() => { + return createReadStream(task.filepath); + }); + } else if (stats.isDirectory() && this._supportsDirectory) { + task.data.name = trailingSlashIt(task.data.name); + task.data.type = 'directory'; + task.data.sourcePath = trailingSlashIt(task.filepath); + task.data.sourceType = 'buffer'; + task.source = Buffer.concat([]); + } else if (stats.isSymbolicLink() && this._supportsSymlink) { + const linkPath = readlinkSync(task.filepath); + const dirName = dirname(task.filepath); + task.data.type = 'symlink'; + task.data.linkname = relativePath(dirName, resolvePath(dirName, linkPath)); + task.data.sourceType = 'buffer'; + task.source = Buffer.concat([]); + } else { + if (stats.isDirectory()) { + this.emit('warning', new ArchiverError('DIRECTORYNOTSUPPORTED', task.data)); + } else if (stats.isSymbolicLink()) { + this.emit('warning', new ArchiverError('SYMLINKNOTSUPPORTED', task.data)); + } else { + this.emit('warning', new ArchiverError('ENTRYNOTSUPPORTED', task.data)); + } + return null; + } + task.data = this._normalizeEntryData(task.data, stats); + return task; + } + + abort(): this { + if (this._state.aborted || this._state.finalized) { + return this; + } + this._abort(); + return this; + } + + append(source: any, data: any): this { + if (this._state.finalize || this._state.aborted) { + this.emit('error', new ArchiverError('QUEUECLOSED')); + return this; + } + data = this._normalizeEntryData(data); + if (typeof data.name !== 'string' || data.name.length === 0) { + this.emit('error', new ArchiverError('ENTRYNAMEREQUIRED')); + return this; + } + if (data.type === 'directory' && !this._supportsDirectory) { + this.emit('error', new ArchiverError('DIRECTORYNOTSUPPORTED', { name: data.name })); + return this; + } + source = normalizeInputSource(source); + if (Buffer.isBuffer(source)) { + data.sourceType = 'buffer'; + } else if (isStream(source)) { + data.sourceType = 'stream'; + } else { + this.emit('error', new ArchiverError('INPUTSTEAMBUFFERREQUIRED', { name: data.name })); + return this; + } + this._entriesCount++; + this._queue.push({ + data: data, + source: source, + }); + return this; + } + + directory(dirpath: string, destpath?: string | boolean, data?: any): this { + if (this._state.finalize || this._state.aborted) { + this.emit('error', new ArchiverError('QUEUECLOSED')); + return this; + } + if (typeof dirpath !== 'string' || dirpath.length === 0) { + this.emit('error', new ArchiverError('DIRECTORYDIRPATHREQUIRED')); + return this; + } + this._pending++; + if (destpath === false) { + destpath = ''; + } else if (typeof destpath !== 'string') { + destpath = dirpath; + } + let dataFunction: false | ((entryData: any) => any) = false; + if (typeof data === 'function') { + dataFunction = data as any; + data = {}; + } else if (typeof data !== 'object') { + data = {}; + } + const globOptions: any = { + stat: true, + dot: true, + }; + const onGlobEnd = function (this: Archiver) { + this._pending--; + this._maybeFinalize(); + }; + const onGlobError = function (this: Archiver, err: any) { + this.emit('error', err); + }; + const onGlobMatch = function (this: Archiver, match: any) { + globber.pause(); + let ignoreMatch = false; + let entryData = Object.assign({}, data); + entryData.name = match.relative; + entryData.prefix = destpath as string; + entryData.stats = match.stat; + entryData.callback = globber.resume.bind(globber); + try { + if (dataFunction) { + entryData = dataFunction(entryData); + if (entryData === false) { + ignoreMatch = true; + } else if (typeof entryData !== 'object') { + throw new ArchiverError('DIRECTORYFUNCTIONINVALIDDATA', { dirpath: dirpath }); + } + } + } catch (e) { + this.emit('error', e); + return; + } + if (ignoreMatch) { + globber.resume(); + return; + } + this._append(match.absolute, entryData); + }; + const globber = readdirGlob(dirpath, globOptions); + globber.on('error', onGlobError.bind(this)); + globber.on('match', onGlobMatch.bind(this)); + globber.on('end', onGlobEnd.bind(this)); + return this; + } + + file(filepath: string, data?: any): this { + if (this._state.finalize || this._state.aborted) { + this.emit('error', new ArchiverError('QUEUECLOSED')); + return this; + } + if (typeof filepath !== 'string' || filepath.length === 0) { + this.emit('error', new ArchiverError('FILEFILEPATHREQUIRED')); + return this; + } + this._append(filepath, data); + return this; + } + + glob(pattern: string, options?: any, data?: any): this { + this._pending++; + options = { + stat: true, + pattern: pattern, + ...options, + }; + const onGlobEnd = function (this: Archiver) { + this._pending--; + this._maybeFinalize(); + }; + const onGlobError = function (this: Archiver, err: any) { + this.emit('error', err); + }; + const onGlobMatch = function (this: Archiver, match: any) { + globber.pause(); + const entryData = Object.assign({}, data); + entryData.callback = globber.resume.bind(globber); + entryData.stats = match.stat; + entryData.name = match.relative; + this._append(match.absolute, entryData); + }; + const globber = new ReaddirGlob(options.cwd || '.', options); + globber.on('error', onGlobError.bind(this)); + globber.on('match', onGlobMatch.bind(this)); + globber.on('end', onGlobEnd.bind(this)); + return this; + } + + finalize(): Promise { + if (this._state.aborted) { + this.emit('error', abortedError); + return Promise.reject(abortedError); + } + if (this._state.finalize) { + this.emit('error', finalizingError); + return Promise.reject(finalizingError); + } + this._state.finalize = true; + if (this._pending === 0 && this._queue.idle() && this._statQueue.idle()) { + this._finalize(); + } + const self = this; + return new Promise((resolve, reject) => { + let errored = false; + self._module.on('end', () => { + if (!errored) { + resolve(); + } + }); + self._module.on('error', (err: any) => { + errored = true; + reject(err); + }); + }); + } + + symlink(filepath: string, target: string, mode?: number): this { + if (this._state.finalize || this._state.aborted) { + this.emit('error', new ArchiverError('QUEUECLOSED')); + return this; + } + if (typeof filepath !== 'string' || filepath.length === 0) { + this.emit('error', new ArchiverError('SYMLINKFILEPATHREQUIRED')); + return this; + } + if (typeof target !== 'string' || target.length === 0) { + this.emit('error', new ArchiverError('SYMLINKTARGETREQUIRED', { filepath: filepath })); + return this; + } + if (!this._supportsSymlink) { + this.emit('error', new ArchiverError('SYMLINKNOTSUPPORTED', { filepath: filepath })); + return this; + } + const data: any = {}; + data.type = 'symlink'; + data.name = filepath.replace(/\\/g, '/'); + data.linkname = target.replace(/\\/g, '/'); + data.sourceType = 'buffer'; + if (typeof mode === 'number') { + data.mode = mode; + } + this._entriesCount++; + this._queue.push({ + data: data, + source: Buffer.concat([]), + }); + return this; + } + + pointer(): number { + return this._pointer; + } +} diff --git a/src/error.ts b/src/error.ts new file mode 100644 index 000000000..0d87dc7e3 --- /dev/null +++ b/src/error.ts @@ -0,0 +1,34 @@ +const ERROR_CODES: Record = { + ABORTED: 'archive was aborted', + DIRECTORYDIRPATHREQUIRED: 'diretory dirpath argument must be a non-empty string value', + DIRECTORYFUNCTIONINVALIDDATA: 'invalid data returned by directory custom data function', + ENTRYNAMEREQUIRED: 'entry name must be a non-empty string value', + FILEFILEPATHREQUIRED: 'file filepath argument must be a non-empty string value', + FINALIZING: 'archive already finalizing', + QUEUECLOSED: 'queue closed', + NOENDMETHOD: 'no suitable finalize/end method defined by module', + DIRECTORYNOTSUPPORTED: 'support for directory entries not defined by module', + FORMATSET: 'archive format already set', + INPUTSTEAMBUFFERREQUIRED: 'input source must be valid Stream or Buffer instance', + MODULESET: 'module already set', + SYMLINKNOTSUPPORTED: 'support for symlink entries not defined by module', + SYMLINKFILEPATHREQUIRED: 'symlink filepath argument must be a non-empty string value', + SYMLINKTARGETREQUIRED: 'symlink target argument must be a non-empty string value', + ENTRYNOTSUPPORTED: 'entry not supported', +}; + +export class ArchiverError extends Error { + code: string; + data: any; + + constructor(code: string, data?: any) { + super(ERROR_CODES[code] || code); + this.name = 'ArchiverError'; + this.code = code; + this.data = data; + Error.captureStackTrace(this, this.constructor); + } +} + +// Backwards compatible export style for CommonJS interop +export default ArchiverError; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 000000000..5d5e7fd70 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,39 @@ +import Archiver from './core.js'; +import Json from './plugins/json.js'; +import Tar from './plugins/tar.js'; +import Zip from './plugins/zip.js'; + +export { Archiver }; + +export class ZipArchive extends Archiver { + constructor(options?: any) { + super(options); + this._format = 'zip'; + this._module = new Zip(options); + this._supportsDirectory = true; + this._supportsSymlink = true; + this._modulePipe(); + } +} + +export class TarArchive extends Archiver { + constructor(options?: any) { + super(options); + this._format = 'tar'; + this._module = new Tar(options); + this._supportsDirectory = true; + this._supportsSymlink = true; + this._modulePipe(); + } +} + +export class JsonArchive extends Archiver { + constructor(options?: any) { + super(options); + this._format = 'json'; + this._module = new Json(options); + this._supportsDirectory = true; + this._supportsSymlink = true; + this._modulePipe(); + } +} diff --git a/src/plugins/json.ts b/src/plugins/json.ts new file mode 100644 index 000000000..e20ce6eee --- /dev/null +++ b/src/plugins/json.ts @@ -0,0 +1,56 @@ +import { createRequire } from 'module'; +import { Transform } from 'readable-stream'; +import type { EntryData } from '../types.js'; +import { collectStream } from '../utils.js'; + +const require = createRequire(import.meta.url); +const crc32 = require('buffer-crc32'); + +export default class Json extends Transform { + files: EntryData[]; + + constructor(options?: any) { + super({ ...options }); + this.files = []; + } + + _transform( + chunk: Buffer, + encoding: string, + callback: (err?: Error | null, data?: Buffer) => void, + ) { + callback(null, chunk); + } + + _writeStringified() { + const fileString = JSON.stringify(this.files); + this.write(fileString); + } + + append(source: any, data: EntryData, callback: (err?: any, data?: any) => void) { + data.crc32 = 0 as any; + const self = this; + + function onend(err: any, sourceBuffer?: Buffer) { + if (err) { + callback(err); + return; + } + data.size = (sourceBuffer && sourceBuffer.length) || 0; + data.crc32 = crc32.unsigned(sourceBuffer || Buffer.alloc(0)); + self.files.push(data); + callback(null, data); + } + + if (data.sourceType === 'buffer') { + onend(null, source); + } else if (data.sourceType === 'stream') { + collectStream(source, onend); + } + } + + finalize() { + this._writeStringified(); + this.end(); + } +} diff --git a/src/plugins/tar.ts b/src/plugins/tar.ts new file mode 100644 index 000000000..6240d6442 --- /dev/null +++ b/src/plugins/tar.ts @@ -0,0 +1,81 @@ +import zlib from 'zlib'; +import engine from 'tar-stream'; +import type { EntryData } from '../types.js'; +import { collectStream } from '../utils.js'; + +export default class Tar { + options: any; + engine: any; + compressor: any; + + constructor(options?: any) { + options = this.options = { + gzip: false, + ...options, + }; + if (typeof options.gzipOptions !== 'object') { + options.gzipOptions = {}; + } + this.engine = engine.pack(options); + this.compressor = false; + if (options.gzip) { + this.compressor = zlib.createGzip(options.gzipOptions); + this.compressor.on('error', this._onCompressorError.bind(this)); + } + } + + _onCompressorError(err: any) { + this.engine.emit('error', err); + } + + append(source: any, data: EntryData, callback: (err?: any, data?: any) => void) { + const self = this; + data.mtime = data.date as any; + + function appendFn(err: any, sourceBuffer?: Buffer) { + if (err) { + callback(err); + return; + } + self.engine.entry(data, sourceBuffer, function (err: any) { + callback(err, data); + }); + } + + if (data.sourceType === 'buffer') { + appendFn(null, source); + } else if (data.sourceType === 'stream' && data.stats) { + data.size = data.stats.size as any; + const entry = self.engine.entry(data, function (err: any) { + callback(err, data); + }); + source.pipe(entry); + } else if (data.sourceType === 'stream') { + collectStream(source, appendFn); + } + } + + finalize() { + this.engine.finalize(); + } + + on(...args: any[]) { + return this.engine.on.apply(this.engine, args as any); + } + + pipe(destination: any, options?: any) { + if (this.compressor) { + return this.engine.pipe.apply(this.engine, [this.compressor]).pipe(destination, options); + } else { + return this.engine.pipe(destination, options); + } + } + + unpipe(...args: any[]) { + if (this.compressor) { + return this.compressor.unpipe.apply(this.compressor, args as any); + } else { + return this.engine.unpipe.apply(this.engine, args as any); + } + } +} diff --git a/src/plugins/zip.ts b/src/plugins/zip.ts new file mode 100644 index 000000000..21204f51c --- /dev/null +++ b/src/plugins/zip.ts @@ -0,0 +1,38 @@ +import engine from 'zip-stream'; +import type { EntryData } from '../types.js'; + +export default class Zip { + options: any; + engine: any; + + constructor(options?: any) { + options = this.options = { + comment: '', + forceUTC: false, + namePrependSlash: false, + store: false, + ...options, + }; + this.engine = new engine(options); + } + + append(source: any, data: EntryData, callback: (err?: any) => void) { + this.engine.entry(source, data, callback); + } + + finalize() { + this.engine.finalize(); + } + + on(...args: any[]) { + return this.engine.on.apply(this.engine, args as any); + } + + pipe(...args: any[]) { + return this.engine.pipe.apply(this.engine, args as any); + } + + unpipe(...args: any[]) { + return this.engine.unpipe.apply(this.engine, args as any); + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 000000000..0b2498477 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,43 @@ +import type { Stats } from 'node:fs'; +import type { TransformOptions } from 'stream'; + +export type Source = Buffer | NodeJS.ReadableStream; + +export interface EntryData { + name: string | null; + date?: string | Date | null; + mode?: number | null; + prefix?: string | null; + sourcePath?: string | null; + stats?: Stats | false; + sourceType?: 'buffer' | 'stream'; + type?: 'file' | 'directory' | 'symlink'; + linkname?: string; + callback?: () => void; + size?: number; + crc32?: number; + [key: string]: any; +} + +export interface CoreOptions extends TransformOptions { + statConcurrency?: number; +} + +export interface ArchiveModule { + append?: ( + source: Source | any, + data: EntryData, + callback: (err?: any, data?: any) => void, + ) => void; + finalize?: () => void; + end?: () => void; + on?: (event: string, listener: (...args: any[]) => void) => any; + pipe?: (dest: any, options?: any) => any; + unpipe?: (...args: any[]) => any; +} + +export interface QueueTask { + source: Source | null; + filepath?: string; + data: EntryData; +} diff --git a/src/types/lazystream.d.ts b/src/types/lazystream.d.ts new file mode 100644 index 000000000..cc0c9764b --- /dev/null +++ b/src/types/lazystream.d.ts @@ -0,0 +1,11 @@ +declare module 'lazystream' { + import { Readable as NodeReadable } from 'stream'; + + export class LazystreamReadable extends NodeReadable { + constructor(factory: () => NodeJS.ReadableStream); + } + + export { LazystreamReadable as Readable }; + const _default: { Readable: typeof LazystreamReadable }; + export default _default; +} diff --git a/src/types/normalize-path.d.ts b/src/types/normalize-path.d.ts new file mode 100644 index 000000000..c1aa6513d --- /dev/null +++ b/src/types/normalize-path.d.ts @@ -0,0 +1,4 @@ +declare module 'normalize-path' { + function normalizePath(p: string, stripDrive?: boolean): string; + export default normalizePath; +} diff --git a/src/types/readdir-glob.d.ts b/src/types/readdir-glob.d.ts new file mode 100644 index 000000000..309677b12 --- /dev/null +++ b/src/types/readdir-glob.d.ts @@ -0,0 +1,6 @@ +declare module 'readdir-glob' { + export function readdirGlob(path: string, options?: any): any; + export const ReaddirGlob: any; + const _default: { readdirGlob: typeof readdirGlob; ReaddirGlob: any }; + export default _default; +} diff --git a/src/types/tar-stream.d.ts b/src/types/tar-stream.d.ts new file mode 100644 index 000000000..3358ca4be --- /dev/null +++ b/src/types/tar-stream.d.ts @@ -0,0 +1,6 @@ +declare module 'tar-stream' { + export function pack(options?: any): any; + export function extract(options?: any): any; + const _default: { pack: typeof pack; extract: typeof extract }; + export default _default; +} diff --git a/src/types/zip-stream.d.ts b/src/types/zip-stream.d.ts new file mode 100644 index 000000000..ed2c5b25d --- /dev/null +++ b/src/types/zip-stream.d.ts @@ -0,0 +1,14 @@ +declare module 'zip-stream' { + import { Writable, Readable } from 'stream'; + + interface ZipStream { + entry(source: any, data: any, callback: (err?: any) => void): void; + finalize(): void; + pipe(dest: NodeJS.WritableStream, options?: any): any; + on(event: string, listener: (...args: any[]) => void): any; + unpipe(...args: any[]): any; + } + + const ZipStreamFactory: any; + export default ZipStreamFactory; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 000000000..f73d4446a --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,67 @@ +import { isStream } from 'is-stream'; +import normalizePath from 'normalize-path'; +import { PassThrough } from 'readable-stream'; + +export function collectStream( + source: NodeJS.ReadableStream, + callback: (err: Error | null, buf?: Buffer) => void, +): void { + const collection: Buffer[] = []; + let size = 0; + + source.on('error', callback as any); + + source.on('data', function (chunk: Buffer) { + collection.push(chunk); + size += chunk.length; + }); + + source.on('end', function () { + const buf = Buffer.alloc(size); + let offset = 0; + + collection.forEach(function (data) { + data.copy(buf, offset); + offset += data.length; + }); + + callback(null, buf); + }); +} + +export function dateify(dateish?: string | Date | null): Date { + let result: Date; + + if (dateish instanceof Date) { + result = dateish; + } else if (typeof dateish === 'string') { + result = new Date(dateish); + } else { + result = new Date(); + } + + return result; +} + +export function normalizeInputSource(source: any): Buffer | NodeJS.ReadableStream { + if (source === null) { + return Buffer.alloc(0); + } else if (typeof source === 'string') { + return Buffer.from(source); + } else if (isStream(source)) { + // Always pipe through a PassThrough to ensure proper pausing behavior + return (source as NodeJS.ReadableStream).pipe(new PassThrough()) as NodeJS.ReadableStream; + } + + return source; +} + +export function sanitizePath(filepath: string): string { + return normalizePath(filepath, false) + .replace(/^\w+:/, '') + .replace(/^(\.\.\/|\/)+/, ''); +} + +export function trailingSlashIt(str: string): string { + return str.slice(-1) !== '/' ? str + '/' : str; +} From 448136bfeb43e04aa809416c335333b9efc74678 Mon Sep 17 00:00:00 2001 From: Lukasz Siatka Date: Wed, 28 Jan 2026 11:47:14 +0100 Subject: [PATCH 6/8] Chore: adds tests to the CI/CD pipeline --- .github/workflows/cicd.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index 8a14ef3ee..449d7b65d 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -37,6 +37,8 @@ jobs: run: yarn install --immutable - name: Run build run: yarn build + - name: Run tests + run: yarn test - name: Publish package if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }} run: yarn release From c881d31a1ada88f36f5a191e128de2d5663b73b8 Mon Sep 17 00:00:00 2001 From: Lukasz Siatka Date: Wed, 28 Jan 2026 11:59:53 +0100 Subject: [PATCH 7/8] Chore: bumps up version to 7.0.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 05d1ac260..aa9f6874e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@evidenceprime/archiver", - "version": "7.0.3", + "version": "7.0.4", "description": "a streaming interface for archive generation", "homepage": "https://github.com/evidenceprime/node-archiver", "author": { From 078fe1c58dd7b58ea3f29f15806de10bcd47c884 Mon Sep 17 00:00:00 2001 From: Lukasz Siatka Date: Wed, 28 Jan 2026 14:39:18 +0100 Subject: [PATCH 8/8] Chore: simplifies the dateify function implementation --- src/utils.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index f73d4446a..5dfed28e1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -30,17 +30,13 @@ export function collectStream( } export function dateify(dateish?: string | Date | null): Date { - let result: Date; - if (dateish instanceof Date) { - result = dateish; + return dateish; } else if (typeof dateish === 'string') { - result = new Date(dateish); + return new Date(dateish); } else { - result = new Date(); + return new Date(); } - - return result; } export function normalizeInputSource(source: any): Buffer | NodeJS.ReadableStream {