HttpController.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. const DocumentManager = require('./DocumentManager')
  2. const HistoryManager = require('./HistoryManager')
  3. const ProjectManager = require('./ProjectManager')
  4. const RedisManager = require('./RedisManager')
  5. const Errors = require('./Errors')
  6. const logger = require('@overleaf/logger')
  7. const Settings = require('@overleaf/settings')
  8. const Metrics = require('./Metrics')
  9. const DeleteQueueManager = require('./DeleteQueueManager')
  10. const { getTotalSizeOfLines } = require('./Limits')
  11. const async = require('async')
  12. function getDoc(req, res, next) {
  13. let fromVersion
  14. const docId = req.params.doc_id
  15. const projectId = req.params.project_id
  16. logger.debug({ projectId, docId }, 'getting doc via http')
  17. const timer = new Metrics.Timer('http.getDoc')
  18. if (req.query.fromVersion != null) {
  19. fromVersion = parseInt(req.query.fromVersion, 10)
  20. } else {
  21. fromVersion = -1
  22. }
  23. DocumentManager.getDocAndRecentOpsWithLock(
  24. projectId,
  25. docId,
  26. fromVersion,
  27. (error, lines, version, ops, ranges, pathname) => {
  28. timer.done()
  29. if (error) {
  30. return next(error)
  31. }
  32. logger.debug({ projectId, docId }, 'got doc via http')
  33. if (lines == null || version == null) {
  34. return next(new Errors.NotFoundError('document not found'))
  35. }
  36. res.json({
  37. id: docId,
  38. lines,
  39. version,
  40. ops,
  41. ranges,
  42. pathname,
  43. ttlInS: RedisManager.DOC_OPS_TTL,
  44. })
  45. }
  46. )
  47. }
  48. function getComment(req, res, next) {
  49. const docId = req.params.doc_id
  50. const projectId = req.params.project_id
  51. const commentId = req.params.comment_id
  52. logger.debug({ projectId, docId, commentId }, 'getting comment via http')
  53. DocumentManager.getCommentWithLock(
  54. projectId,
  55. docId,
  56. commentId,
  57. (error, comment) => {
  58. if (error) {
  59. return next(error)
  60. }
  61. if (comment == null) {
  62. return next(new Errors.NotFoundError('comment not found'))
  63. }
  64. res.json(comment)
  65. }
  66. )
  67. }
  68. // return the doc from redis if present, but don't load it from mongo
  69. function peekDoc(req, res, next) {
  70. const docId = req.params.doc_id
  71. const projectId = req.params.project_id
  72. logger.debug({ projectId, docId }, 'peeking at doc via http')
  73. RedisManager.getDoc(projectId, docId, function (error, lines, version) {
  74. if (error) {
  75. return next(error)
  76. }
  77. if (lines == null || version == null) {
  78. return next(new Errors.NotFoundError('document not found'))
  79. }
  80. res.json({ id: docId, lines, version })
  81. })
  82. }
  83. function getProjectDocsAndFlushIfOld(req, res, next) {
  84. const projectId = req.params.project_id
  85. const projectStateHash = req.query.state
  86. // exclude is string of existing docs "id:version,id:version,..."
  87. const excludeItems =
  88. req.query.exclude != null ? req.query.exclude.split(',') : []
  89. logger.debug({ projectId, exclude: excludeItems }, 'getting docs via http')
  90. const timer = new Metrics.Timer('http.getAllDocs')
  91. const excludeVersions = {}
  92. for (const item of excludeItems) {
  93. const [id, version] = item.split(':')
  94. excludeVersions[id] = version
  95. }
  96. logger.debug(
  97. { projectId, projectStateHash, excludeVersions },
  98. 'excluding versions'
  99. )
  100. ProjectManager.getProjectDocsAndFlushIfOld(
  101. projectId,
  102. projectStateHash,
  103. excludeVersions,
  104. (error, result) => {
  105. timer.done()
  106. if (error instanceof Errors.ProjectStateChangedError) {
  107. res.sendStatus(409) // conflict
  108. } else if (error) {
  109. next(error)
  110. } else {
  111. logger.debug(
  112. {
  113. projectId,
  114. result: result.map(doc => `${doc._id}:${doc.v}`),
  115. },
  116. 'got docs via http'
  117. )
  118. res.send(result)
  119. }
  120. }
  121. )
  122. }
  123. function clearProjectState(req, res, next) {
  124. const projectId = req.params.project_id
  125. const timer = new Metrics.Timer('http.clearProjectState')
  126. logger.debug({ projectId }, 'clearing project state via http')
  127. ProjectManager.clearProjectState(projectId, error => {
  128. timer.done()
  129. if (error) {
  130. next(error)
  131. } else {
  132. res.sendStatus(200)
  133. }
  134. })
  135. }
  136. function setDoc(req, res, next) {
  137. const docId = req.params.doc_id
  138. const projectId = req.params.project_id
  139. const { lines, source, user_id: userId, undoing } = req.body
  140. const lineSize = getTotalSizeOfLines(lines)
  141. if (lineSize > Settings.max_doc_length) {
  142. logger.warn(
  143. { projectId, docId, source, lineSize, userId },
  144. 'document too large, returning 406 response'
  145. )
  146. return res.sendStatus(406)
  147. }
  148. logger.debug(
  149. { projectId, docId, lines, source, userId, undoing },
  150. 'setting doc via http'
  151. )
  152. const timer = new Metrics.Timer('http.setDoc')
  153. DocumentManager.setDocWithLock(
  154. projectId,
  155. docId,
  156. lines,
  157. source,
  158. userId,
  159. undoing,
  160. true,
  161. (error, result) => {
  162. timer.done()
  163. if (error) {
  164. return next(error)
  165. }
  166. logger.debug({ projectId, docId }, 'set doc via http')
  167. res.json(result)
  168. }
  169. )
  170. }
  171. function appendToDoc(req, res, next) {
  172. const docId = req.params.doc_id
  173. const projectId = req.params.project_id
  174. const { lines, source, user_id: userId } = req.body
  175. const timer = new Metrics.Timer('http.appendToDoc')
  176. DocumentManager.appendToDocWithLock(
  177. projectId,
  178. docId,
  179. lines,
  180. source,
  181. userId,
  182. (error, result) => {
  183. timer.done()
  184. if (error instanceof Errors.FileTooLargeError) {
  185. logger.warn('refusing to append to file, it would become too large')
  186. return res.sendStatus(422)
  187. }
  188. if (error) {
  189. return next(error)
  190. }
  191. logger.debug(
  192. { projectId, docId, lines, source, userId },
  193. 'appending to doc via http'
  194. )
  195. res.json(result)
  196. }
  197. )
  198. }
  199. function flushDocIfLoaded(req, res, next) {
  200. const docId = req.params.doc_id
  201. const projectId = req.params.project_id
  202. logger.debug({ projectId, docId }, 'flushing doc via http')
  203. const timer = new Metrics.Timer('http.flushDoc')
  204. DocumentManager.flushDocIfLoadedWithLock(projectId, docId, error => {
  205. timer.done()
  206. if (error) {
  207. return next(error)
  208. }
  209. logger.debug({ projectId, docId }, 'flushed doc via http')
  210. res.sendStatus(204) // No Content
  211. })
  212. }
  213. function deleteDoc(req, res, next) {
  214. const docId = req.params.doc_id
  215. const projectId = req.params.project_id
  216. const ignoreFlushErrors = req.query.ignore_flush_errors === 'true'
  217. const timer = new Metrics.Timer('http.deleteDoc')
  218. logger.debug({ projectId, docId }, 'deleting doc via http')
  219. DocumentManager.flushAndDeleteDocWithLock(
  220. projectId,
  221. docId,
  222. { ignoreFlushErrors },
  223. error => {
  224. timer.done()
  225. // There is no harm in flushing project history if the previous call
  226. // failed and sometimes it is required
  227. HistoryManager.flushProjectChangesAsync(projectId)
  228. if (error) {
  229. return next(error)
  230. }
  231. logger.debug({ projectId, docId }, 'deleted doc via http')
  232. res.sendStatus(204) // No Content
  233. }
  234. )
  235. }
  236. function flushProject(req, res, next) {
  237. const projectId = req.params.project_id
  238. logger.debug({ projectId }, 'flushing project via http')
  239. const timer = new Metrics.Timer('http.flushProject')
  240. ProjectManager.flushProjectWithLocks(projectId, error => {
  241. timer.done()
  242. if (error) {
  243. return next(error)
  244. }
  245. logger.debug({ projectId }, 'flushed project via http')
  246. res.sendStatus(204) // No Content
  247. })
  248. }
  249. function deleteProject(req, res, next) {
  250. const projectId = req.params.project_id
  251. logger.debug({ projectId }, 'deleting project via http')
  252. const options = {}
  253. if (req.query.background) {
  254. options.background = true
  255. } // allow non-urgent flushes to be queued
  256. if (req.query.shutdown) {
  257. options.skip_history_flush = true
  258. } // don't flush history when realtime shuts down
  259. if (req.query.background) {
  260. ProjectManager.queueFlushAndDeleteProject(projectId, error => {
  261. if (error) {
  262. return next(error)
  263. }
  264. logger.debug({ projectId }, 'queue delete of project via http')
  265. res.sendStatus(204)
  266. }) // No Content
  267. } else {
  268. const timer = new Metrics.Timer('http.deleteProject')
  269. ProjectManager.flushAndDeleteProjectWithLocks(projectId, options, error => {
  270. timer.done()
  271. if (error) {
  272. return next(error)
  273. }
  274. logger.debug({ projectId }, 'deleted project via http')
  275. res.sendStatus(204) // No Content
  276. })
  277. }
  278. }
  279. function deleteMultipleProjects(req, res, next) {
  280. const projectIds = req.body.project_ids || []
  281. logger.debug({ projectIds }, 'deleting multiple projects via http')
  282. async.eachSeries(
  283. projectIds,
  284. (projectId, cb) => {
  285. logger.debug({ projectId }, 'queue delete of project via http')
  286. ProjectManager.queueFlushAndDeleteProject(projectId, cb)
  287. },
  288. error => {
  289. if (error) {
  290. return next(error)
  291. }
  292. res.sendStatus(204) // No Content
  293. }
  294. )
  295. }
  296. function acceptChanges(req, res, next) {
  297. const { project_id: projectId, doc_id: docId } = req.params
  298. let changeIds = req.body.change_ids
  299. if (changeIds == null) {
  300. changeIds = [req.params.change_id]
  301. }
  302. logger.debug(
  303. { projectId, docId },
  304. `accepting ${changeIds.length} changes via http`
  305. )
  306. const timer = new Metrics.Timer('http.acceptChanges')
  307. DocumentManager.acceptChangesWithLock(projectId, docId, changeIds, error => {
  308. timer.done()
  309. if (error) {
  310. return next(error)
  311. }
  312. logger.debug(
  313. { projectId, docId },
  314. `accepted ${changeIds.length} changes via http`
  315. )
  316. res.sendStatus(204) // No Content
  317. })
  318. }
  319. function resolveComment(req, res, next) {
  320. const {
  321. project_id: projectId,
  322. doc_id: docId,
  323. comment_id: commentId,
  324. } = req.params
  325. const userId = req.body.user_id
  326. logger.debug({ projectId, docId, commentId }, 'resolving comment via http')
  327. DocumentManager.updateCommentStateWithLock(
  328. projectId,
  329. docId,
  330. commentId,
  331. userId,
  332. true,
  333. error => {
  334. if (error) {
  335. return next(error)
  336. }
  337. logger.debug({ projectId, docId, commentId }, 'resolved comment via http')
  338. res.sendStatus(204) // No Content
  339. }
  340. )
  341. }
  342. function reopenComment(req, res, next) {
  343. const {
  344. project_id: projectId,
  345. doc_id: docId,
  346. comment_id: commentId,
  347. } = req.params
  348. const userId = req.body.user_id
  349. logger.debug({ projectId, docId, commentId }, 'reopening comment via http')
  350. DocumentManager.updateCommentStateWithLock(
  351. projectId,
  352. docId,
  353. commentId,
  354. userId,
  355. false,
  356. error => {
  357. if (error) {
  358. return next(error)
  359. }
  360. logger.debug({ projectId, docId, commentId }, 'reopened comment via http')
  361. res.sendStatus(204) // No Content
  362. }
  363. )
  364. }
  365. function deleteComment(req, res, next) {
  366. const {
  367. project_id: projectId,
  368. doc_id: docId,
  369. comment_id: commentId,
  370. } = req.params
  371. const userId = req.body.user_id
  372. logger.debug({ projectId, docId, commentId }, 'deleting comment via http')
  373. const timer = new Metrics.Timer('http.deleteComment')
  374. DocumentManager.deleteCommentWithLock(
  375. projectId,
  376. docId,
  377. commentId,
  378. userId,
  379. error => {
  380. timer.done()
  381. if (error) {
  382. return next(error)
  383. }
  384. logger.debug({ projectId, docId, commentId }, 'deleted comment via http')
  385. res.sendStatus(204) // No Content
  386. }
  387. )
  388. }
  389. function updateProject(req, res, next) {
  390. const timer = new Metrics.Timer('http.updateProject')
  391. const projectId = req.params.project_id
  392. const { projectHistoryId, userId, updates = [], version, source } = req.body
  393. logger.debug({ projectId, updates, version }, 'updating project via http')
  394. ProjectManager.updateProjectWithLocks(
  395. projectId,
  396. projectHistoryId,
  397. userId,
  398. updates,
  399. version,
  400. source,
  401. error => {
  402. timer.done()
  403. if (error) {
  404. return next(error)
  405. }
  406. logger.debug({ projectId }, 'updated project via http')
  407. res.sendStatus(204) // No Content
  408. }
  409. )
  410. }
  411. function resyncProjectHistory(req, res, next) {
  412. const projectId = req.params.project_id
  413. const { projectHistoryId, docs, files, historyRangesMigration } = req.body
  414. logger.debug(
  415. { projectId, docs, files },
  416. 'queuing project history resync via http'
  417. )
  418. const opts = {}
  419. if (historyRangesMigration) {
  420. opts.historyRangesMigration = historyRangesMigration
  421. }
  422. HistoryManager.resyncProjectHistory(
  423. projectId,
  424. projectHistoryId,
  425. docs,
  426. files,
  427. opts,
  428. error => {
  429. if (error) {
  430. return next(error)
  431. }
  432. logger.debug({ projectId }, 'queued project history resync via http')
  433. res.sendStatus(204)
  434. }
  435. )
  436. }
  437. function flushQueuedProjects(req, res, next) {
  438. res.setTimeout(10 * 60 * 1000)
  439. const options = {
  440. limit: req.query.limit || 1000,
  441. timeout: 5 * 60 * 1000,
  442. min_delete_age: req.query.min_delete_age || 5 * 60 * 1000,
  443. }
  444. DeleteQueueManager.flushAndDeleteOldProjects(options, (err, flushed) => {
  445. if (err) {
  446. logger.err({ err }, 'error flushing old projects')
  447. res.sendStatus(500)
  448. } else {
  449. logger.info({ flushed }, 'flush of queued projects completed')
  450. res.send({ flushed })
  451. }
  452. })
  453. }
  454. /**
  455. * Block a project from getting loaded in docupdater
  456. *
  457. * The project is blocked only if it's not already loaded in docupdater. The
  458. * response indicates whether the project has been blocked or not.
  459. */
  460. function blockProject(req, res, next) {
  461. const projectId = req.params.project_id
  462. RedisManager.blockProject(projectId, (err, blocked) => {
  463. if (err) {
  464. return next(err)
  465. }
  466. res.json({ blocked })
  467. })
  468. }
  469. /**
  470. * Unblock a project
  471. */
  472. function unblockProject(req, res, next) {
  473. const projectId = req.params.project_id
  474. RedisManager.unblockProject(projectId, (err, wasBlocked) => {
  475. if (err) {
  476. return next(err)
  477. }
  478. res.json({ wasBlocked })
  479. })
  480. }
  481. module.exports = {
  482. getDoc,
  483. peekDoc,
  484. getProjectDocsAndFlushIfOld,
  485. clearProjectState,
  486. appendToDoc,
  487. setDoc,
  488. flushDocIfLoaded,
  489. deleteDoc,
  490. flushProject,
  491. deleteProject,
  492. deleteMultipleProjects,
  493. acceptChanges,
  494. resolveComment,
  495. reopenComment,
  496. deleteComment,
  497. updateProject,
  498. resyncProjectHistory,
  499. flushQueuedProjects,
  500. blockProject,
  501. unblockProject,
  502. getComment,
  503. }