HttpController.js 7.3 KB

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