ClsiCacheController.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. const { NotFoundError } = require('../Errors/Errors')
  2. const {
  3. fetchStreamWithResponse,
  4. RequestFailedError,
  5. fetchJson,
  6. } = require('@overleaf/fetch-utils')
  7. const Path = require('path')
  8. const { pipeline } = require('stream/promises')
  9. const logger = require('@overleaf/logger')
  10. const ClsiCacheManager = require('./ClsiCacheManager')
  11. const CompileController = require('./CompileController')
  12. const { expressify } = require('@overleaf/promise-utils')
  13. const ClsiCacheHandler = require('./ClsiCacheHandler')
  14. const ProjectGetter = require('../Project/ProjectGetter')
  15. /**
  16. * Download a file from a specific build on the clsi-cache.
  17. *
  18. * @param req
  19. * @param res
  20. * @return {Promise<*>}
  21. */
  22. async function downloadFromCache(req, res) {
  23. const { Project_id: projectId, buildId, filename } = req.params
  24. const userId = CompileController._getUserIdForCompile(req)
  25. const signal = AbortSignal.timeout(60 * 1000)
  26. let location, projectName
  27. try {
  28. ;[{ location }, { name: projectName }] = await Promise.all([
  29. ClsiCacheHandler.getOutputFile(
  30. projectId,
  31. userId,
  32. buildId,
  33. filename,
  34. signal
  35. ),
  36. ProjectGetter.promises.getProject(projectId, { name: 1 }),
  37. ])
  38. } catch (err) {
  39. if (err instanceof NotFoundError) {
  40. // res.sendStatus() sends a description of the status as body.
  41. // Using res.status().end() avoids sending that fake body.
  42. return res.status(404).end()
  43. } else {
  44. throw err
  45. }
  46. }
  47. const { stream, response } = await fetchStreamWithResponse(location, {
  48. signal,
  49. })
  50. if (req.destroyed) {
  51. // The client has disconnected already, avoid trying to write into the broken connection.
  52. return
  53. }
  54. for (const key of ['Content-Length', 'Content-Type']) {
  55. if (response.headers.has(key)) res.setHeader(key, response.headers.get(key))
  56. }
  57. const ext = Path.extname(filename)
  58. res.attachment(
  59. ext === '.pdf'
  60. ? `${CompileController._getSafeProjectName({ name: projectName })}.pdf`
  61. : filename
  62. )
  63. try {
  64. res.writeHead(response.status)
  65. await pipeline(stream, res)
  66. } catch (err) {
  67. const reqAborted = Boolean(req.destroyed)
  68. const streamingStarted = Boolean(res.headersSent)
  69. if (!streamingStarted) {
  70. if (err instanceof RequestFailedError) {
  71. res.sendStatus(err.response.status)
  72. } else {
  73. res.sendStatus(500)
  74. }
  75. }
  76. if (
  77. streamingStarted &&
  78. reqAborted &&
  79. err.code === 'ERR_STREAM_PREMATURE_CLOSE'
  80. ) {
  81. // Ignore noisy spurious error
  82. return
  83. }
  84. logger.warn(
  85. {
  86. err,
  87. projectId,
  88. location,
  89. filename,
  90. reqAborted,
  91. streamingStarted,
  92. },
  93. 'CLSI-cache proxy error'
  94. )
  95. }
  96. }
  97. /**
  98. * Prepare a compile response from the clsi-cache.
  99. *
  100. * @param req
  101. * @param res
  102. * @return {Promise<void>}
  103. */
  104. async function getLatestBuildFromCache(req, res) {
  105. const { Project_id: projectId } = req.params
  106. const userId = CompileController._getUserIdForCompile(req)
  107. try {
  108. const {
  109. internal: { location: metaLocation },
  110. external: { isUpToDate, allFiles, zone, shard },
  111. } = await ClsiCacheManager.getLatestBuildFromCache(
  112. projectId,
  113. userId,
  114. 'output.overleaf.json'
  115. )
  116. if (!isUpToDate) return res.sendStatus(410)
  117. const meta = await fetchJson(metaLocation, {
  118. signal: AbortSignal.timeout(5 * 1000),
  119. })
  120. const [, editorId, buildId] = metaLocation.match(
  121. /\/build\/([a-f0-9-]+?)-([a-f0-9]+-[a-f0-9]+)\//
  122. )
  123. let baseURL = `/project/${projectId}`
  124. if (userId) {
  125. baseURL += `/user/${userId}`
  126. }
  127. const { ranges, contentId, clsiServerId, compileGroup, size, options } =
  128. meta
  129. const outputFiles = allFiles
  130. .filter(
  131. path => path !== 'output.overleaf.json' && path !== 'output.tar.gz'
  132. )
  133. .map(path => {
  134. const f = {
  135. url: `${baseURL}/build/${editorId}-${buildId}/output/${path}`,
  136. downloadURL: `/download/project/${projectId}/build/${editorId}-${buildId}/output/cached/${path}`,
  137. build: buildId,
  138. path,
  139. type: path.split('.').pop(),
  140. }
  141. if (path === 'output.pdf') {
  142. Object.assign(f, {
  143. size,
  144. editorId,
  145. })
  146. if (clsiServerId !== shard) {
  147. // Enable PDF caching and attempt to download from VM first.
  148. // (clsi VMs do not have the editorId in the path on disk, omit it).
  149. Object.assign(f, {
  150. url: `${baseURL}/build/${buildId}/output/output.pdf`,
  151. ranges,
  152. contentId,
  153. })
  154. }
  155. }
  156. return f
  157. })
  158. let { pdfCachingMinChunkSize, pdfDownloadDomain } =
  159. await CompileController._getSplitTestOptions(req, res)
  160. pdfDownloadDomain += `/zone/${zone}`
  161. res.json({
  162. fromCache: true,
  163. status: 'success',
  164. outputFiles,
  165. compileGroup,
  166. clsiServerId,
  167. clsiCacheShard: shard,
  168. pdfDownloadDomain,
  169. pdfCachingMinChunkSize,
  170. options,
  171. })
  172. } catch (err) {
  173. if (err instanceof NotFoundError) {
  174. res.sendStatus(404)
  175. } else {
  176. throw err
  177. }
  178. }
  179. }
  180. module.exports = {
  181. downloadFromCache: expressify(downloadFromCache),
  182. getLatestBuildFromCache: expressify(getLatestBuildFromCache),
  183. }