| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- const RequestParser = require('./RequestParser')
- const CompileManager = require('./CompileManager')
- const Settings = require('@overleaf/settings')
- const Metrics = require('./Metrics')
- const ProjectPersistenceManager = require('./ProjectPersistenceManager')
- const logger = require('@overleaf/logger')
- const Errors = require('./Errors')
- let lastSuccessfulCompileTimestamp = 0
- function timeSinceLastSuccessfulCompile() {
- return Date.now() - lastSuccessfulCompileTimestamp
- }
- function addUrlToOutputFile(outputFile, projectId, userId) {
- return {
- url:
- `${Settings.apis.clsi.url}/project/${projectId}` +
- (userId != null ? `/user/${userId}` : '') +
- (outputFile.build != null ? `/build/${outputFile.build}` : '') +
- `/output/${outputFile.path}`,
- ...outputFile,
- }
- }
- function compile(req, res, next) {
- const timer = new Metrics.Timer('compile-request')
- RequestParser.parse(req.body, function (error, request) {
- if (error) {
- return next(error)
- }
- timer.opts = request.metricsOpts
- request.project_id = req.params.project_id
- if (req.params.user_id != null) {
- request.user_id = req.params.user_id
- }
- ProjectPersistenceManager.markProjectAsJustAccessed(
- request.project_id,
- function (error) {
- if (error) {
- return next(error)
- }
- CompileManager.doCompileWithLock(request, (error, result) => {
- let { buildId, outputFiles, stats, timings } = result || {}
- let code, status
- if (outputFiles == null) {
- outputFiles = []
- }
- if (error instanceof Errors.AlreadyCompilingError) {
- code = 423 // Http 423 Locked
- status = 'compile-in-progress'
- } else if (error instanceof Errors.FilesOutOfSyncError) {
- code = 409 // Http 409 Conflict
- status = 'retry'
- logger.warn(
- {
- projectId: request.project_id,
- userId: request.user_id,
- },
- 'files out of sync, please retry'
- )
- } else if (error?.code === 'EPIPE') {
- // docker returns EPIPE when shutting down
- code = 503 // send 503 Unavailable response
- status = 'unavailable'
- } else if (error?.terminated) {
- status = 'terminated'
- } else if (error?.validate) {
- status = `validation-${error.validate}`
- } else if (error?.timedout) {
- status = 'timedout'
- logger.debug(
- { err: error, projectId: request.project_id },
- 'timeout running compile'
- )
- } else if (error) {
- status = 'error'
- code = 500
- logger.error(
- { err: error, projectId: request.project_id },
- 'error running compile'
- )
- } else {
- if (
- outputFiles.some(
- file => file.path === 'output.pdf' && file.size > 0
- )
- ) {
- status = 'success'
- lastSuccessfulCompileTimestamp = Date.now()
- } else if (request.stopOnFirstError) {
- status = 'stopped-on-first-error'
- } else {
- status = 'failure'
- logger.warn(
- { projectId: request.project_id, outputFiles },
- 'project failed to compile successfully, no output.pdf generated'
- )
- }
- // log an error if any core files are found
- if (outputFiles.some(file => file.path === 'core')) {
- logger.error(
- { projectId: request.project_id, req, outputFiles },
- 'core file found in output'
- )
- }
- }
- if (error) {
- outputFiles = error.outputFiles || []
- buildId = error.buildId
- }
- timer.done()
- res.status(code || 200).send({
- compile: {
- status,
- error: error?.message || error,
- stats,
- timings,
- outputUrlPrefix: Settings.apis.clsi.outputUrlPrefix,
- outputFiles:
- outputFiles.length === 0
- ? []
- : outputFiles
- .map(file =>
- addUrlToOutputFile(
- file,
- request.project_id,
- request.user_id
- )
- )
- .concat(
- addUrlToOutputFile(
- {
- build: buildId,
- path: 'output.zip',
- type: 'zip',
- },
- request.project_id,
- request.user_id
- )
- ),
- },
- })
- })
- }
- )
- })
- }
- function stopCompile(req, res, next) {
- const { project_id: projectId, user_id: userId } = req.params
- CompileManager.stopCompile(projectId, userId, function (error) {
- if (error) {
- return next(error)
- }
- res.sendStatus(204)
- })
- }
- function clearCache(req, res, next) {
- ProjectPersistenceManager.clearProject(
- req.params.project_id,
- req.params.user_id,
- function (error) {
- if (error) {
- return next(error)
- }
- // No content
- res.sendStatus(204)
- }
- )
- }
- function syncFromCode(req, res, next) {
- const { file } = req.query
- const line = parseInt(req.query.line, 10)
- const column = parseInt(req.query.column, 10)
- const { imageName } = req.query
- const projectId = req.params.project_id
- const userId = req.params.user_id
- if (imageName && !_isImageNameAllowed(imageName)) {
- return res.status(400).send('invalid image')
- }
- CompileManager.syncFromCode(
- projectId,
- userId,
- file,
- line,
- column,
- imageName,
- function (error, pdfPositions) {
- if (error) {
- return next(error)
- }
- res.json({
- pdf: pdfPositions,
- })
- }
- )
- }
- function syncFromPdf(req, res, next) {
- const page = parseInt(req.query.page, 10)
- const h = parseFloat(req.query.h)
- const v = parseFloat(req.query.v)
- const { imageName } = req.query
- const projectId = req.params.project_id
- const userId = req.params.user_id
- if (imageName && !_isImageNameAllowed(imageName)) {
- return res.status(400).send('invalid image')
- }
- CompileManager.syncFromPdf(
- projectId,
- userId,
- page,
- h,
- v,
- imageName,
- function (error, codePositions) {
- if (error) {
- return next(error)
- }
- res.json({
- code: codePositions,
- })
- }
- )
- }
- function wordcount(req, res, next) {
- const file = req.query.file || 'main.tex'
- const projectId = req.params.project_id
- const userId = req.params.user_id
- const { image } = req.query
- if (image && !_isImageNameAllowed(image)) {
- return res.status(400).send('invalid image')
- }
- logger.debug({ image, file, projectId }, 'word count request')
- CompileManager.wordcount(
- projectId,
- userId,
- file,
- image,
- function (error, result) {
- if (error) {
- return next(error)
- }
- res.json({
- texcount: result,
- })
- }
- )
- }
- function status(req, res, next) {
- res.send('OK')
- }
- function _isImageNameAllowed(imageName) {
- const ALLOWED_IMAGES =
- Settings.clsi && Settings.clsi.docker && Settings.clsi.docker.allowedImages
- return !ALLOWED_IMAGES || ALLOWED_IMAGES.includes(imageName)
- }
- module.exports = {
- compile,
- stopCompile,
- clearCache,
- syncFromCode,
- syncFromPdf,
- wordcount,
- status,
- timeSinceLastSuccessfulCompile,
- }
|