ClsiCacheHandler.js 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. const {
  2. fetchNothing,
  3. fetchRedirectWithResponse,
  4. RequestFailedError,
  5. } = require('@overleaf/fetch-utils')
  6. const logger = require('@overleaf/logger')
  7. const Settings = require('@overleaf/settings')
  8. const OError = require('@overleaf/o-error')
  9. const { NotFoundError, InvalidNameError } = require('../Errors/Errors')
  10. function validateFilename(filename) {
  11. if (
  12. ![
  13. 'output.blg',
  14. 'output.log',
  15. 'output.pdf',
  16. 'output.overleaf.json',
  17. 'output.tar.gz',
  18. ].includes(filename) ||
  19. filename.endsWith('.blg')
  20. ) {
  21. throw new InvalidNameError('bad filename')
  22. }
  23. }
  24. async function clearCache(projectId, userId) {
  25. let path = `/project/${projectId}`
  26. if (userId) {
  27. path += `/user/${userId}`
  28. }
  29. path += '/output'
  30. await Promise.all(
  31. Settings.apis.clsiCache.instances.map(async ({ url, zone }) => {
  32. const u = new URL(url)
  33. u.pathname = path
  34. try {
  35. await fetchNothing(u, {
  36. method: 'DELETE',
  37. signal: AbortSignal.timeout(15_000),
  38. })
  39. } catch (err) {
  40. throw OError.tag(err, 'clear clsi-cache', { url, zone })
  41. }
  42. })
  43. )
  44. }
  45. async function getLatestOutputFile(
  46. projectId,
  47. userId,
  48. filename,
  49. signal = AbortSignal.timeout(15_000)
  50. ) {
  51. validateFilename(filename)
  52. let path = `/project/${projectId}`
  53. if (userId) {
  54. path += `/user/${userId}`
  55. }
  56. path += `/latest/output/${filename}`
  57. for (const { url, zone } of Settings.apis.clsiCache.instances) {
  58. const u = new URL(url)
  59. u.pathname = path
  60. try {
  61. const {
  62. location,
  63. response: { headers },
  64. } = await fetchRedirectWithResponse(u, {
  65. signal,
  66. })
  67. // Success, return the cache entry.
  68. return {
  69. location,
  70. zone: headers.get('X-Zone'),
  71. lastModified: new Date(headers.get('X-Last-Modified')),
  72. size: parseInt(headers.get('X-Content-Length'), 10),
  73. allFiles: JSON.parse(headers.get('X-All-Files')),
  74. }
  75. } catch (err) {
  76. if (err instanceof RequestFailedError && err.response.status === 404) {
  77. break // No clsi-cache instance has cached something for this project/user.
  78. }
  79. logger.warn(
  80. { err, projectId, userId, url, zone },
  81. 'getLatestOutputFile from clsi-cache failed'
  82. )
  83. // This clsi-cache instance is down, try the next backend.
  84. }
  85. }
  86. throw new NotFoundError('nothing cached yet')
  87. }
  88. async function prepareCacheSource(
  89. projectId,
  90. userId,
  91. { sourceProjectId, templateId, templateVersionId, lastUpdated, zone, signal }
  92. ) {
  93. const url = new URL(
  94. `/project/${projectId}/user/${userId}/import-from`,
  95. Settings.apis.clsiCache.instances.find(i => i.zone === zone).url
  96. )
  97. try {
  98. await fetchNothing(url, {
  99. method: 'POST',
  100. json: {
  101. sourceProjectId,
  102. lastUpdated,
  103. templateId,
  104. templateVersionId,
  105. },
  106. signal,
  107. })
  108. } catch (err) {
  109. if (err instanceof RequestFailedError && err.response.status === 404) {
  110. throw new NotFoundError()
  111. }
  112. throw err
  113. }
  114. }
  115. module.exports = {
  116. clearCache,
  117. getLatestOutputFile,
  118. prepareCacheSource,
  119. }