HttpController.js 13 KB

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