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 |
|
19 |
|
20 | const jszip = require('jszip')
|
21 | const path = require('path')
|
22 |
|
23 | const io = require('./index')
|
24 | const { InvalidArgumentError } = require('../lib/error')
|
25 |
|
26 | /**
|
27 | * Manages a zip archive.
|
28 | */
|
29 | class 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 | */
|
166 | function 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 | */
|
181 | function 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
|
221 | module.exports.Zip = Zip
|
222 | module.exports.load = load
|
223 | module.exports.unzip = unzip
|