zip.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. // Licensed to the Software Freedom Conservancy (SFC) under one
  2. // or more contributor license agreements. See the NOTICE file
  3. // distributed with this work for additional information
  4. // regarding copyright ownership. The SFC licenses this file
  5. // to you under the Apache License, Version 2.0 (the
  6. // "License"); you may not use this file except in compliance
  7. // with the License. You may obtain a copy of the License at
  8. //
  9. // http://www.apache.org/licenses/LICENSE-2.0
  10. //
  11. // Unless required by applicable law or agreed to in writing,
  12. // software distributed under the License is distributed on an
  13. // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  14. // KIND, either express or implied. See the License for the
  15. // specific language governing permissions and limitations
  16. // under the License.
  17. 'use strict';
  18. const jszip = require('jszip');
  19. const path = require('path');
  20. const io = require('./index');
  21. const {InvalidArgumentError} = require('../lib/error');
  22. /**
  23. * Manages a zip archive.
  24. */
  25. class Zip {
  26. constructor() {
  27. /** @private @const */
  28. this.z_ = new jszip;
  29. /** @private @const {!Set<!Promise<?>>} */
  30. this.pendingAdds_ = new Set;
  31. }
  32. /**
  33. * Adds a file to this zip.
  34. *
  35. * @param {string} filePath path to the file to add.
  36. * @param {string=} zipPath path to the file in the zip archive, defaults
  37. * to the basename of `filePath`.
  38. * @return {!Promise<?>} a promise that will resolve when added.
  39. */
  40. addFile(filePath, zipPath = path.basename(filePath)) {
  41. let add = io.read(filePath)
  42. .then(buffer => this.z_.file(/** @type {string} */(zipPath), buffer));
  43. this.pendingAdds_.add(add);
  44. return add.then(
  45. () => this.pendingAdds_.delete(add),
  46. (e) => {
  47. this.pendingAdds_.delete(add);
  48. throw e;
  49. });
  50. }
  51. /**
  52. * Recursively adds a directory and all of its contents to this archive.
  53. *
  54. * @param {string} dirPath path to the directory to add.
  55. * @param {string=} zipPath path to the folder in the archive to add the
  56. * directory contents to. Defaults to the root folder.
  57. * @return {!Promise<?>} returns a promise that will resolve when the
  58. * the operation is complete.
  59. */
  60. addDir(dirPath, zipPath = '') {
  61. return io.walkDir(dirPath).then(entries => {
  62. let archive = this.z_;
  63. if (zipPath) {
  64. archive = archive.folder(zipPath);
  65. }
  66. let files = [];
  67. entries.forEach(spec => {
  68. if (spec.dir) {
  69. archive.folder(spec.path);
  70. } else {
  71. files.push(
  72. this.addFile(
  73. path.join(dirPath, spec.path),
  74. path.join(zipPath, spec.path)));
  75. }
  76. });
  77. return Promise.all(files);
  78. });
  79. }
  80. /**
  81. * @param {string} path File path to test for within the archive.
  82. * @return {boolean} Whether this zip archive contains an entry with the given
  83. * path.
  84. */
  85. has(path) {
  86. return this.z_.file(path) !== null;
  87. }
  88. /**
  89. * Returns the contents of the file in this zip archive with the given `path`.
  90. * The returned promise will be rejected with an {@link InvalidArgumentError}
  91. * if either `path` does not exist within the archive, or if `path` refers
  92. * to a directory.
  93. *
  94. * @param {string} path the path to the file whose contents to return.
  95. * @return {!Promise<!Buffer>} a promise that will be resolved with the file's
  96. * contents as a buffer.
  97. */
  98. getFile(path) {
  99. let file = this.z_.file(path);
  100. if (!file) {
  101. return Promise.reject(
  102. new InvalidArgumentError(`No such file in zip archive: ${path}`));
  103. }
  104. if (file.dir) {
  105. return Promise.reject(
  106. new InvalidArgumentError(
  107. `The requested file is a directory: ${path}`));
  108. }
  109. return Promise.resolve(file.async('nodebuffer'));
  110. }
  111. /**
  112. * Returns the compressed data for this archive in a buffer. _This method will
  113. * not wait for any outstanding {@link #addFile add}
  114. * {@link #addDir operations} before encoding the archive._
  115. *
  116. * @param {string} compression The desired compression.
  117. * Must be `STORE` (the default) or `DEFLATE`.
  118. * @return {!Promise<!Buffer>} a promise that will resolve with this archive
  119. * as a buffer.
  120. */
  121. toBuffer(compression = 'STORE') {
  122. if (compression !== 'STORE' && compression !== 'DEFLATE') {
  123. return Promise.reject(
  124. new InvalidArgumentError(
  125. `compression must be one of {STORE, DEFLATE}, got ${compression}`));
  126. }
  127. return Promise.resolve(
  128. this.z_.generateAsync({compression, type: 'nodebuffer'}));
  129. }
  130. }
  131. /**
  132. * Asynchronously opens a zip archive.
  133. *
  134. * @param {string} path to the zip archive to load.
  135. * @return {!Promise<!Zip>} a promise that will resolve with the opened
  136. * archive.
  137. */
  138. function load(path) {
  139. return io.read(path).then(data => {
  140. let zip = new Zip;
  141. return zip.z_.loadAsync(data).then(() => zip);
  142. });
  143. }
  144. /**
  145. * Asynchronously unzips an archive file.
  146. *
  147. * @param {string} src path to the source file to unzip.
  148. * @param {string} dst path to the destination directory.
  149. * @return {!Promise<string>} a promise that will resolve with `dst` once the
  150. * archive has been unzipped.
  151. */
  152. function unzip(src, dst) {
  153. return load(src).then(zip => {
  154. let promisedDirs = new Map;
  155. let promises = [];
  156. zip.z_.forEach((relPath, file) => {
  157. let p;
  158. if (file.dir) {
  159. p = createDir(relPath);
  160. } else {
  161. let dirname = path.dirname(relPath);
  162. if (dirname === '.') {
  163. p = writeFile(relPath, file);
  164. } else {
  165. p = createDir(dirname).then(() => writeFile(relPath, file));
  166. }
  167. }
  168. promises.push(p);
  169. });
  170. return Promise.all(promises).then(() => dst);
  171. function createDir(dir) {
  172. let p = promisedDirs.get(dir);
  173. if (!p) {
  174. p = io.mkdirp(path.join(dst, dir));
  175. promisedDirs.set(dir, p);
  176. }
  177. return p;
  178. }
  179. function writeFile(relPath, file) {
  180. return file.async('nodebuffer')
  181. .then(buffer => io.write(path.join(dst, relPath), buffer));
  182. }
  183. });
  184. }
  185. // PUBLIC API
  186. exports.Zip = Zip;
  187. exports.load = load;
  188. exports.unzip = unzip;