HttpController.js 11 KB

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