HttpController.js 13 KB

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