TpdsUpdateSender.mjs 7.0 KB

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