ClsiCacheController.mjs 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import { NotFoundError, ResourceGoneError } from '../Errors/Errors.js'
  2. import {
  3. fetchStreamWithResponse,
  4. RequestFailedError,
  5. } from '@overleaf/fetch-utils'
  6. import Path from 'node:path'
  7. import { pipeline } from 'node:stream/promises'
  8. import logger from '@overleaf/logger'
  9. import ClsiCacheManager from './ClsiCacheManager.js'
  10. import CompileController from './CompileController.mjs'
  11. import { expressify } from '@overleaf/promise-utils'
  12. import ClsiCacheHandler from './ClsiCacheHandler.js'
  13. import ProjectGetter from '../Project/ProjectGetter.js'
  14. import { MeteredStream } from '@overleaf/stream-utils'
  15. import Metrics from '@overleaf/metrics'
  16. /**
  17. * Download a file from a specific build on the clsi-cache.
  18. *
  19. * @param req
  20. * @param res
  21. * @return {Promise<*>}
  22. */
  23. async function downloadFromCache(req, res) {
  24. const { Project_id: projectId, buildId, filename } = req.params
  25. const userId = CompileController._getUserIdForCompile(req)
  26. const signal = AbortSignal.timeout(60 * 1000)
  27. let location, projectName
  28. try {
  29. ;[{ location }, { name: projectName }] = await Promise.all([
  30. ClsiCacheHandler.getOutputFile(
  31. projectId,
  32. userId,
  33. buildId,
  34. filename,
  35. signal
  36. ),
  37. ProjectGetter.promises.getProject(projectId, { name: 1 }),
  38. ])
  39. } catch (err) {
  40. if (err instanceof NotFoundError) {
  41. // res.sendStatus() sends a description of the status as body.
  42. // Using res.status().end() avoids sending that fake body.
  43. return res.status(404).end()
  44. } else {
  45. throw err
  46. }
  47. }
  48. const { stream, response } = await fetchStreamWithResponse(location, {
  49. signal,
  50. })
  51. if (req.destroyed) {
  52. // The client has disconnected already, avoid trying to write into the broken connection.
  53. return
  54. }
  55. for (const key of ['Content-Length', 'Content-Type']) {
  56. if (response.headers.has(key)) res.setHeader(key, response.headers.get(key))
  57. }
  58. const ext = Path.extname(filename)
  59. res.attachment(
  60. ext === '.pdf'
  61. ? `${CompileController._getSafeProjectName({ name: projectName })}.pdf`
  62. : filename
  63. )
  64. try {
  65. res.writeHead(response.status)
  66. await pipeline(
  67. stream,
  68. new MeteredStream(Metrics, 'clsi_cache_egress', {
  69. path: ClsiCacheHandler.getEgressLabel(filename),
  70. }),
  71. res
  72. )
  73. } catch (err) {
  74. const reqAborted = Boolean(req.destroyed)
  75. const streamingStarted = Boolean(res.headersSent)
  76. if (!streamingStarted) {
  77. if (err instanceof RequestFailedError) {
  78. res.sendStatus(err.response.status)
  79. } else {
  80. res.sendStatus(500)
  81. }
  82. }
  83. if (
  84. streamingStarted &&
  85. reqAborted &&
  86. err.code === 'ERR_STREAM_PREMATURE_CLOSE'
  87. ) {
  88. // Ignore noisy spurious error
  89. return
  90. }
  91. logger.warn(
  92. {
  93. err,
  94. projectId,
  95. location,
  96. filename,
  97. reqAborted,
  98. streamingStarted,
  99. },
  100. 'CLSI-cache proxy error'
  101. )
  102. }
  103. }
  104. /**
  105. * Prepare a compile response from the clsi-cache.
  106. *
  107. * @param req
  108. * @param res
  109. * @return {Promise<void>}
  110. */
  111. async function getLatestBuildFromCache(req, res) {
  112. const { Project_id: projectId } = req.params
  113. const userId = CompileController._getUserIdForCompile(req)
  114. try {
  115. const {
  116. zone,
  117. outputFiles,
  118. compileGroup,
  119. clsiServerId,
  120. clsiCacheShard,
  121. options,
  122. stats,
  123. timings,
  124. } = await ClsiCacheManager.getLatestCompileResult(projectId, userId)
  125. let { pdfCachingMinChunkSize, pdfDownloadDomain } =
  126. await CompileController._getSplitTestOptions(req, res)
  127. pdfDownloadDomain += `/zone/${zone}`
  128. res.json({
  129. fromCache: true,
  130. status: 'success',
  131. outputFiles,
  132. compileGroup,
  133. clsiServerId,
  134. clsiCacheShard,
  135. pdfDownloadDomain,
  136. pdfCachingMinChunkSize,
  137. options,
  138. stats,
  139. timings,
  140. })
  141. } catch (err) {
  142. if (err instanceof NotFoundError) {
  143. res.sendStatus(404)
  144. } else if (err instanceof ResourceGoneError) {
  145. res.sendStatus(410)
  146. } else {
  147. throw err
  148. }
  149. }
  150. }
  151. export default {
  152. downloadFromCache: expressify(downloadFromCache),
  153. getLatestBuildFromCache: expressify(getLatestBuildFromCache),
  154. }