|
1 |
| -const fs = require('fs') |
| 1 | +const debug = require('debug')('extract-zip') |
| 2 | +const { createWriteStream, promises: fs } = require('fs') |
| 3 | +const getStream = require('get-stream') |
2 | 4 | const path = require('path')
|
| 5 | +const { promisify } = require('util') |
| 6 | +const stream = require('stream') |
3 | 7 | const yauzl = require('yauzl')
|
4 |
| -const concat = require('concat-stream') |
5 |
| -const debug = require('debug')('extract-zip') |
6 | 8 |
|
7 |
| -module.exports = function (zipPath, opts, cb) { |
8 |
| - debug('creating target directory', opts.dir) |
| 9 | +const openZip = promisify(yauzl.open) |
| 10 | +const pipeline = promisify(stream.pipeline) |
9 | 11 |
|
10 |
| - if (path.isAbsolute(opts.dir) === false) { |
11 |
| - return cb(new Error('Target directory is expected to be absolute')) |
| 12 | +class Extractor { |
| 13 | + constructor (zipPath, opts) { |
| 14 | + this.zipPath = zipPath |
| 15 | + this.opts = opts |
12 | 16 | }
|
13 | 17 |
|
14 |
| - fs.mkdir(opts.dir, { recursive: true }, function (err) { |
15 |
| - if (err) return cb(err) |
16 |
| - |
17 |
| - fs.realpath(opts.dir, function (err, canonicalDir) { |
18 |
| - if (err) return cb(err) |
19 |
| - |
20 |
| - opts.dir = canonicalDir |
21 |
| - |
22 |
| - openZip(opts) |
23 |
| - }) |
24 |
| - }) |
25 |
| - |
26 |
| - function openZip () { |
27 |
| - debug('opening', zipPath, 'with opts', opts) |
| 18 | + async extract () { |
| 19 | + debug('opening', this.zipPath, 'with opts', this.opts) |
28 | 20 |
|
29 |
| - yauzl.open(zipPath, { lazyEntries: true }, function (err, zipfile) { |
30 |
| - if (err) return cb(err) |
| 21 | + this.zipfile = await openZip(this.zipPath, { lazyEntries: true }) |
| 22 | + this.canceled = false |
31 | 23 |
|
32 |
| - let cancelled = false |
33 |
| - |
34 |
| - zipfile.on('error', function (err) { |
35 |
| - if (err) { |
36 |
| - cancelled = true |
37 |
| - return cb(err) |
38 |
| - } |
| 24 | + return new Promise((resolve, reject) => { |
| 25 | + this.zipfile.on('error', err => { |
| 26 | + this.canceled = true |
| 27 | + reject(err) |
39 | 28 | })
|
40 |
| - zipfile.readEntry() |
| 29 | + this.zipfile.readEntry() |
41 | 30 |
|
42 |
| - zipfile.on('close', function () { |
43 |
| - if (!cancelled) { |
| 31 | + this.zipfile.on('close', () => { |
| 32 | + if (!this.canceled) { |
44 | 33 | debug('zip extraction complete')
|
45 |
| - cb() |
| 34 | + resolve() |
46 | 35 | }
|
47 | 36 | })
|
48 | 37 |
|
49 |
| - zipfile.on('entry', function (entry) { |
| 38 | + this.zipfile.on('entry', async entry => { |
50 | 39 | /* istanbul ignore if */
|
51 |
| - if (cancelled) { |
52 |
| - debug('skipping entry', entry.fileName, { cancelled: cancelled }) |
| 40 | + if (this.canceled) { |
| 41 | + debug('skipping entry', entry.fileName, { cancelled: this.canceled }) |
53 | 42 | return
|
54 | 43 | }
|
55 | 44 |
|
56 | 45 | debug('zipfile entry', entry.fileName)
|
57 | 46 |
|
58 | 47 | if (entry.fileName.startsWith('__MACOSX/')) {
|
59 |
| - zipfile.readEntry() |
| 48 | + this.zipfile.readEntry() |
60 | 49 | return
|
61 | 50 | }
|
62 | 51 |
|
63 |
| - const destDir = path.dirname(path.join(opts.dir, entry.fileName)) |
| 52 | + const destDir = path.dirname(path.join(this.opts.dir, entry.fileName)) |
64 | 53 |
|
65 |
| - fs.mkdir(destDir, { recursive: true }, function (err) { |
66 |
| - /* istanbul ignore if */ |
67 |
| - if (err) { |
68 |
| - cancelled = true |
69 |
| - zipfile.close() |
70 |
| - return cb(err) |
71 |
| - } |
| 54 | + try { |
| 55 | + await fs.mkdir(destDir, { recursive: true }) |
72 | 56 |
|
73 |
| - fs.realpath(destDir, function (err, canonicalDestDir) { |
74 |
| - /* istanbul ignore if */ |
75 |
| - if (err) { |
76 |
| - cancelled = true |
77 |
| - zipfile.close() |
78 |
| - return cb(err) |
79 |
| - } |
80 |
| - |
81 |
| - const relativeDestDir = path.relative(opts.dir, canonicalDestDir) |
82 |
| - |
83 |
| - if (relativeDestDir.split(path.sep).indexOf('..') !== -1) { |
84 |
| - cancelled = true |
85 |
| - zipfile.close() |
86 |
| - return cb(new Error('Out of bound path "' + canonicalDestDir + '" found while processing file ' + entry.fileName)) |
87 |
| - } |
88 |
| - |
89 |
| - extractEntry(entry, function (err) { |
90 |
| - // if any extraction fails then abort everything |
91 |
| - if (err) { |
92 |
| - cancelled = true |
93 |
| - zipfile.close() |
94 |
| - return cb(err) |
95 |
| - } |
96 |
| - debug('finished processing', entry.fileName) |
97 |
| - zipfile.readEntry() |
98 |
| - }) |
99 |
| - }) |
100 |
| - }) |
101 |
| - }) |
| 57 | + const canonicalDestDir = await fs.realpath(destDir) |
| 58 | + const relativeDestDir = path.relative(this.opts.dir, canonicalDestDir) |
102 | 59 |
|
103 |
| - function extractEntry (entry, done) { |
104 |
| - /* istanbul ignore if */ |
105 |
| - if (cancelled) { |
106 |
| - debug('skipping entry extraction', entry.fileName, { cancelled: cancelled }) |
107 |
| - return setImmediate(done) |
108 |
| - } |
| 60 | + if (relativeDestDir.split(path.sep).includes('..')) { |
| 61 | + throw new Error(`Out of bound path "${canonicalDestDir}" found while processing file ${entry.fileName}`) |
| 62 | + } |
109 | 63 |
|
110 |
| - if (opts.onEntry) { |
111 |
| - opts.onEntry(entry, zipfile) |
| 64 | + await this.extractEntry(entry) |
| 65 | + debug('finished processing', entry.fileName) |
| 66 | + this.zipfile.readEntry() |
| 67 | + } catch (err) { |
| 68 | + this.canceled = true |
| 69 | + this.zipfile.close() |
| 70 | + reject(err) |
112 | 71 | }
|
| 72 | + }) |
| 73 | + }) |
| 74 | + } |
113 | 75 |
|
114 |
| - const dest = path.join(opts.dir, entry.fileName) |
115 |
| - |
116 |
| - // convert external file attr int into a fs stat mode int |
117 |
| - let mode = (entry.externalFileAttributes >> 16) & 0xFFFF |
118 |
| - // check if it's a symlink or dir (using stat mode constants) |
119 |
| - const IFMT = 61440 |
120 |
| - const IFDIR = 16384 |
121 |
| - const IFLNK = 40960 |
122 |
| - const symlink = (mode & IFMT) === IFLNK |
123 |
| - let isDir = (mode & IFMT) === IFDIR |
| 76 | + async extractEntry (entry) { |
| 77 | + /* istanbul ignore if */ |
| 78 | + if (this.canceled) { |
| 79 | + debug('skipping entry extraction', entry.fileName, { cancelled: this.canceled }) |
| 80 | + return |
| 81 | + } |
| 82 | + |
| 83 | + if (this.opts.onEntry) { |
| 84 | + this.opts.onEntry(entry, this.zipfile) |
| 85 | + } |
| 86 | + |
| 87 | + const dest = path.join(this.opts.dir, entry.fileName) |
| 88 | + |
| 89 | + // convert external file attr int into a fs stat mode int |
| 90 | + const mode = (entry.externalFileAttributes >> 16) & 0xFFFF |
| 91 | + // check if it's a symlink or dir (using stat mode constants) |
| 92 | + const IFMT = 61440 |
| 93 | + const IFDIR = 16384 |
| 94 | + const IFLNK = 40960 |
| 95 | + const symlink = (mode & IFMT) === IFLNK |
| 96 | + let isDir = (mode & IFMT) === IFDIR |
| 97 | + |
| 98 | + // Failsafe, borrowed from jsZip |
| 99 | + if (!isDir && entry.fileName.endsWith('/')) { |
| 100 | + isDir = true |
| 101 | + } |
| 102 | + |
| 103 | + // check for windows weird way of specifying a directory |
| 104 | + // https://github.com/maxogden/extract-zip/issues/13#issuecomment-154494566 |
| 105 | + const madeBy = entry.versionMadeBy >> 8 |
| 106 | + if (!isDir) isDir = (madeBy === 0 && entry.externalFileAttributes === 16) |
| 107 | + |
| 108 | + debug('extracting entry', { filename: entry.fileName, isDir: isDir, isSymlink: symlink }) |
| 109 | + |
| 110 | + // reverse umask first (~) |
| 111 | + const umask = ~process.umask() |
| 112 | + // & with processes umask to override invalid perms |
| 113 | + const procMode = this.getExtractedMode(mode, isDir) & umask |
| 114 | + |
| 115 | + // always ensure folders are created |
| 116 | + const destDir = isDir ? dest : path.dirname(dest) |
| 117 | + |
| 118 | + const mkdirOptions = { recursive: true } |
| 119 | + if (isDir) { |
| 120 | + mkdirOptions.mode = procMode |
| 121 | + } |
| 122 | + debug('mkdir', { dir: destDir, ...mkdirOptions }) |
| 123 | + await fs.mkdir(destDir, mkdirOptions) |
| 124 | + if (isDir) return |
| 125 | + |
| 126 | + debug('opening read stream', dest) |
| 127 | + const readStream = await promisify(this.zipfile.openReadStream.bind(this.zipfile))(entry) |
| 128 | + |
| 129 | + if (symlink) { |
| 130 | + const link = await getStream(readStream) |
| 131 | + debug('creating symlink', link, dest) |
| 132 | + await fs.symlink(link, dest) |
| 133 | + } else { |
| 134 | + await pipeline(readStream, createWriteStream(dest, { mode: procMode })) |
| 135 | + } |
| 136 | + } |
124 | 137 |
|
125 |
| - // Failsafe, borrowed from jsZip |
126 |
| - if (!isDir && entry.fileName.slice(-1) === '/') { |
127 |
| - isDir = true |
| 138 | + getExtractedMode (entryMode, isDir) { |
| 139 | + let mode = entryMode |
| 140 | + // Set defaults, if necessary |
| 141 | + if (mode === 0) { |
| 142 | + if (isDir) { |
| 143 | + if (this.opts.defaultDirMode) { |
| 144 | + mode = parseInt(this.opts.defaultDirMode, 10) |
128 | 145 | }
|
129 | 146 |
|
130 |
| - // check for windows weird way of specifying a directory |
131 |
| - // https://github.com/maxogden/extract-zip/issues/13#issuecomment-154494566 |
132 |
| - const madeBy = entry.versionMadeBy >> 8 |
133 |
| - if (!isDir) isDir = (madeBy === 0 && entry.externalFileAttributes === 16) |
134 |
| - |
135 |
| - // if no mode then default to default modes |
136 |
| - if (mode === 0) { |
137 |
| - if (isDir) { |
138 |
| - if (opts.defaultDirMode) mode = parseInt(opts.defaultDirMode, 10) |
139 |
| - if (!mode) mode = 0o755 |
140 |
| - } else { |
141 |
| - if (opts.defaultFileMode) mode = parseInt(opts.defaultFileMode, 10) |
142 |
| - if (!mode) mode = 0o644 |
143 |
| - } |
| 147 | + if (!mode) { |
| 148 | + mode = 0o755 |
| 149 | + } |
| 150 | + } else { |
| 151 | + if (this.opts.defaultFileMode) { |
| 152 | + mode = parseInt(this.opts.defaultFileMode, 10) |
144 | 153 | }
|
145 | 154 |
|
146 |
| - debug('extracting entry', { filename: entry.fileName, isDir: isDir, isSymlink: symlink }) |
147 |
| - |
148 |
| - // reverse umask first (~) |
149 |
| - const umask = ~process.umask() |
150 |
| - // & with processes umask to override invalid perms |
151 |
| - const procMode = mode & umask |
| 155 | + if (!mode) { |
| 156 | + mode = 0o644 |
| 157 | + } |
| 158 | + } |
| 159 | + } |
152 | 160 |
|
153 |
| - // always ensure folders are created |
154 |
| - const destDir = isDir ? dest : path.dirname(dest) |
| 161 | + return mode |
| 162 | + } |
| 163 | +} |
155 | 164 |
|
156 |
| - debug('mkdirp', { dir: destDir }) |
157 |
| - fs.mkdir(destDir, { recursive: true }, function (err) { |
158 |
| - /* istanbul ignore if */ |
159 |
| - if (err) { |
160 |
| - debug('mkdirp error', destDir, { error: err }) |
161 |
| - cancelled = true |
162 |
| - return done(err) |
163 |
| - } |
| 165 | +module.exports = async function (zipPath, opts) { |
| 166 | + debug('creating target directory', opts.dir) |
164 | 167 |
|
165 |
| - if (isDir) return done() |
166 |
| - |
167 |
| - debug('opening read stream', dest) |
168 |
| - zipfile.openReadStream(entry, function (err, readStream) { |
169 |
| - /* istanbul ignore if */ |
170 |
| - if (err) { |
171 |
| - debug('openReadStream error', err) |
172 |
| - cancelled = true |
173 |
| - return done(err) |
174 |
| - } |
175 |
| - |
176 |
| - readStream.on('error', function (err) { |
177 |
| - /* istanbul ignore next */ |
178 |
| - console.log('read err', err) |
179 |
| - }) |
180 |
| - |
181 |
| - if (symlink) writeSymlink() |
182 |
| - else writeStream() |
183 |
| - |
184 |
| - function writeStream () { |
185 |
| - const writeStream = fs.createWriteStream(dest, { mode: procMode }) |
186 |
| - readStream.pipe(writeStream) |
187 |
| - |
188 |
| - writeStream.on('finish', function () { |
189 |
| - done() |
190 |
| - }) |
191 |
| - |
192 |
| - writeStream.on('error', /* istanbul ignore next */ function (err) { |
193 |
| - debug('write error', { error: err }) |
194 |
| - cancelled = true |
195 |
| - return done(err) |
196 |
| - }) |
197 |
| - } |
198 |
| - |
199 |
| - // AFAICT the content of the symlink file itself is the symlink target filename string |
200 |
| - function writeSymlink () { |
201 |
| - readStream.pipe(concat(function (data) { |
202 |
| - const link = data.toString() |
203 |
| - debug('creating symlink', link, dest) |
204 |
| - fs.symlink(link, dest, function (err) { |
205 |
| - if (err) cancelled = true |
206 |
| - done(err) |
207 |
| - }) |
208 |
| - })) |
209 |
| - } |
210 |
| - }) |
211 |
| - }) |
212 |
| - } |
213 |
| - }) |
| 168 | + if (!path.isAbsolute(opts.dir)) { |
| 169 | + throw new Error('Target directory is expected to be absolute') |
214 | 170 | }
|
| 171 | + |
| 172 | + await fs.mkdir(opts.dir, { recursive: true }) |
| 173 | + opts.dir = await fs.realpath(opts.dir) |
| 174 | + return new Extractor(zipPath, opts).extract() |
215 | 175 | }
|
0 commit comments