const Utils = require("./util"); const pth = require("path"); const ZipEntry = require("./zipEntry"); const ZipFile = require("./zipFile"); const get_Bool = (...val) => Utils.findLast(val, (c) => typeof c === "boolean"); const get_Str = (...val) => Utils.findLast(val, (c) => typeof c === "string"); const get_Fun = (...val) => Utils.findLast(val, (c) => typeof c === "function"); const defaultOptions = { // option "noSort" : if true it disables files sorting noSort: false, // read entries during load (initial loading may be slower) readEntries: false, // default method is none method: Utils.Constants.NONE, // file system fs: null }; module.exports = function (/**String*/ input, /** object */ options) { let inBuffer = null; // create object based default options, allowing them to be overwritten const opts = Object.assign(Object.create(null), defaultOptions); // test input variable if (input && "object" === typeof input) { // if value is not buffer we accept it to be object with options if (!(input instanceof Uint8Array)) { Object.assign(opts, input); input = opts.input ? opts.input : undefined; if (opts.input) delete opts.input; } // if input is buffer if (Buffer.isBuffer(input)) { inBuffer = input; opts.method = Utils.Constants.BUFFER; input = undefined; } } // assign options Object.assign(opts, options); // instanciate utils filesystem const filetools = new Utils(opts); if (typeof opts.decoder !== "object" || typeof opts.decoder.encode !== "function" || typeof opts.decoder.decode !== "function") { opts.decoder = Utils.decoder; } // if input is file name we retrieve its content if (input && "string" === typeof input) { // load zip file if (filetools.fs.existsSync(input)) { opts.method = Utils.Constants.FILE; opts.filename = input; inBuffer = filetools.fs.readFileSync(input); } else { throw Utils.Errors.INVALID_FILENAME(); } } // create variable const _zip = new ZipFile(inBuffer, opts); const { canonical, sanitize, zipnamefix } = Utils; function getEntry(/**Object*/ entry) { if (entry && _zip) { var item; // If entry was given as a file name if (typeof entry === "string") item = _zip.getEntry(pth.posix.normalize(entry)); // if entry was given as a ZipEntry object if (typeof entry === "object" && typeof entry.entryName !== "undefined" && typeof entry.header !== "undefined") item = _zip.getEntry(entry.entryName); if (item) { return item; } } return null; } function fixPath(zipPath) { const { join, normalize, sep } = pth.posix; // convert windows file separators and normalize return join(".", normalize(sep + zipPath.split("\\").join(sep) + sep)); } function filenameFilter(filterfn) { if (filterfn instanceof RegExp) { // if filter is RegExp wrap it return (function (rx) { return function (filename) { return rx.test(filename); }; })(filterfn); } else if ("function" !== typeof filterfn) { // if filter is not function we will replace it return () => true; } return filterfn; } // keep last character on folders const relativePath = (local, entry) => { let lastChar = entry.slice(-1); lastChar = lastChar === filetools.sep ? filetools.sep : ""; return pth.relative(local, entry) + lastChar; }; return { /** * Extracts the given entry from the archive and returns the content as a Buffer object * @param {ZipEntry|string} entry ZipEntry object or String with the full path of the entry * @param {Buffer|string} [pass] - password * @return Buffer or Null in case of error */ readFile: function (entry, pass) { var item = getEntry(entry); return (item && item.getData(pass)) || null; }, /** * Returns how many child elements has on entry (directories) on files it is always 0 * @param {ZipEntry|string} entry ZipEntry object or String with the full path of the entry * @returns {integer} */ childCount: function (entry) { const item = getEntry(entry); if (item) { return _zip.getChildCount(item); } }, /** * Asynchronous readFile * @param {ZipEntry|string} entry ZipEntry object or String with the full path of the entry * @param {callback} callback * * @return Buffer or Null in case of error */ readFileAsync: function (entry, callback) { var item = getEntry(entry); if (item) { item.getDataAsync(callback); } else { callback(null, "getEntry failed for:" + entry); } }, /** * Extracts the given entry from the archive and returns the content as plain text in the given encoding * @param {ZipEntry|string} entry - ZipEntry object or String with the full path of the entry * @param {string} encoding - Optional. If no encoding is specified utf8 is used * * @return String */ readAsText: function (entry, encoding) { var item = getEntry(entry); if (item) { var data = item.getData(); if (data && data.length) { return data.toString(encoding || "utf8"); } } return ""; }, /** * Asynchronous readAsText * @param {ZipEntry|string} entry ZipEntry object or String with the full path of the entry * @param {callback} callback * @param {string} [encoding] - Optional. If no encoding is specified utf8 is used * * @return String */ readAsTextAsync: function (entry, callback, encoding) { var item = getEntry(entry); if (item) { item.getDataAsync(function (data, err) { if (err) { callback(data, err); return; } if (data && data.length) { callback(data.toString(encoding || "utf8")); } else { callback(""); } }); } else { callback(""); } }, /** * Remove the entry from the file or the entry and all it's nested directories and files if the given entry is a directory * * @param {ZipEntry|string} entry * @returns {void} */ deleteFile: function (entry, withsubfolders = true) { // @TODO: test deleteFile var item = getEntry(entry); if (item) { _zip.deleteFile(item.entryName, withsubfolders); } }, /** * Remove the entry from the file or directory without affecting any nested entries * * @param {ZipEntry|string} entry * @returns {void} */ deleteEntry: function (entry) { // @TODO: test deleteEntry var item = getEntry(entry); if (item) { _zip.deleteEntry(item.entryName); } }, /** * Adds a comment to the zip. The zip must be rewritten after adding the comment. * * @param {string} comment */ addZipComment: function (comment) { // @TODO: test addZipComment _zip.comment = comment; }, /** * Returns the zip comment * * @return String */ getZipComment: function () { return _zip.comment || ""; }, /** * Adds a comment to a specified zipEntry. The zip must be rewritten after adding the comment * The comment cannot exceed 65535 characters in length * * @param {ZipEntry} entry * @param {string} comment */ addZipEntryComment: function (entry, comment) { var item = getEntry(entry); if (item) { item.comment = comment; } }, /** * Returns the comment of the specified entry * * @param {ZipEntry} entry * @return String */ getZipEntryComment: function (entry) { var item = getEntry(entry); if (item) { return item.comment || ""; } return ""; }, /** * Updates the content of an existing entry inside the archive. The zip must be rewritten after updating the content * * @param {ZipEntry} entry * @param {Buffer} content */ updateFile: function (entry, content) { var item = getEntry(entry); if (item) { item.setData(content); } }, /** * Adds a file from the disk to the archive * * @param {string} localPath File to add to zip * @param {string} [zipPath] Optional path inside the zip * @param {string} [zipName] Optional name for the file * @param {string} [comment] Optional file comment */ addLocalFile: function (localPath, zipPath, zipName, comment) { if (filetools.fs.existsSync(localPath)) { // fix ZipPath zipPath = zipPath ? fixPath(zipPath) : ""; // p - local file name const p = pth.win32.basename(pth.win32.normalize(localPath)); // add file name into zippath zipPath += zipName ? zipName : p; // read file attributes const _attr = filetools.fs.statSync(localPath); // get file content const data = _attr.isFile() ? filetools.fs.readFileSync(localPath) : Buffer.alloc(0); // if folder if (_attr.isDirectory()) zipPath += filetools.sep; // add file into zip file this.addFile(zipPath, data, comment, _attr); } else { throw Utils.Errors.FILE_NOT_FOUND(localPath); } }, /** * Callback for showing if everything was done. * * @callback doneCallback * @param {Error} err - Error object * @param {boolean} done - was request fully completed */ /** * Adds a file from the disk to the archive * * @param {(object|string)} options - options object, if it is string it us used as localPath. * @param {string} options.localPath - Local path to the file. * @param {string} [options.comment] - Optional file comment. * @param {string} [options.zipPath] - Optional path inside the zip * @param {string} [options.zipName] - Optional name for the file * @param {doneCallback} callback - The callback that handles the response. */ addLocalFileAsync: function (options, callback) { options = typeof options === "object" ? options : { localPath: options }; const localPath = pth.resolve(options.localPath); const { comment } = options; let { zipPath, zipName } = options; const self = this; filetools.fs.stat(localPath, function (err, stats) { if (err) return callback(err, false); // fix ZipPath zipPath = zipPath ? fixPath(zipPath) : ""; // p - local file name const p = pth.win32.basename(pth.win32.normalize(localPath)); // add file name into zippath zipPath += zipName ? zipName : p; if (stats.isFile()) { filetools.fs.readFile(localPath, function (err, data) { if (err) return callback(err, false); self.addFile(zipPath, data, comment, stats); return setImmediate(callback, undefined, true); }); } else if (stats.isDirectory()) { zipPath += filetools.sep; self.addFile(zipPath, Buffer.alloc(0), comment, stats); return setImmediate(callback, undefined, true); } }); }, /** * Adds a local directory and all its nested files and directories to the archive * * @param {string} localPath - local path to the folder * @param {string} [zipPath] - optional path inside zip * @param {(RegExp|function)} [filter] - optional RegExp or Function if files match will be included. */ addLocalFolder: function (localPath, zipPath, filter) { // Prepare filter filter = filenameFilter(filter); // fix ZipPath zipPath = zipPath ? fixPath(zipPath) : ""; // normalize the path first localPath = pth.normalize(localPath); if (filetools.fs.existsSync(localPath)) { const items = filetools.findFiles(localPath); const self = this; if (items.length) { for (const filepath of items) { const p = pth.join(zipPath, relativePath(localPath, filepath)); if (filter(p)) { self.addLocalFile(filepath, pth.dirname(p)); } } } } else { throw Utils.Errors.FILE_NOT_FOUND(localPath); } }, /** * Asynchronous addLocalFolder * @param {string} localPath * @param {callback} callback * @param {string} [zipPath] optional path inside zip * @param {RegExp|function} [filter] optional RegExp or Function if files match will * be included. */ addLocalFolderAsync: function (localPath, callback, zipPath, filter) { // Prepare filter filter = filenameFilter(filter); // fix ZipPath zipPath = zipPath ? fixPath(zipPath) : ""; // normalize the path first localPath = pth.normalize(localPath); var self = this; filetools.fs.open(localPath, "r", function (err) { if (err && err.code === "ENOENT") { callback(undefined, Utils.Errors.FILE_NOT_FOUND(localPath)); } else if (err) { callback(undefined, err); } else { var items = filetools.findFiles(localPath); var i = -1; var next = function () { i += 1; if (i < items.length) { var filepath = items[i]; var p = relativePath(localPath, filepath).split("\\").join("/"); //windows fix p = p .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^\x20-\x7E]/g, ""); // accent fix if (filter(p)) { filetools.fs.stat(filepath, function (er0, stats) { if (er0) callback(undefined, er0); if (stats.isFile()) { filetools.fs.readFile(filepath, function (er1, data) { if (er1) { callback(undefined, er1); } else { self.addFile(zipPath + p, data, "", stats); next(); } }); } else { self.addFile(zipPath + p + "/", Buffer.alloc(0), "", stats); next(); } }); } else { process.nextTick(() => { next(); }); } } else { callback(true, undefined); } }; next(); } }); }, /** * Adds a local directory and all its nested files and directories to the archive * * @param {object | string} options - options object, if it is string it us used as localPath. * @param {string} options.localPath - Local path to the folder. * @param {string} [options.zipPath] - optional path inside zip. * @param {RegExp|function} [options.filter] - optional RegExp or Function if files match will be included. * @param {function|string} [options.namefix] - optional function to help fix filename * @param {doneCallback} callback - The callback that handles the response. * */ addLocalFolderAsync2: function (options, callback) { const self = this; options = typeof options === "object" ? options : { localPath: options }; localPath = pth.resolve(fixPath(options.localPath)); let { zipPath, filter, namefix } = options; if (filter instanceof RegExp) { filter = (function (rx) { return function (filename) { return rx.test(filename); }; })(filter); } else if ("function" !== typeof filter) { filter = function () { return true; }; } // fix ZipPath zipPath = zipPath ? fixPath(zipPath) : ""; // Check Namefix function if (namefix == "latin1") { namefix = (str) => str .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^\x20-\x7E]/g, ""); // accent fix (latin1 characers only) } if (typeof namefix !== "function") namefix = (str) => str; // internal, create relative path + fix the name const relPathFix = (entry) => pth.join(zipPath, namefix(relativePath(localPath, entry))); const fileNameFix = (entry) => pth.win32.basename(pth.win32.normalize(namefix(entry))); filetools.fs.open(localPath, "r", function (err) { if (err && err.code === "ENOENT") { callback(undefined, Utils.Errors.FILE_NOT_FOUND(localPath)); } else if (err) { callback(undefined, err); } else { filetools.findFilesAsync(localPath, function (err, fileEntries) { if (err) return callback(err); fileEntries = fileEntries.filter((dir) => filter(relPathFix(dir))); if (!fileEntries.length) callback(undefined, false); setImmediate( fileEntries.reverse().reduce(function (next, entry) { return function (err, done) { if (err || done === false) return setImmediate(next, err, false); self.addLocalFileAsync( { localPath: entry, zipPath: pth.dirname(relPathFix(entry)), zipName: fileNameFix(entry) }, next ); }; }, callback) ); }); } }); }, /** * Adds a local directory and all its nested files and directories to the archive * * @param {string} localPath - path where files will be extracted * @param {object} props - optional properties * @param {string} [props.zipPath] - optional path inside zip * @param {RegExp|function} [props.filter] - optional RegExp or Function if files match will be included. * @param {function|string} [props.namefix] - optional function to help fix filename */ addLocalFolderPromise: function (localPath, props) { return new Promise((resolve, reject) => { this.addLocalFolderAsync2(Object.assign({ localPath }, props), (err, done) => { if (err) reject(err); if (done) resolve(this); }); }); }, /** * Allows you to create a entry (file or directory) in the zip file. * If you want to create a directory the entryName must end in / and a null buffer should be provided. * Comment and attributes are optional * * @param {string} entryName * @param {Buffer | string} content - file content as buffer or utf8 coded string * @param {string} [comment] - file comment * @param {number | object} [attr] - number as unix file permissions, object as filesystem Stats object */ addFile: function (entryName, content, comment, attr) { entryName = zipnamefix(entryName); let entry = getEntry(entryName); const update = entry != null; // prepare new entry if (!update) { entry = new ZipEntry(opts); entry.entryName = entryName; } entry.comment = comment || ""; const isStat = "object" === typeof attr && attr instanceof filetools.fs.Stats; // last modification time from file stats if (isStat) { entry.header.time = attr.mtime; } // Set file attribute var fileattr = entry.isDirectory ? 0x10 : 0; // (MS-DOS directory flag) // extended attributes field for Unix // set file type either S_IFDIR / S_IFREG let unix = entry.isDirectory ? 0x4000 : 0x8000; if (isStat) { // File attributes from file stats unix |= 0xfff & attr.mode; } else if ("number" === typeof attr) { // attr from given attr values unix |= 0xfff & attr; } else { // Default values: unix |= entry.isDirectory ? 0o755 : 0o644; // permissions (drwxr-xr-x) or (-r-wr--r--) } fileattr = (fileattr | (unix << 16)) >>> 0; // add attributes entry.attr = fileattr; entry.setData(content); if (!update) _zip.setEntry(entry); return entry; }, /** * Returns an array of ZipEntry objects representing the files and folders inside the archive * * @param {string} [password] * @returns Array */ getEntries: function (password) { _zip.password = password; return _zip ? _zip.entries : []; }, /** * Returns a ZipEntry object representing the file or folder specified by ``name``. * * @param {string} name * @return ZipEntry */ getEntry: function (/**String*/ name) { return getEntry(name); }, getEntryCount: function () { return _zip.getEntryCount(); }, forEach: function (callback) { return _zip.forEach(callback); }, /** * Extracts the given entry to the given targetPath * If the entry is a directory inside the archive, the entire directory and it's subdirectories will be extracted * * @param {string|ZipEntry} entry - ZipEntry object or String with the full path of the entry * @param {string} targetPath - Target folder where to write the file * @param {boolean} [maintainEntryPath=true] - If maintainEntryPath is true and the entry is inside a folder, the entry folder will be created in targetPath as well. Default is TRUE * @param {boolean} [overwrite=false] - If the file already exists at the target path, the file will be overwriten if this is true. * @param {boolean} [keepOriginalPermission=false] - The file will be set as the permission from the entry if this is true. * @param {string} [outFileName] - String If set will override the filename of the extracted file (Only works if the entry is a file) * * @return Boolean */ extractEntryTo: function (entry, targetPath, maintainEntryPath, overwrite, keepOriginalPermission, outFileName) { overwrite = get_Bool(false, overwrite); keepOriginalPermission = get_Bool(false, keepOriginalPermission); maintainEntryPath = get_Bool(true, maintainEntryPath); outFileName = get_Str(keepOriginalPermission, outFileName); var item = getEntry(entry); if (!item) { throw Utils.Errors.NO_ENTRY(); } var entryName = canonical(item.entryName); var target = sanitize(targetPath, outFileName && !item.isDirectory ? outFileName : maintainEntryPath ? entryName : pth.basename(entryName)); if (item.isDirectory) { var children = _zip.getEntryChildren(item); children.forEach(function (child) { if (child.isDirectory) return; var content = child.getData(); if (!content) { throw Utils.Errors.CANT_EXTRACT_FILE(); } var name = canonical(child.entryName); var childName = sanitize(targetPath, maintainEntryPath ? name : pth.basename(name)); // The reverse operation for attr depend on method addFile() const fileAttr = keepOriginalPermission ? child.header.fileAttr : undefined; filetools.writeFileTo(childName, content, overwrite, fileAttr); }); return true; } var content = item.getData(_zip.password); if (!content) throw Utils.Errors.CANT_EXTRACT_FILE(); if (filetools.fs.existsSync(target) && !overwrite) { throw Utils.Errors.CANT_OVERRIDE(); } // The reverse operation for attr depend on method addFile() const fileAttr = keepOriginalPermission ? entry.header.fileAttr : undefined; filetools.writeFileTo(target, content, overwrite, fileAttr); return true; }, /** * Test the archive * @param {string} [pass] */ test: function (pass) { if (!_zip) { return false; } for (var entry in _zip.entries) { try { if (entry.isDirectory) { continue; } var content = _zip.entries[entry].getData(pass); if (!content) { return false; } } catch (err) { return false; } } return true; }, /** * Extracts the entire archive to the given location * * @param {string} targetPath Target location * @param {boolean} [overwrite=false] If the file already exists at the target path, the file will be overwriten if this is true. * Default is FALSE * @param {boolean} [keepOriginalPermission=false] The file will be set as the permission from the entry if this is true. * Default is FALSE * @param {string|Buffer} [pass] password */ extractAllTo: function (targetPath, overwrite, keepOriginalPermission, pass) { keepOriginalPermission = get_Bool(false, keepOriginalPermission); pass = get_Str(keepOriginalPermission, pass); overwrite = get_Bool(false, overwrite); if (!_zip) throw Utils.Errors.NO_ZIP(); _zip.entries.forEach(function (entry) { var entryName = sanitize(targetPath, canonical(entry.entryName)); if (entry.isDirectory) { filetools.makeDir(entryName); return; } var content = entry.getData(pass); if (!content) { throw Utils.Errors.CANT_EXTRACT_FILE(); } // The reverse operation for attr depend on method addFile() const fileAttr = keepOriginalPermission ? entry.header.fileAttr : undefined; filetools.writeFileTo(entryName, content, overwrite, fileAttr); try { filetools.fs.utimesSync(entryName, entry.header.time, entry.header.time); } catch (err) { throw Utils.Errors.CANT_EXTRACT_FILE(); } }); }, /** * Asynchronous extractAllTo * * @param {string} targetPath Target location * @param {boolean} [overwrite=false] If the file already exists at the target path, the file will be overwriten if this is true. * Default is FALSE * @param {boolean} [keepOriginalPermission=false] The file will be set as the permission from the entry if this is true. * Default is FALSE * @param {function} callback The callback will be executed when all entries are extracted successfully or any error is thrown. */ extractAllToAsync: function (targetPath, overwrite, keepOriginalPermission, callback) { callback = get_Fun(overwrite, keepOriginalPermission, callback); keepOriginalPermission = get_Bool(false, keepOriginalPermission); overwrite = get_Bool(false, overwrite); if (!callback) { return new Promise((resolve, reject) => { this.extractAllToAsync(targetPath, overwrite, keepOriginalPermission, function (err) { if (err) { reject(err); } else { resolve(this); } }); }); } if (!_zip) { callback(Utils.Errors.NO_ZIP()); return; } targetPath = pth.resolve(targetPath); // convert entryName to const getPath = (entry) => sanitize(targetPath, pth.normalize(canonical(entry.entryName))); const getError = (msg, file) => new Error(msg + ': "' + file + '"'); // separate directories from files const dirEntries = []; const fileEntries = []; _zip.entries.forEach((e) => { if (e.isDirectory) { dirEntries.push(e); } else { fileEntries.push(e); } }); // Create directory entries first synchronously // this prevents race condition and assures folders are there before writing files for (const entry of dirEntries) { const dirPath = getPath(entry); // The reverse operation for attr depend on method addFile() const dirAttr = keepOriginalPermission ? entry.header.fileAttr : undefined; try { filetools.makeDir(dirPath); if (dirAttr) filetools.fs.chmodSync(dirPath, dirAttr); // in unix timestamp will change if files are later added to folder, but still filetools.fs.utimesSync(dirPath, entry.header.time, entry.header.time); } catch (er) { callback(getError("Unable to create folder", dirPath)); } } fileEntries.reverse().reduce(function (next, entry) { return function (err) { if (err) { next(err); } else { const entryName = pth.normalize(canonical(entry.entryName)); const filePath = sanitize(targetPath, entryName); entry.getDataAsync(function (content, err_1) { if (err_1) { next(err_1); } else if (!content) { next(Utils.Errors.CANT_EXTRACT_FILE()); } else { // The reverse operation for attr depend on method addFile() const fileAttr = keepOriginalPermission ? entry.header.fileAttr : undefined; filetools.writeFileToAsync(filePath, content, overwrite, fileAttr, function (succ) { if (!succ) { next(getError("Unable to write file", filePath)); } filetools.fs.utimes(filePath, entry.header.time, entry.header.time, function (err_2) { if (err_2) { next(getError("Unable to set times", filePath)); } else { next(); } }); }); } }); } }; }, callback)(); }, /** * Writes the newly created zip file to disk at the specified location or if a zip was opened and no ``targetFileName`` is provided, it will overwrite the opened zip * * @param {string} targetFileName * @param {function} callback */ writeZip: function (targetFileName, callback) { if (arguments.length === 1) { if (typeof targetFileName === "function") { callback = targetFileName; targetFileName = ""; } } if (!targetFileName && opts.filename) { targetFileName = opts.filename; } if (!targetFileName) return; var zipData = _zip.compressToBuffer(); if (zipData) { var ok = filetools.writeFileTo(targetFileName, zipData, true); if (typeof callback === "function") callback(!ok ? new Error("failed") : null, ""); } }, /** * * @param {string} targetFileName * @param {object} [props] * @param {boolean} [props.overwrite=true] If the file already exists at the target path, the file will be overwriten if this is true. * @param {boolean} [props.perm] The file will be set as the permission from the entry if this is true. * @returns {Promise} */ writeZipPromise: function (/**String*/ targetFileName, /* object */ props) { const { overwrite, perm } = Object.assign({ overwrite: true }, props); return new Promise((resolve, reject) => { // find file name if (!targetFileName && opts.filename) targetFileName = opts.filename; if (!targetFileName) reject("ADM-ZIP: ZIP File Name Missing"); this.toBufferPromise().then((zipData) => { const ret = (done) => (done ? resolve(done) : reject("ADM-ZIP: Wasn't able to write zip file")); filetools.writeFileToAsync(targetFileName, zipData, overwrite, perm, ret); }, reject); }); }, /** * @returns {Promise} A promise to the Buffer. */ toBufferPromise: function () { return new Promise((resolve, reject) => { _zip.toAsyncBuffer(resolve, reject); }); }, /** * Returns the content of the entire zip file as a Buffer object * * @prop {function} [onSuccess] * @prop {function} [onFail] * @prop {function} [onItemStart] * @prop {function} [onItemEnd] * @returns {Buffer} */ toBuffer: function (onSuccess, onFail, onItemStart, onItemEnd) { if (typeof onSuccess === "function") { _zip.toAsyncBuffer(onSuccess, onFail, onItemStart, onItemEnd); return null; } return _zip.compressToBuffer(); } }; };