UrlCache.js 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. /* eslint-disable
  2. no-return-assign,
  3. */
  4. // TODO: This file was created by bulk-decaffeinate.
  5. // Fix any style issues and re-enable lint.
  6. /*
  7. * decaffeinate suggestions:
  8. * DS101: Remove unnecessary use of Array.from
  9. * DS102: Remove unnecessary code created because of implicit returns
  10. * DS207: Consider shorter variations of null checks
  11. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  12. */
  13. const UrlFetcher = require('./UrlFetcher')
  14. const Settings = require('@overleaf/settings')
  15. const fs = require('fs')
  16. const Path = require('path')
  17. const { callbackify } = require('util')
  18. const Metrics = require('./Metrics')
  19. const PENDING_DOWNLOADS = new Map()
  20. function getProjectDir(projectId) {
  21. return Path.join(Settings.path.clsiCacheDir, projectId)
  22. }
  23. function getCachePath(projectId, url, lastModified) {
  24. // The url is a filestore URL.
  25. // It is sufficient to look at the path and mtime for uniqueness.
  26. const mtime = (lastModified && lastModified.getTime()) || 0
  27. const key = new URL(url).pathname.replace(/\//g, '-') + '-' + mtime
  28. return Path.join(getProjectDir(projectId), key)
  29. }
  30. async function clearProject(projectId, options) {
  31. const timer = new Metrics.Timer('url_cache', {
  32. status: options?.reason || 'unknown',
  33. path: 'delete',
  34. })
  35. await fs.promises.rm(getProjectDir(projectId), {
  36. force: true,
  37. recursive: true,
  38. })
  39. timer.done()
  40. }
  41. async function createProjectDir(projectId) {
  42. await fs.promises.mkdir(getProjectDir(projectId), { recursive: true })
  43. }
  44. async function downloadUrlToFile(projectId, url, destPath, lastModified) {
  45. const cachePath = getCachePath(projectId, url, lastModified)
  46. try {
  47. const timer = new Metrics.Timer('url_cache', {
  48. status: 'cache-hit',
  49. path: 'copy',
  50. })
  51. await fs.promises.copyFile(cachePath, destPath)
  52. // the metric is only updated if the file is present in the cache
  53. timer.done()
  54. return
  55. } catch (e) {
  56. if (e.code !== 'ENOENT') {
  57. throw e
  58. }
  59. }
  60. // time the download
  61. {
  62. const timer = new Metrics.Timer('url_cache', {
  63. status: 'cache-miss',
  64. path: 'download',
  65. })
  66. try {
  67. await download(url, cachePath)
  68. } finally {
  69. timer.done()
  70. }
  71. }
  72. // time the file copy
  73. {
  74. const timer = new Metrics.Timer('url_cache', {
  75. status: 'cache-miss',
  76. path: 'copy',
  77. })
  78. await fs.promises.copyFile(cachePath, destPath)
  79. timer.done()
  80. }
  81. }
  82. async function download(url, cachePath) {
  83. let pending = PENDING_DOWNLOADS.get(cachePath)
  84. if (pending) {
  85. return pending
  86. }
  87. pending = UrlFetcher.promises.pipeUrlToFileWithRetry(url, cachePath)
  88. PENDING_DOWNLOADS.set(cachePath, pending)
  89. try {
  90. await pending
  91. } finally {
  92. PENDING_DOWNLOADS.delete(cachePath)
  93. }
  94. }
  95. module.exports = {
  96. clearProject: callbackify(clearProject),
  97. createProjectDir: callbackify(createProjectDir),
  98. downloadUrlToFile: callbackify(downloadUrlToFile),
  99. promises: {
  100. clearProject,
  101. createProjectDir,
  102. downloadUrlToFile,
  103. },
  104. }