crazy4groovy
1/20/2018 - 4:22 AM

NodeJS module for downloading a (http) URI.

NodeJS module for downloading a (http) URI.

/* similar functionality to `simple-get`: https://github.com/feross/simple-get/pull/41 */

const fs = require('fs')
const path = require('path')
const urlParser = require('url').parse
const client = {
  'http:': require('http'),
  'https:': require('https')
}
// const decompressResponse = require('decompress-response') // npm lib


function HttpStatusError(message, removeFile) {
  this.message = message
  this.removeFile = removeFile
  this.stack = new Error().stack
}
HttpStatusError.prototype = Object.create(Error.prototype)
HttpStatusError.prototype.constructor = HttpStatusError


/* inspired by `mkdirp.sync`: https://github.com/substack/node-mkdirp/blob/master/index.js#L55 */
const _0777 = parseInt('0777', 8) & (~process.umask())
function mkdirs(dirs) {
  try {
    fs.mkdirSync(dirs, _0777)
  } catch (err0) {
    if (err0.code === 'EEXIST') return
    if (err0.code === 'ENOENT') {
      mkdirs(path.dirname(dirs)) // chop off last dir, mkdir
      mkdirs(dirs) // all dirs, mkdir
      return
    }
    // sanity check
    try {
      if (!(fs.statSync(dirs).isDirectory())) throw err0
    } catch (ignore) {
      throw err0
    }
  }
}


async function httpGet(
  url /* : string */,
  fileDir /* : string */,
  fileName /* : string */,
  timeoutMs = (8 * 1000) /* : number */) /* : Promise<function> */ =>
{
  const { protocol } = urlParser(url) // validate url early on

  mkdirs(fileDir)
  const filePath = path.join(fileDir, fileName)
  if (fs.existsSync(filePath)) {
    throw new Error(`ERROR: Cannot download, file already exists: ${filePath}`)
  }

  const fileWriteStream = fs.createWriteStream(filePath)
  const removeFile = (closeStream = true) => { // this is what gets resolved/rejected
    closeStream && fs.closeSync(fs.openSync(filePath, 'r')) // helpful steps for Windows
    fs.unlinkSync(filePath)
  }

  return new Promise((resolve, reject) => {
    let statusOK = false
    const handleFileClose = () => {
      if (statusOK) resolve(removeFile)
      else reject(new HttpStatusError('HTTP response: error status', removeFile))
    }
    const req = client[protocol]
      .get(url, response => {
        // response = decompressResponse(response)
        response.pipe(fileWriteStream)
        fileWriteStream.on('finish', () => fileWriteStream.close(handleFileClose)) // close() is async, calls cb after close completes
      })
      .on('response', incomingMessage => {
        statusOK = statusOK || (incomingMessage.statusCode < 400)
      })
      .on('error', err => {
        reject(new HttpStatusError(err.message, removeFile))
      })

    req.setTimeout(timeoutMs, () => reject(new HttpStatusError('HTTP response: timeout', removeFile)))
    req.end()
  })
}
httpGet.HttpStatusError = HttpStatusError
httpGet.mkdirs = mkdirs

module.exports = httpGet