HttpController.js 7.9 KB

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