|
"use strict"; |
|
|
|
const _ = require('lodash'); |
|
const la = require('lazy-ass'); |
|
const is = require('check-more-types'); |
|
const cp = require('child_process'); |
|
const os = require('os'); |
|
const yauzl = require('yauzl'); |
|
const debug = require('debug')('cypress:cli:unzip'); |
|
const extract = require('extract-zip'); |
|
const Promise = require('bluebird'); |
|
const readline = require('readline'); |
|
const { |
|
throwFormErrorText, |
|
errors |
|
} = require('../errors'); |
|
const fs = require('../fs'); |
|
const util = require('../util'); |
|
const unzipTools = { |
|
extract |
|
}; |
|
|
|
|
|
const unzip = ({ |
|
zipFilePath, |
|
installDir, |
|
progress |
|
}) => { |
|
debug('unzipping from %s', zipFilePath); |
|
debug('into', installDir); |
|
if (!zipFilePath) { |
|
throw new Error('Missing zip filename'); |
|
} |
|
const startTime = Date.now(); |
|
let yauzlDoneTime = 0; |
|
return fs.ensureDirAsync(installDir).then(() => { |
|
return new Promise((resolve, reject) => { |
|
return yauzl.open(zipFilePath, (err, zipFile) => { |
|
yauzlDoneTime = Date.now(); |
|
if (err) { |
|
debug('error using yauzl %s', err.message); |
|
return reject(err); |
|
} |
|
const total = zipFile.entryCount; |
|
debug('zipFile entries count', total); |
|
const started = new Date(); |
|
let percent = 0; |
|
let count = 0; |
|
const notify = percent => { |
|
const elapsed = +new Date() - +started; |
|
const eta = util.calculateEta(percent, elapsed); |
|
progress.onProgress(percent, util.secsRemaining(eta)); |
|
}; |
|
const tick = () => { |
|
count += 1; |
|
percent = count / total * 100; |
|
const displayPercent = percent.toFixed(0); |
|
return notify(displayPercent); |
|
}; |
|
const unzipWithNode = () => { |
|
debug('unzipping with node.js (slow)'); |
|
const opts = { |
|
dir: installDir, |
|
onEntry: tick |
|
}; |
|
debug('calling Node extract tool %s %o', zipFilePath, opts); |
|
return unzipTools.extract(zipFilePath, opts).then(() => { |
|
debug('node unzip finished'); |
|
return resolve(); |
|
}).catch(err => { |
|
const error = err || new Error('Unknown error with Node extract tool'); |
|
debug('error %s', error.message); |
|
return reject(error); |
|
}); |
|
}; |
|
const unzipFallback = _.once(unzipWithNode); |
|
const unzipWithUnzipTool = () => { |
|
debug('unzipping via `unzip`'); |
|
const inflatingRe = /inflating:/; |
|
const sp = cp.spawn('unzip', ['-o', zipFilePath, '-d', installDir]); |
|
sp.on('error', err => { |
|
debug('unzip tool error: %s', err.message); |
|
unzipFallback(); |
|
}); |
|
sp.on('close', code => { |
|
debug('unzip tool close with code %d', code); |
|
if (code === 0) { |
|
percent = 100; |
|
notify(percent); |
|
return resolve(); |
|
} |
|
debug('`unzip` failed %o', { |
|
code |
|
}); |
|
return unzipFallback(); |
|
}); |
|
sp.stdout.on('data', data => { |
|
if (inflatingRe.test(data)) { |
|
return tick(); |
|
} |
|
}); |
|
sp.stderr.on('data', data => { |
|
debug('`unzip` stderr %s', data); |
|
}); |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
const unzipWithOsx = () => { |
|
debug('unzipping via `ditto`'); |
|
const copyingFileRe = /^copying file/; |
|
const sp = cp.spawn('ditto', ['-xkV', zipFilePath, installDir]); |
|
|
|
|
|
sp.on('error', err => { |
|
debug(err.message); |
|
unzipFallback(); |
|
}); |
|
sp.on('close', code => { |
|
if (code === 0) { |
|
|
|
|
|
percent = 100; |
|
notify(percent); |
|
return resolve(); |
|
} |
|
debug('`ditto` failed %o', { |
|
code |
|
}); |
|
return unzipFallback(); |
|
}); |
|
return readline.createInterface({ |
|
input: sp.stderr |
|
}).on('line', line => { |
|
if (copyingFileRe.test(line)) { |
|
return tick(); |
|
} |
|
}); |
|
}; |
|
switch (os.platform()) { |
|
case 'darwin': |
|
return unzipWithOsx(); |
|
case 'linux': |
|
return unzipWithUnzipTool(); |
|
case 'win32': |
|
return unzipWithNode(); |
|
default: |
|
return; |
|
} |
|
}); |
|
}).tap(() => { |
|
debug('unzip completed %o', { |
|
yauzlMs: yauzlDoneTime - startTime, |
|
unzipMs: Date.now() - yauzlDoneTime |
|
}); |
|
}); |
|
}); |
|
}; |
|
function isMaybeWindowsMaxPathLengthError(err) { |
|
return os.platform() === 'win32' && err.code === 'ENOENT' && err.syscall === 'realpath'; |
|
} |
|
const start = async ({ |
|
zipFilePath, |
|
installDir, |
|
progress |
|
}) => { |
|
la(is.unemptyString(installDir), 'missing installDir'); |
|
if (!progress) { |
|
progress = { |
|
onProgress: () => { |
|
return {}; |
|
} |
|
}; |
|
} |
|
try { |
|
const installDirExists = await fs.pathExists(installDir); |
|
if (installDirExists) { |
|
debug('removing existing unzipped binary', installDir); |
|
await fs.removeAsync(installDir); |
|
} |
|
await unzip({ |
|
zipFilePath, |
|
installDir, |
|
progress |
|
}); |
|
} catch (err) { |
|
const errorTemplate = isMaybeWindowsMaxPathLengthError(err) ? errors.failedUnzipWindowsMaxPathLength : errors.failedUnzip; |
|
await throwFormErrorText(errorTemplate)(err); |
|
} |
|
}; |
|
module.exports = { |
|
start, |
|
utils: { |
|
unzip, |
|
unzipTools |
|
} |
|
}; |