| // 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('node: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.replace(/\\/g, '/')), 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 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) => { |
| const promisedDirs = new Map() |
| const 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 |
| module.exports = { Zip, load, unzip } |