| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- const { promisify } = require('node:util')
- const { promisifyMultiResult } = require('@overleaf/promise-utils')
- const Settings = require('@overleaf/settings')
- const Errors = require('./Errors')
- const Metrics = require('./Metrics')
- const logger = require('@overleaf/logger')
- const request = require('requestretry').defaults({
- maxAttempts: 2,
- retryDelay: 10,
- })
- // 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.
- const MAX_HTTP_REQUEST_LENGTH = 5000 // 5 seconds
- function updateMetric(method, error, response) {
- // find the status, with special handling for connection timeouts
- // https://github.com/request/request#timeouts
- let status
- if (error && error.connect === true) {
- status = `${error.code} (connect)`
- } else if (error) {
- status = error.code
- } else if (response) {
- status = response.statusCode
- }
- Metrics.inc(method, 1, { status })
- if (error && error.attempts > 1) {
- Metrics.inc(`${method}-retries`, 1, { status: 'error' })
- }
- if (response && response.attempts > 1) {
- Metrics.inc(`${method}-retries`, 1, { status: 'success' })
- }
- }
- 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)
- }
- 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'))
- }
- 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'))
- }
- 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',
- })
- }
- if (body.otMigrationStage > 0) {
- // Use history-ot
- body.lines = { content: body.lines.join('\n') }
- body.ranges = {}
- }
- callback(
- null,
- body.lines,
- body.version,
- body.ranges,
- body.pathname,
- body.projectHistoryId?.toString(),
- body.historyRangesSupport || false,
- body.resolvedCommentIds || []
- )
- } else if (res.statusCode === 404) {
- callback(new Errors.NotFoundError(`doc not not found: ${urlPath}`))
- } else if (res.statusCode === 413) {
- callback(
- new Errors.FileTooLargeError(`doc exceeds maximum size: ${urlPath}`)
- )
- } else {
- callback(
- new Error(`error accessing web API: ${urlPath} ${res.statusCode}`)
- )
- }
- })
- }
- 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)
- }
- 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,
- },
- 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'))
- }
- if (res.statusCode >= 200 && res.statusCode < 300) {
- callback(null, body)
- } else if (res.statusCode === 404) {
- callback(new Errors.NotFoundError(`doc not not found: ${urlPath}`))
- } else if (res.statusCode === 413) {
- callback(
- new Errors.FileTooLargeError(`doc exceeds maximum size: ${urlPath}`)
- )
- } else {
- callback(
- new Error(`error accessing web API: ${urlPath} ${res.statusCode}`)
- )
- }
- }
- )
- }
- module.exports = {
- getDoc,
- setDoc,
- promises: {
- getDoc: promisifyMultiResult(getDoc, [
- 'lines',
- 'version',
- 'ranges',
- 'pathname',
- 'projectHistoryId',
- 'historyRangesSupport',
- 'resolvedCommentIds',
- ]),
- setDoc: promisify(setDoc),
- },
- }
|