803 lines
23 KiB
JavaScript
803 lines
23 KiB
JavaScript
let CompileController
|
|
const { URL, URLSearchParams } = require('url')
|
|
const { pipeline } = require('stream/promises')
|
|
const { Cookie } = require('tough-cookie')
|
|
const OError = require('@overleaf/o-error')
|
|
const Metrics = require('@overleaf/metrics')
|
|
const ProjectGetter = require('../Project/ProjectGetter')
|
|
const CompileManager = require('./CompileManager')
|
|
const ClsiManager = require('./ClsiManager')
|
|
const logger = require('@overleaf/logger')
|
|
const Settings = require('@overleaf/settings')
|
|
const SessionManager = require('../Authentication/SessionManager')
|
|
const { RateLimiter } = require('../../infrastructure/RateLimiter')
|
|
const ClsiCookieManager = require('./ClsiCookieManager')(
|
|
Settings.apis.clsi?.backendGroupName
|
|
)
|
|
const Path = require('path')
|
|
const AnalyticsManager = require('../Analytics/AnalyticsManager')
|
|
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
|
|
const { callbackify } = require('@overleaf/promise-utils')
|
|
const {
|
|
fetchStreamWithResponse,
|
|
RequestFailedError,
|
|
} = require('@overleaf/fetch-utils')
|
|
|
|
const COMPILE_TIMEOUT_MS = 10 * 60 * 1000
|
|
|
|
const pdfDownloadRateLimiter = new RateLimiter('full-pdf-download', {
|
|
points: 1000,
|
|
duration: 60 * 60,
|
|
})
|
|
|
|
function getOutputFilesArchiveSpecification(projectId, userId, buildId) {
|
|
const fileName = 'output.zip'
|
|
return {
|
|
path: fileName,
|
|
url: CompileController._getFileUrl(projectId, userId, buildId, fileName),
|
|
type: 'zip',
|
|
}
|
|
}
|
|
|
|
function getImageNameForProject(projectId, callback) {
|
|
ProjectGetter.getProject(projectId, { imageName: 1 }, (err, project) => {
|
|
if (err) return callback(err)
|
|
if (!project) return callback(new Error('project not found'))
|
|
callback(null, project.imageName)
|
|
})
|
|
}
|
|
|
|
async function getPdfCachingMinChunkSize(req, res) {
|
|
const { variant } = await SplitTestHandler.promises.getAssignment(
|
|
req,
|
|
res,
|
|
'pdf-caching-min-chunk-size'
|
|
)
|
|
if (variant === 'default') return 1_000_000
|
|
return parseInt(variant, 10)
|
|
}
|
|
|
|
async function _getSplitTestOptions(req, res) {
|
|
// Use the query flags from the editor request for overriding the split test.
|
|
let query = {}
|
|
try {
|
|
const u = new URL(req.headers.referer || req.url, Settings.siteUrl)
|
|
query = Object.fromEntries(u.searchParams.entries())
|
|
} catch (e) {}
|
|
const editorReq = { ...req, query }
|
|
|
|
// Lookup the clsi-cache flag in the backend.
|
|
// We may need to turn off the feature on a short notice, without requiring
|
|
// all users to reload their editor page to disable the feature.
|
|
const { variant: compileFromClsiCacheVariant } =
|
|
await SplitTestHandler.promises.getAssignment(
|
|
editorReq,
|
|
res,
|
|
'compile-from-clsi-cache'
|
|
)
|
|
const compileFromClsiCache = compileFromClsiCacheVariant === 'enabled'
|
|
const { variant: populateClsiCacheVariant } =
|
|
await SplitTestHandler.promises.getAssignment(
|
|
editorReq,
|
|
res,
|
|
'populate-clsi-cache'
|
|
)
|
|
const populateClsiCache = populateClsiCacheVariant === 'enabled'
|
|
|
|
const pdfDownloadDomain = Settings.pdfDownloadDomain
|
|
|
|
if (!req.query.enable_pdf_caching) {
|
|
// The frontend does not want to do pdf caching.
|
|
return {
|
|
compileFromClsiCache,
|
|
populateClsiCache,
|
|
pdfDownloadDomain,
|
|
enablePdfCaching: false,
|
|
}
|
|
}
|
|
|
|
// Double check with the latest split test assignment.
|
|
// We may need to turn off the feature on a short notice, without requiring
|
|
// all users to reload their editor page to disable the feature.
|
|
const { variant } = await SplitTestHandler.promises.getAssignment(
|
|
editorReq,
|
|
res,
|
|
'pdf-caching-mode'
|
|
)
|
|
const enablePdfCaching = variant === 'enabled'
|
|
if (!enablePdfCaching) {
|
|
// Skip the lookup of the chunk size when caching is not enabled.
|
|
return {
|
|
compileFromClsiCache,
|
|
populateClsiCache,
|
|
pdfDownloadDomain,
|
|
enablePdfCaching: false,
|
|
}
|
|
}
|
|
const pdfCachingMinChunkSize = await getPdfCachingMinChunkSize(editorReq, res)
|
|
return {
|
|
compileFromClsiCache,
|
|
populateClsiCache,
|
|
pdfDownloadDomain,
|
|
enablePdfCaching,
|
|
pdfCachingMinChunkSize,
|
|
}
|
|
}
|
|
const getSplitTestOptionsCb = callbackify(_getSplitTestOptions)
|
|
|
|
module.exports = CompileController = {
|
|
compile(req, res, next) {
|
|
res.setTimeout(COMPILE_TIMEOUT_MS)
|
|
const projectId = req.params.Project_id
|
|
const isAutoCompile = !!req.query.auto_compile
|
|
const fileLineErrors = !!req.query.file_line_errors
|
|
const stopOnFirstError = !!req.body.stopOnFirstError
|
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
|
const options = {
|
|
isAutoCompile,
|
|
fileLineErrors,
|
|
stopOnFirstError,
|
|
editorId: req.body.editorId,
|
|
}
|
|
|
|
if (req.body.rootDoc_id) {
|
|
options.rootDoc_id = req.body.rootDoc_id
|
|
} else if (
|
|
req.body.settingsOverride &&
|
|
req.body.settingsOverride.rootDoc_id
|
|
) {
|
|
// Can be removed after deploy
|
|
options.rootDoc_id = req.body.settingsOverride.rootDoc_id
|
|
}
|
|
if (req.body.compiler) {
|
|
options.compiler = req.body.compiler
|
|
}
|
|
if (req.body.draft) {
|
|
options.draft = req.body.draft
|
|
}
|
|
if (['validate', 'error', 'silent'].includes(req.body.check)) {
|
|
options.check = req.body.check
|
|
}
|
|
if (req.body.incrementalCompilesEnabled) {
|
|
options.incrementalCompilesEnabled = true
|
|
}
|
|
|
|
getSplitTestOptionsCb(req, res, (err, splitTestOptions) => {
|
|
if (err) return next(err)
|
|
let {
|
|
compileFromClsiCache,
|
|
populateClsiCache,
|
|
enablePdfCaching,
|
|
pdfCachingMinChunkSize,
|
|
pdfDownloadDomain,
|
|
} = splitTestOptions
|
|
options.compileFromClsiCache = compileFromClsiCache
|
|
options.populateClsiCache = populateClsiCache
|
|
options.enablePdfCaching = enablePdfCaching
|
|
if (enablePdfCaching) {
|
|
options.pdfCachingMinChunkSize = pdfCachingMinChunkSize
|
|
}
|
|
|
|
CompileManager.compile(
|
|
projectId,
|
|
userId,
|
|
options,
|
|
(
|
|
error,
|
|
status,
|
|
outputFiles,
|
|
clsiServerId,
|
|
limits,
|
|
validationProblems,
|
|
stats,
|
|
timings,
|
|
outputUrlPrefix,
|
|
buildId
|
|
) => {
|
|
if (error) {
|
|
Metrics.inc('compile-error')
|
|
return next(error)
|
|
}
|
|
Metrics.inc('compile-status', 1, { status })
|
|
if (pdfDownloadDomain && outputUrlPrefix) {
|
|
pdfDownloadDomain += outputUrlPrefix
|
|
}
|
|
|
|
if (
|
|
limits &&
|
|
SplitTestHandler.getPercentile(
|
|
AnalyticsManager.getIdsFromSession(req.session).analyticsId,
|
|
'compile-result-backend',
|
|
'release'
|
|
) === 1
|
|
) {
|
|
// For a compile request to be sent to clsi we need limits.
|
|
// If we get here without having the limits object populated, it is
|
|
// a reasonable assumption to make that nothing was compiled.
|
|
// We need to know the limits in order to make use of the events.
|
|
AnalyticsManager.recordEventForSession(
|
|
req.session,
|
|
'compile-result-backend',
|
|
{
|
|
projectId,
|
|
ownerAnalyticsId: limits.ownerAnalyticsId,
|
|
status,
|
|
compileTime: timings?.compileE2E,
|
|
timeout: limits.timeout === 60 ? 'short' : 'long',
|
|
server: clsiServerId?.includes('-c2d-') ? 'faster' : 'normal',
|
|
isAutoCompile,
|
|
isInitialCompile: stats?.isInitialCompile === 1,
|
|
restoredClsiCache: stats?.restoredClsiCache === 1,
|
|
stopOnFirstError,
|
|
}
|
|
)
|
|
}
|
|
|
|
const outputFilesArchive = buildId
|
|
? getOutputFilesArchiveSpecification(projectId, userId, buildId)
|
|
: null
|
|
|
|
res.json({
|
|
status,
|
|
outputFiles,
|
|
outputFilesArchive,
|
|
compileGroup: limits?.compileGroup,
|
|
clsiServerId,
|
|
validationProblems,
|
|
stats,
|
|
timings,
|
|
outputUrlPrefix,
|
|
pdfDownloadDomain,
|
|
pdfCachingMinChunkSize,
|
|
})
|
|
}
|
|
)
|
|
})
|
|
},
|
|
|
|
stopCompile(req, res, next) {
|
|
const projectId = req.params.Project_id
|
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
|
CompileManager.stopCompile(projectId, userId, function (error) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
res.sendStatus(200)
|
|
})
|
|
},
|
|
|
|
// Used for submissions through the public API
|
|
compileSubmission(req, res, next) {
|
|
res.setTimeout(COMPILE_TIMEOUT_MS)
|
|
const submissionId = req.params.submission_id
|
|
const options = {}
|
|
if (req.body?.rootResourcePath != null) {
|
|
options.rootResourcePath = req.body.rootResourcePath
|
|
}
|
|
if (req.body?.compiler) {
|
|
options.compiler = req.body.compiler
|
|
}
|
|
if (req.body?.draft) {
|
|
options.draft = req.body.draft
|
|
}
|
|
if (['validate', 'error', 'silent'].includes(req.body?.check)) {
|
|
options.check = req.body.check
|
|
}
|
|
options.compileGroup =
|
|
req.body?.compileGroup || Settings.defaultFeatures.compileGroup
|
|
options.compileBackendClass = Settings.apis.clsi.submissionBackendClass
|
|
options.timeout =
|
|
req.body?.timeout || Settings.defaultFeatures.compileTimeout
|
|
ClsiManager.sendExternalRequest(
|
|
submissionId,
|
|
req.body,
|
|
options,
|
|
function (error, status, outputFiles, clsiServerId, validationProblems) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
res.json({
|
|
status,
|
|
outputFiles,
|
|
clsiServerId,
|
|
validationProblems,
|
|
})
|
|
}
|
|
)
|
|
},
|
|
|
|
_getSplitTestOptions,
|
|
|
|
_getUserIdForCompile(req) {
|
|
if (!Settings.disablePerUserCompiles) {
|
|
return SessionManager.getLoggedInUserId(req.session)
|
|
}
|
|
return null
|
|
},
|
|
_compileAsUser(req, callback) {
|
|
callback(null, CompileController._getUserIdForCompile(req))
|
|
},
|
|
_downloadAsUser(req, callback) {
|
|
callback(null, CompileController._getUserIdForCompile(req))
|
|
},
|
|
|
|
downloadPdf(req, res, next) {
|
|
Metrics.inc('pdf-downloads')
|
|
const projectId = req.params.Project_id
|
|
const rateLimit = function (callback) {
|
|
pdfDownloadRateLimiter
|
|
.consume(req.ip, 1, { method: 'ip' })
|
|
.then(() => {
|
|
callback(null, true)
|
|
})
|
|
.catch(err => {
|
|
if (err instanceof Error) {
|
|
callback(err)
|
|
} else {
|
|
callback(null, false)
|
|
}
|
|
})
|
|
}
|
|
|
|
ProjectGetter.getProject(projectId, { name: 1 }, function (err, project) {
|
|
if (err) {
|
|
return next(err)
|
|
}
|
|
res.contentType('application/pdf')
|
|
const filename = `${CompileController._getSafeProjectName(project)}.pdf`
|
|
|
|
if (req.query.popupDownload) {
|
|
res.setContentDisposition('attachment', { filename })
|
|
} else {
|
|
res.setContentDisposition('inline', { filename })
|
|
}
|
|
|
|
rateLimit(function (err, canContinue) {
|
|
if (err) {
|
|
logger.err({ err }, 'error checking rate limit for pdf download')
|
|
res.sendStatus(500)
|
|
} else if (!canContinue) {
|
|
logger.debug(
|
|
{ projectId, ip: req.ip },
|
|
'rate limit hit downloading pdf'
|
|
)
|
|
res.sendStatus(500)
|
|
} else {
|
|
CompileController._downloadAsUser(req, function (error, userId) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
const url = CompileController._getFileUrl(
|
|
projectId,
|
|
userId,
|
|
req.params.build_id,
|
|
'output.pdf'
|
|
)
|
|
CompileController.proxyToClsi(
|
|
projectId,
|
|
'output-file',
|
|
url,
|
|
{},
|
|
req,
|
|
res,
|
|
next
|
|
)
|
|
})
|
|
}
|
|
})
|
|
})
|
|
},
|
|
|
|
_getSafeProjectName(project) {
|
|
return project.name.replace(/[^\p{L}\p{Nd}]/gu, '_')
|
|
},
|
|
|
|
deleteAuxFiles(req, res, next) {
|
|
const projectId = req.params.Project_id
|
|
const { clsiserverid } = req.query
|
|
CompileController._compileAsUser(req, function (error, userId) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
CompileManager.deleteAuxFiles(
|
|
projectId,
|
|
userId,
|
|
clsiserverid,
|
|
function (error) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
res.sendStatus(200)
|
|
}
|
|
)
|
|
})
|
|
},
|
|
|
|
// this is only used by templates, so is not called with a userId
|
|
compileAndDownloadPdf(req, res, next) {
|
|
const projectId = req.params.project_id
|
|
// pass userId as null, since templates are an "anonymous" compile
|
|
CompileManager.compile(projectId, null, {}, (err, _status, outputFiles) => {
|
|
if (err) {
|
|
logger.err(
|
|
{ err, projectId },
|
|
'something went wrong compile and downloading pdf'
|
|
)
|
|
res.sendStatus(500)
|
|
return
|
|
}
|
|
const pdf = outputFiles.find(f => f.path === 'output.pdf')
|
|
if (!pdf) {
|
|
logger.warn(
|
|
{ projectId },
|
|
'something went wrong compile and downloading pdf: no pdf'
|
|
)
|
|
res.sendStatus(500)
|
|
return
|
|
}
|
|
CompileController.proxyToClsi(
|
|
projectId,
|
|
'output-file',
|
|
pdf.url,
|
|
{},
|
|
req,
|
|
res,
|
|
next
|
|
)
|
|
})
|
|
},
|
|
|
|
getFileFromClsi(req, res, next) {
|
|
const projectId = req.params.Project_id
|
|
CompileController._downloadAsUser(req, function (error, userId) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
|
|
const qs = {}
|
|
|
|
const url = CompileController._getFileUrl(
|
|
projectId,
|
|
userId,
|
|
req.params.build_id,
|
|
req.params.file
|
|
)
|
|
CompileController.proxyToClsi(
|
|
projectId,
|
|
'output-file',
|
|
url,
|
|
qs,
|
|
req,
|
|
res,
|
|
next
|
|
)
|
|
})
|
|
},
|
|
|
|
getFileFromClsiWithoutUser(req, res, next) {
|
|
const submissionId = req.params.submission_id
|
|
const url = CompileController._getFileUrl(
|
|
submissionId,
|
|
null,
|
|
req.params.build_id,
|
|
req.params.file
|
|
)
|
|
const limits = {
|
|
compileGroup:
|
|
req.body?.compileGroup ||
|
|
req.query?.compileGroup ||
|
|
Settings.defaultFeatures.compileGroup,
|
|
compileBackendClass: Settings.apis.clsi.submissionBackendClass,
|
|
}
|
|
CompileController.proxyToClsiWithLimits(
|
|
submissionId,
|
|
'output-file',
|
|
url,
|
|
{},
|
|
limits,
|
|
req,
|
|
res,
|
|
next
|
|
)
|
|
},
|
|
|
|
// compute a GET file url for a given project, user (optional), build (optional) and file
|
|
_getFileUrl(projectId, userId, buildId, file) {
|
|
let url
|
|
if (userId != null && buildId != null) {
|
|
url = `/project/${projectId}/user/${userId}/build/${buildId}/output/${file}`
|
|
} else if (userId != null) {
|
|
url = `/project/${projectId}/user/${userId}/output/${file}`
|
|
} else if (buildId != null) {
|
|
url = `/project/${projectId}/build/${buildId}/output/${file}`
|
|
} else {
|
|
url = `/project/${projectId}/output/${file}`
|
|
}
|
|
return url
|
|
},
|
|
|
|
// compute a POST url for a project, user (optional) and action
|
|
_getUrl(projectId, userId, action) {
|
|
let path = `/project/${projectId}`
|
|
if (userId != null) {
|
|
path += `/user/${userId}`
|
|
}
|
|
return `${path}/${action}`
|
|
},
|
|
|
|
proxySyncPdf(req, res, next) {
|
|
const projectId = req.params.Project_id
|
|
const { page, h, v, editorId, buildId } = req.query
|
|
if (!page?.match(/^\d+$/)) {
|
|
return next(new Error('invalid page parameter'))
|
|
}
|
|
if (!h?.match(/^-?\d+\.\d+$/)) {
|
|
return next(new Error('invalid h parameter'))
|
|
}
|
|
if (!v?.match(/^-?\d+\.\d+$/)) {
|
|
return next(new Error('invalid v parameter'))
|
|
}
|
|
// whether this request is going to a per-user container
|
|
CompileController._compileAsUser(req, function (error, userId) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
getImageNameForProject(projectId, (error, imageName) => {
|
|
if (error) return next(error)
|
|
|
|
getSplitTestOptionsCb(req, res, (error, splitTestOptions) => {
|
|
if (error) return next(error)
|
|
const { compileFromClsiCache } = splitTestOptions
|
|
|
|
const url = CompileController._getUrl(projectId, userId, 'sync/pdf')
|
|
|
|
CompileController.proxyToClsi(
|
|
projectId,
|
|
'sync-to-pdf',
|
|
url,
|
|
{ page, h, v, imageName, editorId, buildId, compileFromClsiCache },
|
|
req,
|
|
res,
|
|
next
|
|
)
|
|
})
|
|
})
|
|
})
|
|
},
|
|
|
|
proxySyncCode(req, res, next) {
|
|
const projectId = req.params.Project_id
|
|
const { file, line, column, editorId, buildId } = req.query
|
|
if (file == null) {
|
|
return next(new Error('missing file parameter'))
|
|
}
|
|
// Check that we are dealing with a simple file path (this is not
|
|
// strictly needed because synctex uses this parameter as a label
|
|
// to look up in the synctex output, and does not open the file
|
|
// itself). Since we have valid synctex paths like foo/./bar we
|
|
// allow those by replacing /./ with /
|
|
const testPath = file.replace('/./', '/')
|
|
if (Path.resolve('/', testPath) !== `/${testPath}`) {
|
|
return next(new Error('invalid file parameter'))
|
|
}
|
|
if (!line?.match(/^\d+$/)) {
|
|
return next(new Error('invalid line parameter'))
|
|
}
|
|
if (!column?.match(/^\d+$/)) {
|
|
return next(new Error('invalid column parameter'))
|
|
}
|
|
CompileController._compileAsUser(req, function (error, userId) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
getImageNameForProject(projectId, (error, imageName) => {
|
|
if (error) return next(error)
|
|
|
|
getSplitTestOptionsCb(req, res, (error, splitTestOptions) => {
|
|
if (error) return next(error)
|
|
const { compileFromClsiCache } = splitTestOptions
|
|
|
|
const url = CompileController._getUrl(projectId, userId, 'sync/code')
|
|
CompileController.proxyToClsi(
|
|
projectId,
|
|
'sync-to-code',
|
|
url,
|
|
{
|
|
file,
|
|
line,
|
|
column,
|
|
imageName,
|
|
editorId,
|
|
buildId,
|
|
compileFromClsiCache,
|
|
},
|
|
req,
|
|
res,
|
|
next
|
|
)
|
|
})
|
|
})
|
|
})
|
|
},
|
|
|
|
proxyToClsi(projectId, action, url, qs, req, res, next) {
|
|
CompileManager.getProjectCompileLimits(projectId, function (error, limits) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
CompileController.proxyToClsiWithLimits(
|
|
projectId,
|
|
action,
|
|
url,
|
|
qs,
|
|
limits,
|
|
req,
|
|
res,
|
|
next
|
|
)
|
|
})
|
|
},
|
|
|
|
proxyToClsiWithLimits(projectId, action, url, qs, limits, req, res, next) {
|
|
_getPersistenceOptions(
|
|
req,
|
|
projectId,
|
|
limits.compileGroup,
|
|
limits.compileBackendClass,
|
|
(err, persistenceOptions) => {
|
|
if (err) {
|
|
OError.tag(err, 'error getting cookie jar for clsi request')
|
|
return next(err)
|
|
}
|
|
url = new URL(`${Settings.apis.clsi.url}${url}`)
|
|
url.search = new URLSearchParams({
|
|
...persistenceOptions.qs,
|
|
...qs,
|
|
}).toString()
|
|
const timer = new Metrics.Timer(
|
|
'proxy_to_clsi',
|
|
1,
|
|
{ path: action },
|
|
[0, 100, 1000, 2000, 5000, 10000, 15000, 20000, 30000, 45000, 60000]
|
|
)
|
|
Metrics.inc('proxy_to_clsi', 1, { path: action, status: 'start' })
|
|
fetchStreamWithResponse(url.href, {
|
|
method: req.method,
|
|
signal: AbortSignal.timeout(60 * 1000),
|
|
headers: persistenceOptions.headers,
|
|
})
|
|
.then(({ stream, response }) => {
|
|
if (req.destroyed) {
|
|
// The client has disconnected already, avoid trying to write into the broken connection.
|
|
Metrics.inc('proxy_to_clsi', 1, {
|
|
path: action,
|
|
status: 'req-aborted',
|
|
})
|
|
return
|
|
}
|
|
Metrics.inc('proxy_to_clsi', 1, {
|
|
path: action,
|
|
status: response.status,
|
|
})
|
|
|
|
for (const key of ['Content-Length', 'Content-Type']) {
|
|
if (response.headers.has(key)) {
|
|
res.setHeader(key, response.headers.get(key))
|
|
}
|
|
}
|
|
res.writeHead(response.status)
|
|
return pipeline(stream, res)
|
|
})
|
|
.then(() => {
|
|
timer.labels.status = 'success'
|
|
timer.done()
|
|
})
|
|
.catch(err => {
|
|
const reqAborted = Boolean(req.destroyed)
|
|
const status = reqAborted ? 'req-aborted-late' : 'error'
|
|
timer.labels.status = status
|
|
const duration = timer.done()
|
|
Metrics.inc('proxy_to_clsi', 1, { path: action, status })
|
|
const streamingStarted = Boolean(res.headersSent)
|
|
if (!streamingStarted) {
|
|
if (err instanceof RequestFailedError) {
|
|
res.sendStatus(err.response.status)
|
|
} else {
|
|
res.sendStatus(500)
|
|
}
|
|
}
|
|
if (
|
|
streamingStarted &&
|
|
reqAborted &&
|
|
err.code === 'ERR_STREAM_PREMATURE_CLOSE'
|
|
) {
|
|
// Ignore noisy spurious error
|
|
return
|
|
}
|
|
if (
|
|
err instanceof RequestFailedError &&
|
|
['sync-to-code', 'sync-to-pdf', 'output-file'].includes(action)
|
|
) {
|
|
// Ignore noisy error
|
|
// https://github.com/overleaf/internal/issues/15201
|
|
return
|
|
}
|
|
logger.warn(
|
|
{
|
|
err,
|
|
projectId,
|
|
url,
|
|
action,
|
|
reqAborted,
|
|
streamingStarted,
|
|
duration,
|
|
},
|
|
'CLSI proxy error'
|
|
)
|
|
})
|
|
}
|
|
)
|
|
},
|
|
|
|
wordCount(req, res, next) {
|
|
const projectId = req.params.Project_id
|
|
const file = req.query.file || false
|
|
const { clsiserverid } = req.query
|
|
CompileController._compileAsUser(req, function (error, userId) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
CompileManager.wordCount(
|
|
projectId,
|
|
userId,
|
|
file,
|
|
clsiserverid,
|
|
function (error, body) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
res.json(body)
|
|
}
|
|
)
|
|
})
|
|
},
|
|
}
|
|
|
|
function _getPersistenceOptions(
|
|
req,
|
|
projectId,
|
|
compileGroup,
|
|
compileBackendClass,
|
|
callback
|
|
) {
|
|
const { clsiserverid } = req.query
|
|
const userId = SessionManager.getLoggedInUserId(req)
|
|
if (clsiserverid && typeof clsiserverid === 'string') {
|
|
callback(null, {
|
|
qs: { clsiserverid, compileGroup, compileBackendClass },
|
|
headers: {},
|
|
})
|
|
} else {
|
|
ClsiCookieManager.getServerId(
|
|
projectId,
|
|
userId,
|
|
compileGroup,
|
|
compileBackendClass,
|
|
(err, clsiServerId) => {
|
|
if (err) return callback(err)
|
|
callback(null, {
|
|
qs: { compileGroup, compileBackendClass },
|
|
headers: clsiServerId
|
|
? {
|
|
Cookie: new Cookie({
|
|
key: Settings.clsiCookie.key,
|
|
value: clsiServerId,
|
|
}).cookieString(),
|
|
}
|
|
: {},
|
|
})
|
|
}
|
|
)
|
|
}
|
|
}
|