mirror of
https://github.com/overleaf/overleaf.git
synced 2024-10-24 21:12:38 -04:00
de4091f955
Add a lock around doc archiving GitOrigin-RevId: eaf85dbc3b491edd15eeb2c1a84df3a2883fb61d
301 lines
7.4 KiB
JavaScript
301 lines
7.4 KiB
JavaScript
const DocManager = require('./DocManager')
|
|
const logger = require('@overleaf/logger')
|
|
const DocArchive = require('./DocArchiveManager')
|
|
const HealthChecker = require('./HealthChecker')
|
|
const Errors = require('./Errors')
|
|
const Settings = require('@overleaf/settings')
|
|
|
|
function getDoc(req, res, next) {
|
|
const { doc_id: docId, project_id: projectId } = req.params
|
|
const includeDeleted = req.query.include_deleted === 'true'
|
|
logger.debug({ projectId, docId }, 'getting doc')
|
|
DocManager.getFullDoc(projectId, docId, function (error, doc) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
logger.debug({ docId, projectId }, 'got doc')
|
|
if (doc == null) {
|
|
res.sendStatus(404)
|
|
} else if (doc.deleted && !includeDeleted) {
|
|
res.sendStatus(404)
|
|
} else {
|
|
res.json(_buildDocView(doc))
|
|
}
|
|
})
|
|
}
|
|
|
|
function peekDoc(req, res, next) {
|
|
const { doc_id: docId, project_id: projectId } = req.params
|
|
logger.debug({ projectId, docId }, 'peeking doc')
|
|
DocManager.peekDoc(projectId, docId, function (error, doc) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
if (doc == null) {
|
|
res.sendStatus(404)
|
|
} else {
|
|
res.setHeader('x-doc-status', doc.inS3 ? 'archived' : 'active')
|
|
res.json(_buildDocView(doc))
|
|
}
|
|
})
|
|
}
|
|
|
|
function isDocDeleted(req, res, next) {
|
|
const { doc_id: docId, project_id: projectId } = req.params
|
|
DocManager.isDocDeleted(projectId, docId, function (error, deleted) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
res.json({ deleted })
|
|
})
|
|
}
|
|
|
|
function getRawDoc(req, res, next) {
|
|
const { doc_id: docId, project_id: projectId } = req.params
|
|
logger.debug({ projectId, docId }, 'getting raw doc')
|
|
DocManager.getDocLines(projectId, docId, function (error, doc) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
if (doc == null) {
|
|
res.sendStatus(404)
|
|
} else {
|
|
res.setHeader('content-type', 'text/plain')
|
|
res.send(_buildRawDocView(doc))
|
|
}
|
|
})
|
|
}
|
|
|
|
function getAllDocs(req, res, next) {
|
|
const { project_id: projectId } = req.params
|
|
logger.debug({ projectId }, 'getting all docs')
|
|
DocManager.getAllNonDeletedDocs(
|
|
projectId,
|
|
{ lines: true, rev: true },
|
|
function (error, docs) {
|
|
if (docs == null) {
|
|
docs = []
|
|
}
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
res.json(_buildDocsArrayView(projectId, docs))
|
|
}
|
|
)
|
|
}
|
|
|
|
function getAllDeletedDocs(req, res, next) {
|
|
const { project_id: projectId } = req.params
|
|
logger.debug({ projectId }, 'getting all deleted docs')
|
|
DocManager.getAllDeletedDocs(
|
|
projectId,
|
|
{ name: true, deletedAt: true },
|
|
function (error, docs) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
res.json(
|
|
docs.map(doc => ({
|
|
_id: doc._id.toString(),
|
|
name: doc.name,
|
|
deletedAt: doc.deletedAt,
|
|
}))
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
function getAllRanges(req, res, next) {
|
|
const { project_id: projectId } = req.params
|
|
logger.debug({ projectId }, 'getting all ranges')
|
|
DocManager.getAllNonDeletedDocs(
|
|
projectId,
|
|
{ ranges: true },
|
|
function (error, docs) {
|
|
if (docs == null) {
|
|
docs = []
|
|
}
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
res.json(_buildDocsArrayView(projectId, docs))
|
|
}
|
|
)
|
|
}
|
|
|
|
function updateDoc(req, res, next) {
|
|
const { doc_id: docId, project_id: projectId } = req.params
|
|
const lines = req.body?.lines
|
|
const version = req.body?.version
|
|
const ranges = req.body?.ranges
|
|
|
|
if (lines == null || !(lines instanceof Array)) {
|
|
logger.error({ projectId, docId }, 'no doc lines provided')
|
|
res.sendStatus(400) // Bad Request
|
|
return
|
|
}
|
|
|
|
if (version == null || typeof version !== 'number') {
|
|
logger.error({ projectId, docId }, 'no doc version provided')
|
|
res.sendStatus(400) // Bad Request
|
|
return
|
|
}
|
|
|
|
if (ranges == null) {
|
|
logger.error({ projectId, docId }, 'no doc ranges provided')
|
|
res.sendStatus(400) // Bad Request
|
|
return
|
|
}
|
|
|
|
const bodyLength = lines.reduce((len, line) => line.length + len, 0)
|
|
if (bodyLength > Settings.max_doc_length) {
|
|
logger.error({ projectId, docId, bodyLength }, 'document body too large')
|
|
res.status(413).send('document body too large')
|
|
return
|
|
}
|
|
|
|
logger.debug({ projectId, docId }, 'got http request to update doc')
|
|
DocManager.updateDoc(
|
|
projectId,
|
|
docId,
|
|
lines,
|
|
version,
|
|
ranges,
|
|
function (error, modified, rev) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
res.json({
|
|
modified,
|
|
rev,
|
|
})
|
|
}
|
|
)
|
|
}
|
|
|
|
function patchDoc(req, res, next) {
|
|
const { doc_id: docId, project_id: projectId } = req.params
|
|
logger.debug({ projectId, docId }, 'patching doc')
|
|
|
|
const allowedFields = ['deleted', 'deletedAt', 'name']
|
|
const meta = {}
|
|
Object.entries(req.body).forEach(([field, value]) => {
|
|
if (allowedFields.includes(field)) {
|
|
meta[field] = value
|
|
} else {
|
|
logger.fatal({ field }, 'joi validation for pathDoc is broken')
|
|
}
|
|
})
|
|
DocManager.patchDoc(projectId, docId, meta, function (error) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
res.sendStatus(204)
|
|
})
|
|
}
|
|
|
|
function _buildDocView(doc) {
|
|
const docView = { _id: doc._id?.toString() }
|
|
for (const attribute of ['lines', 'rev', 'version', 'ranges', 'deleted']) {
|
|
if (doc[attribute] != null) {
|
|
docView[attribute] = doc[attribute]
|
|
}
|
|
}
|
|
return docView
|
|
}
|
|
|
|
function _buildRawDocView(doc) {
|
|
return (doc?.lines ?? []).join('\n')
|
|
}
|
|
|
|
function _buildDocsArrayView(projectId, docs) {
|
|
const docViews = []
|
|
for (const doc of docs) {
|
|
if (doc != null) {
|
|
// There can end up being null docs for some reason :( (probably a race condition)
|
|
docViews.push(_buildDocView(doc))
|
|
} else {
|
|
logger.error(
|
|
{ err: new Error('null doc'), projectId },
|
|
'encountered null doc'
|
|
)
|
|
}
|
|
}
|
|
return docViews
|
|
}
|
|
|
|
function archiveAllDocs(req, res, next) {
|
|
const { project_id: projectId } = req.params
|
|
logger.debug({ projectId }, 'archiving all docs')
|
|
DocArchive.archiveAllDocs(projectId, function (error) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
res.sendStatus(204)
|
|
})
|
|
}
|
|
|
|
function archiveDoc(req, res, next) {
|
|
const { doc_id: docId, project_id: projectId } = req.params
|
|
logger.debug({ projectId, docId }, 'archiving a doc')
|
|
DocArchive.archiveDoc(projectId, docId, function (error) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
res.sendStatus(204)
|
|
})
|
|
}
|
|
|
|
function unArchiveAllDocs(req, res, next) {
|
|
const { project_id: projectId } = req.params
|
|
logger.debug({ projectId }, 'unarchiving all docs')
|
|
DocArchive.unArchiveAllDocs(projectId, function (err) {
|
|
if (err) {
|
|
if (err instanceof Errors.DocRevValueError) {
|
|
logger.warn({ err }, 'Failed to unarchive doc')
|
|
return res.sendStatus(409)
|
|
}
|
|
return next(err)
|
|
}
|
|
res.sendStatus(200)
|
|
})
|
|
}
|
|
|
|
function destroyProject(req, res, next) {
|
|
const { project_id: projectId } = req.params
|
|
logger.debug({ projectId }, 'destroying all docs')
|
|
DocArchive.destroyProject(projectId, function (error) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
res.sendStatus(204)
|
|
})
|
|
}
|
|
|
|
function healthCheck(req, res) {
|
|
HealthChecker.check(function (err) {
|
|
if (err) {
|
|
logger.err({ err }, 'error performing health check')
|
|
res.sendStatus(500)
|
|
} else {
|
|
res.sendStatus(200)
|
|
}
|
|
})
|
|
}
|
|
|
|
module.exports = {
|
|
getDoc,
|
|
peekDoc,
|
|
isDocDeleted,
|
|
getRawDoc,
|
|
getAllDocs,
|
|
getAllDeletedDocs,
|
|
getAllRanges,
|
|
updateDoc,
|
|
patchDoc,
|
|
archiveAllDocs,
|
|
archiveDoc,
|
|
unArchiveAllDocs,
|
|
destroyProject,
|
|
healthCheck,
|
|
}
|