HttpController.js 13 KB

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