HttpController.js 8.5 KB

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