TpdsUpdateSender.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  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 fetch = require('node-fetch')
  8. const settings = require('@overleaf/settings')
  9. const CollaboratorsGetter =
  10. require('../Collaborators/CollaboratorsGetter').promises
  11. const UserGetter = require('../User/UserGetter.js').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_all_user_ids: JSON.stringify([userId]),
  51. sl_project_owner_user_id: projectUserIds[0],
  52. sl_folder_id: folderId,
  53. },
  54. uri: buildTpdsUrl(userId, projectName, path),
  55. title: 'addFile',
  56. streamOrigin,
  57. }
  58. await enqueue(userId, 'pipeStreamFrom', job)
  59. }
  60. }
  61. async function addFile(params) {
  62. metrics.inc('tpds.add-file')
  63. const { projectId, fileId, path, projectName, rev, folderId } = params
  64. const streamOrigin =
  65. settings.apis.filestore.url +
  66. Path.join(`/project/${projectId}`, `/file/${fileId}`)
  67. await addEntity({
  68. projectId,
  69. path,
  70. projectName,
  71. rev,
  72. folderId,
  73. streamOrigin,
  74. entityId: fileId,
  75. entityType: 'file',
  76. })
  77. }
  78. function buildMovePaths(params) {
  79. if (params.newProjectName) {
  80. return {
  81. startPath: Path.join('/', params.projectName, '/'),
  82. endPath: Path.join('/', params.newProjectName, '/'),
  83. }
  84. } else {
  85. return {
  86. startPath: Path.join('/', params.projectName, '/', params.startPath),
  87. endPath: Path.join('/', params.projectName, '/', params.endPath),
  88. }
  89. }
  90. }
  91. function buildTpdsUrl(userId, projectName, filePath) {
  92. const projectPath = encodeURIComponent(Path.join(projectName, '/', filePath))
  93. return `${tpdsUrl}/user/${userId}/entity/${projectPath}`
  94. }
  95. async function deleteEntity(params) {
  96. metrics.inc('tpds.delete-entity')
  97. const {
  98. projectId,
  99. path,
  100. projectName,
  101. entityId,
  102. entityType,
  103. subtreeEntityIds,
  104. } = params
  105. const projectUserIds = await getProjectUsersIds(projectId)
  106. for (const userId of projectUserIds) {
  107. const job = {
  108. method: 'delete',
  109. headers: {
  110. sl_project_id: projectId,
  111. sl_all_user_ids: JSON.stringify([userId]),
  112. sl_project_owner_user_id: projectUserIds[0],
  113. sl_entity_id: entityId,
  114. sl_entity_type: entityType,
  115. },
  116. uri: buildTpdsUrl(userId, projectName, path),
  117. // We're sending a body with the DELETE request. This is unconventional,
  118. // but Express does handle it on the other side. Ideally, this operation
  119. // would be moved to a POST endpoint.
  120. json: { subtreeEntityIds },
  121. title: 'deleteEntity',
  122. sl_all_user_ids: JSON.stringify([userId]),
  123. }
  124. await enqueue(userId, 'standardHttpRequest', job)
  125. }
  126. }
  127. async function createProject(params) {
  128. if (!tpdsUrl) return // Server CE/Pro
  129. const { projectId, projectName, ownerId, userId } = params
  130. const job = {
  131. method: 'post',
  132. headers: {
  133. sl_project_id: projectId.toString(),
  134. sl_all_user_ids: JSON.stringify([userId.toString()]),
  135. sl_project_owner_user_id: ownerId.toString(),
  136. },
  137. uri: Path.join(
  138. tpdsUrl,
  139. 'user',
  140. userId.toString(),
  141. 'project',
  142. 'new',
  143. encodeURIComponent(projectName)
  144. ),
  145. title: 'createProject',
  146. sl_all_user_ids: JSON.stringify([userId]),
  147. }
  148. await enqueue(userId, 'standardHttpRequest', job)
  149. }
  150. async function deleteProject(params) {
  151. const { projectId } = params
  152. // deletion only applies to project archiver
  153. const projectArchiverUrl = _.get(settings, [
  154. 'apis',
  155. 'project_archiver',
  156. 'url',
  157. ])
  158. // silently do nothing if project archiver url is not in settings
  159. if (!projectArchiverUrl) {
  160. return
  161. }
  162. metrics.inc('tpds.delete-project')
  163. // send the request directly to project archiver, bypassing third-party-datastore
  164. try {
  165. const response = await fetch(
  166. `${settings.apis.project_archiver.url}/project/${projectId}`,
  167. { method: 'DELETE' }
  168. )
  169. if (!response.ok) {
  170. logger.error(
  171. { statusCode: response.status, project_id: projectId },
  172. 'error deleting project in third party datastore (project_archiver)'
  173. )
  174. return false
  175. }
  176. return true
  177. } catch (err) {
  178. logger.error(
  179. { err, projectId },
  180. 'error deleting project in third party datastore (project_archiver)'
  181. )
  182. return false
  183. }
  184. }
  185. async function enqueue(group, method, job) {
  186. const tpdsWorkerUrl = _.get(settings, ['apis', 'tpdsworker', 'url'])
  187. // silently do nothing if worker url is not in settings
  188. if (!tpdsWorkerUrl) {
  189. return
  190. }
  191. try {
  192. const url = new URL('/enqueue/web_to_tpds_http_requests', tpdsWorkerUrl)
  193. const response = await fetch(url, {
  194. method: 'POST',
  195. headers: { 'Content-Type': 'application/json' },
  196. body: JSON.stringify({ group, job, method }),
  197. signal: AbortSignal.timeout(5 * 1000),
  198. })
  199. if (!response.ok) {
  200. // log error and continue
  201. logger.error(
  202. { statusCode: response.status, group, job, method },
  203. 'error enqueueing tpdsworker job'
  204. )
  205. }
  206. } catch (err) {
  207. // log error and continue
  208. logger.error({ err, group, job, method }, 'error enqueueing tpdsworker job')
  209. }
  210. }
  211. async function getProjectUsersIds(projectId) {
  212. // get list of all user ids with access to project. project owner
  213. // will always be the first entry in the list.
  214. const [ownerUserId, ...invitedUserIds] =
  215. await CollaboratorsGetter.getInvitedMemberIds(projectId)
  216. // if there are no invited users, always return the owner
  217. if (!invitedUserIds.length) {
  218. return [ownerUserId]
  219. }
  220. // filter invited users to only return those with dropbox linked
  221. const dropboxUsers = await UserGetter.getUsers(
  222. {
  223. _id: { $in: invitedUserIds.map(id => ObjectId(id)) },
  224. 'dropbox.access_token.uid': { $ne: null },
  225. },
  226. {
  227. _id: 1,
  228. }
  229. )
  230. const dropboxUserIds = dropboxUsers.map(user => user._id)
  231. return [ownerUserId, ...dropboxUserIds]
  232. }
  233. async function moveEntity(params) {
  234. metrics.inc('tpds.move-entity')
  235. const { projectId, rev, entityId, entityType, folderId } = params
  236. const projectUserIds = await getProjectUsersIds(projectId)
  237. const { endPath, startPath } = buildMovePaths(params)
  238. for (const userId of projectUserIds) {
  239. const headers = {
  240. sl_project_id: projectId,
  241. sl_entity_rev: rev,
  242. sl_all_user_ids: JSON.stringify([userId]),
  243. sl_project_owner_user_id: projectUserIds[0],
  244. }
  245. if (entityId != null) {
  246. headers.sl_entity_id = entityId
  247. }
  248. if (entityType != null) {
  249. headers.sl_entity_type = entityType
  250. }
  251. if (folderId != null) {
  252. headers.sl_folder_id = folderId
  253. }
  254. const job = {
  255. method: 'put',
  256. title: 'moveEntity',
  257. uri: `${tpdsUrl}/user/${userId}/entity`,
  258. headers,
  259. json: {
  260. user_id: userId,
  261. endPath,
  262. startPath,
  263. },
  264. }
  265. await enqueue(userId, 'standardHttpRequest', job)
  266. }
  267. }
  268. async function pollDropboxForUser(userId) {
  269. metrics.inc('tpds.poll-dropbox')
  270. const job = {
  271. method: 'post',
  272. uri: `${tpdsUrl}/user/poll`,
  273. json: {
  274. user_ids: [userId],
  275. },
  276. }
  277. // Queue poll requests in the user queue along with file updates, in order
  278. // to avoid race conditions between polling and updates.
  279. await enqueue(userId, 'standardHttpRequest', job)
  280. }
  281. const TpdsUpdateSender = {
  282. addDoc: callbackify(addDoc),
  283. addEntity: callbackify(addEntity),
  284. addFile: callbackify(addFile),
  285. deleteEntity: callbackify(deleteEntity),
  286. createProject: callbackify(createProject),
  287. deleteProject: callbackify(deleteProject),
  288. enqueue: callbackify(enqueue),
  289. moveEntity: callbackify(moveEntity),
  290. pollDropboxForUser: callbackify(pollDropboxForUser),
  291. promises: {
  292. addDoc,
  293. addEntity,
  294. addFile,
  295. deleteEntity,
  296. createProject,
  297. deleteProject,
  298. enqueue,
  299. moveEntity,
  300. pollDropboxForUser,
  301. },
  302. }
  303. module.exports = TpdsUpdateSender