UNPKG

zip-lib

Version:

zip and unzip library for node

225 lines (224 loc) 8.14 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Zip = void 0; const yazl = require("yazl"); const fs_1 = require("fs"); const fs = require("fs/promises"); const exfs = require("./fs"); const path = require("path"); const cancelable_1 = require("./cancelable"); /** * Compress files or folders to a zip file. */ class Zip extends cancelable_1.Cancelable { /** * */ constructor(options) { super(); this.options = options; this.isPipe = false; this.zipFiles = []; this.zipFolders = []; } /** * Adds a file from the file system at realPath into the zipfile 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 into the zipfile 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 }); } /** * Generate zip file. * @param zipFile the zip file path. */ async archive(zipFile) { if (!zipFile) { return Promise.reject(new Error("zipPath must not be empty")); } const token = new cancelable_1.CancellationToken(); this.token = token; this.isPipe = false; 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. this.yazlFile = new yazl.ZipFile(); return new Promise(async (c, e) => { this.yazlFile.once("error", (err) => { e(this.wrapError(err, token.isCancelled)); }); const zip = this.yazlFile; if (!token.isCancelled) { this.zipStream = (0, fs_1.createWriteStream)(zipFile); this.zipStream.once("error", (err) => { e(this.wrapError(err, token.isCancelled)); }); this.zipStream.once("close", () => { if (token.isCancelled) { e(this.canceledError()); } else { c(void 0); } }); zip.outputStream.once("error", (err) => { e(this.wrapError(err, token.isCancelled)); }); zip.outputStream.pipe(this.zipStream); this.isPipe = true; } try { const files = this.zipFiles; for (const file of files) { const entry = await exfs.getFileEntry(file.path); await this.addEntry(zip, entry, file, token); } if (this.zipFolders.length > 0) { await this.walkDir(this.zipFolders, token); } } catch (error) { e(this.wrapError(error, token.isCancelled)); return; } zip.end(); }); } /** * 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.stopPipe(this.canceledError()); } async addEntry(zip, entry, file, token) { if (entry.isSymbolicLink) { if (this.followSymlink()) { if (entry.type === "dir") { const realPath = await exfs.realpath(file.path); await this.walkDir([{ path: realPath, metadataPath: file.metadataPath }], token); } else { zip.addFile(file.path, file.metadataPath, this.getYazlOption()); } } 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((c, e) => { const fileStream = (0, fs_1.createReadStream)(file.path); fileStream.once("error", (err) => { const wrappedError = this.wrapError(err, token.isCancelled); this.stopPipe(wrappedError); e(wrappedError); }); fileStream.once("close", () => { c(); }); // 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(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(this.yazlFile, 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) { this.yazlFile.addEmptyDirectory(folder.metadataPath); } } } } stopPipe(err) { if (this.isPipe) { this.yazlFile.outputStream.unpipe(this.zipStream); this.zipStream.destroy(err); this.isPipe = false; } } 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;