UNPKG

smac

Version:

Scriptcraft SMA Server controller

294 lines (272 loc) 10.2 kB
import extractZip from 'extract-zip' import * as fs from 'fs-extra' import { ErrorResult, Nothing, Result } from 'ghetto-monad' import os from 'os' import path from 'path' import progress from 'progress' import request from 'request' import requestProgress from 'request-progress' import rmrf from 'rimraf' import url from 'url' import util from 'util' import { WorldDefinition } from './SMAServerConfig' const gitHubIssuesUrl = 'https://github.com/Magikcraft/scriptcraft-sma/issues' export async function downloadZipFile( worldSpec: WorldDefinition, targetPath: string ) { const tmpPath = findSuitableTempDirectory(worldSpec.name) if (tmpPath.isNothing) { return tmpPath } const fileName = worldSpec.downloadUrl.split('/').pop() const downloadedFile = path.join(tmpPath.value, fileName || worldSpec.name) if (fs.existsSync(downloadedFile)) { console.log('Worlds already downloaded as', downloadedFile) } else { console.log(`Downloading world ${worldSpec.name}`) console.log('Downloading', worldSpec.downloadUrl) console.log('Saving to', downloadedFile) const download = await requestWorldZip( getRequestOptions(worldSpec), downloadedFile ) if (download.isNothing || download.isError) { return download } } const source = await extractDownload(downloadedFile) if (source.isError || source.isNothing) { return source } const worldPath = await copyIntoPlace(source.value, targetPath) return worldPath } function findSuitableTempDirectory(worldName: string) { var now = Date.now() var candidateTmpDirs = [ process.env.npm_config_tmp, os.tmpdir(), path.join(process.cwd(), 'tmp'), ] for (var i = 0; i < candidateTmpDirs.length; i++) { var candidatePath = candidateTmpDirs[i] if (!candidatePath) continue try { candidatePath = path.join(path.resolve(candidatePath), worldName) //@ts-ignore fs.mkdirsSync(candidatePath, '0777') // Make double sure we have 0777 permissions; some operating systems // default umask does not allow write by default. fs.chmodSync(candidatePath, '0777') var testFile = path.join(candidatePath, now + '.tmp') fs.writeFileSync(testFile, 'test') fs.unlinkSync(testFile) return new Result(candidatePath) } catch (e) { console.log(candidatePath, 'is not writable:', e.message) } } console.error( 'Can not find a writable tmp directory, please report issue ' + `on ${gitHubIssuesUrl} with as much ` + 'information as possible.' ) return new Nothing() } async function requestWorldZip( requestOptions, filePath ): Promise<Result<string> | ErrorResult<Error> | Nothing> { return new Promise(resolve => { const writePath = filePath + '-download-' + Date.now() console.log('Receiving...') var bar = null as any requestProgress( request(requestOptions, (error, response, body) => { console.log('') if (!error && response.statusCode === 200) { fs.writeFileSync(writePath, body) console.log( 'Received ' + Math.floor(body.length / 1024) + 'K total.' ) fs.renameSync(writePath, filePath) resolve(new Result(filePath)) } else if (response) { console.error( 'Error requesting archive.\n' + 'Status: ' + response.statusCode + '\n' + 'Request options: ' + JSON.stringify(requestOptions, null, 2) + '\n' + 'Response headers: ' + JSON.stringify(response.headers, null, 2) + '\n' + 'Make sure your network and proxy settings are correct.\n\n' + 'If you continue to have issues, please report this full log at ' + gitHubIssuesUrl ) resolve(new Nothing()) } else { resolve(handleRequestError(error)) } }) ) .on('progress', function(state) { try { if (!bar) { bar = new progress(' [:bar] :percent', { total: state.size.total, width: 40, }) } bar.curr = state.size.transferred bar.tick() } catch (e) { // It doesn't really matter if the progress bar doesn't update. } }) .on('error', handleRequestError) }) } export function getRequestOptions(worldSpec) { let strictSSL = !!process.env.npm_config_strict_ssl if (process.version == 'v0.10.34') { console.log( 'Node v0.10.34 detected, turning off strict ssl due to https://github.com/joyent/node/issues/8894' ) strictSSL = false } const options = { uri: worldSpec.downloadUrl, encoding: null, // Get response as a buffer followRedirect: true, // The default download path redirects to a CDN URL. headers: {}, strictSSL: strictSSL, } as any const proxyUrl = process.env.npm_config_https_proxy || process.env.npm_config_http_proxy || process.env.npm_config_proxy if (proxyUrl) { // Print using proxy var proxy = url.parse(proxyUrl) if (proxy.auth) { // Mask password proxy.auth = proxy.auth.replace(/:.*$/, ':******') } console.log('Using proxy ' + url.format(proxy)) // Enable proxy options.proxy = proxyUrl } // Use the user-agent string from the npm config options.headers['User-Agent'] = process.env.npm_config_user_agent // Use certificate authority settings from npm let ca = process.env.npm_config_ca as any if (!ca && process.env.npm_config_cafile) { try { ca = fs .readFileSync(process.env.npm_config_cafile, { encoding: 'utf8', }) .split(/\n(?=-----BEGIN CERTIFICATE-----)/g) // Comments at the beginning of the file result in the first // item not containing a certificate - in this case the // download will fail if (ca!.length > 0 && !/-----BEGIN CERTIFICATE-----/.test(ca[0])) { ca.shift() } } catch (e) { console.error( 'Could not read cafile', process.env.npm_config_cafile, e ) } } if (ca) { console.log('Using npmconf ca') options.agentOptions = { ca: ca, } options.ca = ca } return options } export function extractDownload( filePath ): Promise<ErrorResult<Error> | Result<string>> { return new Promise((resolve, reject) => { // extract to a unique directory in case multiple processes are // installing and extracting at once const extractedPath = filePath + '-extract-' + Date.now() var options = { cwd: extractedPath } // @ts-ignore fs.mkdirsSync(extractedPath, '0777') // Make double sure we have 0777 permissions; some operating systems // default umask does not allow write by default. fs.chmodSync(extractedPath, '0777') if (filePath.substr(-4) === '.zip') { console.log('Extracting zip contents') extractZip(path.resolve(filePath), { dir: extractedPath }, function( err ) { if (err) { console.error('Error extracting zip') resolve(new ErrorResult(err)) } else { resolve(new Result(extractedPath)) } }) } }) } function handleRequestError(error) { if ( error && error.stack && error.stack.indexOf('SELF_SIGNED_CERT_IN_CHAIN') != -1 ) { console.error( 'Error making request, SELF_SIGNED_CERT_IN_CHAIN. ' + 'Please read https://github.com/Medium/phantomjs#i-am-behind-a-corporate-proxy-that-uses-self-signed-ssl-certificates-to-intercept-encrypted-traffic' ) return new ErrorResult(new Error('SSL Error during download')) } else if (error) { console.error( 'Error making request.\n' + error.stack + '\n\n' + `Please report this full log at ${gitHubIssuesUrl}` ) return new ErrorResult(new Error()) } else { console.error( 'Something unexpected happened, please report this full ' + `log at ${gitHubIssuesUrl}` ) return new ErrorResult(new Error()) } } async function copyIntoPlace(extractedPath: string, targetPath: string) { const rm = util.promisify(rmrf) console.log('Removing', targetPath) try { await rm(targetPath) // Look for the extracted directory, so we can rename it. console.log(`Copying extracted worlds to ${targetPath}`) await fs.move(extractedPath, targetPath, { overwrite: true, }) } catch (error) { console.log('Error copying ' + extractedPath + ' to ' + targetPath) console.log(error) return new ErrorResult(error) } console.log('Copied worlds to ' + targetPath) return new Result(targetPath) }