UNPKG

6.3 kBJavaScriptView Raw
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
18'use strict'
19
20const jszip = require('jszip')
21const path = require('path')
22
23const io = require('./index')
24const { InvalidArgumentError } = require('../lib/error')
25
26/**
27 * Manages a zip archive.
28 */
29class Zip {
30 constructor() {
31 /** @private @const */
32 this.z_ = new jszip()
33
34 /** @private @const {!Set<!Promise<?>>} */
35 this.pendingAdds_ = new Set()
36 }
37
38 /**
39 * Adds a file to this zip.
40 *
41 * @param {string} filePath path to the file to add.
42 * @param {string=} zipPath path to the file in the zip archive, defaults
43 * to the basename of `filePath`.
44 * @return {!Promise<?>} a promise that will resolve when added.
45 */
46 addFile(filePath, zipPath = path.basename(filePath)) {
47 let add = io
48 .read(filePath)
49 .then((buffer) =>
50 this.z_.file(
51 /** @type {string} */ (zipPath.replace(/\\/g, '/')),
52 buffer
53 )
54 )
55 this.pendingAdds_.add(add)
56 return add.then(
57 () => this.pendingAdds_.delete(add),
58 (e) => {
59 this.pendingAdds_.delete(add)
60 throw e
61 }
62 )
63 }
64
65 /**
66 * Recursively adds a directory and all of its contents to this archive.
67 *
68 * @param {string} dirPath path to the directory to add.
69 * @param {string=} zipPath path to the folder in the archive to add the
70 * directory contents to. Defaults to the root folder.
71 * @return {!Promise<?>} returns a promise that will resolve when the
72 * the operation is complete.
73 */
74 addDir(dirPath, zipPath = '') {
75 return io.walkDir(dirPath).then((entries) => {
76 let archive = this.z_
77 if (zipPath) {
78 archive = archive.folder(zipPath)
79 }
80
81 let files = []
82 entries.forEach((spec) => {
83 if (spec.dir) {
84 archive.folder(spec.path)
85 } else {
86 files.push(
87 this.addFile(
88 path.join(dirPath, spec.path),
89 path.join(zipPath, spec.path)
90 )
91 )
92 }
93 })
94
95 return Promise.all(files)
96 })
97 }
98
99 /**
100 * @param {string} path File path to test for within the archive.
101 * @return {boolean} Whether this zip archive contains an entry with the given
102 * path.
103 */
104 has(path) {
105 return this.z_.file(path) !== null
106 }
107
108 /**
109 * Returns the contents of the file in this zip archive with the given `path`.
110 * The returned promise will be rejected with an {@link InvalidArgumentError}
111 * if either `path` does not exist within the archive, or if `path` refers
112 * to a directory.
113 *
114 * @param {string} path the path to the file whose contents to return.
115 * @return {!Promise<!Buffer>} a promise that will be resolved with the file's
116 * contents as a buffer.
117 */
118 getFile(path) {
119 let file = this.z_.file(path)
120 if (!file) {
121 return Promise.reject(
122 new InvalidArgumentError(`No such file in zip archive: ${path}`)
123 )
124 }
125
126 if (file.dir) {
127 return Promise.reject(
128 new InvalidArgumentError(`The requested file is a directory: ${path}`)
129 )
130 }
131
132 return Promise.resolve(file.async('nodebuffer'))
133 }
134
135 /**
136 * Returns the compressed data for this archive in a buffer. _This method will
137 * not wait for any outstanding {@link #addFile add}
138 * {@link #addDir operations} before encoding the archive._
139 *
140 * @param {string} compression The desired compression.
141 * Must be `STORE` (the default) or `DEFLATE`.
142 * @return {!Promise<!Buffer>} a promise that will resolve with this archive
143 * as a buffer.
144 */
145 toBuffer(compression = 'STORE') {
146 if (compression !== 'STORE' && compression !== 'DEFLATE') {
147 return Promise.reject(
148 new InvalidArgumentError(
149 `compression must be one of {STORE, DEFLATE}, got ${compression}`
150 )
151 )
152 }
153 return Promise.resolve(
154 this.z_.generateAsync({ compression, type: 'nodebuffer' })
155 )
156 }
157}
158
159/**
160 * Asynchronously opens a zip archive.
161 *
162 * @param {string} path to the zip archive to load.
163 * @return {!Promise<!Zip>} a promise that will resolve with the opened
164 * archive.
165 */
166function load(path) {
167 return io.read(path).then((data) => {
168 let zip = new Zip()
169 return zip.z_.loadAsync(data).then(() => zip)
170 })
171}
172
173/**
174 * Asynchronously unzips an archive file.
175 *
176 * @param {string} src path to the source file to unzip.
177 * @param {string} dst path to the destination directory.
178 * @return {!Promise<string>} a promise that will resolve with `dst` once the
179 * archive has been unzipped.
180 */
181function unzip(src, dst) {
182 return load(src).then((zip) => {
183 const promisedDirs = new Map()
184 const promises = []
185
186 zip.z_.forEach((relPath, file) => {
187 let p
188 if (file.dir) {
189 p = createDir(relPath)
190 } else {
191 let dirname = path.dirname(relPath)
192 if (dirname === '.') {
193 p = writeFile(relPath, file)
194 } else {
195 p = createDir(dirname).then(() => writeFile(relPath, file))
196 }
197 }
198 promises.push(p)
199 })
200
201 return Promise.all(promises).then(() => dst)
202
203 function createDir(dir) {
204 let p = promisedDirs.get(dir)
205 if (!p) {
206 p = io.mkdirp(path.join(dst, dir))
207 promisedDirs.set(dir, p)
208 }
209 return p
210 }
211
212 function writeFile(relPath, file) {
213 return file
214 .async('nodebuffer')
215 .then((buffer) => io.write(path.join(dst, relPath), buffer))
216 }
217 })
218}
219
220// PUBLIC API
221module.exports.Zip = Zip
222module.exports.load = load
223module.exports.unzip = unzip