smac
Version:
Scriptcraft SMA Server controller
294 lines (272 loc) • 10.2 kB
text/typescript
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)
}