blob: a889d9af29152782ec8bcea379b70e8e2c63a8a8 [file] [log] [blame]
// 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<!Promise<?>>} */
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<!Buffer>} 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<!Buffer>} 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<!Zip>} 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<string>} 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;