UNPKG

zip-lib

Version:

A zip and unzip library for Node.js.

364 lines (363 loc) 13.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.Zip = void 0; const node_fs_1 = require("node:fs"); const fs = __importStar(require("node:fs/promises")); const path = __importStar(require("node:path")); const yazl = __importStar(require("yazl")); const cancelable_1 = require("./cancelable"); const exfs = __importStar(require("./fs")); /** * Compress files or folders to a zip file. */ class Zip extends cancelable_1.Cancelable { /** * */ constructor(options) { super(); this.options = options; this.token = null; this.activeArchive = null; this.zipFiles = []; this.zipFolders = []; } /** * Adds a file from the file system at `realPath` to the zip file as `metadataPath`. * @param file * @param metadataPath Typically, `metadataPath` would be calculated as `path.relative(root, realPath)`. * A valid metadataPath must not start with "/" or /[A-Za-z]:\//, and must not contain "..". */ addFile(file, metadataPath) { let mPath = metadataPath; if (!mPath) { mPath = path.basename(file); } this.zipFiles.push({ path: file, metadataPath: mPath, }); } /** * Adds a folder from the file system at `realPath` to the zip file as `metadataPath`. * @param folder * @param metadataPath Typically, `metadataPath` would be calculated as `path.relative(root, realPath)`. * A valid metadataPath must not start with "/" or /[A-Za-z]:\//, and must not contain "..". */ addFolder(folder, metadataPath) { this.zipFolders.push({ path: folder, metadataPath, }); } async archive(zipFile) { const token = new cancelable_1.CancellationToken(); this.token = token; const zip = await this.prepareArchive(zipFile); const activeArchive = { zip, mode: zipFile ? "file" : "buffer", }; this.activeArchive = activeArchive; try { return await new Promise((resolve, reject) => { let disposeCancel = () => { // noop }; const settle = this.createPromiseSettler((value) => { disposeCancel(); resolve(value); }, (error) => { disposeCancel(); reject(error); }); disposeCancel = token.onCancelled(() => { this.stop(this.canceledError(), activeArchive); settle.reject(this.canceledError()); }); this.bindArchiveOutput(zip, zipFile, token, settle, activeArchive); this.addQueuedEntries(zip, token) .catch((error) => { settle.reject(this.wrapError(error, token.isCancelled)); }) .finally(() => { zip.end(); }); }); } finally { if (this.token === token) { this.token = null; } if (this.activeArchive === activeArchive) { this.activeArchive = null; } } } async prepareArchive(zipFile) { if (zipFile) { await exfs.ensureFolder(path.dirname(zipFile)); } // Re-instantiate yazl every time the archive method is called to ensure that files are not added repeatedly. // This will also make the Zip class reusable. return new yazl.ZipFile(); } bindArchiveOutput(zip, zipFile, token, settle, activeArchive) { if (zipFile) { void this.archiveToFile(zip, zipFile, token, settle, activeArchive); return; } void this.archiveToBuffer(zip, token, settle); } async archiveToFile(zip, zipFile, token, settle, activeArchive) { const zipStream = (0, node_fs_1.createWriteStream)(zipFile); activeArchive.zipStream = zipStream; const archivePromise = new Promise((resolve) => { zip.once("error", (err) => { settle.reject(this.wrapError(err, token.isCancelled)); resolve(); }); zipStream.once("error", (err) => { settle.reject(this.wrapError(err, token.isCancelled)); resolve(); }); zipStream.once("close", () => { if (token.isCancelled) { settle.reject(this.canceledError()); } else { settle.resolve(void 0); } resolve(); }); }); zip.outputStream.pipe(zipStream); await archivePromise; } async archiveToBuffer(zip, token, settle) { const chunks = []; const archivePromise = new Promise((resolve) => { zip.once("error", (err) => { settle.reject(this.wrapError(err, token.isCancelled)); resolve(); }); zip.outputStream.on("data", (chunk) => { chunks.push(chunk); }); zip.outputStream.once("end", () => { settle.resolve(Buffer.concat(chunks)); resolve(); }); zip.outputStream.once("error", (err) => { settle.reject(this.wrapError(err, token.isCancelled)); resolve(); }); }); await archivePromise; } createPromiseSettler(resolve, reject) { let settled = false; return { resolve: (value) => { if (settled) { return; } settled = true; resolve(value); }, reject: (error) => { if (settled) { return; } settled = true; reject(error); }, }; } async addQueuedEntries(zip, token) { for (const file of this.zipFiles) { const entry = await exfs.getFileEntry(file.path); await this.addEntry(zip, entry, file, token); } if (this.zipFolders.length > 0) { await this.walkDir(zip, this.zipFolders, token); } } /** * Cancel compression. * If the cancel method is called after the archive is complete, nothing will happen. */ cancel() { if (this.token) { this.token.cancel(); this.token = null; } this.stop(this.canceledError()); } async addEntry(zip, entry, file, token) { if (entry.isSymbolicLink) { if (this.followSymlink()) { try { const realPath = await exfs.realpath(file.path); const actualStat = await fs.stat(realPath); if (actualStat.isDirectory()) { await this.walkDir(zip, [{ path: realPath, metadataPath: file.metadataPath }], token); } else { const targetEntry = { path: realPath, isSymbolicLink: false, type: "file", mtime: actualStat.mtime, mode: actualStat.mode, }; await this.addFileStream(zip, targetEntry, file.metadataPath, token); } } catch (_error) { // Symlink is broken, ignore it } } else { await this.addSymlink(zip, entry, file.metadataPath); } } else { if (entry.type === "dir") { zip.addEmptyDirectory(file.metadataPath, { mtime: entry.mtime, mode: entry.mode, }); } else { await this.addFileStream(zip, entry, file.metadataPath, token); } } } addFileStream(zip, file, metadataPath, token) { return new Promise((resolve, reject) => { const fileStream = (0, node_fs_1.createReadStream)(file.path); const disposeCancel = token.onCancelled(() => { fileStream.destroy(this.canceledError()); }); fileStream.once("error", (err) => { disposeCancel(); const wrappedError = this.wrapError(err, token.isCancelled); this.stop(wrappedError); reject(wrappedError); }); fileStream.once("close", () => { disposeCancel(); resolve(); }); // If the file attribute is known, add the entry using `addReadStream`, // this can reduce the number of calls to the `fs.stat` method. zip.addReadStream(fileStream, metadataPath, Object.assign(Object.assign({}, this.getYazlOption()), { mode: file.mode, mtime: file.mtime })); }); } async addSymlink(zip, file, metadataPath) { const linkTarget = await fs.readlink(file.path); zip.addBuffer(Buffer.from(linkTarget), metadataPath, Object.assign(Object.assign({}, this.getYazlOption()), { mtime: file.mtime, mode: file.mode })); } async walkDir(zip, folders, token) { for (const folder of folders) { if (token.isCancelled) { return; } const entries = await exfs.readdirp(folder.path); if (entries.length > 0) { for (const entry of entries) { if (token.isCancelled) { return; } const relativePath = path.relative(folder.path, entry.path); const metadataPath = folder.metadataPath ? path.join(folder.metadataPath, relativePath) : relativePath; await this.addEntry(zip, entry, { path: entry.path, metadataPath }, token); } } else { // If the folder is empty and the metadataPath has a value, // an empty folder should be created based on the metadataPath if (folder.metadataPath) { zip.addEmptyDirectory(folder.metadataPath); } } } } stop(err, activeArchive) { const archive = activeArchive !== null && activeArchive !== void 0 ? activeArchive : this.activeArchive; if (!archive) { return; } if (archive.mode === "file" && archive.zipStream) { archive.zip.outputStream.unpipe(archive.zipStream); archive.zipStream.destroy(err); } if (archive.mode === "buffer") { archive.zip.outputStream.destroy(err); } if (!activeArchive || this.activeArchive === activeArchive) { this.activeArchive = null; } } followSymlink() { let followSymlink = false; if (this.options && this.options.followSymlinks === true) { followSymlink = true; } return followSymlink; } /** * Retrieves the yazl options based on the current settings. * * @returns The yazl options with the specified compression level, * or undefined if options or compressionLevel are not properly set. */ getYazlOption() { if (!this.options) { return undefined; } if (typeof this.options.compressionLevel !== "number") { return undefined; } return { compressionLevel: this.options.compressionLevel, }; } } exports.Zip = Zip;