TpdsController.mjs 6.3 KB

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