UrlFetcher.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. /* eslint-disable
  2. handle-callback-err,
  3. no-return-assign,
  4. no-unused-vars,
  5. node/no-deprecated-api,
  6. */
  7. // TODO: This file was created by bulk-decaffeinate.
  8. // Fix any style issues and re-enable lint.
  9. /*
  10. * decaffeinate suggestions:
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * DS207: Consider shorter variations of null checks
  13. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  14. */
  15. let UrlFetcher
  16. const request = require('request').defaults({ jar: false })
  17. const fs = require('fs')
  18. const logger = require('logger-sharelatex')
  19. const settings = require('@overleaf/settings')
  20. const URL = require('url')
  21. const async = require('async')
  22. const { promisify } = require('util')
  23. const oneMinute = 60 * 1000
  24. module.exports = UrlFetcher = {
  25. pipeUrlToFileWithRetry(url, filePath, callback) {
  26. const doDownload = function (cb) {
  27. UrlFetcher.pipeUrlToFile(url, filePath, cb)
  28. }
  29. async.retry(3, doDownload, callback)
  30. },
  31. pipeUrlToFile(url, filePath, _callback) {
  32. if (_callback == null) {
  33. _callback = function (error) {}
  34. }
  35. const callbackOnce = function (error) {
  36. if (timeoutHandler != null) {
  37. clearTimeout(timeoutHandler)
  38. }
  39. _callback(error)
  40. return (_callback = function () {})
  41. }
  42. const u = URL.parse(url)
  43. if (
  44. settings.filestoreDomainOveride &&
  45. u.host !== settings.apis.clsiPerf.host
  46. ) {
  47. url = `${settings.filestoreDomainOveride}${u.path}`
  48. }
  49. var timeoutHandler = setTimeout(
  50. function () {
  51. timeoutHandler = null
  52. logger.error({ url, filePath }, 'Timed out downloading file to cache')
  53. return callbackOnce(
  54. new Error(`Timed out downloading file to cache ${url}`)
  55. )
  56. },
  57. // FIXME: maybe need to close fileStream here
  58. 3 * oneMinute
  59. )
  60. logger.log({ url, filePath }, 'started downloading url to cache')
  61. const urlStream = request.get({ url, timeout: oneMinute })
  62. urlStream.pause() // stop data flowing until we are ready
  63. // attach handlers before setting up pipes
  64. urlStream.on('error', function (error) {
  65. logger.error({ err: error, url, filePath }, 'error downloading url')
  66. return callbackOnce(
  67. error || new Error(`Something went wrong downloading the URL ${url}`)
  68. )
  69. })
  70. urlStream.on('end', () =>
  71. logger.log({ url, filePath }, 'finished downloading file into cache')
  72. )
  73. return urlStream.on('response', function (res) {
  74. if (res.statusCode >= 200 && res.statusCode < 300) {
  75. const atomicWrite = filePath + '~'
  76. const fileStream = fs.createWriteStream(atomicWrite)
  77. // attach handlers before setting up pipes
  78. fileStream.on('error', function (error) {
  79. logger.error(
  80. { err: error, url, filePath },
  81. 'error writing file into cache'
  82. )
  83. return fs.unlink(atomicWrite, function (err) {
  84. if (err != null) {
  85. logger.err({ err, filePath }, 'error deleting file from cache')
  86. }
  87. return callbackOnce(error)
  88. })
  89. })
  90. fileStream.on('finish', function () {
  91. logger.log({ url, filePath }, 'finished writing file into cache')
  92. fs.rename(atomicWrite, filePath, error => {
  93. if (error) {
  94. fs.unlink(atomicWrite, () => callbackOnce(error))
  95. } else {
  96. callbackOnce()
  97. }
  98. })
  99. })
  100. fileStream.on('pipe', () =>
  101. logger.log({ url, filePath }, 'piping into filestream')
  102. )
  103. urlStream.pipe(fileStream)
  104. return urlStream.resume() // now we are ready to handle the data
  105. } else {
  106. logger.error(
  107. { statusCode: res.statusCode, url, filePath },
  108. 'unexpected status code downloading url to cache'
  109. )
  110. // https://nodejs.org/api/http.html#http_class_http_clientrequest
  111. // If you add a 'response' event handler, then you must consume
  112. // the data from the response object, either by calling
  113. // response.read() whenever there is a 'readable' event, or by
  114. // adding a 'data' handler, or by calling the .resume()
  115. // method. Until the data is consumed, the 'end' event will not
  116. // fire. Also, until the data is read it will consume memory
  117. // that can eventually lead to a 'process out of memory' error.
  118. urlStream.resume() // discard the data
  119. return callbackOnce(
  120. new Error(
  121. `URL returned non-success status code: ${res.statusCode} ${url}`
  122. )
  123. )
  124. }
  125. })
  126. },
  127. }
  128. module.exports.promises = {
  129. pipeUrlToFileWithRetry: promisify(UrlFetcher.pipeUrlToFileWithRetry),
  130. }