From 3b1346f1dbebee54dded72fa3d1db536f9243619 Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Fri, 19 Apr 2024 13:17:31 +0200 Subject: [PATCH 01/17] Fullfill spaces --- packages/web-client/src/sse/index.ts | 8 +- .../web-runtime/src/container/bootstrap.ts | 78 +++++- packages/web-runtime/src/container/sse.ts | 239 +++++++++++++++++- 3 files changed, 322 insertions(+), 3 deletions(-) diff --git a/packages/web-client/src/sse/index.ts b/packages/web-client/src/sse/index.ts index 6514eec4788..e5c16a27bf5 100644 --- a/packages/web-client/src/sse/index.ts +++ b/packages/web-client/src/sse/index.ts @@ -10,7 +10,13 @@ export enum MESSAGE_TYPE { ITEM_TRASHED = 'item-trashed', ITEM_RESTORED = 'item-restored', ITEM_MOVED = 'item-moved', - FOLDER_CREATED = 'folder-created' + FOLDER_CREATED = 'folder-created', + SPACE_MEMBER_ADDED = 'space-member-added', + SPACE_MEMBER_REMOVED = 'space-member-removed', + SPACE_SHARE_UPDATED = 'space-share-updated', + SHARE_CREATED = 'share-created', + SHARE_REMOVED = 'share-removed', + SHARE_UPDATED = 'share-updated' } export class RetriableError extends Error { diff --git a/packages/web-runtime/src/container/bootstrap.ts b/packages/web-runtime/src/container/bootstrap.ts index 20b3216814f..4e43df52f01 100644 --- a/packages/web-runtime/src/container/bootstrap.ts +++ b/packages/web-runtime/src/container/bootstrap.ts @@ -62,7 +62,13 @@ import { onSSEItemTrashedEvent, onSSEFolderCreatedEvent, onSSEFileTouchedEvent, - onSSEItemMovedEvent + onSSEItemMovedEvent, + onSSESpaceMemberAddedEvent, + onSSESpaceMemberRemovedEvent, + onSSESpaceShareUpdatedEvent, + onSSEShareCreatedEvent, + onSSEShareRemovedEvent, + onSSEShareUpdatedEvent } from './sse' const getEmbedConfigFromQuery = ( @@ -757,6 +763,76 @@ export const registerSSEEventListeners = ({ clientService }) ) + + clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.SPACE_MEMBER_ADDED, (msg) => + onSSESpaceMemberAddedEvent({ + topic: MESSAGE_TYPE.SPACE_MEMBER_ADDED, + resourcesStore, + spacesStore, + userStore, + msg, + clientService, + router + }) + ) + + clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.SPACE_MEMBER_REMOVED, (msg) => + onSSESpaceMemberRemovedEvent({ + topic: MESSAGE_TYPE.SPACE_MEMBER_REMOVED, + resourcesStore, + spacesStore, + userStore, + messageStore, + msg, + clientService, + language, + router + }) + ) + + clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.SPACE_SHARE_UPDATED, (msg) => + onSSESpaceShareUpdatedEvent({ + topic: MESSAGE_TYPE.SPACE_SHARE_UPDATED, + resourcesStore, + spacesStore, + userStore, + msg, + clientService + }) + ) + + clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.SHARE_CREATED, (msg) => + onSSEShareCreatedEvent({ + topic: MESSAGE_TYPE.SHARE_CREATED, + resourcesStore, + spacesStore, + userStore, + msg, + clientService + }) + ) + + clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.SHARE_REMOVED, (msg) => + onSSEShareRemovedEvent({ + topic: MESSAGE_TYPE.SHARE_REMOVED, + resourcesStore, + spacesStore, + userStore, + msg, + clientService + }) + ) + + clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.SHARE_UPDATED, (msg) => + onSSEShareUpdatedEvent({ + topic: MESSAGE_TYPE.SHARE_UPDATED, + resourcesStore, + spacesStore, + userStore, + msg, + clientService + }) + ) } export const setViewOptions = ({ resourcesStore }: { resourcesStore: ResourcesStore }) => { diff --git a/packages/web-runtime/src/container/sse.ts b/packages/web-runtime/src/container/sse.ts index ac5259ce4da..6f32af8438b 100644 --- a/packages/web-runtime/src/container/sse.ts +++ b/packages/web-runtime/src/container/sse.ts @@ -3,6 +3,7 @@ import { createFileRouteOptions, getIndicators, ImageDimension, + isLocationSpacesActive, MessageStore, PreviewService, ResourcesStore, @@ -10,7 +11,7 @@ import { UserStore } from '@ownclouders/web-pkg' import PQueue from 'p-queue' -import { extractNodeId, extractStorageId } from '@ownclouders/web-client' +import { buildSpace, extractNodeId, extractStorageId } from '@ownclouders/web-client' import { z } from 'zod' import { Router } from 'vue-router' import { Language } from 'vue3-gettext' @@ -514,3 +515,239 @@ export const onSSEFolderCreatedEvent = async ({ console.error(`Unable to parse sse event ${topic} data`, e) } } + +export const onSSESpaceMemberAddedEvent = async ({ + topic, + resourcesStore, + spacesStore, + userStore, + msg, + clientService, + router +}: { + topic: string + resourcesStore: ResourcesStore + spacesStore: SpacesStore + userStore: UserStore + msg: MessageEvent + clientService: ClientService + router: Router +}) => { + try { + const sseData = eventSchema.parse(JSON.parse(msg.data)) + console.debug(`SSE event '${topic}'`, sseData) + + if (sseData.initiatorid === clientService.initiatorId) { + /** + * If the request was initiated by the current client (browser tab), + * there's no need to proceed with the action since the web already + * handles its own business logic. Therefore, we'll return early here. + */ + return + } + + let space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + const { data } = await clientService.graphAuthenticated.drives.getDrive(sseData.itemid) + space = buildSpace(data) + spacesStore.addSpaces([space]) + } + + if (!isLocationSpacesActive(router, 'files-spaces-projects')) { + return + } + + resourcesStore.upsertResource(space) + } catch (e) { + console.error(`Unable to parse sse event ${topic} data`, e) + } +} + +export const onSSESpaceMemberRemovedEvent = async ({ + topic, + resourcesStore, + spacesStore, + userStore, + messageStore, + msg, + clientService, + language, + router +}: { + topic: string + resourcesStore: ResourcesStore + spacesStore: SpacesStore + userStore: UserStore + msg: MessageEvent + clientService: ClientService + messageStore: MessageStore + language: Language + router: Router +}) => { + try { + const sseData = eventSchema.parse(JSON.parse(msg.data)) + console.debug(`SSE event '${topic}'`, sseData) + + if (sseData.initiatorid === clientService.initiatorId) { + /** + * If the request was initiated by the current client (browser tab), + * there's no need to proceed with the action since the web already + * handles its own business logic. Therefore, we'll return early here. + */ + return + } + + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } + + spacesStore.removeSpace(space) + + if (isLocationSpacesActive(router, 'files-spaces-projects')) { + return resourcesStore.removeResources([space]) + } + + console.log(resourcesStore.currentFolder) + console.log(space) + if ( + isLocationSpacesActive(router, 'files-spaces-generic') && + space.id === resourcesStore.currentFolder.storageId + ) { + return messageStore.showMessage({ + title: language.$gettext( + 'The space you were accessing has been unshared. Please navigate to another location.' + ) + }) + } + } catch (e) { + console.error(`Unable to parse sse event ${topic} data`, e) + } +} + +export const onSSESpaceShareUpdatedEvent = async ({ + topic, + resourcesStore, + spacesStore, + userStore, + msg, + clientService +}: { + topic: string + resourcesStore: ResourcesStore + spacesStore: SpacesStore + userStore: UserStore + msg: MessageEvent + clientService: ClientService +}) => { + try { + const sseData = eventSchema.parse(JSON.parse(msg.data)) + console.debug(`SSE event '${topic}'`, sseData) + + if (sseData.initiatorid === clientService.initiatorId) { + /** + * If the request was initiated by the current client (browser tab), + * there's no need to proceed with the action since the web already + * handles its own business logic. Therefore, we'll return early here. + */ + return + } + } catch (e) { + console.error(`Unable to parse sse event ${topic} data`, e) + } +} + +export const onSSEShareCreatedEvent = async ({ + topic, + resourcesStore, + spacesStore, + userStore, + msg, + clientService +}: { + topic: string + resourcesStore: ResourcesStore + spacesStore: SpacesStore + userStore: UserStore + msg: MessageEvent + clientService: ClientService +}) => { + try { + const sseData = eventSchema.parse(JSON.parse(msg.data)) + console.debug(`SSE event '${topic}'`, sseData) + + if (sseData.initiatorid === clientService.initiatorId) { + /** + * If the request was initiated by the current client (browser tab), + * there's no need to proceed with the action since the web already + * handles its own business logic. Therefore, we'll return early here. + */ + return + } + } catch (e) { + console.error(`Unable to parse sse event ${topic} data`, e) + } +} +export const onSSEShareUpdatedEvent = async ({ + topic, + resourcesStore, + spacesStore, + userStore, + msg, + clientService +}: { + topic: string + resourcesStore: ResourcesStore + spacesStore: SpacesStore + userStore: UserStore + msg: MessageEvent + clientService: ClientService +}) => { + try { + const sseData = eventSchema.parse(JSON.parse(msg.data)) + console.debug(`SSE event '${topic}'`, sseData) + + if (sseData.initiatorid === clientService.initiatorId) { + /** + * If the request was initiated by the current client (browser tab), + * there's no need to proceed with the action since the web already + * handles its own business logic. Therefore, we'll return early here. + */ + return + } + } catch (e) { + console.error(`Unable to parse sse event ${topic} data`, e) + } +} + +export const onSSEShareRemovedEvent = async ({ + topic, + resourcesStore, + spacesStore, + userStore, + msg, + clientService +}: { + topic: string + resourcesStore: ResourcesStore + spacesStore: SpacesStore + userStore: UserStore + msg: MessageEvent + clientService: ClientService +}) => { + try { + const sseData = eventSchema.parse(JSON.parse(msg.data)) + console.debug(`SSE event '${topic}'`, sseData) + + if (sseData.initiatorid === clientService.initiatorId) { + /** + * If the request was initiated by the current client (browser tab), + * there's no need to proceed with the action since the web already + * handles its own business logic. Therefore, we'll return early here. + */ + return + } + } catch (e) { + console.error(`Unable to parse sse event ${topic} data`, e) + } +} From 6a8db8b782236c41be112ce5aaa73e0582ed3dde Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Fri, 19 Apr 2024 13:23:31 +0200 Subject: [PATCH 02/17] Adjust wording --- packages/web-runtime/src/container/sse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web-runtime/src/container/sse.ts b/packages/web-runtime/src/container/sse.ts index 6f32af8438b..43243c394e3 100644 --- a/packages/web-runtime/src/container/sse.ts +++ b/packages/web-runtime/src/container/sse.ts @@ -616,7 +616,7 @@ export const onSSESpaceMemberRemovedEvent = async ({ ) { return messageStore.showMessage({ title: language.$gettext( - 'The space you were accessing has been unshared. Please navigate to another location.' + 'Your access to this space has been revoked. Please navigate to another location.' ) }) } From 8b23ecd588cde35135041ed80884682b01dcdd34 Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Mon, 22 Apr 2024 11:45:56 +0200 Subject: [PATCH 03/17] WIP --- packages/web-client/src/sse/index.ts | 5 +- .../web-runtime/src/container/bootstrap.ts | 47 ++++- packages/web-runtime/src/container/sse.ts | 163 +++++++++++++++--- 3 files changed, 183 insertions(+), 32 deletions(-) diff --git a/packages/web-client/src/sse/index.ts b/packages/web-client/src/sse/index.ts index e5c16a27bf5..867c0ff4de8 100644 --- a/packages/web-client/src/sse/index.ts +++ b/packages/web-client/src/sse/index.ts @@ -16,7 +16,10 @@ export enum MESSAGE_TYPE { SPACE_SHARE_UPDATED = 'space-share-updated', SHARE_CREATED = 'share-created', SHARE_REMOVED = 'share-removed', - SHARE_UPDATED = 'share-updated' + SHARE_UPDATED = 'share-updated', + LINK_CREATED = 'link-created', + LINK_REMOVED = 'link-removed', + LINK_UPDATED = 'link-updated' } export class RetriableError extends Error { diff --git a/packages/web-runtime/src/container/bootstrap.ts b/packages/web-runtime/src/container/bootstrap.ts index 4e43df52f01..53a6475a8e6 100644 --- a/packages/web-runtime/src/container/bootstrap.ts +++ b/packages/web-runtime/src/container/bootstrap.ts @@ -68,7 +68,9 @@ import { onSSESpaceShareUpdatedEvent, onSSEShareCreatedEvent, onSSEShareRemovedEvent, - onSSEShareUpdatedEvent + onSSEShareUpdatedEvent, + onSSELinkCreatedEvent, + onSSELinkRemovedEvent } from './sse' const getEmbedConfigFromQuery = ( @@ -769,7 +771,6 @@ export const registerSSEEventListeners = ({ topic: MESSAGE_TYPE.SPACE_MEMBER_ADDED, resourcesStore, spacesStore, - userStore, msg, clientService, router @@ -781,7 +782,6 @@ export const registerSSEEventListeners = ({ topic: MESSAGE_TYPE.SPACE_MEMBER_REMOVED, resourcesStore, spacesStore, - userStore, messageStore, msg, clientService, @@ -793,11 +793,13 @@ export const registerSSEEventListeners = ({ clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.SPACE_SHARE_UPDATED, (msg) => onSSESpaceShareUpdatedEvent({ topic: MESSAGE_TYPE.SPACE_SHARE_UPDATED, - resourcesStore, spacesStore, - userStore, + resourcesStore, + messageStore, msg, - clientService + clientService, + language, + router }) ) @@ -833,6 +835,39 @@ export const registerSSEEventListeners = ({ clientService }) ) + + clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.LINK_CREATED, (msg) => + onSSELinkCreatedEvent({ + topic: MESSAGE_TYPE.SHARE_CREATED, + resourcesStore, + spacesStore, + userStore, + msg, + clientService + }) + ) + + clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.LINK_REMOVED, (msg) => + onSSELinkRemovedEvent({ + topic: MESSAGE_TYPE.SHARE_REMOVED, + resourcesStore, + spacesStore, + userStore, + msg, + clientService + }) + ) + + clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.LINK_UPDATED, (msg) => + onSSELinkRemovedEvent({ + topic: MESSAGE_TYPE.SHARE_UPDATED, + resourcesStore, + spacesStore, + userStore, + msg, + clientService + }) + ) } export const setViewOptions = ({ resourcesStore }: { resourcesStore: ResourcesStore }) => { diff --git a/packages/web-runtime/src/container/sse.ts b/packages/web-runtime/src/container/sse.ts index 43243c394e3..797e6edf001 100644 --- a/packages/web-runtime/src/container/sse.ts +++ b/packages/web-runtime/src/container/sse.ts @@ -520,7 +520,6 @@ export const onSSESpaceMemberAddedEvent = async ({ topic, resourcesStore, spacesStore, - userStore, msg, clientService, router @@ -528,7 +527,6 @@ export const onSSESpaceMemberAddedEvent = async ({ topic: string resourcesStore: ResourcesStore spacesStore: SpacesStore - userStore: UserStore msg: MessageEvent clientService: ClientService router: Router @@ -546,12 +544,9 @@ export const onSSESpaceMemberAddedEvent = async ({ return } - let space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!space) { - const { data } = await clientService.graphAuthenticated.drives.getDrive(sseData.itemid) - space = buildSpace(data) - spacesStore.addSpaces([space]) - } + const { data } = await clientService.graphAuthenticated.drives.getDrive(sseData.itemid) + const space = buildSpace(data) + spacesStore.upsertSpace(space) if (!isLocationSpacesActive(router, 'files-spaces-projects')) { return @@ -567,7 +562,6 @@ export const onSSESpaceMemberRemovedEvent = async ({ topic, resourcesStore, spacesStore, - userStore, messageStore, msg, clientService, @@ -577,7 +571,6 @@ export const onSSESpaceMemberRemovedEvent = async ({ topic: string resourcesStore: ResourcesStore spacesStore: SpacesStore - userStore: UserStore msg: MessageEvent clientService: ClientService messageStore: MessageStore @@ -597,27 +590,84 @@ export const onSSESpaceMemberRemovedEvent = async ({ return } - const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!space) { - return + try { + const { data } = await clientService.graphAuthenticated.drives.listMyDrives( + '', + `id eq '${sseData.spaceid}'` + ) + const space = buildSpace(data.value[0]) + return spacesStore.upsertSpace(space) + } catch (_) { + const removedSpace = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!removedSpace) { + return + } + + spacesStore.removeSpace(removedSpace) + + if (isLocationSpacesActive(router, 'files-spaces-projects')) { + return resourcesStore.removeResources([removedSpace]) + } + + if ( + isLocationSpacesActive(router, 'files-spaces-generic') && + removedSpace.id === resourcesStore.currentFolder.storageId + ) { + return messageStore.showMessage({ + title: language.$gettext( + 'Your access to this space has been revoked. Please navigate to another location.' + ) + }) + } } + } catch (e) { + console.error(`Unable to parse sse event ${topic} data`, e) + } +} - spacesStore.removeSpace(space) +export const onSSESpaceShareUpdatedEvent = async ({ + topic, + spacesStore, + resourcesStore, + messageStore, + msg, + clientService, + router, + language +}: { + topic: string + spacesStore: SpacesStore + resourcesStore: ResourcesStore + messageStore: MessageStore + msg: MessageEvent + clientService: ClientService + router: Router + language: Language +}) => { + try { + const sseData = eventSchema.parse(JSON.parse(msg.data)) + console.debug(`SSE event '${topic}'`, sseData) - if (isLocationSpacesActive(router, 'files-spaces-projects')) { - return resourcesStore.removeResources([space]) + if (sseData.initiatorid === clientService.initiatorId) { + /** + * If the request was initiated by the current client (browser tab), + * there's no need to proceed with the action since the web already + * handles its own business logic. Therefore, we'll return early here. + */ + return } - console.log(resourcesStore.currentFolder) - console.log(space) + const { data } = await clientService.graphAuthenticated.drives.getDrive(sseData.itemid) + const space = buildSpace(data) + spacesStore.upsertSpace(space) + + //TODO: Check that the actual user permission has changed, affectedUserId must be present in event if ( isLocationSpacesActive(router, 'files-spaces-generic') && space.id === resourcesStore.currentFolder.storageId ) { return messageStore.showMessage({ - title: language.$gettext( - 'Your access to this space has been revoked. Please navigate to another location.' - ) + title: language.$gettext('Your space role has been updated.') }) } } catch (e) { @@ -625,7 +675,38 @@ export const onSSESpaceMemberRemovedEvent = async ({ } } -export const onSSESpaceShareUpdatedEvent = async ({ +export const onSSEShareCreatedEvent = async ({ + topic, + resourcesStore, + spacesStore, + userStore, + msg, + clientService +}: { + topic: string + resourcesStore: ResourcesStore + spacesStore: SpacesStore + userStore: UserStore + msg: MessageEvent + clientService: ClientService +}) => { + try { + const sseData = eventSchema.parse(JSON.parse(msg.data)) + console.debug(`SSE event '${topic}'`, sseData) + + if (sseData.initiatorid === clientService.initiatorId) { + /** + * If the request was initiated by the current client (browser tab), + * there's no need to proceed with the action since the web already + * handles its own business logic. Therefore, we'll return early here. + */ + return + } + } catch (e) { + console.error(`Unable to parse sse event ${topic} data`, e) + } +} +export const onSSEShareUpdatedEvent = async ({ topic, resourcesStore, spacesStore, @@ -657,7 +738,7 @@ export const onSSESpaceShareUpdatedEvent = async ({ } } -export const onSSEShareCreatedEvent = async ({ +export const onSSEShareRemovedEvent = async ({ topic, resourcesStore, spacesStore, @@ -688,7 +769,39 @@ export const onSSEShareCreatedEvent = async ({ console.error(`Unable to parse sse event ${topic} data`, e) } } -export const onSSEShareUpdatedEvent = async ({ + +export const onSSELinkCreatedEvent = async ({ + topic, + resourcesStore, + spacesStore, + userStore, + msg, + clientService +}: { + topic: string + resourcesStore: ResourcesStore + spacesStore: SpacesStore + userStore: UserStore + msg: MessageEvent + clientService: ClientService +}) => { + try { + const sseData = eventSchema.parse(JSON.parse(msg.data)) + console.debug(`SSE event '${topic}'`, sseData) + + if (sseData.initiatorid === clientService.initiatorId) { + /** + * If the request was initiated by the current client (browser tab), + * there's no need to proceed with the action since the web already + * handles its own business logic. Therefore, we'll return early here. + */ + return + } + } catch (e) { + console.error(`Unable to parse sse event ${topic} data`, e) + } +} +export const onSSELinkUpdatedEvent = async ({ topic, resourcesStore, spacesStore, @@ -720,7 +833,7 @@ export const onSSEShareUpdatedEvent = async ({ } } -export const onSSEShareRemovedEvent = async ({ +export const onSSELinkRemovedEvent = async ({ topic, resourcesStore, spacesStore, From 0a726ea175ee1c39e6b97522059890a28942efb3 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Mon, 22 Apr 2024 12:16:47 +0200 Subject: [PATCH 04/17] refactor: add wrapper method for SSE event methods --- .../web-runtime/src/container/bootstrap.ts | 171 ++-- packages/web-runtime/src/container/sse.ts | 965 ++++++------------ 2 files changed, 405 insertions(+), 731 deletions(-) diff --git a/packages/web-runtime/src/container/bootstrap.ts b/packages/web-runtime/src/container/bootstrap.ts index 53a6475a8e6..8f96b5c80e8 100644 --- a/packages/web-runtime/src/container/bootstrap.ts +++ b/packages/web-runtime/src/container/bootstrap.ts @@ -70,7 +70,9 @@ import { onSSEShareRemovedEvent, onSSEShareUpdatedEvent, onSSELinkCreatedEvent, - onSSELinkRemovedEvent + onSSELinkRemovedEvent, + sseEventWrapper, + SseEventWrapperOptions } from './sse' const getEmbedConfigFromQuery = ( @@ -668,204 +670,177 @@ export const registerSSEEventListeners = ({ } ) + const sseEventWrapperOptions = { + resourcesStore, + spacesStore, + messageStore, + userStore, + clientService, + previewService, + language, + router, + resourceQueue + } satisfies Partial + clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.ITEM_RENAMED, (msg) => - onSSEItemRenamedEvent({ + sseEventWrapper({ topic: MESSAGE_TYPE.ITEM_RENAMED, - resourcesStore, - spacesStore, msg, - clientService, - router + ...sseEventWrapperOptions, + method: onSSEItemRenamedEvent }) ) clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.POSTPROCESSING_FINISHED, (msg) => - onSSEProcessingFinishedEvent({ + sseEventWrapper({ topic: MESSAGE_TYPE.POSTPROCESSING_FINISHED, - resourcesStore, - spacesStore, msg, - clientService, - previewService, - resourceQueue + ...sseEventWrapperOptions, + method: onSSEProcessingFinishedEvent }) ) clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.FILE_LOCKED, (msg) => - onSSEFileLockingEvent({ + sseEventWrapper({ topic: MESSAGE_TYPE.FILE_LOCKED, - resourcesStore, - spacesStore, - userStore, msg, - clientService + ...sseEventWrapperOptions, + method: onSSEFileLockingEvent }) ) clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.FILE_UNLOCKED, (msg) => - onSSEFileLockingEvent({ + sseEventWrapper({ topic: MESSAGE_TYPE.FILE_UNLOCKED, - resourcesStore, - spacesStore, - userStore, msg, - clientService + ...sseEventWrapperOptions, + method: onSSEFileLockingEvent }) ) clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.ITEM_TRASHED, (msg) => - onSSEItemTrashedEvent({ + sseEventWrapper({ topic: MESSAGE_TYPE.ITEM_TRASHED, - language, - resourcesStore, - clientService, - messageStore, - msg + msg, + ...sseEventWrapperOptions, + method: onSSEItemTrashedEvent }) ) clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.ITEM_RESTORED, (msg) => - onSSEItemRestoredEvent({ + sseEventWrapper({ topic: MESSAGE_TYPE.ITEM_RESTORED, - resourcesStore, - spacesStore, - userStore, msg, - clientService + ...sseEventWrapperOptions, + method: onSSEItemRestoredEvent }) ) clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.ITEM_MOVED, (msg) => - onSSEItemMovedEvent({ + sseEventWrapper({ topic: MESSAGE_TYPE.ITEM_MOVED, - resourcesStore, - spacesStore, - userStore, msg, - clientService + ...sseEventWrapperOptions, + method: onSSEItemMovedEvent }) ) clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.FOLDER_CREATED, (msg) => - onSSEFolderCreatedEvent({ + sseEventWrapper({ topic: MESSAGE_TYPE.FOLDER_CREATED, - resourcesStore, - spacesStore, msg, - clientService + ...sseEventWrapperOptions, + method: onSSEFolderCreatedEvent }) ) clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.FILE_TOUCHED, (msg) => - onSSEFileTouchedEvent({ + sseEventWrapper({ topic: MESSAGE_TYPE.FILE_TOUCHED, - resourcesStore, - spacesStore, msg, - clientService + ...sseEventWrapperOptions, + method: onSSEFileTouchedEvent }) ) clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.SPACE_MEMBER_ADDED, (msg) => - onSSESpaceMemberAddedEvent({ + sseEventWrapper({ topic: MESSAGE_TYPE.SPACE_MEMBER_ADDED, - resourcesStore, - spacesStore, msg, - clientService, - router + ...sseEventWrapperOptions, + method: onSSESpaceMemberAddedEvent }) ) clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.SPACE_MEMBER_REMOVED, (msg) => - onSSESpaceMemberRemovedEvent({ + sseEventWrapper({ topic: MESSAGE_TYPE.SPACE_MEMBER_REMOVED, - resourcesStore, - spacesStore, - messageStore, msg, - clientService, - language, - router + ...sseEventWrapperOptions, + method: onSSESpaceMemberRemovedEvent }) ) clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.SPACE_SHARE_UPDATED, (msg) => - onSSESpaceShareUpdatedEvent({ + sseEventWrapper({ topic: MESSAGE_TYPE.SPACE_SHARE_UPDATED, - spacesStore, - resourcesStore, - messageStore, msg, - clientService, - language, - router + ...sseEventWrapperOptions, + method: onSSESpaceShareUpdatedEvent }) ) clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.SHARE_CREATED, (msg) => - onSSEShareCreatedEvent({ + sseEventWrapper({ topic: MESSAGE_TYPE.SHARE_CREATED, - resourcesStore, - spacesStore, - userStore, msg, - clientService + ...sseEventWrapperOptions, + method: onSSEShareCreatedEvent }) ) clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.SHARE_REMOVED, (msg) => - onSSEShareRemovedEvent({ + sseEventWrapper({ topic: MESSAGE_TYPE.SHARE_REMOVED, - resourcesStore, - spacesStore, - userStore, msg, - clientService + ...sseEventWrapperOptions, + method: onSSEShareRemovedEvent }) ) clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.SHARE_UPDATED, (msg) => - onSSEShareUpdatedEvent({ + sseEventWrapper({ topic: MESSAGE_TYPE.SHARE_UPDATED, - resourcesStore, - spacesStore, - userStore, msg, - clientService + ...sseEventWrapperOptions, + method: onSSEShareUpdatedEvent }) ) clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.LINK_CREATED, (msg) => - onSSELinkCreatedEvent({ - topic: MESSAGE_TYPE.SHARE_CREATED, - resourcesStore, - spacesStore, - userStore, + sseEventWrapper({ + topic: MESSAGE_TYPE.LINK_CREATED, msg, - clientService + ...sseEventWrapperOptions, + method: onSSELinkCreatedEvent }) ) clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.LINK_REMOVED, (msg) => - onSSELinkRemovedEvent({ - topic: MESSAGE_TYPE.SHARE_REMOVED, - resourcesStore, - spacesStore, - userStore, + sseEventWrapper({ + topic: MESSAGE_TYPE.LINK_REMOVED, msg, - clientService + ...sseEventWrapperOptions, + method: onSSELinkRemovedEvent }) ) clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.LINK_UPDATED, (msg) => - onSSELinkRemovedEvent({ - topic: MESSAGE_TYPE.SHARE_UPDATED, - resourcesStore, - spacesStore, - userStore, + sseEventWrapper({ + topic: MESSAGE_TYPE.LINK_UPDATED, msg, - clientService + ...sseEventWrapperOptions, + method: onSSELinkRemovedEvent }) ) } diff --git a/packages/web-runtime/src/container/sse.ts b/packages/web-runtime/src/container/sse.ts index 797e6edf001..b1f81926896 100644 --- a/packages/web-runtime/src/container/sse.ts +++ b/packages/web-runtime/src/container/sse.ts @@ -24,6 +24,39 @@ const eventSchema = z.object({ etag: z.string().optional() }) +export type EventSchemaType = z.infer + +export interface SseEventOptions { + resourcesStore: ResourcesStore + spacesStore: SpacesStore + userStore: UserStore + messageStore: MessageStore + clientService: ClientService + previewService: PreviewService + router: Router + language: Language + resourceQueue: PQueue + sseData: EventSchemaType +} + +export interface SseEventWrapperOptions extends Omit { + msg: MessageEvent + topic: string + method: (options: SseEventOptions) => Promise | unknown +} + +export const sseEventWrapper = (options: SseEventWrapperOptions) => { + const { topic, msg, method, ...sseEventOptions } = options + try { + const sseData = eventSchema.parse(JSON.parse(msg.data)) + console.debug(`SSE event '${topic}'`, sseData) + + return method({ ...sseEventOptions, sseData }) + } catch (e) { + console.error(`Unable to parse sse event ${topic} data`, e) + } +} + const itemInCurrentFolder = ({ resourcesStore, parentFolderId @@ -51,366 +84,259 @@ const itemInCurrentFolder = ({ } export const onSSEItemRenamedEvent = async ({ - topic, + sseData, resourcesStore, spacesStore, - msg, clientService, router -}: { - topic: string - resourcesStore: ResourcesStore - spacesStore: SpacesStore - msg: MessageEvent - clientService: ClientService - router: Router -}) => { - try { - const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.debug(`SSE event '${topic}'`, sseData) - - if (sseData.initiatorid === clientService.initiatorId) { - /** - * If the request was initiated by the current client (browser tab), - * there's no need to proceed with the action since the web already - * handles its own business logic. Therefore, we'll return early here. - */ - return - } - - const currentFolder = resourcesStore.currentFolder - const resourceIsCurrentFolder = currentFolder.id === sseData.itemid - const resource = resourceIsCurrentFolder - ? currentFolder - : resourcesStore.resources.find((f) => f.id === sseData.itemid) - const space = spacesStore.spaces.find((s) => s.id === resource.storageId) - - if (!resource || !space) { - return - } +}: SseEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } - const updatedResource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) + const currentFolder = resourcesStore.currentFolder + const resourceIsCurrentFolder = currentFolder.id === sseData.itemid + const resource = resourceIsCurrentFolder + ? currentFolder + : resourcesStore.resources.find((f) => f.id === sseData.itemid) + const space = spacesStore.spaces.find((s) => s.id === resource.storageId) + + if (!resource || !space) { + return + } - if (resourceIsCurrentFolder) { - resourcesStore.setCurrentFolder(updatedResource) - return router.push( - createFileRouteOptions(space, { - path: updatedResource.path, - fileId: updatedResource.fileId - }) - ) - } + const updatedResource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) - resourcesStore.upsertResource(updatedResource) - } catch (e) { - console.error(`Unable to parse sse event ${topic} data`, e) + if (resourceIsCurrentFolder) { + resourcesStore.setCurrentFolder(updatedResource) + return router.push( + createFileRouteOptions(space, { + path: updatedResource.path, + fileId: updatedResource.fileId + }) + ) } + + resourcesStore.upsertResource(updatedResource) } export const onSSEFileLockingEvent = async ({ - topic, + sseData, resourcesStore, spacesStore, userStore, - msg, clientService -}: { - topic: string - resourcesStore: ResourcesStore - spacesStore: SpacesStore - userStore: UserStore - msg: MessageEvent - clientService: ClientService -}) => { - try { - const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.debug(`SSE event '${topic}'`, sseData) +}: SseEventOptions) => { + const resource = resourcesStore.resources.find((f) => f.id === sseData.itemid) + const space = spacesStore.spaces.find((s) => s.id === resource.storageId) - const resource = resourcesStore.resources.find((f) => f.id === sseData.itemid) - const space = spacesStore.spaces.find((s) => s.id === resource.storageId) - - if (!resource || !space) { - return - } + if (!resource || !space) { + return + } - const updatedResource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) + const updatedResource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) - resourcesStore.upsertResource(updatedResource) - resourcesStore.updateResourceField({ - id: updatedResource.id, - field: 'indicators', - value: getIndicators({ - space, - resource: updatedResource, - ancestorMetaData: resourcesStore.ancestorMetaData, - user: userStore.user - }) + resourcesStore.upsertResource(updatedResource) + resourcesStore.updateResourceField({ + id: updatedResource.id, + field: 'indicators', + value: getIndicators({ + space, + resource: updatedResource, + ancestorMetaData: resourcesStore.ancestorMetaData, + user: userStore.user }) - } catch (e) { - console.error(`Unable to parse sse event ${topic} data`, e) - } + }) } export const onSSEProcessingFinishedEvent = async ({ - topic, + sseData, resourcesStore, spacesStore, - msg, clientService, resourceQueue, previewService -}: { - topic: string - resourcesStore: ResourcesStore - spacesStore: SpacesStore - msg: MessageEvent - clientService: ClientService - resourceQueue: PQueue - previewService: PreviewService -}) => { - try { - const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.debug(`SSE event '${topic}'`, sseData) - - if (!itemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid })) { - return false - } - const resource = resourcesStore.resources.find((f) => f.id === sseData.itemid) - - /** - * If resource is not loaded, it suggests an upload is in progress. - */ - if (!resource) { - if (sseData.initiatorid === clientService.initiatorId) { - /** - * If the upload is initiated by the current client, - * there's no necessity to retrieve the resources again. - */ - return - } - - return resourceQueue.add(async () => { - const { resource } = await clientService.webdav.listFilesById({ - fileId: sseData.itemid - }) - resourcesStore.upsertResource(resource) - }) - } - - /** - * Resource not changed, don't fetch more data - */ - if (resource.etag === sseData.etag) { - return resourcesStore.updateResourceField({ - id: sseData.itemid, - field: 'processing', - value: false - }) - } +}: SseEventOptions) => { + if (!itemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid })) { + return false + } + const resource = resourcesStore.resources.find((f) => f.id === sseData.itemid) - const space = spacesStore.spaces.find((s) => s.id === sseData.spaceid) - if (!space) { + /** + * If resource is not loaded, it suggests an upload is in progress. + */ + if (!resource) { + if (sseData.initiatorid === clientService.initiatorId) { + /** + * If the upload is initiated by the current client, + * there's no necessity to retrieve the resources again. + */ return } - const updatedResource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid + return resourceQueue.add(async () => { + const { resource } = await clientService.webdav.listFilesById({ + fileId: sseData.itemid + }) + resourcesStore.upsertResource(resource) }) - resourcesStore.upsertResource(updatedResource) + } - const preview = await previewService.loadPreview({ - resource, - space, - dimensions: ImageDimension.Thumbnail + /** + * Resource not changed, don't fetch more data + */ + if (resource.etag === sseData.etag) { + return resourcesStore.updateResourceField({ + id: sseData.itemid, + field: 'processing', + value: false }) + } - if (preview) { - resourcesStore.updateResourceField({ - id: sseData.itemid, - field: 'thumbnail', - value: preview - }) - } - } catch (e) { - console.error(`Unable to parse sse event ${topic} data`, e) + const space = spacesStore.spaces.find((s) => s.id === sseData.spaceid) + if (!space) { + return + } + + const updatedResource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + resourcesStore.upsertResource(updatedResource) + + const preview = await previewService.loadPreview({ + resource, + space, + dimensions: ImageDimension.Thumbnail + }) + + if (preview) { + resourcesStore.updateResourceField({ + id: sseData.itemid, + field: 'thumbnail', + value: preview + }) } } export const onSSEItemTrashedEvent = ({ - topic, + sseData, language, messageStore, resourcesStore, - clientService, - msg -}: { - topic: string - language: Language - resourcesStore: ResourcesStore - clientService: ClientService - messageStore: MessageStore - msg: MessageEvent -}) => { - try { - const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.debug(`SSE event '${topic}'`, sseData) - - if (sseData.initiatorid === clientService.initiatorId) { - /** - * If the request was initiated by the current client (browser tab), - * there's no need to proceed with the action since the web already - * handles its own business logic. Therefore, we'll return early here. - */ - return - } - - const currentFolder = resourcesStore.currentFolder - const resourceIsCurrentFolder = currentFolder.id === sseData.itemid + clientService +}: SseEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } - if (resourceIsCurrentFolder) { - return messageStore.showMessage({ - title: language.$gettext( - 'The folder you were accessing has been removed. Please navigate to another location.' - ) - }) - } + const currentFolder = resourcesStore.currentFolder + const resourceIsCurrentFolder = currentFolder.id === sseData.itemid - const resource = resourcesStore.resources.find((f) => f.id === sseData.itemid) + if (resourceIsCurrentFolder) { + return messageStore.showMessage({ + title: language.$gettext( + 'The folder you were accessing has been removed. Please navigate to another location.' + ) + }) + } - if (!resource) { - return - } + const resource = resourcesStore.resources.find((f) => f.id === sseData.itemid) - resourcesStore.removeResources([resource]) - } catch (e) { - console.error(`Unable to parse sse event ${topic} data`, e) + if (!resource) { + return } + + resourcesStore.removeResources([resource]) } export const onSSEItemRestoredEvent = async ({ - topic, + sseData, resourcesStore, spacesStore, userStore, - msg, clientService -}: { - topic: string - resourcesStore: ResourcesStore - spacesStore: SpacesStore - userStore: UserStore - msg: MessageEvent - clientService: ClientService -}) => { - try { - const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.debug(`SSE event '${topic}'`, sseData) - - if (sseData.initiatorid === clientService.initiatorId) { - /** - * If the request was initiated by the current client (browser tab), - * there's no need to proceed with the action since the web already - * handles its own business logic. Therefore, we'll return early here. - */ - return - } +}: SseEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } - const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!space) { - return - } + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } - const resource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) - if (!resource) { - return - } + if (!resource) { + return + } - if (!itemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { - return false - } + if (!itemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { + return false + } - resourcesStore.upsertResource(resource) - resourcesStore.updateResourceField({ - id: resource.id, - field: 'indicators', - value: getIndicators({ - space, - resource, - ancestorMetaData: resourcesStore.ancestorMetaData, - user: userStore.user - }) + resourcesStore.upsertResource(resource) + resourcesStore.updateResourceField({ + id: resource.id, + field: 'indicators', + value: getIndicators({ + space, + resource, + ancestorMetaData: resourcesStore.ancestorMetaData, + user: userStore.user }) - } catch (e) { - console.error(`Unable to parse sse event ${topic} data`, e) - } + }) } export const onSSEItemMovedEvent = async ({ - topic, + sseData, resourcesStore, spacesStore, userStore, - msg, clientService -}: { - topic: string - resourcesStore: ResourcesStore - spacesStore: SpacesStore - userStore: UserStore - msg: MessageEvent - clientService: ClientService -}) => { - try { - const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.debug(`SSE event '${topic}'`, sseData) - - if (sseData.initiatorid === clientService.initiatorId) { - /** - * If the request was initiated by the current client (browser tab), - * there's no need to proceed with the action since the web already - * handles its own business logic. Therefore, we'll return early here. - */ - return - } +}: SseEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } - const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!space) { - return - } + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } - const resource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) - if (!resource) { - return - } + if (!resource) { + return + } - if (resource.parentFolderId !== resourcesStore.currentFolder.id) { - return resourcesStore.removeResources([resource]) - } + if (resource.parentFolderId !== resourcesStore.currentFolder.id) { + return resourcesStore.removeResources([resource]) + } - resourcesStore.upsertResource(resource) - resourcesStore.updateResourceField({ - id: resource.id, - field: 'indicators', - value: getIndicators({ - resource, - space, - user: userStore.user, - ancestorMetaData: resourcesStore.ancestorMetaData - }) + resourcesStore.upsertResource(resource) + resourcesStore.updateResourceField({ + id: resource.id, + field: 'indicators', + value: getIndicators({ + resource, + space, + user: userStore.user, + ancestorMetaData: resourcesStore.ancestorMetaData }) - } catch (e) { - console.error(`Unable to parse sse event ${topic} data`, e) - } + }) } /** @@ -419,448 +345,221 @@ export const onSSEItemMovedEvent = async ({ * post-processing event won't be triggered in this case. */ export const onSSEFileTouchedEvent = async ({ - topic, + sseData, resourcesStore, spacesStore, - msg, clientService -}: { - topic: string - resourcesStore: ResourcesStore - spacesStore: SpacesStore - msg: MessageEvent - clientService: ClientService -}) => { - try { - const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.debug(`SSE event '${topic}'`, sseData) - - if (sseData.initiatorid === clientService.initiatorId) { - /** - * If the request was initiated by the current client (browser tab), - * there's no need to proceed with the action since the web already - * handles its own business logic. Therefore, we'll return early here. - */ - return - } - - const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!space) { - return - } +}: SseEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } - const resource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } - if (!resource) { - return - } + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) - if (!itemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { - return false - } + if (!resource) { + return + } - resourcesStore.upsertResource(resource) - } catch (e) { - console.error(`Unable to parse sse event ${topic} data`, e) + if (!itemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { + return false } + + resourcesStore.upsertResource(resource) } export const onSSEFolderCreatedEvent = async ({ - topic, + sseData, resourcesStore, spacesStore, - msg, clientService -}: { - topic: string - resourcesStore: ResourcesStore - spacesStore: SpacesStore - msg: MessageEvent - clientService: ClientService -}) => { - try { - const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.debug(`SSE event '${topic}'`, sseData) - - if (sseData.initiatorid === clientService.initiatorId) { - /** - * If the request was initiated by the current client (browser tab), - * there's no need to proceed with the action since the web already - * handles its own business logic. Therefore, we'll return early here. - */ - return - } - - const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!space) { - return - } +}: SseEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } - const resource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } - if (!resource) { - return - } + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) - if (!itemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { - return false - } + if (!resource) { + return + } - resourcesStore.upsertResource(resource) - } catch (e) { - console.error(`Unable to parse sse event ${topic} data`, e) + if (!itemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { + return false } + + resourcesStore.upsertResource(resource) } export const onSSESpaceMemberAddedEvent = async ({ - topic, + sseData, resourcesStore, spacesStore, - msg, clientService, router -}: { - topic: string - resourcesStore: ResourcesStore - spacesStore: SpacesStore - msg: MessageEvent - clientService: ClientService - router: Router -}) => { - try { - const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.debug(`SSE event '${topic}'`, sseData) - - if (sseData.initiatorid === clientService.initiatorId) { - /** - * If the request was initiated by the current client (browser tab), - * there's no need to proceed with the action since the web already - * handles its own business logic. Therefore, we'll return early here. - */ - return - } - - const { data } = await clientService.graphAuthenticated.drives.getDrive(sseData.itemid) - const space = buildSpace(data) - spacesStore.upsertSpace(space) +}: SseEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } - if (!isLocationSpacesActive(router, 'files-spaces-projects')) { - return - } + const { data } = await clientService.graphAuthenticated.drives.getDrive(sseData.itemid) + const space = buildSpace(data) + spacesStore.upsertSpace(space) - resourcesStore.upsertResource(space) - } catch (e) { - console.error(`Unable to parse sse event ${topic} data`, e) + if (!isLocationSpacesActive(router, 'files-spaces-projects')) { + return } + + resourcesStore.upsertResource(space) } export const onSSESpaceMemberRemovedEvent = async ({ - topic, + sseData, resourcesStore, spacesStore, messageStore, - msg, clientService, language, router -}: { - topic: string - resourcesStore: ResourcesStore - spacesStore: SpacesStore - msg: MessageEvent - clientService: ClientService - messageStore: MessageStore - language: Language - router: Router -}) => { - try { - const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.debug(`SSE event '${topic}'`, sseData) - - if (sseData.initiatorid === clientService.initiatorId) { - /** - * If the request was initiated by the current client (browser tab), - * there's no need to proceed with the action since the web already - * handles its own business logic. Therefore, we'll return early here. - */ - return - } - - try { - const { data } = await clientService.graphAuthenticated.drives.listMyDrives( - '', - `id eq '${sseData.spaceid}'` - ) - const space = buildSpace(data.value[0]) - return spacesStore.upsertSpace(space) - } catch (_) { - const removedSpace = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!removedSpace) { - return - } - - spacesStore.removeSpace(removedSpace) - - if (isLocationSpacesActive(router, 'files-spaces-projects')) { - return resourcesStore.removeResources([removedSpace]) - } - - if ( - isLocationSpacesActive(router, 'files-spaces-generic') && - removedSpace.id === resourcesStore.currentFolder.storageId - ) { - return messageStore.showMessage({ - title: language.$gettext( - 'Your access to this space has been revoked. Please navigate to another location.' - ) - }) - } - } - } catch (e) { - console.error(`Unable to parse sse event ${topic} data`, e) +}: SseEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return } -} -export const onSSESpaceShareUpdatedEvent = async ({ - topic, - spacesStore, - resourcesStore, - messageStore, - msg, - clientService, - router, - language -}: { - topic: string - spacesStore: SpacesStore - resourcesStore: ResourcesStore - messageStore: MessageStore - msg: MessageEvent - clientService: ClientService - router: Router - language: Language -}) => { try { - const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.debug(`SSE event '${topic}'`, sseData) - - if (sseData.initiatorid === clientService.initiatorId) { - /** - * If the request was initiated by the current client (browser tab), - * there's no need to proceed with the action since the web already - * handles its own business logic. Therefore, we'll return early here. - */ + const { data } = await clientService.graphAuthenticated.drives.listMyDrives( + '', + `id eq '${sseData.spaceid}'` + ) + const space = buildSpace(data.value[0]) + return spacesStore.upsertSpace(space) + } catch (_) { + const removedSpace = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!removedSpace) { return } - const { data } = await clientService.graphAuthenticated.drives.getDrive(sseData.itemid) - const space = buildSpace(data) - spacesStore.upsertSpace(space) + spacesStore.removeSpace(removedSpace) + + if (isLocationSpacesActive(router, 'files-spaces-projects')) { + return resourcesStore.removeResources([removedSpace]) + } - //TODO: Check that the actual user permission has changed, affectedUserId must be present in event if ( isLocationSpacesActive(router, 'files-spaces-generic') && - space.id === resourcesStore.currentFolder.storageId + removedSpace.id === resourcesStore.currentFolder.storageId ) { return messageStore.showMessage({ - title: language.$gettext('Your space role has been updated.') + title: language.$gettext( + 'Your access to this space has been revoked. Please navigate to another location.' + ) }) } - } catch (e) { - console.error(`Unable to parse sse event ${topic} data`, e) } } -export const onSSEShareCreatedEvent = async ({ - topic, +export const onSSESpaceShareUpdatedEvent = async ({ + sseData, resourcesStore, spacesStore, userStore, - msg, clientService -}: { - topic: string - resourcesStore: ResourcesStore - spacesStore: SpacesStore - userStore: UserStore - msg: MessageEvent - clientService: ClientService -}) => { - try { - const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.debug(`SSE event '${topic}'`, sseData) +}: SseEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } +} - if (sseData.initiatorid === clientService.initiatorId) { - /** - * If the request was initiated by the current client (browser tab), - * there's no need to proceed with the action since the web already - * handles its own business logic. Therefore, we'll return early here. - */ - return - } - } catch (e) { - console.error(`Unable to parse sse event ${topic} data`, e) +export const onSSEShareCreatedEvent = async ({ + sseData, + resourcesStore, + spacesStore, + userStore, + clientService +}: SseEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return } } export const onSSEShareUpdatedEvent = async ({ - topic, + sseData, resourcesStore, spacesStore, userStore, - msg, clientService -}: { - topic: string - resourcesStore: ResourcesStore - spacesStore: SpacesStore - userStore: UserStore - msg: MessageEvent - clientService: ClientService -}) => { - try { - const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.debug(`SSE event '${topic}'`, sseData) - - if (sseData.initiatorid === clientService.initiatorId) { - /** - * If the request was initiated by the current client (browser tab), - * there's no need to proceed with the action since the web already - * handles its own business logic. Therefore, we'll return early here. - */ - return - } - } catch (e) { - console.error(`Unable to parse sse event ${topic} data`, e) +}: SseEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return } } export const onSSEShareRemovedEvent = async ({ - topic, + sseData, resourcesStore, spacesStore, userStore, - msg, clientService -}: { - topic: string - resourcesStore: ResourcesStore - spacesStore: SpacesStore - userStore: UserStore - msg: MessageEvent - clientService: ClientService -}) => { - try { - const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.debug(`SSE event '${topic}'`, sseData) - - if (sseData.initiatorid === clientService.initiatorId) { - /** - * If the request was initiated by the current client (browser tab), - * there's no need to proceed with the action since the web already - * handles its own business logic. Therefore, we'll return early here. - */ - return - } - } catch (e) { - console.error(`Unable to parse sse event ${topic} data`, e) +}: SseEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return } } export const onSSELinkCreatedEvent = async ({ - topic, + sseData, resourcesStore, spacesStore, userStore, - msg, clientService -}: { - topic: string - resourcesStore: ResourcesStore - spacesStore: SpacesStore - userStore: UserStore - msg: MessageEvent - clientService: ClientService -}) => { - try { - const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.debug(`SSE event '${topic}'`, sseData) - - if (sseData.initiatorid === clientService.initiatorId) { - /** - * If the request was initiated by the current client (browser tab), - * there's no need to proceed with the action since the web already - * handles its own business logic. Therefore, we'll return early here. - */ - return - } - } catch (e) { - console.error(`Unable to parse sse event ${topic} data`, e) +}: SseEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return } } export const onSSELinkUpdatedEvent = async ({ - topic, + sseData, resourcesStore, spacesStore, userStore, - msg, clientService -}: { - topic: string - resourcesStore: ResourcesStore - spacesStore: SpacesStore - userStore: UserStore - msg: MessageEvent - clientService: ClientService -}) => { - try { - const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.debug(`SSE event '${topic}'`, sseData) - - if (sseData.initiatorid === clientService.initiatorId) { - /** - * If the request was initiated by the current client (browser tab), - * there's no need to proceed with the action since the web already - * handles its own business logic. Therefore, we'll return early here. - */ - return - } - } catch (e) { - console.error(`Unable to parse sse event ${topic} data`, e) +}: SseEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return } } export const onSSELinkRemovedEvent = async ({ - topic, + sseData, resourcesStore, spacesStore, userStore, - msg, clientService -}: { - topic: string - resourcesStore: ResourcesStore - spacesStore: SpacesStore - userStore: UserStore - msg: MessageEvent - clientService: ClientService -}) => { - try { - const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.debug(`SSE event '${topic}'`, sseData) - - if (sseData.initiatorid === clientService.initiatorId) { - /** - * If the request was initiated by the current client (browser tab), - * there's no need to proceed with the action since the web already - * handles its own business logic. Therefore, we'll return early here. - */ - return - } - } catch (e) { - console.error(`Unable to parse sse event ${topic} data`, e) +}: SseEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return } } From 9136ea2ece3b0c17c8e7613b1073497487793bb7 Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Mon, 22 Apr 2024 14:49:32 +0200 Subject: [PATCH 05/17] add sharing events --- .../web-runtime/src/container/bootstrap.ts | 11 +- packages/web-runtime/src/container/sse.ts | 196 +++++++++++++++++- packages/web-runtime/src/index.ts | 1 + 3 files changed, 200 insertions(+), 8 deletions(-) diff --git a/packages/web-runtime/src/container/bootstrap.ts b/packages/web-runtime/src/container/bootstrap.ts index 8f96b5c80e8..90ac7932cc1 100644 --- a/packages/web-runtime/src/container/bootstrap.ts +++ b/packages/web-runtime/src/container/bootstrap.ts @@ -27,7 +27,8 @@ import { useResourcesStore, ResourcesStore, SpacesStore, - MessageStore + MessageStore, + SharesStore } from '@ownclouders/web-pkg' import { authService } from '../services/auth' import { @@ -72,7 +73,8 @@ import { onSSELinkCreatedEvent, onSSELinkRemovedEvent, sseEventWrapper, - SseEventWrapperOptions + SseEventWrapperOptions, + onSSELinkUpdatedEvent } from './sse' const getEmbedConfigFromQuery = ( @@ -643,6 +645,7 @@ export const registerSSEEventListeners = ({ resourcesStore, spacesStore, messageStore, + sharesStore, clientService, previewService, configStore, @@ -653,6 +656,7 @@ export const registerSSEEventListeners = ({ resourcesStore: ResourcesStore spacesStore: SpacesStore messageStore: MessageStore + sharesStore: SharesStore clientService: ClientService previewService: PreviewService configStore: ConfigStore @@ -675,6 +679,7 @@ export const registerSSEEventListeners = ({ spacesStore, messageStore, userStore, + sharesStore, clientService, previewService, language, @@ -840,7 +845,7 @@ export const registerSSEEventListeners = ({ topic: MESSAGE_TYPE.LINK_UPDATED, msg, ...sseEventWrapperOptions, - method: onSSELinkRemovedEvent + method: onSSELinkUpdatedEvent }) ) } diff --git a/packages/web-runtime/src/container/sse.ts b/packages/web-runtime/src/container/sse.ts index b1f81926896..6310b7bb06d 100644 --- a/packages/web-runtime/src/container/sse.ts +++ b/packages/web-runtime/src/container/sse.ts @@ -3,18 +3,28 @@ import { createFileRouteOptions, getIndicators, ImageDimension, + isLocationSharesActive, isLocationSpacesActive, MessageStore, PreviewService, ResourcesStore, + SharesStore, SpacesStore, UserStore } from '@ownclouders/web-pkg' import PQueue from 'p-queue' -import { buildSpace, extractNodeId, extractStorageId } from '@ownclouders/web-client' +import { + buildIncomingShareResource, + buildOutgoingShareResource, + buildSpace, + extractNodeId, + extractStorageId, + ShareTypes +} from '@ownclouders/web-client' import { z } from 'zod' import { Router } from 'vue-router' import { Language } from 'vue3-gettext' +import { DriveItem } from '@ownclouders/web-client/graph/generated' const eventSchema = z.object({ itemid: z.string(), @@ -31,6 +41,7 @@ export interface SseEventOptions { spacesStore: SpacesStore userStore: UserStore messageStore: MessageStore + sharesStore: SharesStore clientService: ClientService previewService: PreviewService router: Router @@ -443,6 +454,7 @@ export const onSSESpaceMemberRemovedEvent = async ({ return } + //TODO: check if sse event is affected user not equal, fetch space, upsert return try { const { data } = await clientService.graphAuthenticated.drives.listMyDrives( '', @@ -492,13 +504,62 @@ export const onSSEShareCreatedEvent = async ({ sseData, resourcesStore, spacesStore, + sharesStore, userStore, - clientService + clientService, + router }: SseEventOptions) => { if (sseData.initiatorid === clientService.initiatorId) { // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. return } + + if (itemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid })) { + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + resourcesStore.upsertResource(resource) + return resourcesStore.updateResourceField({ + id: resource.id, + field: 'indicators', + value: getIndicators({ + space, + resource, + ancestorMetaData: resourcesStore.ancestorMetaData, + user: userStore.user + }) + }) + } + + if (isLocationSharesActive(router, 'files-shares-with-me')) { + // FIXME: get drive item by id as soon as server supports it + const { data } = await clientService.graphAuthenticated.drives.listSharedWithMe() + const driveItem = (data.value as DriveItem[]).find( + ({ remoteItem }) => remoteItem.id === sseData.itemid + ) + if (!driveItem) { + return + } + const resource = buildIncomingShareResource({ driveItem, graphRoles: sharesStore.graphRoles }) + return resourcesStore.upsertResource(resource) + } + + if (isLocationSharesActive(router, 'files-shares-with-others')) { + // FIXME: get drive item by id as soon as server supports it + const { data } = await clientService.graphAuthenticated.drives.listSharedByMe() + const driveItem = (data.value as DriveItem[]).find(({ id }) => id === sseData.itemid) + if (!driveItem) { + return + } + const resource = buildOutgoingShareResource({ driveItem, user: userStore.user }) + return resourcesStore.upsertResource(resource) + } } export const onSSEShareUpdatedEvent = async ({ sseData, @@ -518,12 +579,72 @@ export const onSSEShareRemovedEvent = async ({ resourcesStore, spacesStore, userStore, - clientService + clientService, + messageStore, + language, + router }: SseEventOptions) => { if (sseData.initiatorid === clientService.initiatorId) { // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. return } + + if (resourcesStore.currentFolder?.storageId === sseData.spaceid) { + return messageStore.showMessage({ + title: language.$gettext( + 'Your access to this share has been revoked. Please navigate to another location.' + ) + }) + } + + if (itemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid })) { + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + resourcesStore.upsertResource(resource) + return resourcesStore.updateResourceField({ + id: resource.id, + field: 'indicators', + value: getIndicators({ + space, + resource, + ancestorMetaData: resourcesStore.ancestorMetaData, + user: userStore.user + }) + }) + } + + if (isLocationSharesActive(router, 'files-shares-with-others')) { + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + if ( + !resource.shareTypes.includes(ShareTypes.user.value) && + !resource.shareTypes.includes(ShareTypes.group.value) + ) { + return resourcesStore.removeResources([resource]) + } + } + + if (isLocationSharesActive(router, 'files-shares-with-me')) { + const removedShareResource = resourcesStore.resources.find((r) => r.fileId === sseData.itemid) + if (!removedShareResource) { + return + } + return resourcesStore.removeResources([removedShareResource]) + } } export const onSSELinkCreatedEvent = async ({ @@ -531,12 +652,47 @@ export const onSSELinkCreatedEvent = async ({ resourcesStore, spacesStore, userStore, - clientService + clientService, + router }: SseEventOptions) => { if (sseData.initiatorid === clientService.initiatorId) { // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. return } + + if (itemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid })) { + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + resourcesStore.upsertResource(resource) + return resourcesStore.updateResourceField({ + id: resource.id, + field: 'indicators', + value: getIndicators({ + space, + resource, + ancestorMetaData: resourcesStore.ancestorMetaData, + user: userStore.user + }) + }) + } + + if (isLocationSharesActive(router, 'files-shares-via-link')) { + // FIXME: get drive item by id as soon as server supports it + const { data } = await clientService.graphAuthenticated.drives.listSharedByMe() + const driveItem = (data.value as DriveItem[]).find(({ id }) => id === sseData.itemid) + if (!driveItem) { + return + } + const resource = buildOutgoingShareResource({ driveItem, user: userStore.user }) + return resourcesStore.upsertResource(resource) + } } export const onSSELinkUpdatedEvent = async ({ sseData, @@ -556,10 +712,40 @@ export const onSSELinkRemovedEvent = async ({ resourcesStore, spacesStore, userStore, - clientService + clientService, + router }: SseEventOptions) => { if (sseData.initiatorid === clientService.initiatorId) { // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. return } + + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + if (itemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid })) { + resourcesStore.upsertResource(resource) + resourcesStore.updateResourceField({ + id: resource.id, + field: 'indicators', + value: getIndicators({ + space, + resource, + ancestorMetaData: resourcesStore.ancestorMetaData, + user: userStore.user + }) + }) + } + + if (isLocationSharesActive(router, 'files-shares-via-link')) { + if (!resource.shareTypes.includes(ShareTypes.link.value)) { + return resourcesStore.removeResources([resource]) + } + } } diff --git a/packages/web-runtime/src/index.ts b/packages/web-runtime/src/index.ts index 320e8513bc5..0a8c48b4249 100644 --- a/packages/web-runtime/src/index.ts +++ b/packages/web-runtime/src/index.ts @@ -192,6 +192,7 @@ export const bootstrapApp = async (configurationPath: string): Promise => resourcesStore, spacesStore, messageStore: messagesStore, + sharesStore, clientService, userStore, previewService, From 7ea73531a3c9d38886efab14ef1214cec84e48e8 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Mon, 22 Apr 2024 15:15:09 +0200 Subject: [PATCH 06/17] feat: add logic for updated shares and space memberships --- packages/web-runtime/src/container/sse.ts | 54 +++++++++++++++-------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/packages/web-runtime/src/container/sse.ts b/packages/web-runtime/src/container/sse.ts index 6310b7bb06d..2441c752da8 100644 --- a/packages/web-runtime/src/container/sse.ts +++ b/packages/web-runtime/src/container/sse.ts @@ -1,6 +1,7 @@ import { ClientService, createFileRouteOptions, + eventBus, getIndicators, ImageDimension, isLocationSharesActive, @@ -24,7 +25,6 @@ import { import { z } from 'zod' import { Router } from 'vue-router' import { Language } from 'vue3-gettext' -import { DriveItem } from '@ownclouders/web-client/graph/generated' const eventSchema = z.object({ itemid: z.string(), @@ -454,7 +454,7 @@ export const onSSESpaceMemberRemovedEvent = async ({ return } - //TODO: check if sse event is affected user not equal, fetch space, upsert return + // TODO: check if sse event is affected user not equal, fetch space, upsert return try { const { data } = await clientService.graphAuthenticated.drives.listMyDrives( '', @@ -491,13 +491,21 @@ export const onSSESpaceShareUpdatedEvent = async ({ sseData, resourcesStore, spacesStore, - userStore, clientService }: SseEventOptions) => { if (sseData.initiatorid === clientService.initiatorId) { // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. return } + + // TODO: check if sse event is affected user not equal, fetch space, upsert return + if (resourcesStore.currentFolder?.storageId === sseData.spaceid) { + const { data } = await clientService.graphAuthenticated.drives.getDrive(sseData.itemid) + const space = buildSpace(data) + spacesStore.upsertSpace(space) + + return eventBus.publish('app.files.list.load') + } } export const onSSEShareCreatedEvent = async ({ @@ -540,9 +548,7 @@ export const onSSEShareCreatedEvent = async ({ if (isLocationSharesActive(router, 'files-shares-with-me')) { // FIXME: get drive item by id as soon as server supports it const { data } = await clientService.graphAuthenticated.drives.listSharedWithMe() - const driveItem = (data.value as DriveItem[]).find( - ({ remoteItem }) => remoteItem.id === sseData.itemid - ) + const driveItem = data.value.find(({ remoteItem }) => remoteItem.id === sseData.itemid) if (!driveItem) { return } @@ -553,7 +559,7 @@ export const onSSEShareCreatedEvent = async ({ if (isLocationSharesActive(router, 'files-shares-with-others')) { // FIXME: get drive item by id as soon as server supports it const { data } = await clientService.graphAuthenticated.drives.listSharedByMe() - const driveItem = (data.value as DriveItem[]).find(({ id }) => id === sseData.itemid) + const driveItem = data.value.find(({ id }) => id === sseData.itemid) if (!driveItem) { return } @@ -564,14 +570,30 @@ export const onSSEShareCreatedEvent = async ({ export const onSSEShareUpdatedEvent = async ({ sseData, resourcesStore, - spacesStore, - userStore, - clientService + sharesStore, + clientService, + router }: SseEventOptions) => { if (sseData.initiatorid === clientService.initiatorId) { // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. return } + + // TODO: check if sse event is affected user not equal, fetch space, upsert return + if (resourcesStore.currentFolder?.storageId === sseData.spaceid) { + return eventBus.publish('app.files.list.load') + } + + if (isLocationSharesActive(router, 'files-shares-with-me')) { + // FIXME: get drive item by id as soon as server supports it + const { data } = await clientService.graphAuthenticated.drives.listSharedWithMe() + const driveItem = data.value.find(({ remoteItem }) => remoteItem.id === sseData.itemid) + if (!driveItem) { + return + } + const resource = buildIncomingShareResource({ driveItem, graphRoles: sharesStore.graphRoles }) + return resourcesStore.upsertResource(resource) + } } export const onSSEShareRemovedEvent = async ({ @@ -589,6 +611,7 @@ export const onSSEShareRemovedEvent = async ({ return } + // TODO: check if sse event is affected user not equal, fetch space, upsert return if (resourcesStore.currentFolder?.storageId === sseData.spaceid) { return messageStore.showMessage({ title: language.$gettext( @@ -686,7 +709,7 @@ export const onSSELinkCreatedEvent = async ({ if (isLocationSharesActive(router, 'files-shares-via-link')) { // FIXME: get drive item by id as soon as server supports it const { data } = await clientService.graphAuthenticated.drives.listSharedByMe() - const driveItem = (data.value as DriveItem[]).find(({ id }) => id === sseData.itemid) + const driveItem = data.value.find(({ id }) => id === sseData.itemid) if (!driveItem) { return } @@ -694,13 +717,8 @@ export const onSSELinkCreatedEvent = async ({ return resourcesStore.upsertResource(resource) } } -export const onSSELinkUpdatedEvent = async ({ - sseData, - resourcesStore, - spacesStore, - userStore, - clientService -}: SseEventOptions) => { + +export const onSSELinkUpdatedEvent = ({ sseData, clientService }: SseEventOptions) => { if (sseData.initiatorid === clientService.initiatorId) { // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. return From 4004f23956b23458c5c68b2e518a74974f08c1ad Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Tue, 23 Apr 2024 13:44:06 +0200 Subject: [PATCH 07/17] Improve --- packages/web-runtime/src/container/sse.ts | 77 +++++++++++++---------- 1 file changed, 45 insertions(+), 32 deletions(-) diff --git a/packages/web-runtime/src/container/sse.ts b/packages/web-runtime/src/container/sse.ts index 2441c752da8..add5353ac9f 100644 --- a/packages/web-runtime/src/container/sse.ts +++ b/packages/web-runtime/src/container/sse.ts @@ -31,7 +31,8 @@ const eventSchema = z.object({ parentitemid: z.string(), spaceid: z.string().optional(), initiatorid: z.string().optional(), - etag: z.string().optional() + etag: z.string().optional(), + affecteduserids: z.array(z.string()).optional().nullable() }) export type EventSchemaType = z.infer @@ -64,7 +65,7 @@ export const sseEventWrapper = (options: SseEventWrapperOptions) => { return method({ ...sseEventOptions, sseData }) } catch (e) { - console.error(`Unable to parse sse event ${topic} data`, e) + console.error(`Unable to process sse event ${topic} data`, e) } } @@ -82,7 +83,8 @@ const itemInCurrentFolder = ({ if (!extractNodeId(currentFolder.id)) { // if we don't have a nodeId here, we have a space (root) as current folder and can only check against the storageId - if (currentFolder.id !== extractStorageId(parentFolderId)) { + const spaceNodeId = currentFolder.id.split('$')[1] + if (`${currentFolder.id}!${spaceNodeId}` !== parentFolderId) { return false } } else { @@ -107,7 +109,7 @@ export const onSSEItemRenamedEvent = async ({ } const currentFolder = resourcesStore.currentFolder - const resourceIsCurrentFolder = currentFolder.id === sseData.itemid + const resourceIsCurrentFolder = currentFolder?.id === sseData.itemid const resource = resourceIsCurrentFolder ? currentFolder : resourcesStore.resources.find((f) => f.id === sseData.itemid) @@ -247,7 +249,7 @@ export const onSSEItemTrashedEvent = ({ } const currentFolder = resourcesStore.currentFolder - const resourceIsCurrentFolder = currentFolder.id === sseData.itemid + const resourceIsCurrentFolder = currentFolder?.id === sseData.itemid if (resourceIsCurrentFolder) { return messageStore.showMessage({ @@ -333,7 +335,7 @@ export const onSSEItemMovedEvent = async ({ return } - if (resource.parentFolderId !== resourcesStore.currentFolder.id) { + if (resource.parentFolderId !== resourcesStore.currentFolder?.id) { return resourcesStore.removeResources([resource]) } @@ -447,6 +449,7 @@ export const onSSESpaceMemberRemovedEvent = async ({ messageStore, clientService, language, + userStore, router }: SseEventOptions) => { if (sseData.initiatorid === clientService.initiatorId) { @@ -454,36 +457,36 @@ export const onSSESpaceMemberRemovedEvent = async ({ return } - // TODO: check if sse event is affected user not equal, fetch space, upsert return - try { + //TODO: RECHECK IF FIXED IN BACKEND + if (!sseData.affecteduserids?.includes(userStore.user.id)) { const { data } = await clientService.graphAuthenticated.drives.listMyDrives( '', `id eq '${sseData.spaceid}'` ) const space = buildSpace(data.value[0]) return spacesStore.upsertSpace(space) - } catch (_) { - const removedSpace = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!removedSpace) { - return - } + } - spacesStore.removeSpace(removedSpace) + const removedSpace = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!removedSpace) { + return + } - if (isLocationSpacesActive(router, 'files-spaces-projects')) { - return resourcesStore.removeResources([removedSpace]) - } + spacesStore.removeSpace(removedSpace) - if ( - isLocationSpacesActive(router, 'files-spaces-generic') && - removedSpace.id === resourcesStore.currentFolder.storageId - ) { - return messageStore.showMessage({ - title: language.$gettext( - 'Your access to this space has been revoked. Please navigate to another location.' - ) - }) - } + if (isLocationSpacesActive(router, 'files-spaces-projects')) { + return resourcesStore.removeResources([removedSpace]) + } + + if ( + isLocationSpacesActive(router, 'files-spaces-generic') && + removedSpace.id === resourcesStore.currentFolder.storageId + ) { + return messageStore.showMessage({ + title: language.$gettext( + 'Your access to this space has been revoked. Please navigate to another location.' + ) + }) } } @@ -491,7 +494,8 @@ export const onSSESpaceShareUpdatedEvent = async ({ sseData, resourcesStore, spacesStore, - clientService + clientService, + userStore }: SseEventOptions) => { if (sseData.initiatorid === clientService.initiatorId) { // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. @@ -499,11 +503,13 @@ export const onSSESpaceShareUpdatedEvent = async ({ } // TODO: check if sse event is affected user not equal, fetch space, upsert return - if (resourcesStore.currentFolder?.storageId === sseData.spaceid) { + if ( + sseData.affecteduserids?.includes(userStore.user.id) && + resourcesStore.currentFolder?.storageId === sseData.spaceid + ) { const { data } = await clientService.graphAuthenticated.drives.getDrive(sseData.itemid) const space = buildSpace(data) spacesStore.upsertSpace(space) - return eventBus.publish('app.files.list.load') } } @@ -572,6 +578,7 @@ export const onSSEShareUpdatedEvent = async ({ resourcesStore, sharesStore, clientService, + userStore, router }: SseEventOptions) => { if (sseData.initiatorid === clientService.initiatorId) { @@ -580,7 +587,10 @@ export const onSSEShareUpdatedEvent = async ({ } // TODO: check if sse event is affected user not equal, fetch space, upsert return - if (resourcesStore.currentFolder?.storageId === sseData.spaceid) { + if ( + sseData.affecteduserids?.includes(userStore.user.id) && + resourcesStore.currentFolder?.storageId === sseData.spaceid + ) { return eventBus.publish('app.files.list.load') } @@ -612,7 +622,10 @@ export const onSSEShareRemovedEvent = async ({ } // TODO: check if sse event is affected user not equal, fetch space, upsert return - if (resourcesStore.currentFolder?.storageId === sseData.spaceid) { + if ( + sseData.affecteduserids?.includes(userStore.user.id) && + resourcesStore.currentFolder?.storageId === sseData.spaceid + ) { return messageStore.showMessage({ title: language.$gettext( 'Your access to this share has been revoked. Please navigate to another location.' From a5954f8bc01a02c760bc272fe9cc6ae827a6fcf1 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Tue, 23 Apr 2024 13:56:07 +0200 Subject: [PATCH 08/17] remove todo comments --- packages/web-runtime/src/container/sse.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/web-runtime/src/container/sse.ts b/packages/web-runtime/src/container/sse.ts index add5353ac9f..84bac629322 100644 --- a/packages/web-runtime/src/container/sse.ts +++ b/packages/web-runtime/src/container/sse.ts @@ -457,7 +457,6 @@ export const onSSESpaceMemberRemovedEvent = async ({ return } - //TODO: RECHECK IF FIXED IN BACKEND if (!sseData.affecteduserids?.includes(userStore.user.id)) { const { data } = await clientService.graphAuthenticated.drives.listMyDrives( '', @@ -502,7 +501,6 @@ export const onSSESpaceShareUpdatedEvent = async ({ return } - // TODO: check if sse event is affected user not equal, fetch space, upsert return if ( sseData.affecteduserids?.includes(userStore.user.id) && resourcesStore.currentFolder?.storageId === sseData.spaceid @@ -586,7 +584,6 @@ export const onSSEShareUpdatedEvent = async ({ return } - // TODO: check if sse event is affected user not equal, fetch space, upsert return if ( sseData.affecteduserids?.includes(userStore.user.id) && resourcesStore.currentFolder?.storageId === sseData.spaceid @@ -621,7 +618,6 @@ export const onSSEShareRemovedEvent = async ({ return } - // TODO: check if sse event is affected user not equal, fetch space, upsert return if ( sseData.affecteduserids?.includes(userStore.user.id) && resourcesStore.currentFolder?.storageId === sseData.spaceid From 2181778d3cad4d8301b273b7fc74202bda3008e2 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Tue, 23 Apr 2024 14:27:35 +0200 Subject: [PATCH 09/17] load indirect indicators --- .../src/composables/piniaStores/resources.ts | 5 + packages/web-runtime/src/container/sse.ts | 107 ++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/packages/web-pkg/src/composables/piniaStores/resources.ts b/packages/web-pkg/src/composables/piniaStores/resources.ts index 72f0a415136..30cd710726c 100644 --- a/packages/web-pkg/src/composables/piniaStores/resources.ts +++ b/packages/web-pkg/src/composables/piniaStores/resources.ts @@ -195,6 +195,10 @@ export const useResourcesStore = defineStore('resources', () => { ancestorMetaData.value = value } + const clearAncestorMetaData = () => { + ancestorMetaData.value = {} + } + const updateAncestorField = < T extends AncestorMetaDataValue, K extends keyof AncestorMetaDataValue @@ -324,6 +328,7 @@ export const useResourcesStore = defineStore('resources', () => { ancestorMetaData, setAncestorMetaData, + clearAncestorMetaData, updateAncestorField, loadAncestorMetaData } diff --git a/packages/web-runtime/src/container/sse.ts b/packages/web-runtime/src/container/sse.ts index 84bac629322..6a8635bc291 100644 --- a/packages/web-runtime/src/container/sse.ts +++ b/packages/web-runtime/src/container/sse.ts @@ -549,6 +549,35 @@ export const onSSEShareCreatedEvent = async ({ }) } + if (sseData.spaceid === resourcesStore.currentFolder?.storageId) { + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + if (resourcesStore.currentFolder.path.startsWith(resource.path)) { + resourcesStore.clearAncestorMetaData() + await resourcesStore.loadAncestorMetaData({ + folder: resourcesStore.currentFolder, + space, + client: clientService.webdav + }) + + return resourcesStore.resources.forEach((file) => { + file.indicators = getIndicators({ + space, + resource: file, + ancestorMetaData: resourcesStore.ancestorMetaData, + user: userStore.user + }) + }) + } + } + if (isLocationSharesActive(router, 'files-shares-with-me')) { // FIXME: get drive item by id as soon as server supports it const { data } = await clientService.graphAuthenticated.drives.listSharedWithMe() @@ -652,6 +681,35 @@ export const onSSEShareRemovedEvent = async ({ }) } + if (sseData.spaceid === resourcesStore.currentFolder?.storageId) { + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + if (resourcesStore.currentFolder.path.startsWith(resource.path)) { + resourcesStore.clearAncestorMetaData() + await resourcesStore.loadAncestorMetaData({ + folder: resourcesStore.currentFolder, + space, + client: clientService.webdav + }) + + return resourcesStore.resources.forEach((file) => { + file.indicators = getIndicators({ + space, + resource: file, + ancestorMetaData: resourcesStore.ancestorMetaData, + user: userStore.user + }) + }) + } + } + if (isLocationSharesActive(router, 'files-shares-with-others')) { const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) if (!space) { @@ -715,6 +773,35 @@ export const onSSELinkCreatedEvent = async ({ }) } + if (sseData.spaceid === resourcesStore.currentFolder?.storageId) { + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + if (resourcesStore.currentFolder.path.startsWith(resource.path)) { + resourcesStore.clearAncestorMetaData() + await resourcesStore.loadAncestorMetaData({ + folder: resourcesStore.currentFolder, + space, + client: clientService.webdav + }) + + return resourcesStore.resources.forEach((file) => { + file.indicators = getIndicators({ + space, + resource: file, + ancestorMetaData: resourcesStore.ancestorMetaData, + user: userStore.user + }) + }) + } + } + if (isLocationSharesActive(router, 'files-shares-via-link')) { // FIXME: get drive item by id as soon as server supports it const { data } = await clientService.graphAuthenticated.drives.listSharedByMe() @@ -770,6 +857,26 @@ export const onSSELinkRemovedEvent = async ({ }) } + if (sseData.spaceid === resourcesStore.currentFolder?.storageId) { + if (resourcesStore.currentFolder.path.startsWith(resource.path)) { + resourcesStore.clearAncestorMetaData() + await resourcesStore.loadAncestorMetaData({ + folder: resourcesStore.currentFolder, + space, + client: clientService.webdav + }) + + return resourcesStore.resources.forEach((file) => { + file.indicators = getIndicators({ + space, + resource: file, + ancestorMetaData: resourcesStore.ancestorMetaData, + user: userStore.user + }) + }) + } + } + if (isLocationSharesActive(router, 'files-shares-via-link')) { if (!resource.shareTypes.includes(ShareTypes.link.value)) { return resourcesStore.removeResources([resource]) From e8584c2a4c34ed7707da9b6ac7268574ab657427 Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Tue, 23 Apr 2024 14:39:37 +0200 Subject: [PATCH 10/17] lin --- packages/web-runtime/src/container/sse.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/web-runtime/src/container/sse.ts b/packages/web-runtime/src/container/sse.ts index 6a8635bc291..c613f412f97 100644 --- a/packages/web-runtime/src/container/sse.ts +++ b/packages/web-runtime/src/container/sse.ts @@ -19,7 +19,6 @@ import { buildOutgoingShareResource, buildSpace, extractNodeId, - extractStorageId, ShareTypes } from '@ownclouders/web-client' import { z } from 'zod' From 3ac8b8b56089e582e41b7c447cc54275f6e5808b Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Wed, 24 Apr 2024 10:40:47 +0200 Subject: [PATCH 11/17] don't update indirect shares - team decision to eliminate code complexity --- packages/web-runtime/src/container/sse.ts | 107 ---------------------- 1 file changed, 107 deletions(-) diff --git a/packages/web-runtime/src/container/sse.ts b/packages/web-runtime/src/container/sse.ts index c613f412f97..a6e8fb16cea 100644 --- a/packages/web-runtime/src/container/sse.ts +++ b/packages/web-runtime/src/container/sse.ts @@ -548,35 +548,6 @@ export const onSSEShareCreatedEvent = async ({ }) } - if (sseData.spaceid === resourcesStore.currentFolder?.storageId) { - const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!space) { - return - } - - const resource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) - - if (resourcesStore.currentFolder.path.startsWith(resource.path)) { - resourcesStore.clearAncestorMetaData() - await resourcesStore.loadAncestorMetaData({ - folder: resourcesStore.currentFolder, - space, - client: clientService.webdav - }) - - return resourcesStore.resources.forEach((file) => { - file.indicators = getIndicators({ - space, - resource: file, - ancestorMetaData: resourcesStore.ancestorMetaData, - user: userStore.user - }) - }) - } - } - if (isLocationSharesActive(router, 'files-shares-with-me')) { // FIXME: get drive item by id as soon as server supports it const { data } = await clientService.graphAuthenticated.drives.listSharedWithMe() @@ -680,35 +651,6 @@ export const onSSEShareRemovedEvent = async ({ }) } - if (sseData.spaceid === resourcesStore.currentFolder?.storageId) { - const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!space) { - return - } - - const resource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) - - if (resourcesStore.currentFolder.path.startsWith(resource.path)) { - resourcesStore.clearAncestorMetaData() - await resourcesStore.loadAncestorMetaData({ - folder: resourcesStore.currentFolder, - space, - client: clientService.webdav - }) - - return resourcesStore.resources.forEach((file) => { - file.indicators = getIndicators({ - space, - resource: file, - ancestorMetaData: resourcesStore.ancestorMetaData, - user: userStore.user - }) - }) - } - } - if (isLocationSharesActive(router, 'files-shares-with-others')) { const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) if (!space) { @@ -772,35 +714,6 @@ export const onSSELinkCreatedEvent = async ({ }) } - if (sseData.spaceid === resourcesStore.currentFolder?.storageId) { - const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!space) { - return - } - - const resource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) - - if (resourcesStore.currentFolder.path.startsWith(resource.path)) { - resourcesStore.clearAncestorMetaData() - await resourcesStore.loadAncestorMetaData({ - folder: resourcesStore.currentFolder, - space, - client: clientService.webdav - }) - - return resourcesStore.resources.forEach((file) => { - file.indicators = getIndicators({ - space, - resource: file, - ancestorMetaData: resourcesStore.ancestorMetaData, - user: userStore.user - }) - }) - } - } - if (isLocationSharesActive(router, 'files-shares-via-link')) { // FIXME: get drive item by id as soon as server supports it const { data } = await clientService.graphAuthenticated.drives.listSharedByMe() @@ -856,26 +769,6 @@ export const onSSELinkRemovedEvent = async ({ }) } - if (sseData.spaceid === resourcesStore.currentFolder?.storageId) { - if (resourcesStore.currentFolder.path.startsWith(resource.path)) { - resourcesStore.clearAncestorMetaData() - await resourcesStore.loadAncestorMetaData({ - folder: resourcesStore.currentFolder, - space, - client: clientService.webdav - }) - - return resourcesStore.resources.forEach((file) => { - file.indicators = getIndicators({ - space, - resource: file, - ancestorMetaData: resourcesStore.ancestorMetaData, - user: userStore.user - }) - }) - } - } - if (isLocationSharesActive(router, 'files-shares-via-link')) { if (!resource.shareTypes.includes(ShareTypes.link.value)) { return resourcesStore.removeResources([resource]) From 6a99b7491593f7b8dfcfc6e5c4d91f4ce770f1a3 Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Thu, 25 Apr 2024 09:24:30 +0200 Subject: [PATCH 12/17] Rearange code --- packages/web-runtime/src/container/sse.ts | 777 ------------------ .../web-runtime/src/container/sse/files.ts | 326 ++++++++ .../web-runtime/src/container/sse/helpers.ts | 41 + .../web-runtime/src/container/sse/index.ts | 4 + .../web-runtime/src/container/sse/shares.ts | 401 +++++++++ .../web-runtime/src/container/sse/types.ts | 44 + 6 files changed, 816 insertions(+), 777 deletions(-) delete mode 100644 packages/web-runtime/src/container/sse.ts create mode 100644 packages/web-runtime/src/container/sse/files.ts create mode 100644 packages/web-runtime/src/container/sse/helpers.ts create mode 100644 packages/web-runtime/src/container/sse/index.ts create mode 100644 packages/web-runtime/src/container/sse/shares.ts create mode 100644 packages/web-runtime/src/container/sse/types.ts diff --git a/packages/web-runtime/src/container/sse.ts b/packages/web-runtime/src/container/sse.ts deleted file mode 100644 index a6e8fb16cea..00000000000 --- a/packages/web-runtime/src/container/sse.ts +++ /dev/null @@ -1,777 +0,0 @@ -import { - ClientService, - createFileRouteOptions, - eventBus, - getIndicators, - ImageDimension, - isLocationSharesActive, - isLocationSpacesActive, - MessageStore, - PreviewService, - ResourcesStore, - SharesStore, - SpacesStore, - UserStore -} from '@ownclouders/web-pkg' -import PQueue from 'p-queue' -import { - buildIncomingShareResource, - buildOutgoingShareResource, - buildSpace, - extractNodeId, - ShareTypes -} from '@ownclouders/web-client' -import { z } from 'zod' -import { Router } from 'vue-router' -import { Language } from 'vue3-gettext' - -const eventSchema = z.object({ - itemid: z.string(), - parentitemid: z.string(), - spaceid: z.string().optional(), - initiatorid: z.string().optional(), - etag: z.string().optional(), - affecteduserids: z.array(z.string()).optional().nullable() -}) - -export type EventSchemaType = z.infer - -export interface SseEventOptions { - resourcesStore: ResourcesStore - spacesStore: SpacesStore - userStore: UserStore - messageStore: MessageStore - sharesStore: SharesStore - clientService: ClientService - previewService: PreviewService - router: Router - language: Language - resourceQueue: PQueue - sseData: EventSchemaType -} - -export interface SseEventWrapperOptions extends Omit { - msg: MessageEvent - topic: string - method: (options: SseEventOptions) => Promise | unknown -} - -export const sseEventWrapper = (options: SseEventWrapperOptions) => { - const { topic, msg, method, ...sseEventOptions } = options - try { - const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.debug(`SSE event '${topic}'`, sseData) - - return method({ ...sseEventOptions, sseData }) - } catch (e) { - console.error(`Unable to process sse event ${topic} data`, e) - } -} - -const itemInCurrentFolder = ({ - resourcesStore, - parentFolderId -}: { - resourcesStore: ResourcesStore - parentFolderId: string -}) => { - const currentFolder = resourcesStore.currentFolder - if (!currentFolder) { - return false - } - - if (!extractNodeId(currentFolder.id)) { - // if we don't have a nodeId here, we have a space (root) as current folder and can only check against the storageId - const spaceNodeId = currentFolder.id.split('$')[1] - if (`${currentFolder.id}!${spaceNodeId}` !== parentFolderId) { - return false - } - } else { - if (currentFolder.id !== parentFolderId) { - return false - } - } - - return true -} - -export const onSSEItemRenamedEvent = async ({ - sseData, - resourcesStore, - spacesStore, - clientService, - router -}: SseEventOptions) => { - if (sseData.initiatorid === clientService.initiatorId) { - // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. - return - } - - const currentFolder = resourcesStore.currentFolder - const resourceIsCurrentFolder = currentFolder?.id === sseData.itemid - const resource = resourceIsCurrentFolder - ? currentFolder - : resourcesStore.resources.find((f) => f.id === sseData.itemid) - const space = spacesStore.spaces.find((s) => s.id === resource.storageId) - - if (!resource || !space) { - return - } - - const updatedResource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) - - if (resourceIsCurrentFolder) { - resourcesStore.setCurrentFolder(updatedResource) - return router.push( - createFileRouteOptions(space, { - path: updatedResource.path, - fileId: updatedResource.fileId - }) - ) - } - - resourcesStore.upsertResource(updatedResource) -} - -export const onSSEFileLockingEvent = async ({ - sseData, - resourcesStore, - spacesStore, - userStore, - clientService -}: SseEventOptions) => { - const resource = resourcesStore.resources.find((f) => f.id === sseData.itemid) - const space = spacesStore.spaces.find((s) => s.id === resource.storageId) - - if (!resource || !space) { - return - } - - const updatedResource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) - - resourcesStore.upsertResource(updatedResource) - resourcesStore.updateResourceField({ - id: updatedResource.id, - field: 'indicators', - value: getIndicators({ - space, - resource: updatedResource, - ancestorMetaData: resourcesStore.ancestorMetaData, - user: userStore.user - }) - }) -} - -export const onSSEProcessingFinishedEvent = async ({ - sseData, - resourcesStore, - spacesStore, - clientService, - resourceQueue, - previewService -}: SseEventOptions) => { - if (!itemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid })) { - return false - } - const resource = resourcesStore.resources.find((f) => f.id === sseData.itemid) - - /** - * If resource is not loaded, it suggests an upload is in progress. - */ - if (!resource) { - if (sseData.initiatorid === clientService.initiatorId) { - /** - * If the upload is initiated by the current client, - * there's no necessity to retrieve the resources again. - */ - return - } - - return resourceQueue.add(async () => { - const { resource } = await clientService.webdav.listFilesById({ - fileId: sseData.itemid - }) - resourcesStore.upsertResource(resource) - }) - } - - /** - * Resource not changed, don't fetch more data - */ - if (resource.etag === sseData.etag) { - return resourcesStore.updateResourceField({ - id: sseData.itemid, - field: 'processing', - value: false - }) - } - - const space = spacesStore.spaces.find((s) => s.id === sseData.spaceid) - if (!space) { - return - } - - const updatedResource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) - resourcesStore.upsertResource(updatedResource) - - const preview = await previewService.loadPreview({ - resource, - space, - dimensions: ImageDimension.Thumbnail - }) - - if (preview) { - resourcesStore.updateResourceField({ - id: sseData.itemid, - field: 'thumbnail', - value: preview - }) - } -} - -export const onSSEItemTrashedEvent = ({ - sseData, - language, - messageStore, - resourcesStore, - clientService -}: SseEventOptions) => { - if (sseData.initiatorid === clientService.initiatorId) { - // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. - return - } - - const currentFolder = resourcesStore.currentFolder - const resourceIsCurrentFolder = currentFolder?.id === sseData.itemid - - if (resourceIsCurrentFolder) { - return messageStore.showMessage({ - title: language.$gettext( - 'The folder you were accessing has been removed. Please navigate to another location.' - ) - }) - } - - const resource = resourcesStore.resources.find((f) => f.id === sseData.itemid) - - if (!resource) { - return - } - - resourcesStore.removeResources([resource]) -} - -export const onSSEItemRestoredEvent = async ({ - sseData, - resourcesStore, - spacesStore, - userStore, - clientService -}: SseEventOptions) => { - if (sseData.initiatorid === clientService.initiatorId) { - // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. - return - } - - const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!space) { - return - } - - const resource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) - - if (!resource) { - return - } - - if (!itemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { - return false - } - - resourcesStore.upsertResource(resource) - resourcesStore.updateResourceField({ - id: resource.id, - field: 'indicators', - value: getIndicators({ - space, - resource, - ancestorMetaData: resourcesStore.ancestorMetaData, - user: userStore.user - }) - }) -} - -export const onSSEItemMovedEvent = async ({ - sseData, - resourcesStore, - spacesStore, - userStore, - clientService -}: SseEventOptions) => { - if (sseData.initiatorid === clientService.initiatorId) { - // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. - return - } - - const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!space) { - return - } - - const resource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) - - if (!resource) { - return - } - - if (resource.parentFolderId !== resourcesStore.currentFolder?.id) { - return resourcesStore.removeResources([resource]) - } - - resourcesStore.upsertResource(resource) - resourcesStore.updateResourceField({ - id: resource.id, - field: 'indicators', - value: getIndicators({ - resource, - space, - user: userStore.user, - ancestorMetaData: resourcesStore.ancestorMetaData - }) - }) -} - -/** - * The FileTouched event is triggered when a new empty file, such as a new text file, - * is about to be created on the server. This event is necessary because the - * post-processing event won't be triggered in this case. - */ -export const onSSEFileTouchedEvent = async ({ - sseData, - resourcesStore, - spacesStore, - clientService -}: SseEventOptions) => { - if (sseData.initiatorid === clientService.initiatorId) { - // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. - return - } - - const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!space) { - return - } - - const resource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) - - if (!resource) { - return - } - - if (!itemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { - return false - } - - resourcesStore.upsertResource(resource) -} - -export const onSSEFolderCreatedEvent = async ({ - sseData, - resourcesStore, - spacesStore, - clientService -}: SseEventOptions) => { - if (sseData.initiatorid === clientService.initiatorId) { - // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. - return - } - - const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!space) { - return - } - - const resource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) - - if (!resource) { - return - } - - if (!itemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { - return false - } - - resourcesStore.upsertResource(resource) -} - -export const onSSESpaceMemberAddedEvent = async ({ - sseData, - resourcesStore, - spacesStore, - clientService, - router -}: SseEventOptions) => { - if (sseData.initiatorid === clientService.initiatorId) { - // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. - return - } - - const { data } = await clientService.graphAuthenticated.drives.getDrive(sseData.itemid) - const space = buildSpace(data) - spacesStore.upsertSpace(space) - - if (!isLocationSpacesActive(router, 'files-spaces-projects')) { - return - } - - resourcesStore.upsertResource(space) -} - -export const onSSESpaceMemberRemovedEvent = async ({ - sseData, - resourcesStore, - spacesStore, - messageStore, - clientService, - language, - userStore, - router -}: SseEventOptions) => { - if (sseData.initiatorid === clientService.initiatorId) { - // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. - return - } - - if (!sseData.affecteduserids?.includes(userStore.user.id)) { - const { data } = await clientService.graphAuthenticated.drives.listMyDrives( - '', - `id eq '${sseData.spaceid}'` - ) - const space = buildSpace(data.value[0]) - return spacesStore.upsertSpace(space) - } - - const removedSpace = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!removedSpace) { - return - } - - spacesStore.removeSpace(removedSpace) - - if (isLocationSpacesActive(router, 'files-spaces-projects')) { - return resourcesStore.removeResources([removedSpace]) - } - - if ( - isLocationSpacesActive(router, 'files-spaces-generic') && - removedSpace.id === resourcesStore.currentFolder.storageId - ) { - return messageStore.showMessage({ - title: language.$gettext( - 'Your access to this space has been revoked. Please navigate to another location.' - ) - }) - } -} - -export const onSSESpaceShareUpdatedEvent = async ({ - sseData, - resourcesStore, - spacesStore, - clientService, - userStore -}: SseEventOptions) => { - if (sseData.initiatorid === clientService.initiatorId) { - // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. - return - } - - if ( - sseData.affecteduserids?.includes(userStore.user.id) && - resourcesStore.currentFolder?.storageId === sseData.spaceid - ) { - const { data } = await clientService.graphAuthenticated.drives.getDrive(sseData.itemid) - const space = buildSpace(data) - spacesStore.upsertSpace(space) - return eventBus.publish('app.files.list.load') - } -} - -export const onSSEShareCreatedEvent = async ({ - sseData, - resourcesStore, - spacesStore, - sharesStore, - userStore, - clientService, - router -}: SseEventOptions) => { - if (sseData.initiatorid === clientService.initiatorId) { - // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. - return - } - - if (itemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid })) { - const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!space) { - return - } - - const resource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) - - resourcesStore.upsertResource(resource) - return resourcesStore.updateResourceField({ - id: resource.id, - field: 'indicators', - value: getIndicators({ - space, - resource, - ancestorMetaData: resourcesStore.ancestorMetaData, - user: userStore.user - }) - }) - } - - if (isLocationSharesActive(router, 'files-shares-with-me')) { - // FIXME: get drive item by id as soon as server supports it - const { data } = await clientService.graphAuthenticated.drives.listSharedWithMe() - const driveItem = data.value.find(({ remoteItem }) => remoteItem.id === sseData.itemid) - if (!driveItem) { - return - } - const resource = buildIncomingShareResource({ driveItem, graphRoles: sharesStore.graphRoles }) - return resourcesStore.upsertResource(resource) - } - - if (isLocationSharesActive(router, 'files-shares-with-others')) { - // FIXME: get drive item by id as soon as server supports it - const { data } = await clientService.graphAuthenticated.drives.listSharedByMe() - const driveItem = data.value.find(({ id }) => id === sseData.itemid) - if (!driveItem) { - return - } - const resource = buildOutgoingShareResource({ driveItem, user: userStore.user }) - return resourcesStore.upsertResource(resource) - } -} -export const onSSEShareUpdatedEvent = async ({ - sseData, - resourcesStore, - sharesStore, - clientService, - userStore, - router -}: SseEventOptions) => { - if (sseData.initiatorid === clientService.initiatorId) { - // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. - return - } - - if ( - sseData.affecteduserids?.includes(userStore.user.id) && - resourcesStore.currentFolder?.storageId === sseData.spaceid - ) { - return eventBus.publish('app.files.list.load') - } - - if (isLocationSharesActive(router, 'files-shares-with-me')) { - // FIXME: get drive item by id as soon as server supports it - const { data } = await clientService.graphAuthenticated.drives.listSharedWithMe() - const driveItem = data.value.find(({ remoteItem }) => remoteItem.id === sseData.itemid) - if (!driveItem) { - return - } - const resource = buildIncomingShareResource({ driveItem, graphRoles: sharesStore.graphRoles }) - return resourcesStore.upsertResource(resource) - } -} - -export const onSSEShareRemovedEvent = async ({ - sseData, - resourcesStore, - spacesStore, - userStore, - clientService, - messageStore, - language, - router -}: SseEventOptions) => { - if (sseData.initiatorid === clientService.initiatorId) { - // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. - return - } - - if ( - sseData.affecteduserids?.includes(userStore.user.id) && - resourcesStore.currentFolder?.storageId === sseData.spaceid - ) { - return messageStore.showMessage({ - title: language.$gettext( - 'Your access to this share has been revoked. Please navigate to another location.' - ) - }) - } - - if (itemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid })) { - const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!space) { - return - } - - const resource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) - - resourcesStore.upsertResource(resource) - return resourcesStore.updateResourceField({ - id: resource.id, - field: 'indicators', - value: getIndicators({ - space, - resource, - ancestorMetaData: resourcesStore.ancestorMetaData, - user: userStore.user - }) - }) - } - - if (isLocationSharesActive(router, 'files-shares-with-others')) { - const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!space) { - return - } - - const resource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) - - if ( - !resource.shareTypes.includes(ShareTypes.user.value) && - !resource.shareTypes.includes(ShareTypes.group.value) - ) { - return resourcesStore.removeResources([resource]) - } - } - - if (isLocationSharesActive(router, 'files-shares-with-me')) { - const removedShareResource = resourcesStore.resources.find((r) => r.fileId === sseData.itemid) - if (!removedShareResource) { - return - } - return resourcesStore.removeResources([removedShareResource]) - } -} - -export const onSSELinkCreatedEvent = async ({ - sseData, - resourcesStore, - spacesStore, - userStore, - clientService, - router -}: SseEventOptions) => { - if (sseData.initiatorid === clientService.initiatorId) { - // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. - return - } - - if (itemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid })) { - const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!space) { - return - } - - const resource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) - - resourcesStore.upsertResource(resource) - return resourcesStore.updateResourceField({ - id: resource.id, - field: 'indicators', - value: getIndicators({ - space, - resource, - ancestorMetaData: resourcesStore.ancestorMetaData, - user: userStore.user - }) - }) - } - - if (isLocationSharesActive(router, 'files-shares-via-link')) { - // FIXME: get drive item by id as soon as server supports it - const { data } = await clientService.graphAuthenticated.drives.listSharedByMe() - const driveItem = data.value.find(({ id }) => id === sseData.itemid) - if (!driveItem) { - return - } - const resource = buildOutgoingShareResource({ driveItem, user: userStore.user }) - return resourcesStore.upsertResource(resource) - } -} - -export const onSSELinkUpdatedEvent = ({ sseData, clientService }: SseEventOptions) => { - if (sseData.initiatorid === clientService.initiatorId) { - // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. - return - } -} - -export const onSSELinkRemovedEvent = async ({ - sseData, - resourcesStore, - spacesStore, - userStore, - clientService, - router -}: SseEventOptions) => { - if (sseData.initiatorid === clientService.initiatorId) { - // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. - return - } - - const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) - if (!space) { - return - } - - const resource = await clientService.webdav.getFileInfo(space, { - fileId: sseData.itemid - }) - - if (itemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid })) { - resourcesStore.upsertResource(resource) - resourcesStore.updateResourceField({ - id: resource.id, - field: 'indicators', - value: getIndicators({ - space, - resource, - ancestorMetaData: resourcesStore.ancestorMetaData, - user: userStore.user - }) - }) - } - - if (isLocationSharesActive(router, 'files-shares-via-link')) { - if (!resource.shareTypes.includes(ShareTypes.link.value)) { - return resourcesStore.removeResources([resource]) - } - } -} diff --git a/packages/web-runtime/src/container/sse/files.ts b/packages/web-runtime/src/container/sse/files.ts new file mode 100644 index 00000000000..ea7c4fa0ddf --- /dev/null +++ b/packages/web-runtime/src/container/sse/files.ts @@ -0,0 +1,326 @@ +import { createFileRouteOptions, getIndicators, ImageDimension } from '@ownclouders/web-pkg' +import { SSEEventOptions } from './types' +import { itemInCurrentFolder } from './helpers' + +export const onSSEItemRenamedEvent = async ({ + sseData, + resourcesStore, + spacesStore, + clientService, + router +}: SSEEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } + + const currentFolder = resourcesStore.currentFolder + const resourceIsCurrentFolder = currentFolder?.id === sseData.itemid + const resource = resourceIsCurrentFolder + ? currentFolder + : resourcesStore.resources.find((f) => f.id === sseData.itemid) + const space = spacesStore.spaces.find((s) => s.id === resource.storageId) + + if (!resource || !space) { + return + } + + const updatedResource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + if (resourceIsCurrentFolder) { + resourcesStore.setCurrentFolder(updatedResource) + return router.push( + createFileRouteOptions(space, { + path: updatedResource.path, + fileId: updatedResource.fileId + }) + ) + } + + resourcesStore.upsertResource(updatedResource) +} + +export const onSSEFileLockingEvent = async ({ + sseData, + resourcesStore, + spacesStore, + userStore, + clientService +}: SSEEventOptions) => { + const resource = resourcesStore.resources.find((f) => f.id === sseData.itemid) + const space = spacesStore.spaces.find((s) => s.id === resource.storageId) + + if (!resource || !space) { + return + } + + const updatedResource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + resourcesStore.upsertResource(updatedResource) + resourcesStore.updateResourceField({ + id: updatedResource.id, + field: 'indicators', + value: getIndicators({ + space, + resource: updatedResource, + ancestorMetaData: resourcesStore.ancestorMetaData, + user: userStore.user + }) + }) +} + +export const onSSEProcessingFinishedEvent = async ({ + sseData, + resourcesStore, + spacesStore, + clientService, + resourceQueue, + previewService +}: SSEEventOptions) => { + if (!itemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid })) { + return false + } + const resource = resourcesStore.resources.find((f) => f.id === sseData.itemid) + + /** + * If resource is not loaded, it suggests an upload is in progress. + */ + if (!resource) { + if (sseData.initiatorid === clientService.initiatorId) { + /** + * If the upload is initiated by the current client, + * there's no necessity to retrieve the resources again. + */ + return + } + + return resourceQueue.add(async () => { + const { resource } = await clientService.webdav.listFilesById({ + fileId: sseData.itemid + }) + resourcesStore.upsertResource(resource) + }) + } + + /** + * Resource not changed, don't fetch more data + */ + if (resource.etag === sseData.etag) { + return resourcesStore.updateResourceField({ + id: sseData.itemid, + field: 'processing', + value: false + }) + } + + const space = spacesStore.spaces.find((s) => s.id === sseData.spaceid) + if (!space) { + return + } + + const updatedResource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + resourcesStore.upsertResource(updatedResource) + + const preview = await previewService.loadPreview({ + resource, + space, + dimensions: ImageDimension.Thumbnail + }) + + if (preview) { + resourcesStore.updateResourceField({ + id: sseData.itemid, + field: 'thumbnail', + value: preview + }) + } +} + +export const onSSEItemTrashedEvent = ({ + sseData, + language, + messageStore, + resourcesStore, + clientService +}: SSEEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } + + const currentFolder = resourcesStore.currentFolder + const resourceIsCurrentFolder = currentFolder?.id === sseData.itemid + + if (resourceIsCurrentFolder) { + return messageStore.showMessage({ + title: language.$gettext( + 'The folder you were accessing has been removed. Please navigate to another location.' + ) + }) + } + + const resource = resourcesStore.resources.find((f) => f.id === sseData.itemid) + + if (!resource) { + return + } + + resourcesStore.removeResources([resource]) +} + +export const onSSEItemRestoredEvent = async ({ + sseData, + resourcesStore, + spacesStore, + userStore, + clientService +}: SSEEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } + + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + if (!resource) { + return + } + + if (!itemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { + return false + } + + resourcesStore.upsertResource(resource) + resourcesStore.updateResourceField({ + id: resource.id, + field: 'indicators', + value: getIndicators({ + space, + resource, + ancestorMetaData: resourcesStore.ancestorMetaData, + user: userStore.user + }) + }) +} + +export const onSSEItemMovedEvent = async ({ + sseData, + resourcesStore, + spacesStore, + userStore, + clientService +}: SSEEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } + + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + if (!resource) { + return + } + + if (resource.parentFolderId !== resourcesStore.currentFolder?.id) { + return resourcesStore.removeResources([resource]) + } + + resourcesStore.upsertResource(resource) + resourcesStore.updateResourceField({ + id: resource.id, + field: 'indicators', + value: getIndicators({ + resource, + space, + user: userStore.user, + ancestorMetaData: resourcesStore.ancestorMetaData + }) + }) +} + +/** + * The FileTouched event is triggered when a new empty file, such as a new text file, + * is about to be created on the server. This event is necessary because the + * post-processing event won't be triggered in this case. + */ +export const onSSEFileTouchedEvent = async ({ + sseData, + resourcesStore, + spacesStore, + clientService +}: SSEEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } + + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + if (!resource) { + return + } + + if (!itemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { + return false + } + + resourcesStore.upsertResource(resource) +} + +export const onSSEFolderCreatedEvent = async ({ + sseData, + resourcesStore, + spacesStore, + clientService +}: SSEEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } + + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + if (!resource) { + return + } + + if (!itemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { + return false + } + + resourcesStore.upsertResource(resource) +} diff --git a/packages/web-runtime/src/container/sse/helpers.ts b/packages/web-runtime/src/container/sse/helpers.ts new file mode 100644 index 00000000000..e9c46b5806c --- /dev/null +++ b/packages/web-runtime/src/container/sse/helpers.ts @@ -0,0 +1,41 @@ +import { ResourcesStore } from '@ownclouders/web-pkg' +import { extractNodeId } from '@ownclouders/web-client' +import { eventSchema, SseEventWrapperOptions } from './types' + +export const sseEventWrapper = (options: SseEventWrapperOptions) => { + const { topic, msg, method, ...sseEventOptions } = options + try { + const sseData = eventSchema.parse(JSON.parse(msg.data)) + console.debug(`SSE event '${topic}'`, sseData) + + return method({ ...sseEventOptions, sseData }) + } catch (e) { + console.error(`Unable to process sse event ${topic} data`, e) + } +} +export const itemInCurrentFolder = ({ + resourcesStore, + parentFolderId +}: { + resourcesStore: ResourcesStore + parentFolderId: string +}) => { + const currentFolder = resourcesStore.currentFolder + if (!currentFolder) { + return false + } + + if (!extractNodeId(currentFolder.id)) { + // if we don't have a nodeId here, we have a space (root) as current folder and can only check against the storageId + const spaceNodeId = currentFolder.id.split('$')[1] + if (`${currentFolder.id}!${spaceNodeId}` !== parentFolderId) { + return false + } + } else { + if (currentFolder.id !== parentFolderId) { + return false + } + } + + return true +} diff --git a/packages/web-runtime/src/container/sse/index.ts b/packages/web-runtime/src/container/sse/index.ts new file mode 100644 index 00000000000..721b07344f2 --- /dev/null +++ b/packages/web-runtime/src/container/sse/index.ts @@ -0,0 +1,4 @@ +export * from './files' +export * from './shares' +export * from './types' +export * from './helpers' diff --git a/packages/web-runtime/src/container/sse/shares.ts b/packages/web-runtime/src/container/sse/shares.ts new file mode 100644 index 00000000000..dda133abcb4 --- /dev/null +++ b/packages/web-runtime/src/container/sse/shares.ts @@ -0,0 +1,401 @@ +import { + buildIncomingShareResource, + buildOutgoingShareResource, + buildSpace, + ShareTypes +} from '@ownclouders/web-client' +import { + eventBus, + getIndicators, + isLocationSharesActive, + isLocationSpacesActive +} from '@ownclouders/web-pkg' +import { SSEEventOptions } from './types' +import { itemInCurrentFolder } from './helpers' + +export const onSSESpaceMemberAddedEvent = async ({ + sseData, + resourcesStore, + spacesStore, + clientService, + router +}: SSEEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } + + const { data } = await clientService.graphAuthenticated.drives.getDrive(sseData.itemid) + const space = buildSpace(data) + spacesStore.upsertSpace(space) + + if (!isLocationSpacesActive(router, 'files-spaces-projects')) { + return + } + + resourcesStore.upsertResource(space) +} + +export const onSSESpaceMemberRemovedEvent = async ({ + sseData, + resourcesStore, + spacesStore, + messageStore, + clientService, + language, + userStore, + router +}: SSEEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } + + if (!sseData.affecteduserids?.includes(userStore.user.id)) { + const { data } = await clientService.graphAuthenticated.drives.listMyDrives( + '', + `id eq '${sseData.spaceid}'` + ) + const space = buildSpace(data.value[0]) + return spacesStore.upsertSpace(space) + } + + const removedSpace = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!removedSpace) { + return + } + + spacesStore.removeSpace(removedSpace) + + if ( + isLocationSpacesActive(router, 'files-spaces-generic') && + removedSpace.id === resourcesStore.currentFolder.storageId + ) { + return messageStore.showMessage({ + title: language.$gettext( + 'Your access to this space has been revoked. Please navigate to another location.' + ) + }) + } + + if (isLocationSpacesActive(router, 'files-spaces-projects')) { + return resourcesStore.removeResources([removedSpace]) + } +} + +export const onSSESpaceShareUpdatedEvent = async ({ + sseData, + resourcesStore, + spacesStore, + clientService, + userStore, + router +}: SSEEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } + + const { data } = await clientService.graphAuthenticated.drives.getDrive(sseData.itemid) + const space = buildSpace(data) + spacesStore.upsertSpace(space) + + if ( + isLocationSpacesActive(router, 'files-spaces-generic') && + sseData.affecteduserids?.includes(userStore.user.id) && + resourcesStore.currentFolder?.storageId === sseData.spaceid + ) { + return eventBus.publish('app.files.list.load') + } +} + +export const onSSEShareCreatedEvent = async ({ + sseData, + resourcesStore, + spacesStore, + sharesStore, + userStore, + clientService, + router +}: SSEEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } + + if ( + isLocationSpacesActive(router, 'files-spaces-generic') && + itemInCurrentFolder({ + resourcesStore, + parentFolderId: sseData.parentitemid + }) + ) { + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + resourcesStore.upsertResource(resource) + return resourcesStore.updateResourceField({ + id: resource.id, + field: 'indicators', + value: getIndicators({ + space, + resource, + ancestorMetaData: resourcesStore.ancestorMetaData, + user: userStore.user + }) + }) + } + + if (isLocationSharesActive(router, 'files-shares-with-me')) { + // FIXME: get drive item by id as soon as server supports it + const { data } = await clientService.graphAuthenticated.drives.listSharedWithMe() + const driveItem = data.value.find(({ remoteItem }) => remoteItem.id === sseData.itemid) + if (!driveItem) { + return + } + const resource = buildIncomingShareResource({ driveItem, graphRoles: sharesStore.graphRoles }) + return resourcesStore.upsertResource(resource) + } + + if (isLocationSharesActive(router, 'files-shares-with-others')) { + // FIXME: get drive item by id as soon as server supports it + const { data } = await clientService.graphAuthenticated.drives.listSharedByMe() + const driveItem = data.value.find(({ id }) => id === sseData.itemid) + if (!driveItem) { + return + } + const resource = buildOutgoingShareResource({ driveItem, user: userStore.user }) + return resourcesStore.upsertResource(resource) + } +} +export const onSSEShareUpdatedEvent = async ({ + sseData, + resourcesStore, + sharesStore, + clientService, + userStore, + router +}: SSEEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } + + if ( + isLocationSpacesActive(router, 'files-spaces-generic') && + sseData.affecteduserids?.includes(userStore.user.id) && + resourcesStore.currentFolder?.storageId === sseData.spaceid + ) { + return eventBus.publish('app.files.list.load') + } + + if (isLocationSharesActive(router, 'files-shares-with-me')) { + // FIXME: get drive item by id as soon as server supports it + const { data } = await clientService.graphAuthenticated.drives.listSharedWithMe() + const driveItem = data.value.find(({ remoteItem }) => remoteItem.id === sseData.itemid) + if (!driveItem) { + return + } + const resource = buildIncomingShareResource({ driveItem, graphRoles: sharesStore.graphRoles }) + return resourcesStore.upsertResource(resource) + } +} + +export const onSSEShareRemovedEvent = async ({ + sseData, + resourcesStore, + spacesStore, + userStore, + clientService, + messageStore, + language, + router +}: SSEEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } + + if ( + isLocationSpacesActive(router, 'files-spaces-generic') && + sseData.affecteduserids?.includes(userStore.user.id) && + resourcesStore.currentFolder?.storageId === sseData.spaceid + ) { + return messageStore.showMessage({ + title: language.$gettext( + 'Your access to this share has been revoked. Please navigate to another location.' + ) + }) + } + + if ( + isLocationSpacesActive(router, 'files-spaces-generic') && + itemInCurrentFolder({ + resourcesStore, + parentFolderId: sseData.parentitemid + }) + ) { + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + resourcesStore.upsertResource(resource) + return resourcesStore.updateResourceField({ + id: resource.id, + field: 'indicators', + value: getIndicators({ + space, + resource, + ancestorMetaData: resourcesStore.ancestorMetaData, + user: userStore.user + }) + }) + } + + if (isLocationSharesActive(router, 'files-shares-with-others')) { + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + if ( + !resource.shareTypes.includes(ShareTypes.user.value) && + !resource.shareTypes.includes(ShareTypes.group.value) + ) { + return resourcesStore.removeResources([resource]) + } + } + + if (isLocationSharesActive(router, 'files-shares-with-me')) { + const removedShareResource = resourcesStore.resources.find((r) => r.fileId === sseData.itemid) + if (!removedShareResource) { + return + } + return resourcesStore.removeResources([removedShareResource]) + } +} + +export const onSSELinkCreatedEvent = async ({ + sseData, + resourcesStore, + spacesStore, + userStore, + clientService, + router +}: SSEEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } + + if ( + isLocationSpacesActive(router, 'files-spaces-generic') && + itemInCurrentFolder({ + resourcesStore, + parentFolderId: sseData.parentitemid + }) + ) { + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + resourcesStore.upsertResource(resource) + return resourcesStore.updateResourceField({ + id: resource.id, + field: 'indicators', + value: getIndicators({ + space, + resource, + ancestorMetaData: resourcesStore.ancestorMetaData, + user: userStore.user + }) + }) + } + + if (isLocationSharesActive(router, 'files-shares-via-link')) { + // FIXME: get drive item by id as soon as server supports it + const { data } = await clientService.graphAuthenticated.drives.listSharedByMe() + const driveItem = data.value.find(({ id }) => id === sseData.itemid) + if (!driveItem) { + return + } + const resource = buildOutgoingShareResource({ driveItem, user: userStore.user }) + return resourcesStore.upsertResource(resource) + } +} + +export const onSSELinkUpdatedEvent = ({ sseData, clientService }: SSEEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } +} + +export const onSSELinkRemovedEvent = async ({ + sseData, + resourcesStore, + spacesStore, + userStore, + clientService, + router +}: SSEEventOptions) => { + if (sseData.initiatorid === clientService.initiatorId) { + // If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early. + return + } + + const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid) + if (!space) { + return + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + if ( + isLocationSpacesActive(router, 'files-spaces-generic') && + itemInCurrentFolder({ + resourcesStore, + parentFolderId: sseData.parentitemid + }) + ) { + resourcesStore.upsertResource(resource) + resourcesStore.updateResourceField({ + id: resource.id, + field: 'indicators', + value: getIndicators({ + space, + resource, + ancestorMetaData: resourcesStore.ancestorMetaData, + user: userStore.user + }) + }) + } + + if (isLocationSharesActive(router, 'files-shares-via-link')) { + if (!resource.shareTypes.includes(ShareTypes.link.value)) { + return resourcesStore.removeResources([resource]) + } + } +} diff --git a/packages/web-runtime/src/container/sse/types.ts b/packages/web-runtime/src/container/sse/types.ts new file mode 100644 index 00000000000..441cdf2e7e7 --- /dev/null +++ b/packages/web-runtime/src/container/sse/types.ts @@ -0,0 +1,44 @@ +import { z } from 'zod' +import { + ClientService, + MessageStore, + PreviewService, + ResourcesStore, + SharesStore, + SpacesStore, + UserStore +} from '@ownclouders/web-pkg' +import { Router } from 'vue-router' +import { Language } from 'vue3-gettext' +import PQueue from 'p-queue' + +export const eventSchema = z.object({ + itemid: z.string(), + parentitemid: z.string(), + spaceid: z.string().optional(), + initiatorid: z.string().optional(), + etag: z.string().optional(), + affecteduserids: z.array(z.string()).optional().nullable() +}) + +export type EventSchemaType = z.infer + +export interface SSEEventOptions { + resourcesStore: ResourcesStore + spacesStore: SpacesStore + userStore: UserStore + messageStore: MessageStore + sharesStore: SharesStore + clientService: ClientService + previewService: PreviewService + router: Router + language: Language + resourceQueue: PQueue + sseData: EventSchemaType +} + +export interface SseEventWrapperOptions extends Omit { + msg: MessageEvent + topic: string + method: (options: SSEEventOptions) => Promise | unknown +} From 93d3329641547e3cd97bc1e949d16a0178b0aefc Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Thu, 25 Apr 2024 09:26:40 +0200 Subject: [PATCH 13/17] Added fixme comments --- packages/web-runtime/src/container/sse/shares.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/web-runtime/src/container/sse/shares.ts b/packages/web-runtime/src/container/sse/shares.ts index dda133abcb4..0557d65642b 100644 --- a/packages/web-runtime/src/container/sse/shares.ts +++ b/packages/web-runtime/src/container/sse/shares.ts @@ -71,6 +71,7 @@ export const onSSESpaceMemberRemovedEvent = async ({ isLocationSpacesActive(router, 'files-spaces-generic') && removedSpace.id === resourcesStore.currentFolder.storageId ) { + // Fixme: Message triggers when user membership was revoked, but still is in a group membership, which is wrong return messageStore.showMessage({ title: language.$gettext( 'Your access to this space has been revoked. Please navigate to another location.' @@ -227,6 +228,7 @@ export const onSSEShareRemovedEvent = async ({ sseData.affecteduserids?.includes(userStore.user.id) && resourcesStore.currentFolder?.storageId === sseData.spaceid ) { + // Fixme: Message triggers when user share was revoked, but still is in a group share, which is wrong return messageStore.showMessage({ title: language.$gettext( 'Your access to this share has been revoked. Please navigate to another location.' From e23e21d8079205f3127a1ead6c19a93ee3670a92 Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Thu, 25 Apr 2024 09:35:43 +0200 Subject: [PATCH 14/17] Remove unused code --- packages/web-pkg/src/composables/piniaStores/resources.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/web-pkg/src/composables/piniaStores/resources.ts b/packages/web-pkg/src/composables/piniaStores/resources.ts index 30cd710726c..72f0a415136 100644 --- a/packages/web-pkg/src/composables/piniaStores/resources.ts +++ b/packages/web-pkg/src/composables/piniaStores/resources.ts @@ -195,10 +195,6 @@ export const useResourcesStore = defineStore('resources', () => { ancestorMetaData.value = value } - const clearAncestorMetaData = () => { - ancestorMetaData.value = {} - } - const updateAncestorField = < T extends AncestorMetaDataValue, K extends keyof AncestorMetaDataValue @@ -328,7 +324,6 @@ export const useResourcesStore = defineStore('resources', () => { ancestorMetaData, setAncestorMetaData, - clearAncestorMetaData, updateAncestorField, loadAncestorMetaData } From 0b11e92ff9f580b2c4a1570ff8b1ea5767159e23 Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Thu, 25 Apr 2024 09:41:28 +0200 Subject: [PATCH 15/17] Add changelog item --- .../unreleased/enhancement-add-sse-events-for-sharing | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 changelog/unreleased/enhancement-add-sse-events-for-sharing diff --git a/changelog/unreleased/enhancement-add-sse-events-for-sharing b/changelog/unreleased/enhancement-add-sse-events-for-sharing new file mode 100644 index 00000000000..581f6c9e398 --- /dev/null +++ b/changelog/unreleased/enhancement-add-sse-events-for-sharing @@ -0,0 +1,7 @@ +Enhancement: Add SSE event for moving + +We've added Server-Sent Events (SSE) for sharing. +This notifies the user when they received or revoked access to a share or membership to a space. + +https://github.com/owncloud/web/pull/10807 +https://github.com/owncloud/web/issues/10647 From 25040f6fb7f012cb629e47703e7c891fabd63b29 Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Thu, 25 Apr 2024 09:44:35 +0200 Subject: [PATCH 16/17] Fix wording --- packages/web-runtime/src/container/sse/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web-runtime/src/container/sse/helpers.ts b/packages/web-runtime/src/container/sse/helpers.ts index e9c46b5806c..24334e62fee 100644 --- a/packages/web-runtime/src/container/sse/helpers.ts +++ b/packages/web-runtime/src/container/sse/helpers.ts @@ -10,7 +10,7 @@ export const sseEventWrapper = (options: SseEventWrapperOptions) => { return method({ ...sseEventOptions, sseData }) } catch (e) { - console.error(`Unable to process sse event ${topic} data`, e) + console.error(`Unable to process sse event ${topic}`, e) } } export const itemInCurrentFolder = ({ From 2a746ca9b6301ea7a103807918bdda40131e8fb8 Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Thu, 25 Apr 2024 09:58:31 +0200 Subject: [PATCH 17/17] Rename func --- packages/web-runtime/src/container/sse/files.ts | 10 +++++----- packages/web-runtime/src/container/sse/helpers.ts | 2 +- packages/web-runtime/src/container/sse/shares.ts | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/web-runtime/src/container/sse/files.ts b/packages/web-runtime/src/container/sse/files.ts index ea7c4fa0ddf..7e5e2a87564 100644 --- a/packages/web-runtime/src/container/sse/files.ts +++ b/packages/web-runtime/src/container/sse/files.ts @@ -1,6 +1,6 @@ import { createFileRouteOptions, getIndicators, ImageDimension } from '@ownclouders/web-pkg' import { SSEEventOptions } from './types' -import { itemInCurrentFolder } from './helpers' +import { isItemInCurrentFolder } from './helpers' export const onSSEItemRenamedEvent = async ({ sseData, @@ -81,7 +81,7 @@ export const onSSEProcessingFinishedEvent = async ({ resourceQueue, previewService }: SSEEventOptions) => { - if (!itemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid })) { + if (!isItemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid })) { return false } const resource = resourcesStore.resources.find((f) => f.id === sseData.itemid) @@ -199,7 +199,7 @@ export const onSSEItemRestoredEvent = async ({ return } - if (!itemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { + if (!isItemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { return false } @@ -287,7 +287,7 @@ export const onSSEFileTouchedEvent = async ({ return } - if (!itemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { + if (!isItemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { return false } @@ -318,7 +318,7 @@ export const onSSEFolderCreatedEvent = async ({ return } - if (!itemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { + if (!isItemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { return false } diff --git a/packages/web-runtime/src/container/sse/helpers.ts b/packages/web-runtime/src/container/sse/helpers.ts index 24334e62fee..ab63e7449a5 100644 --- a/packages/web-runtime/src/container/sse/helpers.ts +++ b/packages/web-runtime/src/container/sse/helpers.ts @@ -13,7 +13,7 @@ export const sseEventWrapper = (options: SseEventWrapperOptions) => { console.error(`Unable to process sse event ${topic}`, e) } } -export const itemInCurrentFolder = ({ +export const isItemInCurrentFolder = ({ resourcesStore, parentFolderId }: { diff --git a/packages/web-runtime/src/container/sse/shares.ts b/packages/web-runtime/src/container/sse/shares.ts index 0557d65642b..5585f54296f 100644 --- a/packages/web-runtime/src/container/sse/shares.ts +++ b/packages/web-runtime/src/container/sse/shares.ts @@ -11,7 +11,7 @@ import { isLocationSpacesActive } from '@ownclouders/web-pkg' import { SSEEventOptions } from './types' -import { itemInCurrentFolder } from './helpers' +import { isItemInCurrentFolder } from './helpers' export const onSSESpaceMemberAddedEvent = async ({ sseData, @@ -126,7 +126,7 @@ export const onSSEShareCreatedEvent = async ({ if ( isLocationSpacesActive(router, 'files-spaces-generic') && - itemInCurrentFolder({ + isItemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid }) @@ -238,7 +238,7 @@ export const onSSEShareRemovedEvent = async ({ if ( isLocationSpacesActive(router, 'files-spaces-generic') && - itemInCurrentFolder({ + isItemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid }) @@ -307,7 +307,7 @@ export const onSSELinkCreatedEvent = async ({ if ( isLocationSpacesActive(router, 'files-spaces-generic') && - itemInCurrentFolder({ + isItemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid }) @@ -377,7 +377,7 @@ export const onSSELinkRemovedEvent = async ({ if ( isLocationSpacesActive(router, 'files-spaces-generic') && - itemInCurrentFolder({ + isItemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid })