HttpController.js 12 KB

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