TpdsController.js 5.6 KB

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