flightcheck
Version:
A simple command line checklist.
304 lines (256 loc) • 9.38 kB
JavaScript
;
const CRATE_NAME="flightcheck"
const path = require('path');
const fs = require('fs');
const os = require('os');
const https = require('https');
const util = require('util');
const url = require('url');
const child_process = require('child_process');
const packageVersion = require('../package.json').version;
const tmpDir = path.join(os.tmpdir(), `${CRATE_NAME}-cache-${packageVersion}`);
const fsUnlink = util.promisify(fs.unlink);
const fsExists = util.promisify(fs.exists);
const fsMkdir = util.promisify(fs.mkdir);
const isWindows = os.platform() === 'win32';
const REPO = 'RossmacD/FlightCheck';
function isGithubUrl(_url) {
return url.parse(_url).hostname === 'api.github.com';
}
function downloadWin(url, dest, opts) {
return new Promise((resolve, reject) => {
let userAgent;
if (opts.headers['user-agent']) {
userAgent = opts.headers['user-agent'];
delete opts.headers['user-agent'];
}
const headerValues = Object.keys(opts.headers)
.map(key => `\\"${key}\\"=\\"${opts.headers[key]}\\"`)
.join('; ');
const headers = `@{${headerValues}}`;
console.log('Downloading with Invoke-WebRequest');
let iwrCmd = `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -URI ${url} -UseBasicParsing -OutFile ${dest} -Headers ${headers}`;
if (userAgent) {
iwrCmd += ' -UserAgent ' + userAgent;
}
iwrCmd = `powershell "${iwrCmd}"`;
child_process.exec(iwrCmd, err => {
if (err) {
reject(err);
return;
}
resolve();
});
});
}
function download(_url, dest, opts) {
// const proxy = proxy_from_env.getProxyForUrl(url.parse(_url));
// if (proxy !== '') {
// var HttpsProxyAgent = require('https-proxy-agent');
// opts = {
// ...opts,
// "agent": new HttpsProxyAgent(proxy)
// };
// }
if (isWindows) {
// This alternative strategy shouldn't be necessary but sometimes on Windows the file does not get closed,
// so unzipping it fails, and I don't know why.
return downloadWin(_url, dest, opts);
}
if (opts.headers && opts.headers.authorization && !isGithubUrl(_url)) {
delete opts.headers.authorization;
}
return new Promise((resolve, reject) => {
console.log(`Download options: ${JSON.stringify(opts)}`);
const outFile = fs.createWriteStream(dest);
const mergedOpts = {
...url.parse(_url),
...opts
};
https.get(mergedOpts, response => {
console.log('statusCode: ' + response.statusCode);
if (response.statusCode === 302) {
console.log('Following redirect to: ' + response.headers.location);
return download(response.headers.location, dest, opts)
.then(resolve, reject);
} else if (response.statusCode !== 200) {
reject(new Error('Download failed with ' + response.statusCode));
return;
}
response.pipe(outFile);
outFile.on('finish', () => {
resolve();
});
}).on('error', async err => {
await fsUnlink(dest);
reject(err);
});
});
}
function get(_url, opts) {
console.log(`GET ${_url}`);
// const proxy = proxy_from_env.getProxyForUrl(url.parse(_url));
// if (proxy !== '') {
// var HttpsProxyAgent = require('https-proxy-agent');
// opts = {
// ...opts,
// "agent": new HttpsProxyAgent(proxy)
// };
// }
return new Promise((resolve, reject) => {
let result = '';
opts = {
...url.parse(_url),
...opts
};
https.get(opts, response => {
if (response.statusCode !== 200) {
reject(new Error('Request failed: ' + response.statusCode));
}
response.on('data', d => {
result += d.toString();
});
response.on('end', () => {
resolve(result);
});
response.on('error', e => {
reject(e);
});
});
});
}
function getApiUrl(repo, tag) {
return `https://api.github.com/repos/${repo}/releases/tags/${tag}`;
}
/**
* @param {{ force: boolean; token: string; version: string; }} opts
* @param {string} assetName
* @param {string} downloadFolder
*/
async function getAssetFromGithub(opts, assetName, downloadFolder) {
const assetDownloadPath = path.join(downloadFolder, assetName);
// We can just use the cached binary
if (!opts.force && await fsExists(assetDownloadPath)) {
console.log('Using cached download: ' + assetDownloadPath);
return assetDownloadPath;
}
const downloadOpts = {
headers: {
'user-agent': `${CRATE_NAME}`
}
};
// if (opts.token) {
// downloadOpts.headers.authorization = `token ${opts.token}`;
// }
console.log(`Finding release for ${opts.version}`);
const release = await get(getApiUrl(REPO, opts.version), downloadOpts);
let jsonRelease;
try {
jsonRelease = JSON.parse(release);
} catch (e) {
throw new Error('Malformed API response: ' + e.stack);
}
if (!jsonRelease.assets) {
throw new Error('Bad API response: ' + JSON.stringify(release));
}
const asset = jsonRelease.assets.find(a => a.name === assetName);
if (!asset) {
throw new Error('Asset not found with name: ' + assetName);
}
console.log(`Downloading from ${asset.url}`);
console.log(`Downloading to ${assetDownloadPath}`);
downloadOpts.headers.accept = 'application/octet-stream';
await download(asset.url, assetDownloadPath, downloadOpts);
}
function unzipWindows(zipPath, destinationDir) {
return new Promise((resolve, reject) => {
zipPath = sanitizePathForPowershell(zipPath);
destinationDir = sanitizePathForPowershell(destinationDir);
const expandCmd = 'powershell -ExecutionPolicy Bypass -Command Expand-Archive ' + ['-Path', zipPath, '-DestinationPath', destinationDir, '-Force'].join(' ');
child_process.exec(expandCmd, (err, _stdout, stderr) => {
if (err) {
reject(err);
return;
}
if (stderr) {
console.log(stderr);
reject(new Error(stderr));
return;
}
console.log('Expand-Archive completed');
resolve();
});
});
}
// Handle whitespace in filepath as powershell split's path with whitespaces
function sanitizePathForPowershell(path) {
path = path.replace(/ /g, '` '); // replace whitespace with "` " as solution provided here https://stackoverflow.com/a/18537344/7374562
return path;
}
function untar(zipPath, destinationDir) {
return new Promise((resolve, reject) => {
const unzipProc = child_process.spawn('tar', ['xvf', zipPath, '-C', destinationDir], { stdio: 'inherit' });
unzipProc.on('error', err => {
reject(err);
});
unzipProc.on('close', code => {
console.log(`tar xvf exited with ${code}`);
if (code !== 0) {
reject(new Error(`tar xvf exited with ${code}`));
return;
}
resolve();
});
});
}
async function unzipBinary(zipPath, destinationDir) {
if (isWindows) {
await unzipWindows(zipPath, destinationDir);
} else {
await untar(zipPath, destinationDir);
}
const expectedName = path.join(destinationDir, `${CRATE_NAME}`);
if (await fsExists(expectedName)) {
return expectedName;
}
if (await fsExists(expectedName + '.exe')) {
return expectedName + '.exe';
}
throw new Error(`Expecting ${CRATE_NAME} or ${CRATE_NAME}.exe unzipped into ${destinationDir}, didn't find one.`);
}
module.exports = async opts => {
if (!opts.version) {
return Promise.reject(new Error('Missing version'));
}
if (!opts.target) {
return Promise.reject(new Error('Missing target'));
}
const extension = isWindows ? '.zip' : '.tar.gz';
const assetName = [`${CRATE_NAME}`, opts.version, opts.target].join('-') + extension;
if (!await fsExists(tmpDir)) {
await fsMkdir(tmpDir);
}
const assetDownloadPath = path.join(tmpDir, assetName);
try {
await getAssetFromGithub(opts, assetName, tmpDir)
} catch (e) {
console.log('Deleting invalid download cache');
try {
await fsUnlink(assetDownloadPath);
} catch (e) {}
throw e;
}
console.log(`Unzipping to ${opts.destDir} from ${assetDownloadPath}`);
try {
const destinationPath = await unzipBinary(assetDownloadPath, opts.destDir);
if (!isWindows) {
await util.promisify(fs.chmod)(destinationPath, '755');
}
} catch (e) {
console.log('Deleting invalid download');
try {
await fsUnlink(assetDownloadPath);
} catch (e) {}
throw e;
}
};