HttpController.js 11 KB

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