ClsiManager.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852
  1. const { callbackify } = require('util')
  2. const { callbackifyMultiResult } = require('@overleaf/promise-utils')
  3. const {
  4. fetchString,
  5. fetchStringWithResponse,
  6. fetchStream,
  7. RequestFailedError,
  8. } = require('@overleaf/fetch-utils')
  9. const Settings = require('@overleaf/settings')
  10. const ProjectGetter = require('../Project/ProjectGetter')
  11. const ProjectEntityHandler = require('../Project/ProjectEntityHandler')
  12. const logger = require('@overleaf/logger')
  13. const OError = require('@overleaf/o-error')
  14. const { Cookie } = require('tough-cookie')
  15. const ClsiCookieManager = require('./ClsiCookieManager')(
  16. Settings.apis.clsi?.backendGroupName
  17. )
  18. const NewBackendCloudClsiCookieManager = require('./ClsiCookieManager')(
  19. Settings.apis.clsi_new?.backendGroupName
  20. )
  21. const ClsiStateManager = require('./ClsiStateManager')
  22. const _ = require('lodash')
  23. const ClsiFormatChecker = require('./ClsiFormatChecker')
  24. const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
  25. const Metrics = require('@overleaf/metrics')
  26. const Errors = require('../Errors/Errors')
  27. const { getBlobLocation } = require('../History/HistoryManager')
  28. const VALID_COMPILERS = ['pdflatex', 'latex', 'xelatex', 'lualatex']
  29. const OUTPUT_FILE_TIMEOUT_MS = 60000
  30. const CLSI_COOKIES_ENABLED = (Settings.clsiCookie?.key ?? '') !== ''
  31. // The timeout in services/clsi/app.js is 10 minutes, so we'll be on the safe side with 12 minutes
  32. const COMPILE_REQUEST_TIMEOUT_MS = 12 * 60 * 1000
  33. function collectMetricsOnBlgFiles(outputFiles) {
  34. let topLevel = 0
  35. let nested = 0
  36. for (const outputFile of outputFiles) {
  37. if (outputFile.type === 'blg') {
  38. if (outputFile.path.includes('/')) {
  39. nested++
  40. } else {
  41. topLevel++
  42. }
  43. }
  44. }
  45. Metrics.count('blg_output_file', topLevel, 1, { path: 'top-level' })
  46. Metrics.count('blg_output_file', nested, 1, { path: 'nested' })
  47. }
  48. async function sendRequest(projectId, userId, options) {
  49. if (options == null) {
  50. options = {}
  51. }
  52. let result = await sendRequestOnce(projectId, userId, options)
  53. if (result.status === 'conflict') {
  54. // Try again, with a full compile
  55. result = await sendRequestOnce(projectId, userId, {
  56. ...options,
  57. syncType: 'full',
  58. })
  59. } else if (result.status === 'unavailable') {
  60. result = await sendRequestOnce(projectId, userId, {
  61. ...options,
  62. syncType: 'full',
  63. forceNewClsiServer: true,
  64. })
  65. }
  66. return result
  67. }
  68. async function sendRequestOnce(projectId, userId, options) {
  69. let req
  70. try {
  71. req = await _buildRequest(projectId, options)
  72. } catch (err) {
  73. if (err.message === 'no main file specified') {
  74. return {
  75. status: 'validation-problems',
  76. validationProblems: { mainFile: err.message },
  77. }
  78. } else {
  79. throw OError.tag(err, 'Could not build request to CLSI', {
  80. projectId,
  81. options,
  82. })
  83. }
  84. }
  85. return await _sendBuiltRequest(projectId, userId, req, options)
  86. }
  87. // for public API requests where there is no project id
  88. async function sendExternalRequest(submissionId, clsiRequest, options) {
  89. if (options == null) {
  90. options = {}
  91. }
  92. return await _sendBuiltRequest(submissionId, null, clsiRequest, options)
  93. }
  94. async function stopCompile(projectId, userId, options) {
  95. if (options == null) {
  96. options = {}
  97. }
  98. const { compileBackendClass, compileGroup } = options
  99. const url = _getCompilerUrl(
  100. compileBackendClass,
  101. compileGroup,
  102. projectId,
  103. userId,
  104. 'compile/stop'
  105. )
  106. const opts = { method: 'POST' }
  107. await _makeRequest(
  108. projectId,
  109. userId,
  110. compileGroup,
  111. compileBackendClass,
  112. url,
  113. opts
  114. )
  115. }
  116. async function deleteAuxFiles(projectId, userId, options, clsiserverid) {
  117. if (options == null) {
  118. options = {}
  119. }
  120. const { compileBackendClass, compileGroup } = options
  121. const url = _getCompilerUrl(
  122. compileBackendClass,
  123. compileGroup,
  124. projectId,
  125. userId
  126. )
  127. const opts = {
  128. method: 'DELETE',
  129. }
  130. try {
  131. await _makeRequestWithClsiServerId(
  132. projectId,
  133. userId,
  134. compileGroup,
  135. compileBackendClass,
  136. url,
  137. opts,
  138. clsiserverid
  139. )
  140. } finally {
  141. // always clear the project state from the docupdater, even if there
  142. // was a problem with the request to the clsi
  143. try {
  144. await DocumentUpdaterHandler.promises.clearProjectState(projectId)
  145. } finally {
  146. await ClsiCookieManager.promises.clearServerId(projectId, userId)
  147. }
  148. }
  149. }
  150. async function _sendBuiltRequest(projectId, userId, req, options, callback) {
  151. if (options.forceNewClsiServer) {
  152. await ClsiCookieManager.promises.clearServerId(projectId, userId)
  153. }
  154. const validationProblems =
  155. await ClsiFormatChecker.promises.checkRecoursesForProblems(
  156. req.compile?.resources
  157. )
  158. if (validationProblems != null) {
  159. logger.debug(
  160. { projectId, validationProblems },
  161. 'problems with users latex before compile was attempted'
  162. )
  163. return {
  164. status: 'validation-problems',
  165. validationProblems,
  166. }
  167. }
  168. const { response, clsiServerId } = await _postToClsi(
  169. projectId,
  170. userId,
  171. req,
  172. options.compileBackendClass,
  173. options.compileGroup
  174. )
  175. const outputFiles = _parseOutputFiles(
  176. projectId,
  177. response && response.compile && response.compile.outputFiles
  178. )
  179. collectMetricsOnBlgFiles(outputFiles)
  180. const compile = response?.compile || {}
  181. return {
  182. status: compile.status,
  183. outputFiles,
  184. clsiServerId,
  185. buildId: compile.buildId,
  186. stats: compile.stats,
  187. timings: compile.timings,
  188. outputUrlPrefix: compile.outputUrlPrefix,
  189. }
  190. }
  191. async function _makeRequestWithClsiServerId(
  192. projectId,
  193. userId,
  194. compileGroup,
  195. compileBackendClass,
  196. url,
  197. opts,
  198. clsiserverid
  199. ) {
  200. if (clsiserverid) {
  201. // ignore cookies and newBackend, go straight to the clsi node
  202. url.searchParams.set('compileGroup', compileGroup)
  203. url.searchParams.set('compileBackendClass', compileBackendClass)
  204. url.searchParams.set('clsiserverid', clsiserverid)
  205. let body
  206. try {
  207. body = await fetchString(url, opts)
  208. } catch (err) {
  209. throw OError.tag(err, 'error making request to CLSI', {
  210. userId,
  211. projectId,
  212. })
  213. }
  214. let json
  215. try {
  216. json = JSON.parse(body)
  217. } catch (err) {
  218. // some responses are empty. Ignore JSON parsing errors.
  219. }
  220. return { body: json }
  221. } else {
  222. return await _makeRequest(
  223. projectId,
  224. userId,
  225. compileGroup,
  226. compileBackendClass,
  227. url,
  228. opts
  229. )
  230. }
  231. }
  232. async function _makeRequest(
  233. projectId,
  234. userId,
  235. compileGroup,
  236. compileBackendClass,
  237. url,
  238. opts
  239. ) {
  240. const currentBackendStartTime = new Date()
  241. const clsiServerId = await ClsiCookieManager.promises.getServerId(
  242. projectId,
  243. userId,
  244. compileGroup,
  245. compileBackendClass
  246. )
  247. opts.headers = {
  248. Accept: 'application/json',
  249. 'Content-Type': 'application/json',
  250. }
  251. if (CLSI_COOKIES_ENABLED) {
  252. const cookie = new Cookie({
  253. key: Settings.clsiCookie.key,
  254. value: clsiServerId,
  255. })
  256. opts.headers.Cookie = cookie.cookieString()
  257. }
  258. const timer = new Metrics.Timer('compile.currentBackend')
  259. let response, body
  260. try {
  261. ;({ body, response } = await fetchStringWithResponse(url, opts))
  262. } catch (err) {
  263. throw OError.tag(err, 'error making request to CLSI', {
  264. projectId,
  265. userId,
  266. })
  267. }
  268. Metrics.inc(`compile.currentBackend.response.${response.status}`)
  269. let json
  270. try {
  271. json = JSON.parse(body)
  272. } catch (err) {
  273. // some responses are empty. Ignore JSON parsing errors
  274. }
  275. timer.done()
  276. let newClsiServerId
  277. if (CLSI_COOKIES_ENABLED) {
  278. newClsiServerId = _getClsiServerIdFromResponse(response)
  279. await ClsiCookieManager.promises.setServerId(
  280. projectId,
  281. userId,
  282. compileGroup,
  283. compileBackendClass,
  284. newClsiServerId,
  285. clsiServerId
  286. )
  287. }
  288. const currentCompileTime = new Date() - currentBackendStartTime
  289. // Start new backend request in the background
  290. const newBackendStartTime = new Date()
  291. _makeNewBackendRequest(
  292. projectId,
  293. userId,
  294. compileGroup,
  295. compileBackendClass,
  296. url,
  297. opts
  298. )
  299. .then(result => {
  300. if (result == null) {
  301. return
  302. }
  303. const { response: newBackendResponse } = result
  304. Metrics.inc(`compile.newBackend.response.${newBackendResponse.status}`)
  305. const newBackendCompileTime = new Date() - newBackendStartTime
  306. const currentStatusCode = response.status
  307. const newStatusCode = newBackendResponse.status
  308. const statusCodeSame = newStatusCode === currentStatusCode
  309. const timeDifference = newBackendCompileTime - currentCompileTime
  310. logger.debug(
  311. {
  312. statusCodeSame,
  313. timeDifference,
  314. currentCompileTime,
  315. newBackendCompileTime,
  316. projectId,
  317. },
  318. 'both clsi requests returned'
  319. )
  320. })
  321. .catch(err => {
  322. logger.warn({ err }, 'Error making request to new CLSI backend')
  323. })
  324. return {
  325. body: json,
  326. clsiServerId: newClsiServerId || clsiServerId,
  327. }
  328. }
  329. async function _makeNewBackendRequest(
  330. projectId,
  331. userId,
  332. compileGroup,
  333. compileBackendClass,
  334. url,
  335. opts
  336. ) {
  337. if (Settings.apis.clsi_new?.url == null) {
  338. return null
  339. }
  340. url = url
  341. .toString()
  342. .replace(Settings.apis.clsi.url, Settings.apis.clsi_new.url)
  343. const clsiServerId =
  344. await NewBackendCloudClsiCookieManager.promises.getServerId(
  345. projectId,
  346. userId,
  347. compileGroup,
  348. compileBackendClass
  349. )
  350. opts.headers = {
  351. Accept: 'application/json',
  352. 'Content-Type': 'application/json',
  353. }
  354. if (CLSI_COOKIES_ENABLED) {
  355. const cookie = new Cookie({
  356. key: Settings.clsiCookie.key,
  357. value: clsiServerId,
  358. })
  359. opts.headers.Cookie = cookie.cookieString()
  360. }
  361. const timer = new Metrics.Timer('compile.newBackend')
  362. let response, body
  363. try {
  364. ;({ body, response } = await fetchStringWithResponse(url, opts))
  365. } catch (err) {
  366. throw OError.tag(err, 'error making request to new CLSI', {
  367. userId,
  368. projectId,
  369. })
  370. }
  371. let json
  372. try {
  373. json = JSON.parse(body)
  374. } catch (err) {
  375. // Some responses are empty. Ignore JSON parsing errors
  376. }
  377. timer.done()
  378. if (CLSI_COOKIES_ENABLED) {
  379. const newClsiServerId = _getClsiServerIdFromResponse(response)
  380. await NewBackendCloudClsiCookieManager.promises.setServerId(
  381. projectId,
  382. userId,
  383. compileGroup,
  384. compileBackendClass,
  385. newClsiServerId,
  386. clsiServerId
  387. )
  388. }
  389. return { response, body: json }
  390. }
  391. function _getCompilerUrl(
  392. compileBackendClass,
  393. compileGroup,
  394. projectId,
  395. userId,
  396. action
  397. ) {
  398. const u = new URL(`/project/${projectId}`, Settings.apis.clsi.url)
  399. if (userId != null) {
  400. u.pathname += `/user/${userId}`
  401. }
  402. if (action != null) {
  403. u.pathname += `/${action}`
  404. }
  405. u.searchParams.set('compileBackendClass', compileBackendClass)
  406. u.searchParams.set('compileGroup', compileGroup)
  407. return u
  408. }
  409. async function _postToClsi(
  410. projectId,
  411. userId,
  412. req,
  413. compileBackendClass,
  414. compileGroup
  415. ) {
  416. const url = _getCompilerUrl(
  417. compileBackendClass,
  418. compileGroup,
  419. projectId,
  420. userId,
  421. 'compile'
  422. )
  423. const opts = {
  424. json: req,
  425. method: 'POST',
  426. signal: AbortSignal.timeout(COMPILE_REQUEST_TIMEOUT_MS),
  427. }
  428. try {
  429. const { body, clsiServerId } = await _makeRequest(
  430. projectId,
  431. userId,
  432. compileGroup,
  433. compileBackendClass,
  434. url,
  435. opts
  436. )
  437. return { response: body, clsiServerId }
  438. } catch (err) {
  439. if (err instanceof RequestFailedError) {
  440. if (err.response.status === 413) {
  441. return { response: { compile: { status: 'project-too-large' } } }
  442. } else if (err.response.status === 409) {
  443. return { response: { compile: { status: 'conflict' } } }
  444. } else if (err.response.status === 423) {
  445. return { response: { compile: { status: 'compile-in-progress' } } }
  446. } else if (err.response.status === 503) {
  447. return { response: { compile: { status: 'unavailable' } } }
  448. } else {
  449. throw new OError(
  450. `CLSI returned non-success code: ${err.response.status}`,
  451. {
  452. projectId,
  453. userId,
  454. compileOptions: req.compile.options,
  455. rootResourcePath: req.compile.rootResourcePath,
  456. clsiResponse: err.body,
  457. statusCode: err.response.status,
  458. }
  459. )
  460. }
  461. } else {
  462. throw new OError(
  463. 'failed to make request to CLSI',
  464. {
  465. projectId,
  466. userId,
  467. compileOptions: req.compile.options,
  468. rootResourcePath: req.compile.rootResourcePath,
  469. },
  470. err
  471. )
  472. }
  473. }
  474. }
  475. function _parseOutputFiles(projectId, rawOutputFiles = []) {
  476. const outputFiles = []
  477. for (const file of rawOutputFiles) {
  478. const f = {
  479. path: file.path, // the clsi is now sending this to web
  480. url: new URL(file.url).pathname, // the location of the file on the clsi, excluding the host part
  481. type: file.type,
  482. build: file.build,
  483. }
  484. if (file.path === 'output.pdf') {
  485. f.contentId = file.contentId
  486. f.ranges = file.ranges || []
  487. f.size = file.size
  488. f.startXRefTable = file.startXRefTable
  489. f.createdAt = new Date()
  490. }
  491. outputFiles.push(f)
  492. }
  493. return outputFiles
  494. }
  495. async function _buildRequest(projectId, options) {
  496. const project = await ProjectGetter.promises.getProject(projectId, {
  497. compiler: 1,
  498. rootDoc_id: 1,
  499. imageName: 1,
  500. rootFolder: 1,
  501. 'overleaf.history.id': 1,
  502. })
  503. if (project == null) {
  504. throw new Errors.NotFoundError(`project does not exist: ${projectId}`)
  505. }
  506. if (!VALID_COMPILERS.includes(project.compiler)) {
  507. project.compiler = 'pdflatex'
  508. }
  509. if (options.incrementalCompilesEnabled || options.syncType != null) {
  510. // new way, either incremental or full
  511. const timer = new Metrics.Timer('editor.compile-getdocs-redis')
  512. let projectStateHash, docUpdaterDocs
  513. try {
  514. ;({ projectStateHash, docs: docUpdaterDocs } =
  515. await getContentFromDocUpdaterIfMatch(projectId, project, options))
  516. } catch (err) {
  517. logger.error({ err, projectId }, 'error checking project state')
  518. // note: we don't bail out when there's an error getting
  519. // incremental files from the docupdater, we just fall back
  520. // to a normal compile below
  521. }
  522. timer.done()
  523. // see if we can send an incremental update to the CLSI
  524. if (docUpdaterDocs != null && options.syncType !== 'full') {
  525. Metrics.inc('compile-from-redis')
  526. return _buildRequestFromDocupdater(
  527. projectId,
  528. options,
  529. project,
  530. projectStateHash,
  531. docUpdaterDocs
  532. )
  533. } else {
  534. Metrics.inc('compile-from-mongo')
  535. return await _buildRequestFromMongo(
  536. projectId,
  537. options,
  538. project,
  539. projectStateHash
  540. )
  541. }
  542. } else {
  543. // old way, always from mongo
  544. const timer = new Metrics.Timer('editor.compile-getdocs-mongo')
  545. const { docs, files } = await _getContentFromMongo(projectId)
  546. timer.done()
  547. return _finaliseRequest(projectId, options, project, docs, files)
  548. }
  549. }
  550. async function getContentFromDocUpdaterIfMatch(projectId, project, options) {
  551. const projectStateHash = ClsiStateManager.computeHash(project, options)
  552. const docs = await DocumentUpdaterHandler.promises.getProjectDocsIfMatch(
  553. projectId,
  554. projectStateHash
  555. )
  556. return { projectStateHash, docs }
  557. }
  558. async function getOutputFileStream(
  559. projectId,
  560. userId,
  561. options,
  562. clsiServerId,
  563. buildId,
  564. outputFilePath
  565. ) {
  566. const { compileBackendClass, compileGroup } = options
  567. const url = new URL(
  568. `${Settings.apis.clsi.url}/project/${projectId}/user/${userId}/build/${buildId}/output/${outputFilePath}`
  569. )
  570. url.searchParams.set('compileBackendClass', compileBackendClass)
  571. url.searchParams.set('compileGroup', compileGroup)
  572. url.searchParams.set('clsiserverid', clsiServerId)
  573. try {
  574. const stream = await fetchStream(url, {
  575. signal: AbortSignal.timeout(OUTPUT_FILE_TIMEOUT_MS),
  576. })
  577. return stream
  578. } catch (err) {
  579. throw new Errors.OutputFileFetchFailedError(
  580. 'failed to fetch output file from CLSI',
  581. {
  582. projectId,
  583. userId,
  584. url,
  585. status: err.response?.status,
  586. }
  587. )
  588. }
  589. }
  590. function _buildRequestFromDocupdater(
  591. projectId,
  592. options,
  593. project,
  594. projectStateHash,
  595. docUpdaterDocs
  596. ) {
  597. const docPath = ProjectEntityHandler.getAllDocPathsFromProject(project)
  598. const docs = {}
  599. for (const doc of docUpdaterDocs || []) {
  600. const path = docPath[doc._id]
  601. docs[path] = doc
  602. }
  603. // send new docs but not files as those are already on the clsi
  604. options = _.clone(options)
  605. options.syncType = 'incremental'
  606. options.syncState = projectStateHash
  607. // create stub doc entries for any possible root docs, if not
  608. // present in the docupdater. This allows finaliseRequest to
  609. // identify the root doc.
  610. const possibleRootDocIds = [options.rootDoc_id, project.rootDoc_id]
  611. for (const rootDocId of possibleRootDocIds) {
  612. if (rootDocId != null && rootDocId in docPath) {
  613. const path = docPath[rootDocId]
  614. if (docs[path] == null) {
  615. docs[path] = { _id: rootDocId, path }
  616. }
  617. }
  618. }
  619. return _finaliseRequest(projectId, options, project, docs, [])
  620. }
  621. async function _buildRequestFromMongo(
  622. projectId,
  623. options,
  624. project,
  625. projectStateHash
  626. ) {
  627. const { docs, files } = await _getContentFromMongo(projectId)
  628. options = {
  629. ...options,
  630. syncType: 'full',
  631. syncState: projectStateHash,
  632. }
  633. return _finaliseRequest(projectId, options, project, docs, files)
  634. }
  635. async function _getContentFromMongo(projectId) {
  636. await DocumentUpdaterHandler.promises.flushProjectToMongo(projectId)
  637. const docs = await ProjectEntityHandler.promises.getAllDocs(projectId)
  638. const files = await ProjectEntityHandler.promises.getAllFiles(projectId)
  639. return { docs, files }
  640. }
  641. function _finaliseRequest(projectId, options, project, docs, files) {
  642. const resources = []
  643. let flags
  644. let rootResourcePath = null
  645. let rootResourcePathOverride = null
  646. let hasMainFile = false
  647. let numberOfDocsInProject = 0
  648. for (let path in docs) {
  649. const doc = docs[path]
  650. path = path.replace(/^\//, '') // Remove leading /
  651. numberOfDocsInProject++
  652. if (doc.lines != null) {
  653. // add doc to resources unless it is just a stub entry
  654. resources.push({
  655. path,
  656. content: doc.lines.join('\n'),
  657. })
  658. }
  659. if (
  660. project.rootDoc_id != null &&
  661. doc._id.toString() === project.rootDoc_id.toString()
  662. ) {
  663. rootResourcePath = path
  664. }
  665. if (
  666. options.rootDoc_id != null &&
  667. doc._id.toString() === options.rootDoc_id.toString()
  668. ) {
  669. rootResourcePathOverride = path
  670. }
  671. if (path === 'main.tex') {
  672. hasMainFile = true
  673. }
  674. }
  675. if (rootResourcePathOverride != null) {
  676. rootResourcePath = rootResourcePathOverride
  677. }
  678. if (rootResourcePath == null) {
  679. if (hasMainFile) {
  680. rootResourcePath = 'main.tex'
  681. } else if (numberOfDocsInProject === 1) {
  682. // only one file, must be the main document
  683. for (const path in docs) {
  684. // Remove leading /
  685. rootResourcePath = path.replace(/^\//, '')
  686. }
  687. } else {
  688. throw new OError('no main file specified', { projectId })
  689. }
  690. }
  691. const historyId = project.overleaf.history.id
  692. if (!historyId) {
  693. throw new OError('project does not have a history id', { projectId })
  694. }
  695. for (let path in files) {
  696. const file = files[path]
  697. path = path.replace(/^\//, '') // Remove leading /
  698. const { bucket, key } = getBlobLocation(historyId, file.hash)
  699. resources.push({
  700. path,
  701. url: `${Settings.apis.filestore.url}/bucket/${bucket}/key/${key}`,
  702. fallbackURL: `${Settings.apis.filestore.url}/project/${project._id}/file/${file._id}`,
  703. modified: file.created?.getTime(),
  704. })
  705. }
  706. if (options.fileLineErrors) {
  707. flags = ['-file-line-error']
  708. }
  709. return {
  710. compile: {
  711. options: {
  712. compiler: project.compiler,
  713. timeout: options.timeout,
  714. imageName: project.imageName,
  715. draft: Boolean(options.draft),
  716. stopOnFirstError: Boolean(options.stopOnFirstError),
  717. check: options.check,
  718. syncType: options.syncType,
  719. syncState: options.syncState,
  720. compileGroup: options.compileGroup,
  721. enablePdfCaching:
  722. (Settings.enablePdfCaching && options.enablePdfCaching) || false,
  723. pdfCachingMinChunkSize: options.pdfCachingMinChunkSize,
  724. flags,
  725. metricsMethod: options.compileGroup,
  726. },
  727. rootResourcePath,
  728. resources,
  729. },
  730. }
  731. }
  732. async function wordCount(projectId, userId, file, options, clsiserverid) {
  733. const { compileBackendClass, compileGroup } = options
  734. const req = await _buildRequest(projectId, options)
  735. const filename = file || req.compile.rootResourcePath
  736. const url = _getCompilerUrl(
  737. compileBackendClass,
  738. compileGroup,
  739. projectId,
  740. userId,
  741. 'wordcount'
  742. )
  743. url.searchParams.set('file', filename)
  744. url.searchParams.set('image', req.compile.options.imageName)
  745. const opts = {
  746. method: 'GET',
  747. }
  748. const { body } = await _makeRequestWithClsiServerId(
  749. projectId,
  750. userId,
  751. compileGroup,
  752. compileBackendClass,
  753. url,
  754. opts,
  755. clsiserverid
  756. )
  757. return body
  758. }
  759. function _getClsiServerIdFromResponse(response) {
  760. const setCookieHeaders = response.headers.raw()['set-cookie'] ?? []
  761. for (const header of setCookieHeaders) {
  762. const cookie = Cookie.parse(header)
  763. if (cookie.key === Settings.clsiCookie.key) {
  764. return cookie.value
  765. }
  766. }
  767. return null
  768. }
  769. module.exports = {
  770. sendRequest: callbackifyMultiResult(sendRequest, [
  771. 'status',
  772. 'outputFiles',
  773. 'clsiServerId',
  774. 'validationProblems',
  775. 'stats',
  776. 'timings',
  777. 'outputUrlPrefix',
  778. 'buildId',
  779. ]),
  780. sendExternalRequest: callbackifyMultiResult(sendExternalRequest, [
  781. 'status',
  782. 'outputFiles',
  783. 'clsiServerId',
  784. 'validationProblems',
  785. 'stats',
  786. 'timings',
  787. 'outputUrlPrefix',
  788. ]),
  789. stopCompile: callbackify(stopCompile),
  790. deleteAuxFiles: callbackify(deleteAuxFiles),
  791. getOutputFileStream: callbackify(getOutputFileStream),
  792. wordCount: callbackify(wordCount),
  793. promises: {
  794. sendRequest,
  795. sendExternalRequest,
  796. stopCompile,
  797. deleteAuxFiles,
  798. getOutputFileStream,
  799. wordCount,
  800. },
  801. }