HttpController.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  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. // 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. true,
  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 appendToDoc(req, res, next) {
  152. const docId = req.params.doc_id
  153. const projectId = req.params.project_id
  154. const { lines, source, user_id: userId } = req.body
  155. const timer = new Metrics.Timer('http.appendToDoc')
  156. DocumentManager.appendToDocWithLock(
  157. projectId,
  158. docId,
  159. lines,
  160. source,
  161. userId,
  162. (error, result) => {
  163. timer.done()
  164. if (error instanceof Errors.FileTooLargeError) {
  165. logger.warn('refusing to append to file, it would become too large')
  166. return res.sendStatus(422)
  167. }
  168. if (error) {
  169. return next(error)
  170. }
  171. logger.debug(
  172. { projectId, docId, lines, source, userId },
  173. 'appending to doc via http'
  174. )
  175. res.json(result)
  176. }
  177. )
  178. }
  179. function flushDocIfLoaded(req, res, next) {
  180. const docId = req.params.doc_id
  181. const projectId = req.params.project_id
  182. logger.debug({ projectId, docId }, 'flushing doc via http')
  183. const timer = new Metrics.Timer('http.flushDoc')
  184. DocumentManager.flushDocIfLoadedWithLock(projectId, docId, error => {
  185. timer.done()
  186. if (error) {
  187. return next(error)
  188. }
  189. logger.debug({ projectId, docId }, 'flushed doc via http')
  190. res.sendStatus(204) // No Content
  191. })
  192. }
  193. function deleteDoc(req, res, next) {
  194. const docId = req.params.doc_id
  195. const projectId = req.params.project_id
  196. const ignoreFlushErrors = req.query.ignore_flush_errors === 'true'
  197. const timer = new Metrics.Timer('http.deleteDoc')
  198. logger.debug({ projectId, docId }, 'deleting doc via http')
  199. DocumentManager.flushAndDeleteDocWithLock(
  200. projectId,
  201. docId,
  202. { ignoreFlushErrors },
  203. error => {
  204. timer.done()
  205. // There is no harm in flushing project history if the previous call
  206. // failed and sometimes it is required
  207. HistoryManager.flushProjectChangesAsync(projectId)
  208. if (error) {
  209. return next(error)
  210. }
  211. logger.debug({ projectId, docId }, 'deleted doc via http')
  212. res.sendStatus(204) // No Content
  213. }
  214. )
  215. }
  216. function flushProject(req, res, next) {
  217. const projectId = req.params.project_id
  218. logger.debug({ projectId }, 'flushing project via http')
  219. const timer = new Metrics.Timer('http.flushProject')
  220. ProjectManager.flushProjectWithLocks(projectId, error => {
  221. timer.done()
  222. if (error) {
  223. return next(error)
  224. }
  225. logger.debug({ projectId }, 'flushed project via http')
  226. res.sendStatus(204) // No Content
  227. })
  228. }
  229. function deleteProject(req, res, next) {
  230. const projectId = req.params.project_id
  231. logger.debug({ projectId }, 'deleting project via http')
  232. const options = {}
  233. if (req.query.background) {
  234. options.background = true
  235. } // allow non-urgent flushes to be queued
  236. if (req.query.shutdown) {
  237. options.skip_history_flush = true
  238. } // don't flush history when realtime shuts down
  239. if (req.query.background) {
  240. ProjectManager.queueFlushAndDeleteProject(projectId, error => {
  241. if (error) {
  242. return next(error)
  243. }
  244. logger.debug({ projectId }, 'queue delete of project via http')
  245. res.sendStatus(204)
  246. }) // No Content
  247. } else {
  248. const timer = new Metrics.Timer('http.deleteProject')
  249. ProjectManager.flushAndDeleteProjectWithLocks(projectId, options, error => {
  250. timer.done()
  251. if (error) {
  252. return next(error)
  253. }
  254. logger.debug({ projectId }, 'deleted project via http')
  255. res.sendStatus(204) // No Content
  256. })
  257. }
  258. }
  259. function deleteMultipleProjects(req, res, next) {
  260. const projectIds = req.body.project_ids || []
  261. logger.debug({ projectIds }, 'deleting multiple projects via http')
  262. async.eachSeries(
  263. projectIds,
  264. (projectId, cb) => {
  265. logger.debug({ projectId }, 'queue delete of project via http')
  266. ProjectManager.queueFlushAndDeleteProject(projectId, cb)
  267. },
  268. error => {
  269. if (error) {
  270. return next(error)
  271. }
  272. res.sendStatus(204) // No Content
  273. }
  274. )
  275. }
  276. function acceptChanges(req, res, next) {
  277. const { project_id: projectId, doc_id: docId } = req.params
  278. let changeIds = req.body.change_ids
  279. if (changeIds == null) {
  280. changeIds = [req.params.change_id]
  281. }
  282. logger.debug(
  283. { projectId, docId },
  284. `accepting ${changeIds.length} changes via http`
  285. )
  286. const timer = new Metrics.Timer('http.acceptChanges')
  287. DocumentManager.acceptChangesWithLock(projectId, docId, changeIds, error => {
  288. timer.done()
  289. if (error) {
  290. return next(error)
  291. }
  292. logger.debug(
  293. { projectId, docId },
  294. `accepted ${changeIds.length} changes via http`
  295. )
  296. res.sendStatus(204) // No Content
  297. })
  298. }
  299. function resolveComment(req, res, next) {
  300. const {
  301. project_id: projectId,
  302. doc_id: docId,
  303. comment_id: commentId,
  304. } = req.params
  305. const userId = req.body.user_id
  306. logger.debug({ projectId, docId, commentId }, 'resolving comment via http')
  307. DocumentManager.updateCommentStateWithLock(
  308. projectId,
  309. docId,
  310. commentId,
  311. userId,
  312. true,
  313. error => {
  314. if (error) {
  315. return next(error)
  316. }
  317. logger.debug({ projectId, docId, commentId }, 'resolved comment via http')
  318. res.sendStatus(204) // No Content
  319. }
  320. )
  321. }
  322. function reopenComment(req, res, next) {
  323. const {
  324. project_id: projectId,
  325. doc_id: docId,
  326. comment_id: commentId,
  327. } = req.params
  328. const userId = req.body.user_id
  329. logger.debug({ projectId, docId, commentId }, 'reopening comment via http')
  330. DocumentManager.updateCommentStateWithLock(
  331. projectId,
  332. docId,
  333. commentId,
  334. userId,
  335. false,
  336. error => {
  337. if (error) {
  338. return next(error)
  339. }
  340. logger.debug({ projectId, docId, commentId }, 'reopened comment via http')
  341. res.sendStatus(204) // No Content
  342. }
  343. )
  344. }
  345. function deleteComment(req, res, next) {
  346. const {
  347. project_id: projectId,
  348. doc_id: docId,
  349. comment_id: commentId,
  350. } = req.params
  351. const userId = req.body.user_id
  352. logger.debug({ projectId, docId, commentId }, 'deleting comment via http')
  353. const timer = new Metrics.Timer('http.deleteComment')
  354. DocumentManager.deleteCommentWithLock(
  355. projectId,
  356. docId,
  357. commentId,
  358. userId,
  359. error => {
  360. timer.done()
  361. if (error) {
  362. return next(error)
  363. }
  364. logger.debug({ projectId, docId, commentId }, 'deleted comment via http')
  365. res.sendStatus(204) // No Content
  366. }
  367. )
  368. }
  369. function updateProject(req, res, next) {
  370. const timer = new Metrics.Timer('http.updateProject')
  371. const projectId = req.params.project_id
  372. const { projectHistoryId, userId, updates = [], version, source } = req.body
  373. logger.debug({ projectId, updates, version }, 'updating project via http')
  374. ProjectManager.updateProjectWithLocks(
  375. projectId,
  376. projectHistoryId,
  377. userId,
  378. updates,
  379. version,
  380. source,
  381. error => {
  382. timer.done()
  383. if (error) {
  384. return next(error)
  385. }
  386. logger.debug({ projectId }, 'updated project via http')
  387. res.sendStatus(204) // No Content
  388. }
  389. )
  390. }
  391. function resyncProjectHistory(req, res, next) {
  392. const projectId = req.params.project_id
  393. const { projectHistoryId, docs, files, historyRangesMigration } = req.body
  394. logger.debug(
  395. { projectId, docs, files },
  396. 'queuing project history resync via http'
  397. )
  398. const opts = {}
  399. if (historyRangesMigration) {
  400. opts.historyRangesMigration = historyRangesMigration
  401. }
  402. HistoryManager.resyncProjectHistory(
  403. projectId,
  404. projectHistoryId,
  405. docs,
  406. files,
  407. opts,
  408. error => {
  409. if (error) {
  410. return next(error)
  411. }
  412. logger.debug({ projectId }, 'queued project history resync via http')
  413. res.sendStatus(204)
  414. }
  415. )
  416. }
  417. function flushQueuedProjects(req, res, next) {
  418. res.setTimeout(10 * 60 * 1000)
  419. const options = {
  420. limit: req.query.limit || 1000,
  421. timeout: 5 * 60 * 1000,
  422. min_delete_age: req.query.min_delete_age || 5 * 60 * 1000,
  423. }
  424. DeleteQueueManager.flushAndDeleteOldProjects(options, (err, flushed) => {
  425. if (err) {
  426. logger.err({ err }, 'error flushing old projects')
  427. res.sendStatus(500)
  428. } else {
  429. logger.info({ flushed }, 'flush of queued projects completed')
  430. res.send({ flushed })
  431. }
  432. })
  433. }
  434. /**
  435. * Block a project from getting loaded in docupdater
  436. *
  437. * The project is blocked only if it's not already loaded in docupdater. The
  438. * response indicates whether the project has been blocked or not.
  439. */
  440. function blockProject(req, res, next) {
  441. const projectId = req.params.project_id
  442. RedisManager.blockProject(projectId, (err, blocked) => {
  443. if (err) {
  444. return next(err)
  445. }
  446. res.json({ blocked })
  447. })
  448. }
  449. /**
  450. * Unblock a project
  451. */
  452. function unblockProject(req, res, next) {
  453. const projectId = req.params.project_id
  454. RedisManager.unblockProject(projectId, (err, wasBlocked) => {
  455. if (err) {
  456. return next(err)
  457. }
  458. res.json({ wasBlocked })
  459. })
  460. }
  461. module.exports = {
  462. getDoc,
  463. peekDoc,
  464. getProjectDocsAndFlushIfOld,
  465. clearProjectState,
  466. appendToDoc,
  467. setDoc,
  468. flushDocIfLoaded,
  469. deleteDoc,
  470. flushProject,
  471. deleteProject,
  472. deleteMultipleProjects,
  473. acceptChanges,
  474. resolveComment,
  475. reopenComment,
  476. deleteComment,
  477. updateProject,
  478. resyncProjectHistory,
  479. flushQueuedProjects,
  480. blockProject,
  481. unblockProject,
  482. }