| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- import { NotFoundError, ResourceGoneError } from '../Errors/Errors.js'
- import {
- fetchStreamWithResponse,
- RequestFailedError,
- } from '@overleaf/fetch-utils'
- import Path from 'node:path'
- import { pipeline } from 'node:stream/promises'
- import logger from '@overleaf/logger'
- import ClsiCacheManager from './ClsiCacheManager.mjs'
- import CompileController from './CompileController.mjs'
- import { expressify } from '@overleaf/promise-utils'
- import ClsiCacheHandler from './ClsiCacheHandler.mjs'
- import ProjectGetter from '../Project/ProjectGetter.mjs'
- import { MeteredStream } from '@overleaf/stream-utils'
- import Metrics from '@overleaf/metrics'
- /**
- * Download a file from a specific build on the clsi-cache.
- *
- * @param req
- * @param res
- * @return {Promise<*>}
- */
- async function downloadFromCache(req, res) {
- const { Project_id: projectId, buildId, filename } = req.params
- return await _downloadFromCacheWithParams(
- req,
- res,
- projectId,
- buildId,
- filename
- )
- }
- /**
- * Download a file from a specific build on the clsi-cache.
- *
- * @param req
- * @param res
- * @param projectId
- * @param buildId
- * @param filename
- * @param projectId
- * @param buildId
- * @param filename
- * @return {Promise<*>}
- */
- async function _downloadFromCacheWithParams(
- req,
- res,
- projectId,
- buildId,
- filename
- ) {
- const userId = CompileController._getUserIdForCompile(req)
- const ac = new AbortController()
- let timer = setTimeout(() => ac.abort(), 10_000)
- let location, projectName
- try {
- ;[{ location }, { name: projectName }] = await Promise.all([
- ClsiCacheHandler.getOutputFile(
- projectId,
- userId,
- buildId,
- filename,
- ac.signal
- ),
- ProjectGetter.promises.getProject(projectId, { name: 1 }),
- ])
- } catch (err) {
- clearTimeout(timer)
- if (err instanceof NotFoundError) {
- // res.sendStatus() sends a description of the status as body.
- // Using res.status().end() avoids sending that fake body.
- return res.status(404).end()
- } else {
- throw err
- }
- }
- let stream, response
- try {
- ;({ stream, response } = await fetchStreamWithResponse(location, {
- signal: ac.signal,
- }))
- } finally {
- clearTimeout(timer)
- }
- if (req.destroyed) {
- // The client has disconnected already, avoid trying to write into the broken connection.
- stream.destroy(new Error('user aborted the request'))
- return
- }
- for (const key of ['Content-Length', 'Content-Type']) {
- if (response.headers.has(key)) res.setHeader(key, response.headers.get(key))
- }
- const ext = Path.extname(filename)
- res.attachment(
- ext === '.pdf'
- ? `${CompileController._getSafeProjectName({ name: projectName })}.pdf`
- : filename
- )
- // Downloads can take a while on a slow connection, increase timeouts to 10min
- const TEN_MINUTES_IN_MS = 10 * 60 * 1000
- res.setTimeout(TEN_MINUTES_IN_MS)
- timer = setTimeout(() => ac.abort(), TEN_MINUTES_IN_MS)
- // Disable buffering in nginx
- res.setHeader('X-Accel-Buffering', 'no')
- try {
- res.writeHead(response.status)
- await pipeline(
- stream,
- new MeteredStream(Metrics, 'clsi_cache_egress', {
- path: ClsiCacheHandler.getEgressLabel(filename),
- }),
- res
- )
- } catch (err) {
- const reqAborted = Boolean(req.destroyed)
- const streamingStarted = Boolean(res.headersSent)
- if (!streamingStarted) {
- if (err instanceof RequestFailedError) {
- res.sendStatus(err.response.status)
- } else {
- res.sendStatus(500)
- }
- }
- if (
- streamingStarted &&
- reqAborted &&
- (err.code === 'ERR_STREAM_PREMATURE_CLOSE' ||
- err.code === 'ERR_STREAM_UNABLE_TO_PIPE')
- ) {
- // Ignore noisy spurious error
- return
- }
- logger.warn(
- {
- err,
- projectId,
- location,
- filename,
- reqAborted,
- streamingStarted,
- },
- 'CLSI-cache proxy error'
- )
- } finally {
- clearTimeout(timer)
- }
- }
- /**
- * Prepare a compile response from the clsi-cache.
- *
- * @param req
- * @param res
- * @return {Promise<void>}
- */
- async function getLatestBuildFromCache(req, res) {
- const { Project_id: projectId } = req.params
- const userId = CompileController._getUserIdForCompile(req)
- try {
- const {
- zone,
- outputFiles,
- compileGroup,
- clsiServerId,
- clsiCacheShard,
- options,
- stats,
- timings,
- } = await ClsiCacheManager.getLatestCompileResult(projectId, userId)
- let { pdfCachingMinChunkSize, pdfDownloadDomain } =
- await CompileController._getSplitTestOptions(req, res)
- pdfDownloadDomain += `/zone/${zone}`
- res.json({
- fromCache: true,
- status: 'success',
- outputFiles,
- compileGroup,
- clsiServerId,
- clsiCacheShard,
- pdfDownloadDomain,
- pdfCachingMinChunkSize,
- options,
- stats,
- timings,
- })
- } catch (err) {
- if (err instanceof NotFoundError) {
- res.sendStatus(404)
- } else if (err instanceof ResourceGoneError) {
- res.sendStatus(410)
- } else {
- throw err
- }
- }
- }
- export default {
- _downloadFromCacheWithParams,
- downloadFromCache: expressify(downloadFromCache),
- getLatestBuildFromCache: expressify(getLatestBuildFromCache),
- }
|