HttpController.js 11 KB

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