// Licensed to the Software Freedom Conservancy (SFC) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The SFC licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. 'use strict'; const jszip = require('jszip'); const path = require('path'); const io = require('./index'); const {InvalidArgumentError} = require('../lib/error'); /** * Manages a zip archive. */ class Zip { constructor() { /** @private @const */ this.z_ = new jszip; /** @private @const {!Set>} */ this.pendingAdds_ = new Set; } /** * Adds a file to this zip. * * @param {string} filePath path to the file to add. * @param {string=} zipPath path to the file in the zip archive, defaults * to the basename of `filePath`. * @return {!Promise} a promise that will resolve when added. */ addFile(filePath, zipPath = path.basename(filePath)) { let add = io.read(filePath) .then(buffer => this.z_.file(/** @type {string} */(zipPath), buffer)); this.pendingAdds_.add(add); return add.then( () => this.pendingAdds_.delete(add), (e) => { this.pendingAdds_.delete(add); throw e; }); } /** * Recursively adds a directory and all of its contents to this archive. * * @param {string} dirPath path to the directory to add. * @param {string=} zipPath path to the folder in the archive to add the * directory contents to. Defaults to the root folder. * @return {!Promise} returns a promise that will resolve when the * the operation is complete. */ addDir(dirPath, zipPath = '') { return io.walkDir(dirPath).then(entries => { let archive = this.z_; if (zipPath) { archive = archive.folder(zipPath); } let files = []; entries.forEach(spec => { if (spec.dir) { archive.folder(spec.path); } else { files.push( this.addFile( path.join(dirPath, spec.path), path.join(zipPath, spec.path))); } }); return Promise.all(files); }); } /** * @param {string} path File path to test for within the archive. * @return {boolean} Whether this zip archive contains an entry with the given * path. */ has(path) { return this.z_.file(path) !== null; } /** * Returns the contents of the file in this zip archive with the given `path`. * The returned promise will be rejected with an {@link InvalidArgumentError} * if either `path` does not exist within the archive, or if `path` refers * to a directory. * * @param {string} path the path to the file whose contents to return. * @return {!Promise} a promise that will be resolved with the file's * contents as a buffer. */ getFile(path) { let file = this.z_.file(path); if (!file) { return Promise.reject( new InvalidArgumentError(`No such file in zip archive: ${path}`)); } if (file.dir) { return Promise.reject( new InvalidArgumentError( `The requested file is a directory: ${path}`)); } return Promise.resolve(file.async('nodebuffer')); } /** * Returns the compressed data for this archive in a buffer. _This method will * not wait for any outstanding {@link #addFile add} * {@link #addDir operations} before encoding the archive._ * * @param {string} compression The desired compression. * Must be `STORE` (the default) or `DEFLATE`. * @return {!Promise} a promise that will resolve with this archive * as a buffer. */ toBuffer(compression = 'STORE') { if (compression !== 'STORE' && compression !== 'DEFLATE') { return Promise.reject( new InvalidArgumentError( `compression must be one of {STORE, DEFLATE}, got ${compression}`)); } return Promise.resolve( this.z_.generateAsync({compression, type: 'nodebuffer'})); } } /** * Asynchronously opens a zip archive. * * @param {string} path to the zip archive to load. * @return {!Promise} a promise that will resolve with the opened * archive. */ function load(path) { return io.read(path).then(data => { let zip = new Zip; return zip.z_.loadAsync(data).then(() => zip); }); } /** * Asynchronously unzips an archive file. * * @param {string} src path to the source file to unzip. * @param {string} dst path to the destination directory. * @return {!Promise} a promise that will resolve with `dst` once the * archive has been unzipped. */ function unzip(src, dst) { return load(src).then(zip => { let promisedDirs = new Map; let promises = []; zip.z_.forEach((relPath, file) => { let p; if (file.dir) { p = createDir(relPath); } else { let dirname = path.dirname(relPath); if (dirname === '.') { p = writeFile(relPath, file); } else { p = createDir(dirname).then(() => writeFile(relPath, file)); } } promises.push(p); }); return Promise.all(promises).then(() => dst); function createDir(dir) { let p = promisedDirs.get(dir); if (!p) { p = io.mkdirp(path.join(dst, dir)); promisedDirs.set(dir, p); } return p; } function writeFile(relPath, file) { return file.async('nodebuffer') .then(buffer => io.write(path.join(dst, relPath), buffer)); } }); } // PUBLIC API exports.Zip = Zip; exports.load = load; exports.unzip = unzip;