UrlFetcher.js 4.7 KB

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