UrlFetcher.js 1.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
  1. const fs = require('fs')
  2. const logger = require('@overleaf/logger')
  3. const Settings = require('@overleaf/settings')
  4. const { fetchStream } = require('@overleaf/fetch-utils')
  5. const { URL } = require('url')
  6. const { pipeline } = require('stream/promises')
  7. const Metrics = require('./Metrics')
  8. async function pipeUrlToFileWithRetry(url, filePath) {
  9. let remainingAttempts = 3
  10. let lastErr
  11. while (remainingAttempts-- > 0) {
  12. const timer = new Metrics.Timer('url_fetcher', {
  13. path: lastErr ? ' retry' : 'fetch',
  14. })
  15. try {
  16. await pipeUrlToFile(url, filePath)
  17. timer.done({ status: 'success' })
  18. return
  19. } catch (err) {
  20. timer.done({ status: 'error' })
  21. logger.warn(
  22. { err, url, filePath, remainingAttempts },
  23. 'error downloading url'
  24. )
  25. lastErr = err
  26. }
  27. }
  28. throw lastErr
  29. }
  30. async function pipeUrlToFile(url, filePath) {
  31. const u = new URL(url)
  32. if (
  33. Settings.filestoreDomainOveride &&
  34. u.host !== Settings.apis.clsiPerf.host
  35. ) {
  36. url = `${Settings.filestoreDomainOveride}${u.pathname}${u.search}`
  37. }
  38. const stream = await fetchStream(url, {
  39. signal: AbortSignal.timeout(60 * 1000),
  40. })
  41. const atomicWrite = filePath + '~'
  42. try {
  43. const output = fs.createWriteStream(atomicWrite)
  44. await pipeline(stream, output)
  45. await fs.promises.rename(atomicWrite, filePath)
  46. Metrics.count('UrlFetcher.downloaded_bytes', output.bytesWritten)
  47. } catch (err) {
  48. try {
  49. await fs.promises.unlink(atomicWrite)
  50. } catch (e) {}
  51. throw err
  52. }
  53. }
  54. module.exports.promises = {
  55. pipeUrlToFileWithRetry,
  56. }