HttpController.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. */
  5. let HttpController
  6. const DocumentManager = require('./DocumentManager')
  7. const HistoryManager = require('./HistoryManager')
  8. const ProjectManager = require('./ProjectManager')
  9. const Errors = require('./Errors')
  10. const logger = require('logger-sharelatex')
  11. const Metrics = require('./Metrics')
  12. const ProjectFlusher = require('./ProjectFlusher')
  13. const DeleteQueueManager = require('./DeleteQueueManager')
  14. const async = require('async')
  15. const TWO_MEGABYTES = 2 * 1024 * 1024
  16. module.exports = HttpController = {
  17. getDoc(req, res, next) {
  18. let fromVersion
  19. const { doc_id } = req.params
  20. const { project_id } = req.params
  21. logger.log({ project_id, doc_id }, 'getting doc via http')
  22. const timer = new Metrics.Timer('http.getDoc')
  23. if (req.query.fromVersion != null) {
  24. fromVersion = parseInt(req.query.fromVersion, 10)
  25. } else {
  26. fromVersion = -1
  27. }
  28. DocumentManager.getDocAndRecentOpsWithLock(
  29. project_id,
  30. doc_id,
  31. fromVersion,
  32. function (error, lines, version, ops, ranges, pathname) {
  33. timer.done()
  34. if (error) {
  35. return next(error)
  36. }
  37. logger.log({ project_id, doc_id }, 'got doc via http')
  38. if (lines == null || version == null) {
  39. return next(new Errors.NotFoundError('document not found'))
  40. }
  41. res.json({
  42. id: doc_id,
  43. lines,
  44. version,
  45. ops,
  46. ranges,
  47. pathname
  48. })
  49. }
  50. )
  51. },
  52. _getTotalSizeOfLines(lines) {
  53. let size = 0
  54. for (const line of lines) {
  55. size += line.length + 1
  56. }
  57. return size
  58. },
  59. getProjectDocsAndFlushIfOld(req, res, next) {
  60. const { project_id } = req.params
  61. const projectStateHash = req.query.state
  62. // exclude is string of existing docs "id:version,id:version,..."
  63. const excludeItems =
  64. req.query.exclude != null ? req.query.exclude.split(',') : []
  65. logger.log({ project_id, exclude: excludeItems }, 'getting docs via http')
  66. const timer = new Metrics.Timer('http.getAllDocs')
  67. const excludeVersions = {}
  68. for (const item of excludeItems) {
  69. const [id, version] = item.split(':')
  70. excludeVersions[id] = version
  71. }
  72. logger.log(
  73. { project_id, projectStateHash, excludeVersions },
  74. 'excluding versions'
  75. )
  76. ProjectManager.getProjectDocsAndFlushIfOld(
  77. project_id,
  78. projectStateHash,
  79. excludeVersions,
  80. function (error, result) {
  81. timer.done()
  82. if (error instanceof Errors.ProjectStateChangedError) {
  83. res.sendStatus(409) // conflict
  84. } else if (error) {
  85. next(error)
  86. } else {
  87. logger.log(
  88. {
  89. project_id,
  90. result: result.map((doc) => `${doc._id}:${doc.v}`)
  91. },
  92. 'got docs via http'
  93. )
  94. res.send(result)
  95. }
  96. }
  97. )
  98. },
  99. clearProjectState(req, res, next) {
  100. const { project_id } = req.params
  101. const timer = new Metrics.Timer('http.clearProjectState')
  102. logger.log({ project_id }, 'clearing project state via http')
  103. ProjectManager.clearProjectState(project_id, function (error) {
  104. timer.done()
  105. if (error) {
  106. next(error)
  107. } else {
  108. res.sendStatus(200)
  109. }
  110. })
  111. },
  112. setDoc(req, res, next) {
  113. const { doc_id } = req.params
  114. const { project_id } = req.params
  115. const { lines, source, user_id, undoing } = req.body
  116. const lineSize = HttpController._getTotalSizeOfLines(lines)
  117. if (lineSize > TWO_MEGABYTES) {
  118. logger.log(
  119. { project_id, doc_id, source, lineSize, user_id },
  120. 'document too large, returning 406 response'
  121. )
  122. return res.sendStatus(406)
  123. }
  124. logger.log(
  125. { project_id, doc_id, lines, source, user_id, undoing },
  126. 'setting doc via http'
  127. )
  128. const timer = new Metrics.Timer('http.setDoc')
  129. DocumentManager.setDocWithLock(
  130. project_id,
  131. doc_id,
  132. lines,
  133. source,
  134. user_id,
  135. undoing,
  136. function (error) {
  137. timer.done()
  138. if (error) {
  139. return next(error)
  140. }
  141. logger.log({ project_id, doc_id }, 'set doc via http')
  142. res.sendStatus(204)
  143. }
  144. )
  145. }, // No Content
  146. flushDocIfLoaded(req, res, next) {
  147. const { doc_id } = req.params
  148. const { project_id } = req.params
  149. logger.log({ project_id, doc_id }, 'flushing doc via http')
  150. const timer = new Metrics.Timer('http.flushDoc')
  151. DocumentManager.flushDocIfLoadedWithLock(project_id, doc_id, function (
  152. error
  153. ) {
  154. timer.done()
  155. if (error) {
  156. return next(error)
  157. }
  158. logger.log({ project_id, doc_id }, 'flushed doc via http')
  159. res.sendStatus(204)
  160. })
  161. }, // No Content
  162. deleteDoc(req, res, next) {
  163. const { doc_id } = req.params
  164. const { project_id } = req.params
  165. const ignoreFlushErrors = req.query.ignore_flush_errors === 'true'
  166. const timer = new Metrics.Timer('http.deleteDoc')
  167. logger.log({ project_id, doc_id }, 'deleting doc via http')
  168. DocumentManager.flushAndDeleteDocWithLock(
  169. project_id,
  170. doc_id,
  171. { ignoreFlushErrors },
  172. function (error) {
  173. timer.done()
  174. // There is no harm in flushing project history if the previous call
  175. // failed and sometimes it is required
  176. HistoryManager.flushProjectChangesAsync(project_id)
  177. if (error) {
  178. return next(error)
  179. }
  180. logger.log({ project_id, doc_id }, 'deleted doc via http')
  181. res.sendStatus(204)
  182. }
  183. )
  184. }, // No Content
  185. flushProject(req, res, next) {
  186. const { project_id } = req.params
  187. logger.log({ project_id }, 'flushing project via http')
  188. const timer = new Metrics.Timer('http.flushProject')
  189. ProjectManager.flushProjectWithLocks(project_id, function (error) {
  190. timer.done()
  191. if (error) {
  192. return next(error)
  193. }
  194. logger.log({ project_id }, 'flushed project via http')
  195. res.sendStatus(204)
  196. })
  197. }, // No Content
  198. deleteProject(req, res, next) {
  199. const { project_id } = req.params
  200. logger.log({ project_id }, 'deleting project via http')
  201. const options = {}
  202. if (req.query.background) {
  203. options.background = true
  204. } // allow non-urgent flushes to be queued
  205. if (req.query.shutdown) {
  206. options.skip_history_flush = true
  207. } // don't flush history when realtime shuts down
  208. if (req.query.background) {
  209. ProjectManager.queueFlushAndDeleteProject(project_id, function (error) {
  210. if (error) {
  211. return next(error)
  212. }
  213. logger.log({ project_id }, 'queue delete of project via http')
  214. res.sendStatus(204)
  215. }) // No Content
  216. } else {
  217. const timer = new Metrics.Timer('http.deleteProject')
  218. ProjectManager.flushAndDeleteProjectWithLocks(
  219. project_id,
  220. options,
  221. function (error) {
  222. timer.done()
  223. if (error) {
  224. return next(error)
  225. }
  226. logger.log({ project_id }, 'deleted project via http')
  227. res.sendStatus(204)
  228. }
  229. )
  230. }
  231. }, // No Content
  232. deleteMultipleProjects(req, res, next) {
  233. const project_ids = req.body.project_ids || []
  234. logger.log({ project_ids }, 'deleting multiple projects via http')
  235. async.eachSeries(
  236. project_ids,
  237. function (project_id, cb) {
  238. logger.log({ project_id }, 'queue delete of project via http')
  239. ProjectManager.queueFlushAndDeleteProject(project_id, cb)
  240. },
  241. function (error) {
  242. if (error) {
  243. return next(error)
  244. }
  245. res.sendStatus(204)
  246. }
  247. )
  248. }, // No Content
  249. acceptChanges(req, res, next) {
  250. const { project_id, doc_id } = req.params
  251. let change_ids = req.body.change_ids
  252. if (change_ids == null) {
  253. change_ids = [req.params.change_id]
  254. }
  255. logger.log(
  256. { project_id, doc_id },
  257. `accepting ${change_ids.length} changes via http`
  258. )
  259. const timer = new Metrics.Timer('http.acceptChanges')
  260. DocumentManager.acceptChangesWithLock(
  261. project_id,
  262. doc_id,
  263. change_ids,
  264. function (error) {
  265. timer.done()
  266. if (error) {
  267. return next(error)
  268. }
  269. logger.log(
  270. { project_id, doc_id },
  271. `accepted ${change_ids.length} changes via http`
  272. )
  273. res.sendStatus(204)
  274. }
  275. )
  276. }, // No Content
  277. deleteComment(req, res, next) {
  278. const { project_id, doc_id, comment_id } = req.params
  279. logger.log({ project_id, doc_id, comment_id }, 'deleting comment via http')
  280. const timer = new Metrics.Timer('http.deleteComment')
  281. DocumentManager.deleteCommentWithLock(
  282. project_id,
  283. doc_id,
  284. comment_id,
  285. function (error) {
  286. timer.done()
  287. if (error) {
  288. return next(error)
  289. }
  290. logger.log(
  291. { project_id, doc_id, comment_id },
  292. 'deleted comment via http'
  293. )
  294. res.sendStatus(204)
  295. }
  296. )
  297. }, // No Content
  298. updateProject(req, res, next) {
  299. const timer = new Metrics.Timer('http.updateProject')
  300. const { project_id } = req.params
  301. const {
  302. projectHistoryId,
  303. userId,
  304. docUpdates,
  305. fileUpdates,
  306. version
  307. } = req.body
  308. logger.log(
  309. { project_id, docUpdates, fileUpdates, version },
  310. 'updating project via http'
  311. )
  312. ProjectManager.updateProjectWithLocks(
  313. project_id,
  314. projectHistoryId,
  315. userId,
  316. docUpdates,
  317. fileUpdates,
  318. version,
  319. function (error) {
  320. timer.done()
  321. if (error) {
  322. return next(error)
  323. }
  324. logger.log({ project_id }, 'updated project via http')
  325. res.sendStatus(204)
  326. }
  327. )
  328. }, // No Content
  329. resyncProjectHistory(req, res, next) {
  330. const { project_id } = req.params
  331. const { projectHistoryId, docs, files } = req.body
  332. logger.log(
  333. { project_id, docs, files },
  334. 'queuing project history resync via http'
  335. )
  336. HistoryManager.resyncProjectHistory(
  337. project_id,
  338. projectHistoryId,
  339. docs,
  340. files,
  341. function (error) {
  342. if (error) {
  343. return next(error)
  344. }
  345. logger.log({ project_id }, 'queued project history resync via http')
  346. res.sendStatus(204)
  347. }
  348. )
  349. },
  350. flushAllProjects(req, res, next) {
  351. res.setTimeout(5 * 60 * 1000)
  352. const options = {
  353. limit: req.query.limit || 1000,
  354. concurrency: req.query.concurrency || 5,
  355. dryRun: req.query.dryRun || false
  356. }
  357. ProjectFlusher.flushAllProjects(options, function (err, project_ids) {
  358. if (err) {
  359. logger.err({ err }, 'error bulk flushing projects')
  360. res.sendStatus(500)
  361. } else {
  362. res.send(project_ids)
  363. }
  364. })
  365. },
  366. flushQueuedProjects(req, res, next) {
  367. res.setTimeout(10 * 60 * 1000)
  368. const options = {
  369. limit: req.query.limit || 1000,
  370. timeout: 5 * 60 * 1000,
  371. min_delete_age: req.query.min_delete_age || 5 * 60 * 1000
  372. }
  373. DeleteQueueManager.flushAndDeleteOldProjects(options, function (
  374. err,
  375. flushed
  376. ) {
  377. if (err) {
  378. logger.err({ err }, 'error flushing old projects')
  379. res.sendStatus(500)
  380. } else {
  381. logger.log({ flushed }, 'flush of queued projects completed')
  382. res.send({ flushed })
  383. }
  384. })
  385. }
  386. }