HttpController.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  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. res.json(_buildDocsArrayView(projectId, docs))
  78. }
  79. )
  80. }
  81. function getAllDeletedDocs(req, res, next) {
  82. const { project_id: projectId } = req.params
  83. logger.debug({ projectId }, 'getting all deleted docs')
  84. DocManager.getAllDeletedDocs(
  85. projectId,
  86. { name: true, deletedAt: true },
  87. function (error, docs) {
  88. if (error) {
  89. return next(error)
  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. )
  100. }
  101. function getAllRanges(req, res, next) {
  102. const { project_id: projectId } = req.params
  103. logger.debug({ projectId }, 'getting all ranges')
  104. DocManager.getAllNonDeletedDocs(
  105. projectId,
  106. { ranges: true },
  107. function (error, docs) {
  108. if (docs == null) {
  109. docs = []
  110. }
  111. if (error) {
  112. return next(error)
  113. }
  114. res.json(_buildDocsArrayView(projectId, docs))
  115. }
  116. )
  117. }
  118. function updateDoc(req, res, next) {
  119. const { doc_id: docId, project_id: projectId } = req.params
  120. const lines = req.body?.lines
  121. const version = req.body?.version
  122. const ranges = req.body?.ranges
  123. if (lines == null || !(lines instanceof Array)) {
  124. logger.error({ projectId, docId }, 'no doc lines provided')
  125. res.sendStatus(400) // Bad Request
  126. return
  127. }
  128. if (version == null || typeof version !== 'number') {
  129. logger.error({ projectId, docId }, 'no doc version provided')
  130. res.sendStatus(400) // Bad Request
  131. return
  132. }
  133. if (ranges == null) {
  134. logger.error({ projectId, docId }, 'no doc ranges provided')
  135. res.sendStatus(400) // Bad Request
  136. return
  137. }
  138. const bodyLength = lines.reduce((len, line) => line.length + len, 0)
  139. if (bodyLength > Settings.max_doc_length) {
  140. logger.error({ projectId, docId, bodyLength }, 'document body too large')
  141. res.status(413).send('document body too large')
  142. return
  143. }
  144. logger.debug({ projectId, docId }, 'got http request to update doc')
  145. DocManager.updateDoc(
  146. projectId,
  147. docId,
  148. lines,
  149. version,
  150. ranges,
  151. function (error, modified, rev) {
  152. if (error) {
  153. return next(error)
  154. }
  155. res.json({
  156. modified,
  157. rev,
  158. })
  159. }
  160. )
  161. }
  162. function patchDoc(req, res, next) {
  163. const { doc_id: docId, project_id: projectId } = req.params
  164. logger.debug({ projectId, docId }, 'patching doc')
  165. const allowedFields = ['deleted', 'deletedAt', 'name']
  166. const meta = {}
  167. Object.entries(req.body).forEach(([field, value]) => {
  168. if (allowedFields.includes(field)) {
  169. meta[field] = value
  170. } else {
  171. logger.fatal({ field }, 'joi validation for pathDoc is broken')
  172. }
  173. })
  174. DocManager.patchDoc(projectId, docId, meta, function (error) {
  175. if (error) {
  176. return next(error)
  177. }
  178. res.sendStatus(204)
  179. })
  180. }
  181. function _buildDocView(doc) {
  182. const docView = { _id: doc._id?.toString() }
  183. for (const attribute of ['lines', 'rev', 'version', 'ranges', 'deleted']) {
  184. if (doc[attribute] != null) {
  185. docView[attribute] = doc[attribute]
  186. }
  187. }
  188. return docView
  189. }
  190. function _buildRawDocView(doc) {
  191. return (doc?.lines ?? []).join('\n')
  192. }
  193. function _buildDocsArrayView(projectId, docs) {
  194. const docViews = []
  195. for (const doc of docs) {
  196. if (doc != null) {
  197. // There can end up being null docs for some reason :( (probably a race condition)
  198. docViews.push(_buildDocView(doc))
  199. } else {
  200. logger.error(
  201. { err: new Error('null doc'), projectId },
  202. 'encountered null doc'
  203. )
  204. }
  205. }
  206. return docViews
  207. }
  208. function archiveAllDocs(req, res, next) {
  209. const { project_id: projectId } = req.params
  210. logger.debug({ projectId }, 'archiving all docs')
  211. DocArchive.archiveAllDocs(projectId, function (error) {
  212. if (error) {
  213. return next(error)
  214. }
  215. res.sendStatus(204)
  216. })
  217. }
  218. function archiveDoc(req, res, next) {
  219. const { doc_id: docId, project_id: projectId } = req.params
  220. logger.debug({ projectId, docId }, 'archiving a doc')
  221. DocArchive.archiveDocById(projectId, docId, function (error) {
  222. if (error) {
  223. return next(error)
  224. }
  225. res.sendStatus(204)
  226. })
  227. }
  228. function unArchiveAllDocs(req, res, next) {
  229. const { project_id: projectId } = req.params
  230. logger.debug({ projectId }, 'unarchiving all docs')
  231. DocArchive.unArchiveAllDocs(projectId, function (err) {
  232. if (err) {
  233. if (err instanceof Errors.DocRevValueError) {
  234. logger.warn({ err }, 'Failed to unarchive doc')
  235. return res.sendStatus(409)
  236. }
  237. return next(err)
  238. }
  239. res.sendStatus(200)
  240. })
  241. }
  242. function destroyProject(req, res, next) {
  243. const { project_id: projectId } = req.params
  244. logger.debug({ projectId }, 'destroying all docs')
  245. DocArchive.destroyProject(projectId, function (error) {
  246. if (error) {
  247. return next(error)
  248. }
  249. res.sendStatus(204)
  250. })
  251. }
  252. function healthCheck(req, res) {
  253. HealthChecker.check(function (err) {
  254. if (err) {
  255. logger.err({ err }, 'error performing health check')
  256. res.sendStatus(500)
  257. } else {
  258. res.sendStatus(200)
  259. }
  260. })
  261. }
  262. module.exports = {
  263. getDoc,
  264. peekDoc,
  265. isDocDeleted,
  266. getRawDoc,
  267. getAllDocs,
  268. getAllDeletedDocs,
  269. getAllRanges,
  270. updateDoc,
  271. patchDoc,
  272. archiveAllDocs,
  273. archiveDoc,
  274. unArchiveAllDocs,
  275. destroyProject,
  276. healthCheck,
  277. }