CompileController.mjs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796
  1. import { URL } from 'node:url'
  2. import { pipeline } from 'node:stream/promises'
  3. import { Cookie } from 'tough-cookie'
  4. import OError from '@overleaf/o-error'
  5. import Metrics from '@overleaf/metrics'
  6. import ProjectGetter from '../Project/ProjectGetter.mjs'
  7. import CompileManager from './CompileManager.mjs'
  8. import ClsiManager from './ClsiManager.mjs'
  9. import logger from '@overleaf/logger'
  10. import Settings from '@overleaf/settings'
  11. import Errors from '../Errors/Errors.js'
  12. import SessionManager from '../Authentication/SessionManager.mjs'
  13. import { RateLimiter } from '../../infrastructure/RateLimiter.mjs'
  14. import Validation from '../../infrastructure/Validation.mjs'
  15. import ClsiCookieManagerFactory from './ClsiCookieManager.mjs'
  16. import Path from 'node:path'
  17. import AnalyticsManager from '../Analytics/AnalyticsManager.mjs'
  18. import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
  19. import { expressify } from '@overleaf/promise-utils'
  20. import {
  21. fetchStreamWithResponse,
  22. RequestFailedError,
  23. } from '@overleaf/fetch-utils'
  24. import Features from '../../infrastructure/Features.mjs'
  25. import ClsiCacheController from './ClsiCacheController.mjs'
  26. import { prepareZipAttachment } from '../../infrastructure/Response.mjs'
  27. const { z, zz, parseReq } = Validation
  28. const ClsiCookieManager = ClsiCookieManagerFactory(
  29. Settings.apis.clsi?.backendGroupName
  30. )
  31. const COMPILE_TIMEOUT_MS = 12 * 60 * 1000
  32. const buildIdSchema = z.string().regex(/[a-z0-9-]/)
  33. const pdfDownloadRateLimiter = new RateLimiter('full-pdf-download', {
  34. points: 1000,
  35. duration: 60 * 60,
  36. })
  37. function getOutputFilesArchiveSpecification(projectId, userId, buildId) {
  38. const fileName = 'output.zip'
  39. return {
  40. path: fileName,
  41. url: _CompileController._getFileUrl(projectId, userId, buildId, fileName),
  42. type: 'zip',
  43. }
  44. }
  45. function getPdfCachingMinChunkSize(req, res) {
  46. return Settings.pdfCachingMinChunkSize
  47. }
  48. function _getSplitTestOptions(req, res) {
  49. // Use the query flags from the editor request for overriding the split test.
  50. let query = {}
  51. try {
  52. const u = new URL(req.headers.referer || req.url, Settings.siteUrl)
  53. query = Object.fromEntries(u.searchParams.entries())
  54. } catch (e) {}
  55. const editorReq = { ...req, query }
  56. const pdfDownloadDomain = Settings.pdfDownloadDomain
  57. const enablePdfCaching = Settings.enablePdfCaching
  58. if (!enablePdfCaching || !req.query.enable_pdf_caching) {
  59. // The frontend does not want to do pdf caching.
  60. return {
  61. pdfDownloadDomain,
  62. enablePdfCaching: false,
  63. }
  64. }
  65. const pdfCachingMinChunkSize = getPdfCachingMinChunkSize(editorReq, res)
  66. return {
  67. pdfDownloadDomain,
  68. enablePdfCaching,
  69. pdfCachingMinChunkSize,
  70. }
  71. }
  72. async function _syncTeX(req, res, direction, validatedOptions) {
  73. const projectId = req.params.Project_id
  74. const { editorId, buildId, clsiserverid: clsiServerId } = req.query
  75. if (!editorId?.match(/^[a-f0-9-]+$/)) throw new Error('invalid ?editorId')
  76. if (!buildId?.match(/^[a-f0-9-]+$/)) throw new Error('invalid ?buildId')
  77. const userId = CompileController._getUserIdForCompile(req)
  78. try {
  79. const body = await CompileManager.promises.syncTeX(projectId, userId, {
  80. direction,
  81. compileFromClsiCache: Features.hasFeature('saas'),
  82. validatedOptions: {
  83. ...validatedOptions,
  84. editorId,
  85. buildId,
  86. },
  87. clsiServerId,
  88. })
  89. res.json(body)
  90. } catch (err) {
  91. if (err instanceof Errors.NotFoundError) return res.status(404).end()
  92. throw err
  93. }
  94. }
  95. const deleteAuxFilesSchema = z.object({
  96. params: z.object({
  97. Project_id: zz.objectId(),
  98. }),
  99. query: z.object({
  100. clsiserverid: z.string().optional(),
  101. }),
  102. })
  103. const wordCountSchema = z.object({
  104. params: z.object({
  105. Project_id: zz.objectId(),
  106. }),
  107. query: z.object({
  108. clsiserverid: z.string().optional(),
  109. file: z.string().optional(),
  110. }),
  111. })
  112. const _CompileController = {
  113. async compile(req, res) {
  114. res.setTimeout(COMPILE_TIMEOUT_MS)
  115. const projectId = req.params.Project_id
  116. const isAutoCompile = !!req.query.auto_compile
  117. const fileLineErrors = !!req.query.file_line_errors
  118. const stopOnFirstError = !!req.body.stopOnFirstError
  119. const userId = SessionManager.getLoggedInUserId(req.session)
  120. const options = {
  121. isAutoCompile,
  122. fileLineErrors,
  123. stopOnFirstError,
  124. editorId: req.body.editorId,
  125. }
  126. if (req.body.rootDoc_id) {
  127. options.rootDoc_id = req.body.rootDoc_id
  128. } else if (
  129. req.body.settingsOverride &&
  130. req.body.settingsOverride.rootDoc_id
  131. ) {
  132. // Can be removed after deploy
  133. options.rootDoc_id = req.body.settingsOverride.rootDoc_id
  134. }
  135. if (req.body.compiler) {
  136. options.compiler = req.body.compiler
  137. }
  138. if (req.body.draft) {
  139. options.draft = req.body.draft
  140. }
  141. if (['validate', 'error', 'silent'].includes(req.body.check)) {
  142. options.check = req.body.check
  143. }
  144. if (req.body.incrementalCompilesEnabled) {
  145. options.incrementalCompilesEnabled = true
  146. }
  147. let { enablePdfCaching, pdfCachingMinChunkSize, pdfDownloadDomain } =
  148. _getSplitTestOptions(req, res)
  149. if (Features.hasFeature('saas')) {
  150. options.compileFromClsiCache = true
  151. options.populateClsiCache = true
  152. }
  153. options.enablePdfCaching = enablePdfCaching
  154. if (enablePdfCaching) {
  155. options.pdfCachingMinChunkSize = pdfCachingMinChunkSize
  156. }
  157. const {
  158. status,
  159. outputFiles,
  160. clsiServerId,
  161. limits,
  162. validationProblems,
  163. stats,
  164. timings,
  165. outputUrlPrefix,
  166. buildId,
  167. clsiCacheShard,
  168. } = await CompileManager.promises
  169. .compile(projectId, userId, options)
  170. .catch(error => {
  171. Metrics.inc('compile-error')
  172. throw error
  173. })
  174. Metrics.inc('compile-status', 1, { status })
  175. if (pdfDownloadDomain && outputUrlPrefix) {
  176. pdfDownloadDomain += outputUrlPrefix
  177. }
  178. if (
  179. limits &&
  180. SplitTestHandler.getPercentile(
  181. AnalyticsManager.getIdsFromSession(req.session).analyticsId,
  182. 'compile-result-backend',
  183. 'release'
  184. ) === 1
  185. ) {
  186. // For a compile request to be sent to clsi we need limits.
  187. // If we get here without having the limits object populated, it is
  188. // a reasonable assumption to make that nothing was compiled.
  189. // We need to know the limits in order to make use of the events.
  190. AnalyticsManager.recordEventForSession(
  191. req.session,
  192. 'compile-result-backend',
  193. {
  194. projectId,
  195. ownerAnalyticsId: limits.ownerAnalyticsId,
  196. status,
  197. compileTime: timings?.compileE2E,
  198. timeout: limits.timeout,
  199. server: clsiServerId?.includes('-c4d-') ? 'faster' : 'normal',
  200. clsiServerId,
  201. isAutoCompile,
  202. isInitialCompile: stats?.isInitialCompile === 1,
  203. restoredClsiCache: stats?.restoredClsiCache === 1,
  204. stopOnFirstError,
  205. isDraftMode: !!options.draft,
  206. }
  207. )
  208. }
  209. const outputFilesArchive = buildId
  210. ? getOutputFilesArchiveSpecification(projectId, userId, buildId)
  211. : null
  212. res.json({
  213. status,
  214. outputFiles,
  215. outputFilesArchive,
  216. compileGroup: limits?.compileGroup,
  217. clsiServerId,
  218. clsiCacheShard,
  219. validationProblems,
  220. stats,
  221. timings,
  222. outputUrlPrefix,
  223. pdfDownloadDomain,
  224. pdfCachingMinChunkSize,
  225. })
  226. },
  227. async stopCompile(req, res) {
  228. const projectId = req.params.Project_id
  229. const userId = SessionManager.getLoggedInUserId(req.session)
  230. await CompileManager.promises.stopCompile(projectId, userId)
  231. res.sendStatus(200)
  232. },
  233. // Used for submissions through the public API
  234. async compileSubmission(req, res) {
  235. res.setTimeout(COMPILE_TIMEOUT_MS)
  236. const submissionId = req.params.submission_id
  237. const options = {}
  238. if (req.body?.rootResourcePath != null) {
  239. options.rootResourcePath = req.body.rootResourcePath
  240. }
  241. if (req.body?.compiler) {
  242. options.compiler = req.body.compiler
  243. }
  244. if (req.body?.draft) {
  245. options.draft = req.body.draft
  246. }
  247. if (['validate', 'error', 'silent'].includes(req.body?.check)) {
  248. options.check = req.body.check
  249. }
  250. options.compileGroup =
  251. req.body?.compileGroup || Settings.defaultFeatures.compileGroup
  252. options.compileBackendClass = Settings.apis.clsi.submissionBackendClass
  253. options.timeout =
  254. req.body?.timeout || Settings.defaultFeatures.compileTimeout
  255. const { status, outputFiles, clsiServerId, validationProblems } =
  256. await ClsiManager.promises.sendExternalRequest(
  257. submissionId,
  258. req.body,
  259. options
  260. )
  261. res.json({
  262. status,
  263. outputFiles,
  264. clsiServerId,
  265. validationProblems,
  266. })
  267. },
  268. _getUserIdForCompile(req) {
  269. if (!Settings.disablePerUserCompiles) {
  270. return SessionManager.getLoggedInUserId(req.session)
  271. }
  272. return null
  273. },
  274. async downloadPdf(req, res) {
  275. Metrics.inc('pdf-downloads')
  276. const projectId = req.params.Project_id
  277. const rateLimit = () =>
  278. pdfDownloadRateLimiter
  279. .consume(req.ip, 1, { method: 'ip' })
  280. .then(() => true)
  281. .catch(err => {
  282. if (err instanceof Error) {
  283. throw err
  284. }
  285. return false
  286. })
  287. const project = await ProjectGetter.promises.getProject(projectId, {
  288. name: 1,
  289. })
  290. res.contentType('application/pdf')
  291. const filename = `${_CompileController._getSafeProjectName(project)}.pdf`
  292. if (req.query.popupDownload) {
  293. res.setContentDisposition('attachment', { filename })
  294. } else {
  295. res.setContentDisposition('inline', { filename })
  296. }
  297. let canContinue
  298. try {
  299. canContinue = await rateLimit()
  300. } catch (err) {
  301. logger.err({ err }, 'error checking rate limit for pdf download')
  302. res.sendStatus(500)
  303. return
  304. }
  305. if (!canContinue) {
  306. logger.debug({ projectId, ip: req.ip }, 'rate limit hit downloading pdf')
  307. res.sendStatus(500) // should it be 429?
  308. } else {
  309. const userId = CompileController._getUserIdForCompile(req)
  310. const url = _CompileController._getFileUrl(
  311. projectId,
  312. userId,
  313. req.params.build_id,
  314. 'output.pdf'
  315. )
  316. // Align params with the generic output file download (via getFileFromClsi / getFileFromClsiWithoutUser).
  317. req.params.file = 'output.pdf'
  318. await CompileController._proxyToClsi(
  319. projectId,
  320. 'output-file',
  321. url,
  322. {},
  323. req,
  324. res
  325. )
  326. }
  327. },
  328. // Keep in sync with the logic for zip files in ProjectDownloadsController
  329. _getSafeProjectName(project) {
  330. return project.name.replace(/[^\p{L}\p{Nd}]/gu, '_')
  331. },
  332. async deleteAuxFiles(req, res) {
  333. const { params, query } = parseReq(req, deleteAuxFilesSchema)
  334. const projectId = params.Project_id
  335. const { clsiserverid } = query
  336. const userId = await CompileController._getUserIdForCompile(req)
  337. await CompileManager.promises.deleteAuxFiles(
  338. projectId,
  339. userId,
  340. clsiserverid
  341. )
  342. res.sendStatus(200)
  343. },
  344. // this is only used by templates, so is not called with a userId
  345. async compileAndDownloadPdf(req, res) {
  346. const projectId = req.params.project_id
  347. let outputFiles
  348. try {
  349. ;({ outputFiles } = await CompileManager.promises
  350. // pass userId as null, since templates are an "anonymous" compile
  351. .compile(projectId, null, {}))
  352. } catch (err) {
  353. logger.err(
  354. { err, projectId },
  355. 'something went wrong compile and downloading pdf'
  356. )
  357. res.sendStatus(500)
  358. return
  359. }
  360. const pdf = outputFiles.find(f => f.path === 'output.pdf')
  361. if (!pdf) {
  362. logger.warn(
  363. { projectId },
  364. 'something went wrong compile and downloading pdf: no pdf'
  365. )
  366. res.sendStatus(500)
  367. return
  368. }
  369. await CompileController._proxyToClsi(
  370. projectId,
  371. 'output-file',
  372. pdf.url,
  373. {},
  374. req,
  375. res
  376. )
  377. },
  378. async getOutputZipFromClsi(req, res) {
  379. const projectId = req.params.Project_id
  380. const userId = CompileController._getUserIdForCompile(req)
  381. const project = await ProjectGetter.promises.getProject(projectId, {
  382. name: 1,
  383. })
  384. const filename = `${_CompileController._getSafeProjectName(project)}-output.zip`
  385. prepareZipAttachment(res, filename)
  386. const qs = {}
  387. const url = _CompileController._getFileUrl(
  388. projectId,
  389. userId,
  390. req.params.build_id,
  391. 'output.zip'
  392. )
  393. await CompileController._proxyToClsi(
  394. projectId,
  395. 'output-zip-file',
  396. url,
  397. qs,
  398. req,
  399. res
  400. )
  401. },
  402. async getFileFromClsi(req, res) {
  403. const projectId = req.params.Project_id
  404. const userId = CompileController._getUserIdForCompile(req)
  405. const qs = {}
  406. const url = _CompileController._getFileUrl(
  407. projectId,
  408. userId,
  409. req.params.build_id,
  410. req.params.file
  411. )
  412. await CompileController._proxyToClsi(
  413. projectId,
  414. 'output-file',
  415. url,
  416. qs,
  417. req,
  418. res
  419. )
  420. },
  421. async getFileFromClsiWithoutUser(req, res) {
  422. const submissionId = req.params.submission_id
  423. const url = _CompileController._getFileUrl(
  424. submissionId,
  425. null,
  426. req.params.build_id,
  427. req.params.file
  428. )
  429. const limits = {
  430. compileGroup:
  431. req.body?.compileGroup ||
  432. req.query?.compileGroup ||
  433. Settings.defaultFeatures.compileGroup,
  434. compileBackendClass: Settings.apis.clsi.submissionBackendClass,
  435. }
  436. await CompileController._proxyToClsiWithLimits(
  437. submissionId,
  438. 'output-file',
  439. url,
  440. {},
  441. limits,
  442. req,
  443. res
  444. )
  445. },
  446. // compute a GET file url for a given project, user (optional), build (optional) and file
  447. _getFileUrl(projectId, userId, buildId, file) {
  448. let url
  449. if (userId != null && buildId != null) {
  450. url = `/project/${projectId}/user/${userId}/build/${buildId}/output/${file}`
  451. } else if (userId != null) {
  452. url = `/project/${projectId}/user/${userId}/output/${file}`
  453. } else if (buildId != null) {
  454. buildId = buildIdSchema.parse(buildId)
  455. url = `/project/${projectId}/build/${buildId}/output/${file}`
  456. } else {
  457. url = `/project/${projectId}/output/${file}`
  458. }
  459. return url
  460. },
  461. async proxySyncPdf(req, res) {
  462. const { page, h, v } = req.query
  463. if (!page?.match(/^\d+$/)) {
  464. throw new Error('invalid page parameter')
  465. }
  466. if (!h?.match(/^-?\d+\.\d+$/)) {
  467. throw new Error('invalid h parameter')
  468. }
  469. if (!v?.match(/^-?\d+\.\d+$/)) {
  470. throw new Error('invalid v parameter')
  471. }
  472. await _syncTeX(req, res, 'pdf', { page, h, v })
  473. },
  474. async proxySyncCode(req, res) {
  475. const { file, line, column } = req.query
  476. if (file == null) {
  477. throw new Error('missing file parameter')
  478. }
  479. // Check that we are dealing with a simple file path (this is not
  480. // strictly needed because synctex uses this parameter as a label
  481. // to look up in the synctex output, and does not open the file
  482. // itself). Since we have valid synctex paths like foo/./bar we
  483. // allow those by replacing /./ with /
  484. const testPath = file.replace('/./', '/')
  485. if (Path.resolve('/', testPath) !== `/${testPath}`) {
  486. throw new Error('invalid file parameter')
  487. }
  488. if (!line?.match(/^\d+$/)) {
  489. throw new Error('invalid line parameter')
  490. }
  491. if (!column?.match(/^\d+$/)) {
  492. throw new Error('invalid column parameter')
  493. }
  494. await _syncTeX(req, res, 'code', { file, line, column })
  495. },
  496. async _proxyToClsi(projectId, action, url, qs, req, res) {
  497. const limits =
  498. await CompileManager.promises.getProjectCompileLimits(projectId)
  499. return CompileController._proxyToClsiWithLimits(
  500. projectId,
  501. action,
  502. url,
  503. qs,
  504. limits,
  505. req,
  506. res
  507. )
  508. },
  509. async _proxyToClsiWithLimits(
  510. projectId,
  511. action,
  512. requestPath,
  513. qs,
  514. limits,
  515. req,
  516. res
  517. ) {
  518. const persistenceOptions = await _getPersistenceOptions(
  519. req,
  520. projectId,
  521. limits.compileGroup,
  522. limits.compileBackendClass
  523. ).catch(err => {
  524. OError.tag(err, 'error getting cookie jar for clsi request')
  525. throw err
  526. })
  527. const url = new URL(
  528. action === 'output-zip-file'
  529. ? Settings.apis.clsi.url
  530. : Settings.apis.clsi.downloadHost
  531. )
  532. url.pathname = requestPath
  533. const searchParams = {
  534. ...persistenceOptions.qs,
  535. ...qs,
  536. }
  537. for (const [key, value] of Object.entries(searchParams)) {
  538. if (value !== undefined) {
  539. // avoid sending "undefined" as a string value
  540. url.searchParams.set(key, value)
  541. }
  542. }
  543. const timer = new Metrics.Timer(
  544. 'proxy_to_clsi',
  545. 1,
  546. { path: action },
  547. [0, 100, 1000, 2000, 5000, 10000, 15000, 20000, 30000, 45000, 60000]
  548. )
  549. Metrics.inc('proxy_to_clsi', 1, { path: action, status: 'start' })
  550. const ac = new AbortController()
  551. let timeout = setTimeout(() => ac.abort(), 10_000)
  552. try {
  553. const { stream, response } = await fetchStreamWithResponse(url.href, {
  554. method: req.method,
  555. signal: ac.signal,
  556. headers: persistenceOptions.headers,
  557. })
  558. if (req.destroyed) {
  559. // The client has disconnected already, avoid trying to write into the broken connection.
  560. Metrics.inc('proxy_to_clsi', 1, {
  561. path: action,
  562. status: 'req-aborted',
  563. })
  564. stream.destroy(new Error('user aborted the request'))
  565. return
  566. }
  567. Metrics.inc('proxy_to_clsi', 1, {
  568. path: action,
  569. status: response.status,
  570. })
  571. for (const key of ['Content-Length', 'Content-Type']) {
  572. if (response.headers.has(key)) {
  573. res.setHeader(key, response.headers.get(key))
  574. }
  575. }
  576. // Downloads can take a while on a slow connection, increase timeouts to 10min
  577. const TEN_MINUTES_IN_MS = 10 * 60 * 1000
  578. res.setTimeout(TEN_MINUTES_IN_MS)
  579. clearTimeout(timeout)
  580. timeout = setTimeout(() => ac.abort(), TEN_MINUTES_IN_MS)
  581. // Disable buffering in nginx
  582. res.setHeader('X-Accel-Buffering', 'no')
  583. res.writeHead(response.status)
  584. await pipeline(stream, res)
  585. timer.labels.status = 'success'
  586. timer.done()
  587. } catch (err) {
  588. if (canTryClsiCacheFallback(req, res, action, err)) {
  589. await ClsiCacheController._downloadFromCacheWithParams(
  590. req,
  591. res,
  592. projectId,
  593. `${req.query.editorId}-${req.params.build_id}`,
  594. req.params.file
  595. )
  596. return
  597. }
  598. const reqAborted = Boolean(req.destroyed)
  599. const status = reqAborted ? 'req-aborted-late' : 'error'
  600. timer.labels.status = status
  601. const duration = timer.done()
  602. Metrics.inc('proxy_to_clsi', 1, { path: action, status })
  603. const streamingStarted = Boolean(res.headersSent)
  604. if (!streamingStarted) {
  605. if (err instanceof RequestFailedError) {
  606. res.sendStatus(err.response.status)
  607. } else {
  608. res.sendStatus(500)
  609. }
  610. }
  611. if (
  612. streamingStarted &&
  613. reqAborted &&
  614. (err.code === 'ERR_STREAM_PREMATURE_CLOSE' ||
  615. err.code === 'ERR_STREAM_UNABLE_TO_PIPE')
  616. ) {
  617. // Ignore noisy spurious error
  618. return
  619. }
  620. if (
  621. err instanceof RequestFailedError &&
  622. ['sync-to-code', 'sync-to-pdf', 'output-file'].includes(action)
  623. ) {
  624. // Ignore noisy error
  625. // https://github.com/overleaf/internal/issues/15201
  626. return
  627. }
  628. logger.warn(
  629. {
  630. err,
  631. projectId,
  632. url,
  633. action,
  634. reqAborted,
  635. streamingStarted,
  636. duration,
  637. },
  638. 'CLSI proxy error'
  639. )
  640. } finally {
  641. clearTimeout(timeout)
  642. }
  643. },
  644. async wordCount(req, res) {
  645. const { params, query } = parseReq(req, wordCountSchema)
  646. const projectId = params.Project_id
  647. const file = query.file || false
  648. const { clsiserverid } = query
  649. const userId = CompileController._getUserIdForCompile(req)
  650. const body = await CompileManager.promises.wordCount(
  651. projectId,
  652. userId,
  653. file,
  654. clsiserverid
  655. )
  656. res.json(body)
  657. },
  658. }
  659. async function _getPersistenceOptions(
  660. req,
  661. projectId,
  662. compileGroup,
  663. compileBackendClass
  664. ) {
  665. const { clsiserverid } = req.query
  666. const userId = SessionManager.getLoggedInUserId(req)
  667. if (clsiserverid && typeof clsiserverid === 'string') {
  668. return {
  669. qs: { clsiserverid, compileGroup, compileBackendClass },
  670. headers: {},
  671. }
  672. } else {
  673. const clsiServerId = await ClsiCookieManager.promises.getServerId(
  674. projectId,
  675. userId,
  676. compileGroup,
  677. compileBackendClass
  678. )
  679. return {
  680. qs: { compileGroup, compileBackendClass },
  681. headers: clsiServerId
  682. ? {
  683. Cookie: new Cookie({
  684. key: Settings.clsiCookie.key,
  685. value: clsiServerId,
  686. }).cookieString(),
  687. }
  688. : {},
  689. }
  690. }
  691. }
  692. function canTryClsiCacheFallback(req, res, action, err) {
  693. const reqAborted = Boolean(req.destroyed)
  694. const streamingStarted = Boolean(res.headersSent)
  695. return (
  696. action === 'output-file' &&
  697. err instanceof RequestFailedError &&
  698. err.response.status === 404 &&
  699. !streamingStarted &&
  700. !reqAborted &&
  701. req.params.build_id &&
  702. req.query.editorId &&
  703. req.params.file &&
  704. // clsi-cache only has a small subset of files available outside the tar-ball
  705. // The ClsiCacheHandler will validate the filename again.
  706. (['output.log', 'output.pdf', 'output.synctex.gz'].includes(
  707. req.params.file
  708. ) ||
  709. req.params.file.endsWith('.blg'))
  710. )
  711. }
  712. const CompileController = {
  713. COMPILE_TIMEOUT_MS,
  714. compile: expressify(_CompileController.compile),
  715. stopCompile: expressify(_CompileController.stopCompile),
  716. compileSubmission: expressify(_CompileController.compileSubmission),
  717. downloadPdf: expressify(_CompileController.downloadPdf), //
  718. compileAndDownloadPdf: expressify(_CompileController.compileAndDownloadPdf),
  719. deleteAuxFiles: expressify(_CompileController.deleteAuxFiles),
  720. getOutputZipFromClsi: expressify(_CompileController.getOutputZipFromClsi),
  721. getFileFromClsi: expressify(_CompileController.getFileFromClsi),
  722. getFileFromClsiWithoutUser: expressify(
  723. _CompileController.getFileFromClsiWithoutUser
  724. ),
  725. proxySyncPdf: expressify(_CompileController.proxySyncPdf),
  726. proxySyncCode: expressify(_CompileController.proxySyncCode),
  727. wordCount: expressify(_CompileController.wordCount),
  728. _getSafeProjectName: _CompileController._getSafeProjectName,
  729. _getSplitTestOptions,
  730. _getUserIdForCompile: _CompileController._getUserIdForCompile,
  731. _proxyToClsi: _CompileController._proxyToClsi,
  732. _proxyToClsiWithLimits: _CompileController._proxyToClsiWithLimits,
  733. }
  734. export default CompileController