HttpController.js 13 KB

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