HttpController.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import DocManager from './DocManager.js'
  2. import logger from '@overleaf/logger'
  3. import DocArchive from './DocArchiveManager.js'
  4. import HealthChecker from './HealthChecker.js'
  5. import Errors from './Errors.js'
  6. import Settings from '@overleaf/settings'
  7. import { expressify } from '@overleaf/promise-utils'
  8. async function getDoc(req, res) {
  9. const { doc_id: docId, project_id: projectId } = req.params
  10. const includeDeleted = req.query.include_deleted === 'true'
  11. logger.debug({ projectId, docId }, 'getting doc')
  12. const doc = await DocManager.getFullDoc(projectId, docId)
  13. logger.debug({ docId, projectId }, 'got doc')
  14. if (doc.deleted && !includeDeleted) {
  15. res.sendStatus(404)
  16. } else {
  17. res.json(_buildDocView(doc))
  18. }
  19. }
  20. async function peekDoc(req, res) {
  21. const { doc_id: docId, project_id: projectId } = req.params
  22. logger.debug({ projectId, docId }, 'peeking doc')
  23. const doc = await DocManager.peekDoc(projectId, docId)
  24. res.setHeader('x-doc-status', doc.inS3 ? 'archived' : 'active')
  25. res.json(_buildDocView(doc))
  26. }
  27. async function isDocDeleted(req, res) {
  28. const { doc_id: docId, project_id: projectId } = req.params
  29. const deleted = await DocManager.isDocDeleted(projectId, docId)
  30. res.json({ deleted })
  31. }
  32. async function getRawDoc(req, res) {
  33. const { doc_id: docId, project_id: projectId } = req.params
  34. logger.debug({ projectId, docId }, 'getting raw doc')
  35. const content = await DocManager.getDocLines(projectId, docId)
  36. res.setHeader('content-type', 'text/plain')
  37. res.send(content)
  38. }
  39. async function getAllDocs(req, res) {
  40. const { project_id: projectId } = req.params
  41. logger.debug({ projectId }, 'getting all docs')
  42. const docs = await DocManager.getAllNonDeletedDocs(projectId, {
  43. lines: true,
  44. rev: true,
  45. })
  46. const docViews = _buildDocsArrayView(projectId, docs)
  47. for (const docView of docViews) {
  48. if (!docView.lines) {
  49. logger.warn({ projectId, docId: docView._id }, 'missing doc lines')
  50. docView.lines = []
  51. }
  52. }
  53. res.json(docViews)
  54. }
  55. async function getAllDocsWithRanges(req, res) {
  56. const { project_id: projectId } = req.params
  57. logger.debug({ projectId }, 'getting all docs with ranges')
  58. const docs = await DocManager.getAllNonDeletedDocs(projectId, {
  59. lines: true,
  60. rev: true,
  61. ranges: true,
  62. })
  63. const docViews = _buildDocsArrayView(projectId, docs)
  64. for (const docView of docViews) {
  65. if (!docView.lines) {
  66. logger.warn({ projectId, docId: docView._id }, 'missing doc lines')
  67. docView.lines = []
  68. }
  69. }
  70. res.json(docViews)
  71. }
  72. async function getAllDocVersions(req, res) {
  73. const { project_id: projectId } = req.params
  74. const docs = await DocManager.getAllDocVersions(projectId)
  75. res.json(docs)
  76. }
  77. async function getAllDeletedDocs(req, res) {
  78. const { project_id: projectId } = req.params
  79. logger.debug({ projectId }, 'getting all deleted docs')
  80. const docs = await DocManager.getAllDeletedDocs(projectId, {
  81. name: true,
  82. deletedAt: true,
  83. })
  84. res.json(
  85. docs.map(doc => ({
  86. _id: doc._id.toString(),
  87. name: doc.name,
  88. deletedAt: doc.deletedAt,
  89. }))
  90. )
  91. }
  92. async function getAllRanges(req, res) {
  93. const { project_id: projectId } = req.params
  94. logger.debug({ projectId }, 'getting all ranges')
  95. const docs = await DocManager.getAllNonDeletedDocs(projectId, {
  96. ranges: true,
  97. })
  98. res.json(_buildDocsArrayView(projectId, docs))
  99. }
  100. async function getCommentThreadIds(req, res) {
  101. const { project_id: projectId } = req.params
  102. const threadIds = await DocManager.getCommentThreadIds(projectId)
  103. res.json(threadIds)
  104. }
  105. async function getTrackedChangesUserIds(req, res) {
  106. const { project_id: projectId } = req.params
  107. const userIds = await DocManager.getTrackedChangesUserIds(projectId)
  108. res.json(userIds)
  109. }
  110. async function projectHasRanges(req, res) {
  111. const { project_id: projectId } = req.params
  112. const projectHasRanges = await DocManager.projectHasRanges(projectId)
  113. res.json({ projectHasRanges })
  114. }
  115. async function updateDoc(req, res) {
  116. const { doc_id: docId, project_id: projectId } = req.params
  117. const lines = req.body?.lines
  118. const version = req.body?.version
  119. const ranges = req.body?.ranges
  120. if (lines == null || !(lines instanceof Array)) {
  121. logger.error({ projectId, docId }, 'no doc lines provided')
  122. res.sendStatus(400) // Bad Request
  123. return
  124. }
  125. if (version == null || typeof version !== 'number') {
  126. logger.error({ projectId, docId }, 'no doc version provided')
  127. res.sendStatus(400) // Bad Request
  128. return
  129. }
  130. if (ranges == null) {
  131. logger.error({ projectId, docId }, 'no doc ranges provided')
  132. res.sendStatus(400) // Bad Request
  133. return
  134. }
  135. const bodyLength = lines.reduce((len, line) => line.length + len, 0)
  136. if (bodyLength > Settings.max_doc_length) {
  137. logger.error({ projectId, docId, bodyLength }, 'document body too large')
  138. res.status(413).send('document body too large')
  139. return
  140. }
  141. logger.debug({ projectId, docId }, 'got http request to update doc')
  142. const { modified, rev } = await DocManager.updateDoc(
  143. projectId,
  144. docId,
  145. lines,
  146. version,
  147. ranges
  148. )
  149. res.json({
  150. modified,
  151. rev,
  152. })
  153. }
  154. async function patchDoc(req, res) {
  155. const { doc_id: docId, project_id: projectId } = req.params
  156. logger.debug({ projectId, docId }, 'patching doc')
  157. const allowedFields = ['deleted', 'deletedAt', 'name']
  158. const meta = {}
  159. Object.entries(req.body).forEach(([field, value]) => {
  160. if (allowedFields.includes(field)) {
  161. meta[field] = value
  162. } else {
  163. logger.fatal({ field }, 'joi validation for pathDoc is broken')
  164. }
  165. })
  166. await DocManager.patchDoc(projectId, docId, meta)
  167. res.sendStatus(204)
  168. }
  169. function _buildDocView(doc) {
  170. const docView = { _id: doc._id?.toString() }
  171. for (const attribute of ['lines', 'rev', 'version', 'ranges', 'deleted']) {
  172. if (doc[attribute] != null) {
  173. docView[attribute] = doc[attribute]
  174. }
  175. }
  176. return docView
  177. }
  178. function _buildDocsArrayView(projectId, docs) {
  179. const docViews = []
  180. for (const doc of docs) {
  181. if (doc != null) {
  182. // There can end up being null docs for some reason :( (probably a race condition)
  183. docViews.push(_buildDocView(doc))
  184. } else {
  185. logger.error(
  186. { err: new Error('null doc'), projectId },
  187. 'encountered null doc'
  188. )
  189. }
  190. }
  191. return docViews
  192. }
  193. async function archiveAllDocs(req, res) {
  194. const { project_id: projectId } = req.params
  195. logger.debug({ projectId }, 'archiving all docs')
  196. await DocArchive.archiveAllDocs(projectId)
  197. res.sendStatus(204)
  198. }
  199. async function archiveDoc(req, res) {
  200. const { doc_id: docId, project_id: projectId } = req.params
  201. logger.debug({ projectId, docId }, 'archiving a doc')
  202. await DocArchive.archiveDoc(projectId, docId)
  203. res.sendStatus(204)
  204. }
  205. async function unArchiveAllDocs(req, res) {
  206. const { project_id: projectId } = req.params
  207. logger.debug({ projectId }, 'unarchiving all docs')
  208. try {
  209. await DocArchive.unArchiveAllDocs(projectId)
  210. } catch (err) {
  211. if (err instanceof Errors.DocRevValueError) {
  212. logger.warn({ err }, 'Failed to unarchive doc')
  213. return res.sendStatus(409)
  214. }
  215. throw err
  216. }
  217. res.sendStatus(200)
  218. }
  219. async function destroyProject(req, res) {
  220. const { project_id: projectId } = req.params
  221. logger.debug({ projectId }, 'destroying all docs')
  222. await DocArchive.destroyProject(projectId)
  223. res.sendStatus(204)
  224. }
  225. async function healthCheck(req, res) {
  226. try {
  227. await HealthChecker.check()
  228. } catch (err) {
  229. logger.err({ err }, 'error performing health check')
  230. res.sendStatus(500)
  231. return
  232. }
  233. res.sendStatus(200)
  234. }
  235. export default {
  236. getDoc: expressify(getDoc),
  237. peekDoc: expressify(peekDoc),
  238. isDocDeleted: expressify(isDocDeleted),
  239. getRawDoc: expressify(getRawDoc),
  240. getAllDocs: expressify(getAllDocs),
  241. getAllDocsWithRanges: expressify(getAllDocsWithRanges),
  242. getAllDeletedDocs: expressify(getAllDeletedDocs),
  243. getAllRanges: expressify(getAllRanges),
  244. getAllDocVersions: expressify(getAllDocVersions),
  245. getTrackedChangesUserIds: expressify(getTrackedChangesUserIds),
  246. getCommentThreadIds: expressify(getCommentThreadIds),
  247. projectHasRanges: expressify(projectHasRanges),
  248. updateDoc: expressify(updateDoc),
  249. patchDoc: expressify(patchDoc),
  250. archiveAllDocs: expressify(archiveAllDocs),
  251. archiveDoc: expressify(archiveDoc),
  252. unArchiveAllDocs: expressify(unArchiveAllDocs),
  253. destroyProject: expressify(destroyProject),
  254. healthCheck: expressify(healthCheck),
  255. }