HttpController.js 8.5 KB

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