2020-02-19 06:14:37 -05:00
|
|
|
const RequestParser = require('./RequestParser')
|
|
|
|
const CompileManager = require('./CompileManager')
|
2021-07-12 12:47:21 -04:00
|
|
|
const Settings = require('@overleaf/settings')
|
2020-02-19 06:14:37 -05:00
|
|
|
const Metrics = require('./Metrics')
|
|
|
|
const ProjectPersistenceManager = require('./ProjectPersistenceManager')
|
2022-03-01 10:09:36 -05:00
|
|
|
const logger = require('@overleaf/logger')
|
2020-02-19 06:14:37 -05:00
|
|
|
const Errors = require('./Errors')
|
2014-02-12 12:27:43 -05:00
|
|
|
|
2022-06-06 07:41:07 -04:00
|
|
|
let lastSuccessfulCompileTimestamp = 0
|
2021-03-30 08:22:11 -04:00
|
|
|
|
2022-06-06 07:41:07 -04:00
|
|
|
function timeSinceLastSuccessfulCompile() {
|
|
|
|
return Date.now() - lastSuccessfulCompileTimestamp
|
|
|
|
}
|
2022-03-02 07:59:57 -05:00
|
|
|
|
2022-06-06 07:41:07 -04:00
|
|
|
function compile(req, res, next) {
|
|
|
|
const timer = new Metrics.Timer('compile-request')
|
|
|
|
RequestParser.parse(req.body, function (error, request) {
|
|
|
|
if (error) {
|
|
|
|
return next(error)
|
2020-02-19 06:14:37 -05:00
|
|
|
}
|
2022-06-06 07:41:07 -04:00
|
|
|
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)
|
|
|
|
}
|
2022-07-07 08:27:20 -04:00
|
|
|
CompileManager.doCompileWithLock(request, (error, result) => {
|
2024-05-24 05:38:12 -04:00
|
|
|
let { buildId, outputFiles, stats, timings } = result || {}
|
2022-07-07 08:27:20 -04:00
|
|
|
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
|
2022-06-06 07:41:07 -04:00
|
|
|
)
|
2022-07-07 08:27:20 -04:00
|
|
|
) {
|
|
|
|
status = 'success'
|
|
|
|
lastSuccessfulCompileTimestamp = Date.now()
|
|
|
|
} else if (request.stopOnFirstError) {
|
|
|
|
status = 'stopped-on-first-error'
|
|
|
|
} else {
|
|
|
|
status = 'failure'
|
2022-06-06 07:41:07 -04:00
|
|
|
logger.warn(
|
2022-07-07 08:27:20 -04:00
|
|
|
{ projectId: request.project_id, outputFiles },
|
|
|
|
'project failed to compile successfully, no output.pdf generated'
|
2022-06-06 07:41:07 -04:00
|
|
|
)
|
2021-07-13 07:04:48 -04:00
|
|
|
}
|
2020-02-19 06:14:14 -05:00
|
|
|
|
2022-07-07 08:27:20 -04:00
|
|
|
// 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'
|
|
|
|
)
|
2022-06-06 07:41:07 -04:00
|
|
|
}
|
2022-07-07 08:27:20 -04:00
|
|
|
}
|
2016-07-14 09:47:36 -04:00
|
|
|
|
2022-07-07 08:27:20 -04:00
|
|
|
if (error) {
|
|
|
|
outputFiles = error.outputFiles || []
|
2024-05-24 05:38:12 -04:00
|
|
|
buildId = error.buildId
|
2022-06-06 07:41:07 -04:00
|
|
|
}
|
2022-07-07 08:27:20 -04:00
|
|
|
|
|
|
|
timer.done()
|
|
|
|
res.status(code || 200).send({
|
|
|
|
compile: {
|
|
|
|
status,
|
|
|
|
error: error?.message || error,
|
|
|
|
stats,
|
|
|
|
timings,
|
2024-05-24 05:38:12 -04:00
|
|
|
buildId,
|
2022-07-07 08:27:20 -04:00
|
|
|
outputUrlPrefix: Settings.apis.clsi.outputUrlPrefix,
|
2024-05-16 13:39:31 -04:00
|
|
|
outputFiles: outputFiles.map(file => ({
|
|
|
|
url:
|
|
|
|
`${Settings.apis.clsi.url}/project/${request.project_id}` +
|
|
|
|
(request.user_id != null ? `/user/${request.user_id}` : '') +
|
|
|
|
(file.build != null ? `/build/${file.build}` : '') +
|
|
|
|
`/output/${file.path}`,
|
|
|
|
...file,
|
|
|
|
})),
|
2022-07-07 08:27:20 -04:00
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
2020-02-19 06:14:37 -05:00
|
|
|
}
|
|
|
|
)
|
2022-06-06 07:41:07 -04:00
|
|
|
})
|
|
|
|
}
|
2014-04-08 10:18:56 -04:00
|
|
|
|
2022-06-06 07:41:07 -04:00
|
|
|
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)
|
2020-02-19 06:14:37 -05:00
|
|
|
}
|
2022-06-06 07:41:07 -04:00
|
|
|
res.sendStatus(204)
|
|
|
|
})
|
|
|
|
}
|
2021-03-30 08:22:11 -04:00
|
|
|
|
2022-06-06 07:41:07 -04:00
|
|
|
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)
|
2021-03-30 08:22:11 -04:00
|
|
|
}
|
2022-06-06 07:41:07 -04:00
|
|
|
)
|
|
|
|
}
|
2021-03-30 08:22:11 -04:00
|
|
|
|
2022-06-06 07:41:07 -04:00
|
|
|
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)
|
2020-02-19 06:14:37 -05:00
|
|
|
}
|
2022-06-06 07:41:07 -04:00
|
|
|
res.json({
|
|
|
|
pdf: pdfPositions,
|
|
|
|
})
|
2020-02-19 06:14:37 -05:00
|
|
|
}
|
2022-06-06 07:41:07 -04:00
|
|
|
)
|
|
|
|
}
|
2021-03-30 08:22:11 -04:00
|
|
|
|
2022-06-06 07:41:07 -04:00
|
|
|
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)
|
2020-02-19 06:14:37 -05:00
|
|
|
}
|
2022-06-06 07:41:07 -04:00
|
|
|
res.json({
|
|
|
|
code: codePositions,
|
|
|
|
})
|
2020-02-19 06:14:37 -05:00
|
|
|
}
|
2022-06-06 07:41:07 -04:00
|
|
|
)
|
|
|
|
}
|
2015-06-08 17:35:24 -04:00
|
|
|
|
2022-06-06 07:41:07 -04:00
|
|
|
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)
|
2020-02-19 06:14:37 -05:00
|
|
|
}
|
2022-06-06 07:41:07 -04:00
|
|
|
res.json({
|
|
|
|
texcount: result,
|
|
|
|
})
|
2020-02-19 06:14:37 -05:00
|
|
|
}
|
2022-06-06 07:41:07 -04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2020-02-19 06:14:37 -05:00
|
|
|
}
|