2024-01-30 10:35:54 -05:00
|
|
|
const { promisify } = require('util')
|
|
|
|
const { promisifyMultiResult } = require('@overleaf/promise-utils')
|
2021-07-12 12:47:15 -04:00
|
|
|
const Settings = require('@overleaf/settings')
|
2020-05-06 06:09:33 -04:00
|
|
|
const Errors = require('./Errors')
|
|
|
|
const Metrics = require('./Metrics')
|
2021-10-06 05:10:28 -04:00
|
|
|
const logger = require('@overleaf/logger')
|
2020-05-06 06:09:33 -04:00
|
|
|
const request = require('requestretry').defaults({
|
|
|
|
maxAttempts: 2,
|
2021-07-13 07:04:42 -04:00
|
|
|
retryDelay: 10,
|
2020-05-06 06:09:33 -04:00
|
|
|
})
|
2014-02-12 05:40:42 -05:00
|
|
|
|
2020-05-06 06:08:21 -04:00
|
|
|
// We have to be quick with HTTP calls because we're holding a lock that
|
|
|
|
// expires after 30 seconds. We can't let any errors in the rest of the stack
|
|
|
|
// hold us up, and need to bail out quickly if there is a problem.
|
2020-05-06 06:09:33 -04:00
|
|
|
const MAX_HTTP_REQUEST_LENGTH = 5000 // 5 seconds
|
2016-04-12 12:10:39 -04:00
|
|
|
|
2021-08-19 10:40:31 -04:00
|
|
|
function updateMetric(method, error, response) {
|
2020-05-06 06:09:33 -04:00
|
|
|
// find the status, with special handling for connection timeouts
|
|
|
|
// https://github.com/request/request#timeouts
|
2021-08-19 10:40:31 -04:00
|
|
|
let status
|
|
|
|
if (error && error.connect === true) {
|
|
|
|
status = `${error.code} (connect)`
|
|
|
|
} else if (error) {
|
|
|
|
status = error.code
|
|
|
|
} else if (response) {
|
|
|
|
status = response.statusCode
|
|
|
|
}
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
Metrics.inc(method, 1, { status })
|
2021-08-19 10:40:31 -04:00
|
|
|
if (error && error.attempts > 1) {
|
2020-05-06 06:09:33 -04:00
|
|
|
Metrics.inc(`${method}-retries`, 1, { status: 'error' })
|
|
|
|
}
|
2021-08-19 10:40:31 -04:00
|
|
|
if (response && response.attempts > 1) {
|
|
|
|
Metrics.inc(`${method}-retries`, 1, { status: 'success' })
|
2020-05-06 06:09:33 -04:00
|
|
|
}
|
|
|
|
}
|
2020-01-14 08:53:50 -05:00
|
|
|
|
2021-08-19 10:40:31 -04:00
|
|
|
function getDoc(projectId, docId, options = {}, _callback) {
|
|
|
|
const timer = new Metrics.Timer('persistenceManager.getDoc')
|
|
|
|
if (typeof options === 'function') {
|
|
|
|
_callback = options
|
|
|
|
options = {}
|
|
|
|
}
|
|
|
|
const callback = function (...args) {
|
|
|
|
timer.done()
|
|
|
|
_callback(...args)
|
|
|
|
}
|
2014-02-12 05:40:42 -05:00
|
|
|
|
2021-08-19 10:40:31 -04:00
|
|
|
const urlPath = `/project/${projectId}/doc/${docId}`
|
|
|
|
const requestParams = {
|
|
|
|
url: `${Settings.apis.web.url}${urlPath}`,
|
|
|
|
method: 'GET',
|
|
|
|
headers: {
|
|
|
|
accept: 'application/json',
|
|
|
|
},
|
|
|
|
auth: {
|
|
|
|
user: Settings.apis.web.user,
|
|
|
|
pass: Settings.apis.web.pass,
|
|
|
|
sendImmediately: true,
|
|
|
|
},
|
|
|
|
jar: false,
|
|
|
|
timeout: MAX_HTTP_REQUEST_LENGTH,
|
|
|
|
}
|
|
|
|
if (options.peek) {
|
|
|
|
requestParams.qs = { peek: 'true' }
|
|
|
|
}
|
|
|
|
request(requestParams, (error, res, body) => {
|
|
|
|
updateMetric('getDoc', error, res)
|
|
|
|
if (error) {
|
|
|
|
logger.error({ err: error, projectId, docId }, 'web API request failed')
|
|
|
|
return callback(new Error('error connecting to web API'))
|
2020-05-06 06:09:33 -04:00
|
|
|
}
|
2021-08-19 10:40:31 -04:00
|
|
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
|
|
try {
|
|
|
|
body = JSON.parse(body)
|
|
|
|
} catch (e) {
|
|
|
|
return callback(e)
|
|
|
|
}
|
|
|
|
if (body.lines == null) {
|
|
|
|
return callback(new Error('web API response had no doc lines'))
|
|
|
|
}
|
|
|
|
if (body.version == null) {
|
|
|
|
return callback(new Error('web API response had no valid doc version'))
|
|
|
|
}
|
|
|
|
if (body.pathname == null) {
|
|
|
|
return callback(new Error('web API response had no valid doc pathname'))
|
|
|
|
}
|
2021-11-22 05:08:43 -05:00
|
|
|
if (!body.pathname) {
|
|
|
|
logger.warn(
|
|
|
|
{ projectId, docId },
|
|
|
|
'missing pathname in PersistenceManager getDoc'
|
|
|
|
)
|
|
|
|
Metrics.inc('pathname', 1, {
|
|
|
|
path: 'PersistenceManager.getDoc',
|
|
|
|
status: body.pathname === '' ? 'zero-length' : 'undefined',
|
|
|
|
})
|
|
|
|
}
|
2023-09-05 17:07:11 -04:00
|
|
|
callback(
|
|
|
|
null,
|
|
|
|
body.lines,
|
|
|
|
body.version,
|
|
|
|
body.ranges,
|
|
|
|
body.pathname,
|
2024-02-09 09:09:40 -05:00
|
|
|
body.projectHistoryId?.toString(),
|
2024-05-28 07:24:06 -04:00
|
|
|
body.historyRangesSupport || false,
|
|
|
|
body.resolvedCommentIds || []
|
2023-09-05 17:07:11 -04:00
|
|
|
)
|
2021-08-19 10:40:31 -04:00
|
|
|
} else if (res.statusCode === 404) {
|
2023-09-05 17:07:11 -04:00
|
|
|
callback(new Errors.NotFoundError(`doc not not found: ${urlPath}`))
|
2022-07-06 05:50:16 -04:00
|
|
|
} else if (res.statusCode === 413) {
|
|
|
|
callback(
|
|
|
|
new Errors.FileTooLargeError(`doc exceeds maximum size: ${urlPath}`)
|
|
|
|
)
|
2021-08-19 10:40:31 -04:00
|
|
|
} else {
|
|
|
|
callback(
|
|
|
|
new Error(`error accessing web API: ${urlPath} ${res.statusCode}`)
|
|
|
|
)
|
2020-05-06 06:09:33 -04:00
|
|
|
}
|
2021-08-19 10:40:31 -04:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
function setDoc(
|
|
|
|
projectId,
|
|
|
|
docId,
|
|
|
|
lines,
|
|
|
|
version,
|
|
|
|
ranges,
|
|
|
|
lastUpdatedAt,
|
|
|
|
lastUpdatedBy,
|
|
|
|
_callback
|
|
|
|
) {
|
|
|
|
const timer = new Metrics.Timer('persistenceManager.setDoc')
|
|
|
|
const callback = function (...args) {
|
|
|
|
timer.done()
|
|
|
|
_callback(...args)
|
|
|
|
}
|
2014-02-12 05:40:42 -05:00
|
|
|
|
2021-08-19 10:40:31 -04:00
|
|
|
const urlPath = `/project/${projectId}/doc/${docId}`
|
|
|
|
request(
|
|
|
|
{
|
|
|
|
url: `${Settings.apis.web.url}${urlPath}`,
|
|
|
|
method: 'POST',
|
|
|
|
json: {
|
|
|
|
lines,
|
|
|
|
ranges,
|
|
|
|
version,
|
|
|
|
lastUpdatedBy,
|
|
|
|
lastUpdatedAt,
|
|
|
|
},
|
|
|
|
auth: {
|
|
|
|
user: Settings.apis.web.user,
|
|
|
|
pass: Settings.apis.web.pass,
|
|
|
|
sendImmediately: true,
|
2020-05-06 06:09:33 -04:00
|
|
|
},
|
2021-08-19 10:40:31 -04:00
|
|
|
jar: false,
|
|
|
|
timeout: MAX_HTTP_REQUEST_LENGTH,
|
|
|
|
},
|
|
|
|
(error, res, body) => {
|
|
|
|
updateMetric('setDoc', error, res)
|
|
|
|
if (error) {
|
|
|
|
logger.error({ err: error, projectId, docId }, 'web API request failed')
|
|
|
|
return callback(new Error('error connecting to web API'))
|
2020-05-06 06:09:33 -04:00
|
|
|
}
|
2021-08-19 10:40:31 -04:00
|
|
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
2022-09-21 08:01:32 -04:00
|
|
|
callback(null, body)
|
2021-08-19 10:40:31 -04:00
|
|
|
} else if (res.statusCode === 404) {
|
2023-09-05 17:07:11 -04:00
|
|
|
callback(new Errors.NotFoundError(`doc not not found: ${urlPath}`))
|
2022-07-06 05:50:16 -04:00
|
|
|
} else if (res.statusCode === 413) {
|
|
|
|
callback(
|
|
|
|
new Errors.FileTooLargeError(`doc exceeds maximum size: ${urlPath}`)
|
|
|
|
)
|
2021-08-19 10:40:31 -04:00
|
|
|
} else {
|
|
|
|
callback(
|
|
|
|
new Error(`error accessing web API: ${urlPath} ${res.statusCode}`)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
2020-05-06 06:09:33 -04:00
|
|
|
}
|
2021-08-19 10:40:31 -04:00
|
|
|
|
2024-01-30 10:35:54 -05:00
|
|
|
module.exports = {
|
|
|
|
getDoc,
|
|
|
|
setDoc,
|
|
|
|
promises: {
|
|
|
|
getDoc: promisifyMultiResult(getDoc, [
|
|
|
|
'lines',
|
|
|
|
'version',
|
|
|
|
'ranges',
|
|
|
|
'pathname',
|
|
|
|
'projectHistoryId',
|
2024-02-09 09:09:40 -05:00
|
|
|
'historyRangesSupport',
|
2024-05-28 07:24:06 -04:00
|
|
|
'resolvedCommentIds',
|
2024-01-30 10:35:54 -05:00
|
|
|
]),
|
|
|
|
setDoc: promisify(setDoc),
|
2024-01-23 05:45:06 -05:00
|
|
|
},
|
2024-01-30 10:35:54 -05:00
|
|
|
}
|