TpdsController.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. const { expressify } = require('../../util/promises')
  2. const TpdsUpdateHandler = require('./TpdsUpdateHandler')
  3. const UpdateMerger = require('./UpdateMerger')
  4. const Errors = require('../Errors/Errors')
  5. const logger = require('@overleaf/logger')
  6. const Path = require('path')
  7. const metrics = require('@overleaf/metrics')
  8. const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
  9. const SessionManager = require('../Authentication/SessionManager')
  10. const HttpErrorHandler = require('../Errors/HttpErrorHandler')
  11. const TpdsQueueManager = require('./TpdsQueueManager')
  12. // mergeUpdate and deleteUpdate are used by Dropbox, where the project is only
  13. // passed as the name, as the first part of the file path. They have to check
  14. // the project exists, find it, and create it if not. They also ignore 'noisy'
  15. // files like .DS_Store, .gitignore, etc.
  16. async function mergeUpdate(req, res) {
  17. metrics.inc('tpds.merge-update')
  18. const { filePath, userId, projectName } = parseParams(req)
  19. const source = req.headers['x-sl-update-source'] || 'unknown'
  20. let metadata
  21. try {
  22. metadata = await TpdsUpdateHandler.promises.newUpdate(
  23. userId,
  24. projectName,
  25. filePath,
  26. req,
  27. source
  28. )
  29. } catch (err) {
  30. if (err.name === 'TooManyRequestsError') {
  31. logger.warn(
  32. { err, userId, filePath },
  33. 'tpds update failed to be processed, too many requests'
  34. )
  35. return res.sendStatus(429)
  36. } else if (err.message === 'project_has_too_many_files') {
  37. logger.warn(
  38. { err, userId, filePath },
  39. 'tpds trying to append to project over file limit'
  40. )
  41. NotificationsBuilder.tpdsFileLimit(userId).create(projectName)
  42. return res.sendStatus(400)
  43. } else {
  44. throw err
  45. }
  46. }
  47. if (metadata == null) {
  48. return res.json({ status: 'rejected' })
  49. }
  50. const payload = {
  51. status: 'applied',
  52. projectId: metadata.projectId.toString(),
  53. entityId: metadata.entityId.toString(),
  54. entityType: metadata.entityType,
  55. folderId: metadata.folderId.toString(),
  56. }
  57. // When the update is a doc edit, the update is merged in docupdater and
  58. // doesn't generate a new rev.
  59. if (metadata.rev != null) {
  60. payload.rev = metadata.rev.toString()
  61. }
  62. res.json(payload)
  63. }
  64. async function deleteUpdate(req, res) {
  65. metrics.inc('tpds.delete-update')
  66. const { filePath, userId, projectName } = parseParams(req)
  67. const source = req.headers['x-sl-update-source'] || 'unknown'
  68. await TpdsUpdateHandler.promises.deleteUpdate(
  69. userId,
  70. projectName,
  71. filePath,
  72. source
  73. )
  74. res.sendStatus(200)
  75. }
  76. /**
  77. * Update endpoint that accepts update details as JSON
  78. */
  79. async function updateFolder(req, res) {
  80. const userId = req.body.userId
  81. const { projectName, filePath } = splitPath(req.body.path)
  82. const metadata = await TpdsUpdateHandler.promises.createFolder(
  83. userId,
  84. projectName,
  85. filePath
  86. )
  87. if (metadata == null) {
  88. return HttpErrorHandler.conflict(req, res, 'Could not create folder', {
  89. userId,
  90. projectName,
  91. filePath,
  92. })
  93. }
  94. res.json({
  95. entityId: metadata.folderId.toString(),
  96. projectId: metadata.projectId.toString(),
  97. path: metadata.path,
  98. folderId: metadata.parentFolderId?.toString() || null,
  99. })
  100. }
  101. // updateProjectContents and deleteProjectContents are used by GitHub. The
  102. // project_id is known so we can skip right ahead to creating/updating/deleting
  103. // the file. These methods will not ignore noisy files like .DS_Store,
  104. // .gitignore, etc because people are generally more explicit with the files
  105. // they want in git.
  106. async function updateProjectContents(req, res, next) {
  107. const projectId = req.params.project_id
  108. const path = `/${req.params[0]}` // UpdateMerger expects leading slash
  109. const source = req.headers['x-sl-update-source'] || 'unknown'
  110. try {
  111. await UpdateMerger.promises.mergeUpdate(null, projectId, path, req, source)
  112. } catch (error) {
  113. if (error.constructor === Errors.InvalidNameError) {
  114. return res.sendStatus(422)
  115. } else {
  116. throw error
  117. }
  118. }
  119. res.sendStatus(200)
  120. }
  121. async function deleteProjectContents(req, res, next) {
  122. const projectId = req.params.project_id
  123. const path = `/${req.params[0]}` // UpdateMerger expects leading slash
  124. const source = req.headers['x-sl-update-source'] || 'unknown'
  125. await UpdateMerger.promises.deleteUpdate(null, projectId, path, source)
  126. res.sendStatus(200)
  127. }
  128. async function getQueues(req, res, next) {
  129. const userId = SessionManager.getLoggedInUserId(req.session)
  130. res.json(await TpdsQueueManager.promises.getQueues(userId))
  131. }
  132. function parseParams(req) {
  133. const userId = req.params.user_id
  134. const { projectName, filePath } = splitPath(req.params[0])
  135. return { filePath, userId, projectName }
  136. }
  137. function splitPath(path) {
  138. let filePath, projectName
  139. path = Path.join('/', path)
  140. if (path.substring(1).indexOf('/') === -1) {
  141. filePath = '/'
  142. projectName = path.substring(1)
  143. } else {
  144. filePath = path.substring(path.indexOf('/', 1))
  145. projectName = path.substring(0, path.indexOf('/', 1))
  146. projectName = projectName.replace('/', '')
  147. }
  148. return { projectName, filePath }
  149. }
  150. module.exports = {
  151. mergeUpdate: expressify(mergeUpdate),
  152. deleteUpdate: expressify(deleteUpdate),
  153. updateFolder: expressify(updateFolder),
  154. updateProjectContents: expressify(updateProjectContents),
  155. deleteProjectContents: expressify(deleteProjectContents),
  156. getQueues: expressify(getQueues),
  157. // for tests only
  158. parseParams,
  159. }