From b0be4646c8fdc9c95ad555054eb2b78b306b2d73 Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Thu, 4 Apr 2024 13:42:09 +0200 Subject: [PATCH 1/9] Early return in sse events, if current client did the request, add folder created event handler --- packages/web-client/src/sse.ts | 3 +- packages/web-client/src/webdav/client/dav.ts | 6 +- packages/web-client/src/webdav/index.ts | 3 +- packages/web-client/src/webdav/types.ts | 1 + .../src/composables/piniaStores/client.ts | 16 ++ .../src/composables/piniaStores/index.ts | 1 + .../src/composables/upload/useUpload.ts | 9 +- .../web-pkg/src/services/client/client.ts | 8 +- .../tests/unit/services/client.spec.ts | 12 +- .../web-runtime/src/container/bootstrap.ts | 38 +++- packages/web-runtime/src/container/sse.ts | 170 +++++++++++++----- packages/web-runtime/src/index.ts | 7 +- 12 files changed, 216 insertions(+), 58 deletions(-) create mode 100644 packages/web-pkg/src/composables/piniaStores/client.ts diff --git a/packages/web-client/src/sse.ts b/packages/web-client/src/sse.ts index f5d2c0a19dc..bc889d08e6f 100644 --- a/packages/web-client/src/sse.ts +++ b/packages/web-client/src/sse.ts @@ -7,7 +7,8 @@ export enum MESSAGE_TYPE { FILE_UNLOCKED = 'file-unlocked', ITEM_RENAMED = 'item-renamed', ITEM_TRASHED = 'item-trashed', - ITEM_RESTORED = 'item-restored' + ITEM_RESTORED = 'item-restored', + FOLDER_CREATED = 'folder-created' } export class RetriableError extends Error { diff --git a/packages/web-client/src/webdav/client/dav.ts b/packages/web-client/src/webdav/client/dav.ts index d50950d1190..312ee9fb986 100644 --- a/packages/web-client/src/webdav/client/dav.ts +++ b/packages/web-client/src/webdav/client/dav.ts @@ -18,6 +18,7 @@ interface DAVOptions { accessToken: Ref baseUrl: string language: Ref + clientInitiatorId: Ref } interface DavResult { @@ -31,12 +32,14 @@ export class DAV { private client: WebDAVClient private davPath: string private language: Ref + private clientInitiatorId: Ref - constructor({ accessToken, baseUrl, language }: DAVOptions) { + constructor({ accessToken, baseUrl, language, clientInitiatorId }: DAVOptions) { this.davPath = urlJoin(baseUrl, 'remote.php/dav') this.accessToken = accessToken this.client = createClient(this.davPath, {}) this.language = language + this.clientInitiatorId = clientInitiatorId } public mkcol(path: string, { headers = {} }: { headers?: Headers } = {}) { @@ -166,6 +169,7 @@ export class DAV { return { 'Accept-Language': unref(this.language), 'Content-Type': 'application/xml; charset=utf-8', + 'Initiator-ID': unref(this.clientInitiatorId), 'X-Requested-With': 'XMLHttpRequest', 'X-Request-ID': uuidV4(), ...headers diff --git a/packages/web-client/src/webdav/index.ts b/packages/web-client/src/webdav/index.ts index b6c280e9f7c..c70c3d8718d 100644 --- a/packages/web-client/src/webdav/index.ts +++ b/packages/web-client/src/webdav/index.ts @@ -27,7 +27,8 @@ export const webdav = (options: WebDavOptions): WebDAV => { const dav = new DAV({ accessToken: options.accessToken, baseUrl: options.baseUrl, - language: options.language + language: options.language, + clientInitiatorId: options.clientInitiatorId }) const pathForFileIdFactory = GetPathForFileIdFactory(dav, options) diff --git a/packages/web-client/src/webdav/types.ts b/packages/web-client/src/webdav/types.ts index ba002e4e845..8a47b3bd53b 100644 --- a/packages/web-client/src/webdav/types.ts +++ b/packages/web-client/src/webdav/types.ts @@ -28,6 +28,7 @@ export interface WebDavOptions { clientService: any language: Ref user: Ref + clientInitiatorId: Ref } export interface WebDAV { diff --git a/packages/web-pkg/src/composables/piniaStores/client.ts b/packages/web-pkg/src/composables/piniaStores/client.ts new file mode 100644 index 00000000000..556b52cf323 --- /dev/null +++ b/packages/web-pkg/src/composables/piniaStores/client.ts @@ -0,0 +1,16 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export const useClientStore = defineStore('client', () => { + const clientInitiatorId = ref() + const setClientInitiatorId = (id: string) => { + clientInitiatorId.value = id + } + + return { + clientInitiatorId, + setClientInitiatorId + } +}) + +export type ClientStore = ReturnType diff --git a/packages/web-pkg/src/composables/piniaStores/index.ts b/packages/web-pkg/src/composables/piniaStores/index.ts index 371b95322bc..358d3b2213c 100644 --- a/packages/web-pkg/src/composables/piniaStores/index.ts +++ b/packages/web-pkg/src/composables/piniaStores/index.ts @@ -11,3 +11,4 @@ export * from './shares' export * from './spaces' export * from './theme' export * from './user' +export * from './client' diff --git a/packages/web-pkg/src/composables/upload/useUpload.ts b/packages/web-pkg/src/composables/upload/useUpload.ts index e068936ffff..f47c3df30bc 100644 --- a/packages/web-pkg/src/composables/upload/useUpload.ts +++ b/packages/web-pkg/src/composables/upload/useUpload.ts @@ -2,7 +2,7 @@ import { computed, unref, watch } from 'vue' import { UppyService } from '../../services/uppy/uppyService' import { v4 as uuidV4 } from 'uuid' import { useGettext } from 'vue3-gettext' -import { useAuthStore, useCapabilityStore, useConfigStore } from '../piniaStores' +import { useAuthStore, useCapabilityStore, useClientStore, useConfigStore } from '../piniaStores' interface UploadOptions { uppyService: UppyService @@ -11,11 +11,15 @@ interface UploadOptions { export function useUpload(options: UploadOptions) { const configStore = useConfigStore() const capabilityStore = useCapabilityStore() + const clientStore = useClientStore() const { current: currentLanguage } = useGettext() const authStore = useAuthStore() const headers = computed((): { [key: string]: string } => { - const headers = { 'Accept-Language': currentLanguage } + const headers = { + 'Accept-Language': currentLanguage, + 'Initiator-ID': clientStore.clientInitiatorId + } if (authStore.publicLinkContextReady) { const password = authStore.publicLinkPassword if (password) { @@ -42,6 +46,7 @@ export function useUpload(options: UploadOptions) { req.setHeader('Authorization', unref(headers).Authorization) req.setHeader('X-Request-ID', uuidV4()) req.setHeader('Accept-Language', unref(headers)['Accept-Language']) + req.setHeader('Initiator-ID', unref(headers)['Initiator-ID']) }, headers: (file) => !!file.xhrUpload || file?.isRemote diff --git a/packages/web-pkg/src/services/client/client.ts b/packages/web-pkg/src/services/client/client.ts index e77a2155ab8..d786aa23604 100644 --- a/packages/web-pkg/src/services/client/client.ts +++ b/packages/web-pkg/src/services/client/client.ts @@ -7,7 +7,7 @@ import { WebDAV } from '@ownclouders/web-client/src/webdav' import { Language } from 'vue3-gettext' import { FetchEventSourceInit } from '@microsoft/fetch-event-source' import { sse } from '@ownclouders/web-client/src/sse' -import { AuthStore, ConfigStore, UserStore } from '../../composables' +import { AuthStore, ClientStore, ConfigStore, UserStore } from '../../composables' import { computed } from 'vue' interface OcClient { @@ -54,6 +54,7 @@ export interface ClientServiceOptions { language: Language authStore: AuthStore userStore: UserStore + clientStore: ClientStore } export class ClientService { @@ -61,6 +62,7 @@ export class ClientService { private language: Language private authStore: AuthStore private userStore: UserStore + private clientStore: ClientStore private httpAuthenticatedClient: HttpClient private httpUnAuthenticatedClient: HttpClient @@ -75,6 +77,7 @@ export class ClientService { this.language = options.language this.authStore = options.authStore this.userStore = options.userStore + this.clientStore = options.clientStore } public get httpAuthenticated(): _HttpClient { @@ -132,7 +135,8 @@ export class ClientService { 'Accept-Language': this.currentLanguage, ...(!!authenticated && { Authorization: 'Bearer ' + this.authStore.accessToken }), 'X-Requested-With': 'XMLHttpRequest', - 'X-Request-ID': uuidV4() + 'X-Request-ID': uuidV4(), + 'Initiator-ID': this.clientStore.clientInitiatorId } }) } diff --git a/packages/web-pkg/tests/unit/services/client.spec.ts b/packages/web-pkg/tests/unit/services/client.spec.ts index 1b9686d37f9..e7ef248fa7c 100644 --- a/packages/web-pkg/tests/unit/services/client.spec.ts +++ b/packages/web-pkg/tests/unit/services/client.spec.ts @@ -1,5 +1,11 @@ import { HttpClient } from '../../../src/http' -import { ClientService, useAuthStore, useConfigStore, useUserStore } from '../../../src/' +import { + ClientService, + useAuthStore, + useClientStore, + useConfigStore, + useUserStore +} from '../../../src/' import { Language } from 'vue3-gettext' import { Graph, OCS, client as _client } from '@ownclouders/web-client' import { createTestingPinia, writable } from 'web-test-helpers' @@ -13,6 +19,7 @@ const getClientServiceMock = () => { const authStore = useAuthStore() const configStore = useConfigStore() const userStore = useUserStore() + const clientStore = useClientStore() writable(configStore).serverUrl = serverUrl return { @@ -20,7 +27,8 @@ const getClientServiceMock = () => { configStore, language: language as Language, authStore, - userStore + userStore, + clientStore }), authStore } diff --git a/packages/web-runtime/src/container/bootstrap.ts b/packages/web-runtime/src/container/bootstrap.ts index 4544d7a5224..144a4384f8d 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, + useClientStore } from '@ownclouders/web-pkg' import { authService } from '../services/auth' import { @@ -60,8 +61,10 @@ import { onSSEItemRenamedEvent, onSSEProcessingFinishedEvent, onSSEItemRestoredEvent, - onSSEItemTrashedEvent + onSSEItemTrashedEvent, + onSSEFolderCreatedEvent } from './sse' +import { ClientStore } from '@ownclouders/web-pkg/src/composables/piniaStores/client' const getEmbedConfigFromQuery = ( doesEmbedEnabledOptionExists: boolean @@ -331,6 +334,7 @@ export const announcePiniaStores = () => { const sharesStore = useSharesStore() const spacesStore = useSpacesStore() const userStore = useUserStore() + const clientStore = useClientStore() return { appsStore, @@ -343,7 +347,8 @@ export const announcePiniaStores = () => { modalStore, sharesStore, spacesStore, - userStore + userStore, + clientStore } } @@ -388,19 +393,24 @@ export const announceClientService = ({ configStore, userStore, authStore, - capabilityStore + capabilityStore, + clientStore }: { app: App configStore: ConfigStore userStore: UserStore authStore: AuthStore capabilityStore: CapabilityStore + clientStore: ClientStore }): void => { + clientStore.setClientInitiatorId(uuidV4()) + const clientService = new ClientService({ configStore, language: app.config.globalProperties.$language, authStore, - userStore + userStore, + clientStore }) app.config.globalProperties.$clientService = clientService app.config.globalProperties.$clientService.webdav = webdav({ @@ -409,6 +419,7 @@ export const announceClientService = ({ capabilities: computed(() => capabilityStore.capabilities), clientService: app.config.globalProperties.$clientService, language: computed(() => app.config.globalProperties.$language.current), + clientInitiatorId: computed(() => clientStore.clientInitiatorId), user: computed(() => userStore.user) }) @@ -644,6 +655,7 @@ export const registerSSEEventListeners = ({ language, resourcesStore, spacesStore, + clientStore, messageStore, clientService, previewService, @@ -653,6 +665,7 @@ export const registerSSEEventListeners = ({ language: Language resourcesStore: ResourcesStore spacesStore: SpacesStore + clientStore: ClientStore messageStore: MessageStore clientService: ClientService previewService: PreviewService @@ -675,6 +688,7 @@ export const registerSSEEventListeners = ({ topic: MESSAGE_TYPE.ITEM_RENAMED, resourcesStore, spacesStore, + clientStore, msg, clientService, router @@ -686,6 +700,7 @@ export const registerSSEEventListeners = ({ topic: MESSAGE_TYPE.POSTPROCESSING_FINISHED, resourcesStore, spacesStore, + clientStore, msg, clientService, previewService, @@ -718,6 +733,7 @@ export const registerSSEEventListeners = ({ topic: MESSAGE_TYPE.ITEM_TRASHED, language, resourcesStore, + clientStore, messageStore, msg }) @@ -728,6 +744,18 @@ export const registerSSEEventListeners = ({ topic: MESSAGE_TYPE.ITEM_RESTORED, resourcesStore, spacesStore, + clientStore, + msg, + clientService + }) + ) + + clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.FOLDER_CREATED, (msg) => + onSSEFolderCreatedEvent({ + topic: MESSAGE_TYPE.FOLDER_CREATED, + resourcesStore, + spacesStore, + clientStore, msg, clientService }) diff --git a/packages/web-runtime/src/container/sse.ts b/packages/web-runtime/src/container/sse.ts index 6d18c05723f..36baa013fbc 100644 --- a/packages/web-runtime/src/container/sse.ts +++ b/packages/web-runtime/src/container/sse.ts @@ -1,5 +1,6 @@ import { ClientService, + ClientStore, createFileRouteOptions, getIndicators, ImageDimension, @@ -17,7 +18,8 @@ import { Language } from 'vue3-gettext' const eventSchema = z.object({ itemid: z.string(), parentitemid: z.string(), - spaceid: z.string().optional() + spaceid: z.string().optional(), + initiatorid: z.string().optional() }) const itemInCurrentFolder = ({ @@ -50,6 +52,7 @@ export const onSSEItemRenamedEvent = async ({ topic, resourcesStore, spacesStore, + clientStore, msg, clientService, router @@ -57,12 +60,23 @@ export const onSSEItemRenamedEvent = async ({ topic: string resourcesStore: ResourcesStore spacesStore: SpacesStore + clientStore msg: MessageEvent clientService: ClientService router: Router }) => { try { const sseData = eventSchema.parse(JSON.parse(msg.data)) + + if (sseData.initiatorid === clientStore.clientInitiatorId) { + /** + * 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 @@ -138,10 +152,9 @@ export const onSSEProcessingFinishedEvent = async ({ topic, resourcesStore, spacesStore, + clientStore, msg, - // eslint-disable-next-line @typescript-eslint/no-unused-vars clientService, - // eslint-disable-next-line @typescript-eslint/no-unused-vars resourceQueue, previewService }: { @@ -155,46 +168,55 @@ export const onSSEProcessingFinishedEvent = async ({ }) => { try { const sseData = eventSchema.parse(JSON.parse(msg.data)) + console.log(sseData) + + if (sseData.initiatorid === clientStore.clientInitiatorId) { + /** + * 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 + } 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 === resource.storageId) - const isFileLoaded = !!resource - if (isFileLoaded) { - resourcesStore.updateResourceField({ - id: sseData.itemid, - field: 'processing', - value: false + /** + * Resource not loaded, this indicates an upload for example + */ + if (!!resource) { + return resourceQueue.add(async () => { + const { resource } = await clientService.webdav.listFilesById({ + fileId: sseData.itemid + }) + resourcesStore.upsertResource(resource) }) + } - if (space) { - const preview = await previewService.loadPreview({ - resource, - space, - dimensions: ImageDimension.Thumbnail - }) + resourcesStore.updateResourceField({ + id: sseData.itemid, + field: 'processing', + value: false + }) + + const space = spacesStore.spaces.find((s) => s.id === resource.storageId) + if (space) { + const preview = await previewService.loadPreview({ + resource, + space, + dimensions: ImageDimension.Thumbnail + }) - if (preview) { - resourcesStore.updateResourceField({ - id: sseData.itemid, - field: 'thumbnail', - value: preview - }) - } + if (preview) { + resourcesStore.updateResourceField({ + id: sseData.itemid, + field: 'thumbnail', + value: preview + }) } - } else { - // FIXME: we currently cannot do this, we need to block this for ongoing uploads and copy operations - // when fixing revert the changelog removal - // resourceQueue.add(async () => { - // const { resource } = await clientService.webdav.listFilesById({ - // fileId: sseData.itemid - // }) - // resource.path = urlJoin(currentFolder.path, resource.name) - // resourcesStore.upsertResource(resource) - // }) } } catch (e) { console.error(`Unable to parse sse event ${topic} data`, e) @@ -206,25 +228,28 @@ export const onSSEItemTrashedEvent = async ({ language, messageStore, resourcesStore, + clientStore, msg }: { topic: string language: Language resourcesStore: ResourcesStore + clientStore: ClientStore messageStore: MessageStore msg: MessageEvent }) => { try { - /** - * TODO: Implement a mechanism to identify the client that initiated the trash event. - * Currently, a timeout is utilized to ensure the frontend business logic execution, - * preventing the user from being prompted to navigate to 'another location' - * if the active current folder has been deleted. - * If the initiating client matches the current client, no action is required. - */ - await new Promise((resolve) => setTimeout(resolve, 500)) - const sseData = eventSchema.parse(JSON.parse(msg.data)) + + if (sseData.initiatorid === clientStore.clientInitiatorId) { + /** + * 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 @@ -252,18 +277,29 @@ export const onSSEItemRestoredEvent = async ({ topic, resourcesStore, spacesStore, + clientStore, msg, clientService }: { topic: string resourcesStore: ResourcesStore spacesStore: SpacesStore + clientStore: ClientStore msg: MessageEvent clientService: ClientService }) => { try { const sseData = eventSchema.parse(JSON.parse(msg.data)) + if (sseData.initiatorid === clientStore.clientInitiatorId) { + /** + * 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 @@ -294,3 +330,53 @@ export const onSSEItemRestoredEvent = async ({ console.error(`Unable to parse sse event ${topic} data`, e) } } + +export const onSSEFolderCreatedEvent = async ({ + topic, + resourcesStore, + spacesStore, + clientStore, + msg, + clientService +}: { + topic: string + resourcesStore: ResourcesStore + spacesStore: SpacesStore + clientStore: ClientStore + msg: MessageEvent + clientService: ClientService +}) => { + try { + const sseData = eventSchema.parse(JSON.parse(msg.data)) + + if (sseData.initiatorid === clientStore.clientInitiatorId) { + /** + * 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 + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + if (!resource) { + return + } + + if (!itemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { + return false + } + + resourcesStore.upsertResource(resource) + } catch (e) { + console.error(`Unable to parse sse event ${topic} data`, e) + } +} diff --git a/packages/web-runtime/src/index.ts b/packages/web-runtime/src/index.ts index eac778f4520..996f5e7cdde 100644 --- a/packages/web-runtime/src/index.ts +++ b/packages/web-runtime/src/index.ts @@ -60,7 +60,8 @@ export const bootstrapApp = async (configurationPath: string): Promise => userStore, resourcesStore, messagesStore, - sharesStore + sharesStore, + clientStore } = announcePiniaStores() app.provide('$router', router) @@ -82,7 +83,8 @@ export const bootstrapApp = async (configurationPath: string): Promise => configStore, userStore, authStore, - capabilityStore + capabilityStore, + clientStore }) // TODO: move to announceArchiverService function app.config.globalProperties.$archiverService = new ArchiverService( @@ -196,6 +198,7 @@ export const bootstrapApp = async (configurationPath: string): Promise => language: gettext, resourcesStore, spacesStore, + clientStore, messageStore: messagesStore, clientService, previewService, From 7ccf498f1616e69a4104271c6876c7f215685be1 Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Thu, 4 Apr 2024 14:16:16 +0200 Subject: [PATCH 2/9] Prevent double fetch resources --- packages/web-runtime/src/container/sse.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/web-runtime/src/container/sse.ts b/packages/web-runtime/src/container/sse.ts index 36baa013fbc..190bcb3515c 100644 --- a/packages/web-runtime/src/container/sse.ts +++ b/packages/web-runtime/src/container/sse.ts @@ -170,24 +170,23 @@ export const onSSEProcessingFinishedEvent = async ({ const sseData = eventSchema.parse(JSON.parse(msg.data)) console.log(sseData) - if (sseData.initiatorid === clientStore.clientInitiatorId) { - /** - * 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 - } - if (!itemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid })) { return false } const resource = resourcesStore.resources.find((f) => f.id === sseData.itemid) /** - * Resource not loaded, this indicates an upload for example + * If resource is not loaded, it suggests an upload is in progress. */ - if (!!resource) { + if (!resource) { + if (sseData.initiatorid === clientStore.clientInitiatorId) { + /** + * 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 From b547727f3ebbb7585ccf03908d9c4e2b679f67d1 Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Fri, 5 Apr 2024 12:14:22 +0200 Subject: [PATCH 3/9] add file touched event --- packages/web-client/src/sse.ts | 1 + .../web-runtime/src/container/bootstrap.ts | 25 +++++++- packages/web-runtime/src/container/sse.ts | 57 ++++++++++++++++++- 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/packages/web-client/src/sse.ts b/packages/web-client/src/sse.ts index bc889d08e6f..7b851e6d840 100644 --- a/packages/web-client/src/sse.ts +++ b/packages/web-client/src/sse.ts @@ -5,6 +5,7 @@ export enum MESSAGE_TYPE { POSTPROCESSING_FINISHED = 'postprocessing-finished', FILE_LOCKED = 'file-locked', FILE_UNLOCKED = 'file-unlocked', + FILE_TOUCHED = 'file-touched', ITEM_RENAMED = 'item-renamed', ITEM_TRASHED = 'item-trashed', ITEM_RESTORED = 'item-restored', diff --git a/packages/web-runtime/src/container/bootstrap.ts b/packages/web-runtime/src/container/bootstrap.ts index 144a4384f8d..cf6ef955bd5 100644 --- a/packages/web-runtime/src/container/bootstrap.ts +++ b/packages/web-runtime/src/container/bootstrap.ts @@ -62,7 +62,8 @@ import { onSSEProcessingFinishedEvent, onSSEItemRestoredEvent, onSSEItemTrashedEvent, - onSSEFolderCreatedEvent + onSSEFolderCreatedEvent, + onSSEFileTouchedEvent } from './sse' import { ClientStore } from '@ownclouders/web-pkg/src/composables/piniaStores/client' @@ -760,6 +761,28 @@ export const registerSSEEventListeners = ({ clientService }) ) + + clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.FOLDER_CREATED, (msg) => + onSSEFolderCreatedEvent({ + topic: MESSAGE_TYPE.FOLDER_CREATED, + resourcesStore, + spacesStore, + clientStore, + msg, + clientService + }) + ) + + clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.FILE_TOUCHED, (msg) => + onSSEFileTouchedEvent({ + topic: MESSAGE_TYPE.FILE_TOUCHED, + resourcesStore, + spacesStore, + clientStore, + 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 190bcb3515c..5b08fc7e9e5 100644 --- a/packages/web-runtime/src/container/sse.ts +++ b/packages/web-runtime/src/container/sse.ts @@ -161,6 +161,7 @@ export const onSSEProcessingFinishedEvent = async ({ topic: string resourcesStore: ResourcesStore spacesStore: SpacesStore + clientStore: ClientStore msg: MessageEvent clientService: ClientService resourceQueue: PQueue @@ -168,7 +169,6 @@ export const onSSEProcessingFinishedEvent = async ({ }) => { try { const sseData = eventSchema.parse(JSON.parse(msg.data)) - console.log(sseData) if (!itemInCurrentFolder({ resourcesStore, parentFolderId: sseData.parentitemid })) { return false @@ -330,6 +330,61 @@ export const onSSEItemRestoredEvent = async ({ } } +/** + * 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 ({ + topic, + resourcesStore, + spacesStore, + clientStore, + msg, + clientService +}: { + topic: string + resourcesStore: ResourcesStore + spacesStore: SpacesStore + clientStore: ClientStore + msg: MessageEvent + clientService: ClientService +}) => { + try { + const sseData = eventSchema.parse(JSON.parse(msg.data)) + + if (sseData.initiatorid === clientStore.clientInitiatorId) { + /** + * 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 + } + + const resource = await clientService.webdav.getFileInfo(space, { + fileId: sseData.itemid + }) + + if (!resource) { + return + } + + if (!itemInCurrentFolder({ resourcesStore, parentFolderId: resource.parentFolderId })) { + return false + } + + resourcesStore.upsertResource(resource) + } catch (e) { + console.error(`Unable to parse sse event ${topic} data`, e) + } +} + export const onSSEFolderCreatedEvent = async ({ topic, resourcesStore, From db347935ddc89ad2a2550d4c4bc8af7f73b34820 Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Fri, 5 Apr 2024 12:31:23 +0200 Subject: [PATCH 4/9] Fix text file thumbnails are hidden on purpose (too short) in table view but shown in tiles view --- .../src/components/FilesList/ResourceTile.vue | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/web-pkg/src/components/FilesList/ResourceTile.vue b/packages/web-pkg/src/components/FilesList/ResourceTile.vue index ff1effe5885..7e65cb7cf6b 100644 --- a/packages/web-pkg/src/components/FilesList/ResourceTile.vue +++ b/packages/web-pkg/src/components/FilesList/ResourceTile.vue @@ -33,7 +33,11 @@ >
- + { + return !isResourceTxtFileAlmostEmpty(resource) + } + return { statusIconAttrs, showStatusIcon, tooltipLabelIcon, resourceDisabled, resourceDescription + tooltipLabelIcon, + shouldDisplayThumbnails } } }) From 4bcb737d4fb40724e29f168ccbc8626cee6dc10f Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Fri, 5 Apr 2024 12:44:53 +0200 Subject: [PATCH 5/9] Add changelog item --- .../enhancement-add-sse-event-for-file-creation | 12 ++++++++++++ .../src/components/FilesList/ResourceTile.vue | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 changelog/unreleased/enhancement-add-sse-event-for-file-creation diff --git a/changelog/unreleased/enhancement-add-sse-event-for-file-creation b/changelog/unreleased/enhancement-add-sse-event-for-file-creation new file mode 100644 index 00000000000..eb22d6b86fc --- /dev/null +++ b/changelog/unreleased/enhancement-add-sse-event-for-file-creation @@ -0,0 +1,12 @@ +Enhancement: Implement Server-Sent Events (SSE) for File Creation + +We've implemented Server-Sent Events (SSE) to notify users in real-time when a file is uploaded, +a new folder is created, or a file is created (e.g., a text file). +With this enhancement, users will see new files automatically appear in another browser tab if they have one open or +when collaborating with others in the same space. + +https://github.com/owncloud/web/pull/10709 +https://github.com/owncloud/web/issues/9782 + + + diff --git a/packages/web-pkg/src/components/FilesList/ResourceTile.vue b/packages/web-pkg/src/components/FilesList/ResourceTile.vue index 7e65cb7cf6b..106d83139c8 100644 --- a/packages/web-pkg/src/components/FilesList/ResourceTile.vue +++ b/packages/web-pkg/src/components/FilesList/ResourceTile.vue @@ -164,7 +164,7 @@ export default defineComponent({ }) const shouldDisplayThumbnails = (resource: Resource) => { - return !isResourceTxtFileAlmostEmpty(resource) + return resource.thumbnail && !isResourceTxtFileAlmostEmpty(resource) } return { From f4716c7e2f186b5c4cf7b1c574152e8fbbd53342 Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Fri, 5 Apr 2024 12:48:48 +0200 Subject: [PATCH 6/9] RM new lines2 --- .../unreleased/enhancement-add-sse-event-for-file-creation | 3 --- 1 file changed, 3 deletions(-) diff --git a/changelog/unreleased/enhancement-add-sse-event-for-file-creation b/changelog/unreleased/enhancement-add-sse-event-for-file-creation index eb22d6b86fc..ed5503b5ed8 100644 --- a/changelog/unreleased/enhancement-add-sse-event-for-file-creation +++ b/changelog/unreleased/enhancement-add-sse-event-for-file-creation @@ -7,6 +7,3 @@ when collaborating with others in the same space. https://github.com/owncloud/web/pull/10709 https://github.com/owncloud/web/issues/9782 - - - From 3458c948bef1b0bf868f8be1d18d920d68070ff0 Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Mon, 8 Apr 2024 10:14:27 +0200 Subject: [PATCH 7/9] React on code review --- packages/web-runtime/src/container/bootstrap.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/web-runtime/src/container/bootstrap.ts b/packages/web-runtime/src/container/bootstrap.ts index cf6ef955bd5..65f2b310d51 100644 --- a/packages/web-runtime/src/container/bootstrap.ts +++ b/packages/web-runtime/src/container/bootstrap.ts @@ -36,7 +36,8 @@ import { LoadingService, PasswordPolicyService, PreviewService, - UppyService + UppyService, + ClientStore } from '@ownclouders/web-pkg' import { init as sentryInit } from '@sentry/vue' import { webdav } from '@ownclouders/web-client/src/webdav' @@ -65,7 +66,6 @@ import { onSSEFolderCreatedEvent, onSSEFileTouchedEvent } from './sse' -import { ClientStore } from '@ownclouders/web-pkg/src/composables/piniaStores/client' const getEmbedConfigFromQuery = ( doesEmbedEnabledOptionExists: boolean @@ -762,17 +762,6 @@ export const registerSSEEventListeners = ({ }) ) - clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.FOLDER_CREATED, (msg) => - onSSEFolderCreatedEvent({ - topic: MESSAGE_TYPE.FOLDER_CREATED, - resourcesStore, - spacesStore, - clientStore, - msg, - clientService - }) - ) - clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.FILE_TOUCHED, (msg) => onSSEFileTouchedEvent({ topic: MESSAGE_TYPE.FILE_TOUCHED, From d2dcf16b507093d0600dd32c66c4a5ec5860c4f3 Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Mon, 8 Apr 2024 12:51:21 +0200 Subject: [PATCH 8/9] Lint --- packages/web-pkg/src/components/FilesList/ResourceTile.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web-pkg/src/components/FilesList/ResourceTile.vue b/packages/web-pkg/src/components/FilesList/ResourceTile.vue index 106d83139c8..bc234d6700a 100644 --- a/packages/web-pkg/src/components/FilesList/ResourceTile.vue +++ b/packages/web-pkg/src/components/FilesList/ResourceTile.vue @@ -172,7 +172,7 @@ export default defineComponent({ showStatusIcon, tooltipLabelIcon, resourceDisabled, - resourceDescription + resourceDescription, tooltipLabelIcon, shouldDisplayThumbnails } From 5c254d09f9e020e141552e699fc31b9a0dc2f6c8 Mon Sep 17 00:00:00 2001 From: Jan Ackermann Date: Mon, 8 Apr 2024 14:43:35 +0200 Subject: [PATCH 9/9] Lint --- packages/web-pkg/src/components/FilesList/ResourceTile.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/web-pkg/src/components/FilesList/ResourceTile.vue b/packages/web-pkg/src/components/FilesList/ResourceTile.vue index bc234d6700a..5d301452f07 100644 --- a/packages/web-pkg/src/components/FilesList/ResourceTile.vue +++ b/packages/web-pkg/src/components/FilesList/ResourceTile.vue @@ -173,7 +173,6 @@ export default defineComponent({ tooltipLabelIcon, resourceDisabled, resourceDescription, - tooltipLabelIcon, shouldDisplayThumbnails } }