TpdsUpdateSender.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. const { ObjectId } = require('mongodb')
  2. const _ = require('lodash')
  3. const { callbackify } = require('util')
  4. const logger = require('@overleaf/logger')
  5. const metrics = require('@overleaf/metrics')
  6. const Path = require('path')
  7. const { fetchNothing } = require('@overleaf/fetch-utils')
  8. const settings = require('@overleaf/settings')
  9. const CollaboratorsGetter =
  10. require('../Collaborators/CollaboratorsGetter').promises
  11. const UserGetter = require('../User/UserGetter').promises
  12. const tpdsUrl = _.get(settings, ['apis', 'thirdPartyDataStore', 'url'])
  13. async function addDoc(params) {
  14. metrics.inc('tpds.add-doc')
  15. const { projectId, path, docId, projectName, rev, folderId } = params
  16. const streamOrigin =
  17. settings.apis.docstore.pubUrl +
  18. Path.join(`/project/${projectId}`, `/doc/${docId}`, '/raw')
  19. await addEntity({
  20. projectId,
  21. path,
  22. projectName,
  23. rev,
  24. folderId,
  25. streamOrigin,
  26. entityId: docId,
  27. entityType: 'doc',
  28. })
  29. }
  30. async function addEntity(params) {
  31. const {
  32. projectId,
  33. path,
  34. projectName,
  35. rev,
  36. folderId,
  37. streamOrigin,
  38. entityId,
  39. entityType,
  40. } = params
  41. const projectUserIds = await getProjectUsersIds(projectId)
  42. for (const userId of projectUserIds) {
  43. const job = {
  44. method: 'post',
  45. headers: {
  46. sl_entity_id: entityId,
  47. sl_entity_type: entityType,
  48. sl_entity_rev: rev,
  49. sl_project_id: projectId,
  50. sl_folder_id: folderId,
  51. },
  52. uri: buildTpdsUrl(userId, projectName, path),
  53. title: 'addFile',
  54. streamOrigin,
  55. }
  56. await enqueue(userId, 'pipeStreamFrom', job)
  57. }
  58. }
  59. async function addFile(params) {
  60. metrics.inc('tpds.add-file')
  61. const { projectId, fileId, path, projectName, rev, folderId } = params
  62. const streamOrigin =
  63. settings.apis.filestore.url +
  64. Path.join(`/project/${projectId}`, `/file/${fileId}`)
  65. await addEntity({
  66. projectId,
  67. path,
  68. projectName,
  69. rev,
  70. folderId,
  71. streamOrigin,
  72. entityId: fileId,
  73. entityType: 'file',
  74. })
  75. }
  76. function buildMovePaths(params) {
  77. if (params.newProjectName) {
  78. return {
  79. startPath: Path.join('/', params.projectName, '/'),
  80. endPath: Path.join('/', params.newProjectName, '/'),
  81. }
  82. } else {
  83. return {
  84. startPath: Path.join('/', params.projectName, '/', params.startPath),
  85. endPath: Path.join('/', params.projectName, '/', params.endPath),
  86. }
  87. }
  88. }
  89. function buildTpdsUrl(userId, projectName, filePath) {
  90. const projectPath = encodeURIComponent(Path.join(projectName, '/', filePath))
  91. return `${tpdsUrl}/user/${userId}/entity/${projectPath}`
  92. }
  93. async function deleteEntity(params) {
  94. metrics.inc('tpds.delete-entity')
  95. const {
  96. projectId,
  97. path,
  98. projectName,
  99. entityId,
  100. entityType,
  101. subtreeEntityIds,
  102. } = params
  103. const projectUserIds = await getProjectUsersIds(projectId)
  104. for (const userId of projectUserIds) {
  105. const job = {
  106. method: 'delete',
  107. headers: {
  108. sl_project_id: projectId,
  109. sl_entity_id: entityId,
  110. sl_entity_type: entityType,
  111. },
  112. uri: buildTpdsUrl(userId, projectName, path),
  113. // We're sending a body with the DELETE request. This is unconventional,
  114. // but Express does handle it on the other side. Ideally, this operation
  115. // would be moved to a POST endpoint.
  116. json: { subtreeEntityIds },
  117. title: 'deleteEntity',
  118. }
  119. await enqueue(userId, 'standardHttpRequest', job)
  120. }
  121. }
  122. async function createProject(params) {
  123. if (!tpdsUrl) return // Overleaf Community Edition/Server Pro
  124. const { projectId, projectName, userId } = params
  125. const job = {
  126. method: 'post',
  127. headers: {
  128. sl_project_id: projectId.toString(),
  129. },
  130. uri: Path.join(
  131. tpdsUrl,
  132. 'user',
  133. userId.toString(),
  134. 'project',
  135. 'new',
  136. encodeURIComponent(projectName)
  137. ),
  138. title: 'createProject',
  139. }
  140. await enqueue(userId, 'standardHttpRequest', job)
  141. }
  142. async function enqueue(group, method, job) {
  143. const tpdsWorkerUrl = _.get(settings, ['apis', 'tpdsworker', 'url'])
  144. // silently do nothing if worker url is not in settings
  145. if (!tpdsWorkerUrl) {
  146. return
  147. }
  148. try {
  149. const url = new URL('/enqueue/web_to_tpds_http_requests', tpdsWorkerUrl)
  150. await fetchNothing(url, {
  151. method: 'POST',
  152. json: { group, job, method },
  153. signal: AbortSignal.timeout(5 * 1000),
  154. })
  155. } catch (err) {
  156. // log error and continue
  157. logger.error({ err, group, job, method }, 'error enqueueing tpdsworker job')
  158. }
  159. }
  160. async function getProjectUsersIds(projectId) {
  161. // get list of all user ids with access to project. project owner
  162. // will always be the first entry in the list.
  163. const userIds = await CollaboratorsGetter.getInvitedMemberIds(projectId)
  164. // filter invited users to only return those with dropbox linked
  165. const dropboxUsers = await UserGetter.getUsers(
  166. {
  167. _id: { $in: userIds.map(id => new ObjectId(id)) },
  168. 'dropbox.access_token.uid': { $ne: null },
  169. },
  170. {
  171. _id: 1,
  172. }
  173. )
  174. const dropboxUserIds = dropboxUsers.map(user => user._id)
  175. return dropboxUserIds
  176. }
  177. async function moveEntity(params) {
  178. metrics.inc('tpds.move-entity')
  179. const { projectId, rev, entityId, entityType, folderId } = params
  180. const projectUserIds = await getProjectUsersIds(projectId)
  181. const { endPath, startPath } = buildMovePaths(params)
  182. for (const userId of projectUserIds) {
  183. const headers = {
  184. sl_project_id: projectId,
  185. sl_entity_rev: rev,
  186. }
  187. if (entityId != null) {
  188. headers.sl_entity_id = entityId
  189. }
  190. if (entityType != null) {
  191. headers.sl_entity_type = entityType
  192. }
  193. if (folderId != null) {
  194. headers.sl_folder_id = folderId
  195. }
  196. const job = {
  197. method: 'put',
  198. title: 'moveEntity',
  199. uri: `${tpdsUrl}/user/${userId}/entity`,
  200. headers,
  201. json: {
  202. user_id: userId,
  203. endPath,
  204. startPath,
  205. },
  206. }
  207. await enqueue(userId, 'standardHttpRequest', job)
  208. }
  209. }
  210. async function pollDropboxForUser(userId) {
  211. metrics.inc('tpds.poll-dropbox')
  212. const job = {
  213. method: 'post',
  214. uri: `${tpdsUrl}/user/poll`,
  215. json: {
  216. user_ids: [userId],
  217. },
  218. }
  219. // Queue poll requests in the user queue along with file updates, in order
  220. // to avoid race conditions between polling and updates.
  221. await enqueue(userId, 'standardHttpRequest', job)
  222. }
  223. const TpdsUpdateSender = {
  224. addDoc: callbackify(addDoc),
  225. addEntity: callbackify(addEntity),
  226. addFile: callbackify(addFile),
  227. deleteEntity: callbackify(deleteEntity),
  228. createProject: callbackify(createProject),
  229. enqueue: callbackify(enqueue),
  230. moveEntity: callbackify(moveEntity),
  231. pollDropboxForUser: callbackify(pollDropboxForUser),
  232. promises: {
  233. addDoc,
  234. addEntity,
  235. addFile,
  236. deleteEntity,
  237. createProject,
  238. enqueue,
  239. moveEntity,
  240. pollDropboxForUser,
  241. },
  242. }
  243. module.exports = TpdsUpdateSender