CompileController.js 21 KB

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