HttpController.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  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. module.exports = {
  14. getDoc,
  15. peekDoc,
  16. getProjectDocsAndFlushIfOld,
  17. clearProjectState,
  18. setDoc,
  19. flushDocIfLoaded,
  20. deleteDoc,
  21. flushProject,
  22. deleteProject,
  23. deleteMultipleProjects,
  24. acceptChanges,
  25. resolveComment,
  26. reopenComment,
  27. deleteComment,
  28. updateProject,
  29. resyncProjectHistory,
  30. flushAllProjects,
  31. flushQueuedProjects,
  32. }
  33. function getDoc(req, res, next) {
  34. let fromVersion
  35. const docId = req.params.doc_id
  36. const projectId = req.params.project_id
  37. logger.debug({ projectId, docId }, 'getting doc via http')
  38. const timer = new Metrics.Timer('http.getDoc')
  39. if (req.query.fromVersion != null) {
  40. fromVersion = parseInt(req.query.fromVersion, 10)
  41. } else {
  42. fromVersion = -1
  43. }
  44. DocumentManager.getDocAndRecentOpsWithLock(
  45. projectId,
  46. docId,
  47. fromVersion,
  48. (error, lines, version, ops, ranges, pathname) => {
  49. timer.done()
  50. if (error) {
  51. return next(error)
  52. }
  53. logger.debug({ projectId, docId }, 'got doc via http')
  54. if (lines == null || version == null) {
  55. return next(new Errors.NotFoundError('document not found'))
  56. }
  57. res.json({
  58. id: docId,
  59. lines,
  60. version,
  61. ops,
  62. ranges,
  63. pathname,
  64. })
  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. (error, result) => {
  161. timer.done()
  162. if (error) {
  163. return next(error)
  164. }
  165. logger.debug({ projectId, docId }, 'set doc via http')
  166. res.json(result)
  167. }
  168. )
  169. }
  170. function flushDocIfLoaded(req, res, next) {
  171. const docId = req.params.doc_id
  172. const projectId = req.params.project_id
  173. logger.debug({ projectId, docId }, 'flushing doc via http')
  174. const timer = new Metrics.Timer('http.flushDoc')
  175. DocumentManager.flushDocIfLoadedWithLock(projectId, docId, error => {
  176. timer.done()
  177. if (error) {
  178. return next(error)
  179. }
  180. logger.debug({ projectId, docId }, 'flushed doc via http')
  181. res.sendStatus(204) // No Content
  182. })
  183. }
  184. function deleteDoc(req, res, next) {
  185. const docId = req.params.doc_id
  186. const projectId = req.params.project_id
  187. const ignoreFlushErrors = req.query.ignore_flush_errors === 'true'
  188. const timer = new Metrics.Timer('http.deleteDoc')
  189. logger.debug({ projectId, docId }, 'deleting doc via http')
  190. DocumentManager.flushAndDeleteDocWithLock(
  191. projectId,
  192. docId,
  193. { ignoreFlushErrors },
  194. error => {
  195. timer.done()
  196. // There is no harm in flushing project history if the previous call
  197. // failed and sometimes it is required
  198. HistoryManager.flushProjectChangesAsync(projectId)
  199. if (error) {
  200. return next(error)
  201. }
  202. logger.debug({ projectId, docId }, 'deleted doc via http')
  203. res.sendStatus(204) // No Content
  204. }
  205. )
  206. }
  207. function flushProject(req, res, next) {
  208. const projectId = req.params.project_id
  209. logger.debug({ projectId }, 'flushing project via http')
  210. const timer = new Metrics.Timer('http.flushProject')
  211. ProjectManager.flushProjectWithLocks(projectId, error => {
  212. timer.done()
  213. if (error) {
  214. return next(error)
  215. }
  216. logger.debug({ projectId }, 'flushed project via http')
  217. res.sendStatus(204) // No Content
  218. })
  219. }
  220. function deleteProject(req, res, next) {
  221. const projectId = req.params.project_id
  222. logger.debug({ projectId }, 'deleting project via http')
  223. const options = {}
  224. if (req.query.background) {
  225. options.background = true
  226. } // allow non-urgent flushes to be queued
  227. if (req.query.shutdown) {
  228. options.skip_history_flush = true
  229. } // don't flush history when realtime shuts down
  230. if (req.query.background) {
  231. ProjectManager.queueFlushAndDeleteProject(projectId, error => {
  232. if (error) {
  233. return next(error)
  234. }
  235. logger.debug({ projectId }, 'queue delete of project via http')
  236. res.sendStatus(204)
  237. }) // No Content
  238. } else {
  239. const timer = new Metrics.Timer('http.deleteProject')
  240. ProjectManager.flushAndDeleteProjectWithLocks(projectId, options, error => {
  241. timer.done()
  242. if (error) {
  243. return next(error)
  244. }
  245. logger.debug({ projectId }, 'deleted project via http')
  246. res.sendStatus(204) // No Content
  247. })
  248. }
  249. }
  250. function deleteMultipleProjects(req, res, next) {
  251. const projectIds = req.body.project_ids || []
  252. logger.debug({ projectIds }, 'deleting multiple projects via http')
  253. async.eachSeries(
  254. projectIds,
  255. (projectId, cb) => {
  256. logger.debug({ projectId }, 'queue delete of project via http')
  257. ProjectManager.queueFlushAndDeleteProject(projectId, cb)
  258. },
  259. error => {
  260. if (error) {
  261. return next(error)
  262. }
  263. res.sendStatus(204) // No Content
  264. }
  265. )
  266. }
  267. function acceptChanges(req, res, next) {
  268. const { project_id: projectId, doc_id: docId } = req.params
  269. let changeIds = req.body.change_ids
  270. if (changeIds == null) {
  271. changeIds = [req.params.change_id]
  272. }
  273. logger.debug(
  274. { projectId, docId },
  275. `accepting ${changeIds.length} changes via http`
  276. )
  277. const timer = new Metrics.Timer('http.acceptChanges')
  278. DocumentManager.acceptChangesWithLock(projectId, docId, changeIds, error => {
  279. timer.done()
  280. if (error) {
  281. return next(error)
  282. }
  283. logger.debug(
  284. { projectId, docId },
  285. `accepted ${changeIds.length} changes via http`
  286. )
  287. res.sendStatus(204) // No Content
  288. })
  289. }
  290. function resolveComment(req, res, next) {
  291. const {
  292. project_id: projectId,
  293. doc_id: docId,
  294. comment_id: commentId,
  295. } = req.params
  296. const userId = req.body.user_id
  297. logger.debug({ projectId, docId, commentId }, 'resolving comment via http')
  298. DocumentManager.updateCommentStateWithLock(
  299. projectId,
  300. docId,
  301. commentId,
  302. userId,
  303. true,
  304. error => {
  305. if (error) {
  306. return next(error)
  307. }
  308. logger.debug({ projectId, docId, commentId }, 'resolved comment via http')
  309. res.sendStatus(204) // No Content
  310. }
  311. )
  312. }
  313. function reopenComment(req, res, next) {
  314. const {
  315. project_id: projectId,
  316. doc_id: docId,
  317. comment_id: commentId,
  318. } = req.params
  319. const userId = req.body.user_id
  320. logger.debug({ projectId, docId, commentId }, 'reopening comment via http')
  321. DocumentManager.updateCommentStateWithLock(
  322. projectId,
  323. docId,
  324. commentId,
  325. userId,
  326. false,
  327. error => {
  328. if (error) {
  329. return next(error)
  330. }
  331. logger.debug({ projectId, docId, commentId }, 'reopened comment via http')
  332. res.sendStatus(204) // No Content
  333. }
  334. )
  335. }
  336. function deleteComment(req, res, next) {
  337. const {
  338. project_id: projectId,
  339. doc_id: docId,
  340. comment_id: commentId,
  341. } = req.params
  342. const userId = req.body.user_id
  343. logger.debug({ projectId, docId, commentId }, 'deleting comment via http')
  344. const timer = new Metrics.Timer('http.deleteComment')
  345. DocumentManager.deleteCommentWithLock(
  346. projectId,
  347. docId,
  348. commentId,
  349. userId,
  350. error => {
  351. timer.done()
  352. if (error) {
  353. return next(error)
  354. }
  355. logger.debug({ projectId, docId, commentId }, 'deleted comment via http')
  356. res.sendStatus(204) // No Content
  357. }
  358. )
  359. }
  360. function updateProject(req, res, next) {
  361. const timer = new Metrics.Timer('http.updateProject')
  362. const projectId = req.params.project_id
  363. const { projectHistoryId, userId, updates = [], version, source } = req.body
  364. logger.debug({ projectId, updates, version }, 'updating project via http')
  365. ProjectManager.updateProjectWithLocks(
  366. projectId,
  367. projectHistoryId,
  368. userId,
  369. updates,
  370. version,
  371. source,
  372. error => {
  373. timer.done()
  374. if (error) {
  375. return next(error)
  376. }
  377. logger.debug({ projectId }, 'updated project via http')
  378. res.sendStatus(204) // No Content
  379. }
  380. )
  381. }
  382. function resyncProjectHistory(req, res, next) {
  383. const projectId = req.params.project_id
  384. const { projectHistoryId, docs, files } = req.body
  385. logger.debug(
  386. { projectId, docs, files },
  387. 'queuing project history resync via http'
  388. )
  389. HistoryManager.resyncProjectHistory(
  390. projectId,
  391. projectHistoryId,
  392. docs,
  393. files,
  394. error => {
  395. if (error) {
  396. return next(error)
  397. }
  398. logger.debug({ projectId }, 'queued project history resync via http')
  399. res.sendStatus(204)
  400. }
  401. )
  402. }
  403. function flushAllProjects(req, res, next) {
  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, (err, projectIds) => {
  411. if (err) {
  412. logger.err({ err }, 'error bulk flushing projects')
  413. res.sendStatus(500)
  414. } else {
  415. res.send(projectIds)
  416. }
  417. })
  418. }
  419. function flushQueuedProjects(req, res, next) {
  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, (err, flushed) => {
  427. if (err) {
  428. logger.err({ err }, 'error flushing old projects')
  429. res.sendStatus(500)
  430. } else {
  431. logger.info({ flushed }, 'flush of queued projects completed')
  432. res.send({ flushed })
  433. }
  434. })
  435. }