From f965b5bdebd77a8af6cf2c807dd726938bce2efa Mon Sep 17 00:00:00 2001 From: Benedikt Kulmann Date: Sun, 10 Oct 2021 13:49:54 +0200 Subject: [PATCH] WIP resource management in store --- .../src/helpers/resource/sameResource.ts | 2 +- packages/web-app-files/src/index.js | 9 +- packages/web-app-files/src/services/index.ts | 1 + .../src/services/listing/index.ts | 2 + .../src/services/listing/listing.ts | 66 +++++++++++ .../listing/loader/favoritesLoader.ts | 19 +++ .../src/services/listing/loader/index.ts | 3 + .../services/listing/loader/personalLoader.ts | 20 ++++ .../services/listing/loader/resourceLoader.ts | 12 ++ packages/web-app-files/src/store/index.js | 8 +- .../src/store/modules/listing.ts | 108 ++++++++++++++++++ .../src/store/modules/resource.ts | 45 ++++++++ .../{helpers/resource => types}/resource.ts | 0 packages/web-app-files/src/types/view.ts | 9 ++ packages/web-app-files/src/views/Personal.vue | 19 ++- packages/web-runtime/src/container/api.ts | 5 + .../src/container/application/classic.ts | 4 + .../src/container/application/index.ts | 5 + .../web-runtime/src/container/bootstrap.ts | 7 +- packages/web-runtime/src/container/types.ts | 4 +- packages/web-runtime/src/index.ts | 3 +- 21 files changed, 341 insertions(+), 10 deletions(-) create mode 100644 packages/web-app-files/src/services/listing/index.ts create mode 100644 packages/web-app-files/src/services/listing/listing.ts create mode 100644 packages/web-app-files/src/services/listing/loader/favoritesLoader.ts create mode 100644 packages/web-app-files/src/services/listing/loader/index.ts create mode 100644 packages/web-app-files/src/services/listing/loader/personalLoader.ts create mode 100644 packages/web-app-files/src/services/listing/loader/resourceLoader.ts create mode 100644 packages/web-app-files/src/store/modules/listing.ts create mode 100644 packages/web-app-files/src/store/modules/resource.ts rename packages/web-app-files/src/{helpers/resource => types}/resource.ts (100%) create mode 100644 packages/web-app-files/src/types/view.ts diff --git a/packages/web-app-files/src/helpers/resource/sameResource.ts b/packages/web-app-files/src/helpers/resource/sameResource.ts index 60b09cebeb1..ed9d660b4d5 100644 --- a/packages/web-app-files/src/helpers/resource/sameResource.ts +++ b/packages/web-app-files/src/helpers/resource/sameResource.ts @@ -1,4 +1,4 @@ -import { Resource } from './resource' +import { Resource } from '../../types/resource' export const isSameResource = (r1: Resource, r2: Resource): boolean => { if (!r1 || !r2) return false diff --git a/packages/web-app-files/src/index.js b/packages/web-app-files/src/index.js index 18934c48261..140a0a82508 100644 --- a/packages/web-app-files/src/index.js +++ b/packages/web-app-files/src/index.js @@ -3,10 +3,12 @@ import quickActionsImport from './quickActions' import store from './store' import { FilterSearch, SDKSearch } from './search' import { bus } from 'web-pkg/src/instance' -import { archiverService, Registry } from './services' +import { archiverService, listingService, Registry } from './services' import fileSideBars from './fileSideBars' import routes from './routes' import get from 'lodash-es/get' +import { PersonalLoader, FavoritesLoader } from './services/listing/loader' +import { View } from './types/view' // just a dummy function to trick gettext tools function $gettext(msg) { @@ -90,7 +92,7 @@ export default { navItems, quickActions, translations, - ready({ router: runtimeRouter, store: runtimeStore }) { + ready({ router: runtimeRouter, store: runtimeStore, sdk: runtimeSdk }) { Registry.filterSearch = new FilterSearch(runtimeStore, runtimeRouter) Registry.sdkSearch = new SDKSearch(runtimeStore, runtimeRouter) @@ -104,5 +106,8 @@ export default { runtimeStore.getters.configuration.server, get(runtimeStore, 'getters.capabilities.files.archivers', []) ) + listingService.initialize(runtimeStore, runtimeSdk) + listingService.registerResourceLoader(View.Personal, new PersonalLoader()) + listingService.registerResourceLoader(View.Favorites, new FavoritesLoader()) } } diff --git a/packages/web-app-files/src/services/index.ts b/packages/web-app-files/src/services/index.ts index e1639871435..8bad53db0f3 100644 --- a/packages/web-app-files/src/services/index.ts +++ b/packages/web-app-files/src/services/index.ts @@ -1,4 +1,5 @@ export * from './archiver' export * from './cache' export * from './client' +export * from './listing' export { default as Registry } from './registry' diff --git a/packages/web-app-files/src/services/listing/index.ts b/packages/web-app-files/src/services/listing/index.ts new file mode 100644 index 00000000000..4649b2b024f --- /dev/null +++ b/packages/web-app-files/src/services/listing/index.ts @@ -0,0 +1,2 @@ +export * from './listing' +export * from './loader/resourceLoader' diff --git a/packages/web-app-files/src/services/listing/listing.ts b/packages/web-app-files/src/services/listing/listing.ts new file mode 100644 index 00000000000..8cd86cba31a --- /dev/null +++ b/packages/web-app-files/src/services/listing/listing.ts @@ -0,0 +1,66 @@ +import { Store } from 'vuex' +import { View } from '../../types/view' +import { DavProperties, DavProperty } from 'web-pkg/src/constants' +import resource, { MutationType as ResourceMutationType } from '../../store/modules/resource' +import { + buildStoreModuleIdentifier, + MutationType as ListingMutationType +} from '../../store/modules/listing' +import OwnCloud from 'owncloud-sdk' +import { ResourceLoader } from './loader' +import { RuntimeError } from 'web-runtime/src/container/error' + +export class ListingService { + store: Store + sdk: OwnCloud + loaders: Map + + public initialize(store: Store, sdk: OwnCloud): void { + this.store = store + this.sdk = sdk + this.loaders = new Map() + } + + public registerResourceLoader(view: View, loader: ResourceLoader) { + this.loaders.set(view, loader) + } + + public async loadResources( + view: View, + path: string, + davProperties: DavProperty[] = DavProperties.Default + ): Promise { + if (!this.loaders.has(view)) { + throw new RuntimeError(`unknown view ${view} in listingService`) + } + + // init store module for the view & path + const identifier = buildStoreModuleIdentifier(view, path) + this.store.registerModule(identifier, resource) + this.store.mutations[`Files/${identifier}/${ResourceMutationType.SET_LOADING}`](true) + this.store.mutations[`Files/${identifier}/${ResourceMutationType.SET_PATH}`](path) + + // announce new view & path in `listing` store module + this.store.mutations[`Files/listing/${ListingMutationType.SET_ACTIVE_VIEW_AND_PATH}`]({ view, path }) + + // load resources + const resourceNode = await this.loaders + .get(view) + .loadResources(this.sdk, path, davProperties) + .catch(e => { + console.error(e) + // TODO: set error state in store? + this.store.mutations[`Files/${identifier}/${ResourceMutationType.SET_LOADING}`](false) + return Promise.reject(e) + }) + this.store.mutations[`Files/${identifier}/${ResourceMutationType.SET_PARENT}`](resourceNode.parent) + this.store.mutations[`Files/${identifier}/${ResourceMutationType.SET_CHILDREN}`]( + resourceNode.children + ) + + // exit `loading` state + this.store.mutations[`Files/${identifier}/${ResourceMutationType.SET_LOADING}`](false) + } +} + +export const listingService = new ListingService() diff --git a/packages/web-app-files/src/services/listing/loader/favoritesLoader.ts b/packages/web-app-files/src/services/listing/loader/favoritesLoader.ts new file mode 100644 index 00000000000..44d36fdcc7a --- /dev/null +++ b/packages/web-app-files/src/services/listing/loader/favoritesLoader.ts @@ -0,0 +1,19 @@ +import { ResourceLoader, ResourceNode } from './resourceLoader' +import { buildResource } from '../../../helpers/resources' +import OwnCloud from 'owncloud-sdk' +import { DavProperty } from 'web-pkg/src/constants' + +export class FavoritesLoader implements ResourceLoader { + async loadResources( + sdk: OwnCloud, + path: string, + davProperties: DavProperty[] + ): Promise { + let resources = await sdk.files.getFavoriteFiles(davProperties) + resources = resources.map(buildResource) + return { + parent: null, + children: resources + } + } +} diff --git a/packages/web-app-files/src/services/listing/loader/index.ts b/packages/web-app-files/src/services/listing/loader/index.ts new file mode 100644 index 00000000000..cebf974190c --- /dev/null +++ b/packages/web-app-files/src/services/listing/loader/index.ts @@ -0,0 +1,3 @@ +export * from './resourceLoader' +export * from './personalLoader' +export * from './favoritesLoader' diff --git a/packages/web-app-files/src/services/listing/loader/personalLoader.ts b/packages/web-app-files/src/services/listing/loader/personalLoader.ts new file mode 100644 index 00000000000..362d915cf70 --- /dev/null +++ b/packages/web-app-files/src/services/listing/loader/personalLoader.ts @@ -0,0 +1,20 @@ +import { ResourceLoader, ResourceNode } from './resourceLoader' +import { buildResource } from '../../../helpers/resources' +import OwnCloud from 'owncloud-sdk' +import { DavProperty } from 'web-pkg/src/constants' + +export class PersonalLoader implements ResourceLoader { + async loadResources( + sdk: OwnCloud, + path: string, + davProperties: DavProperty[] + ): Promise { + let resources = await sdk.files.list(path, 1, davProperties) + resources = resources.map(buildResource) + const currentFolder = resources.shift() + return { + parent: currentFolder, + children: resources + } + } +} diff --git a/packages/web-app-files/src/services/listing/loader/resourceLoader.ts b/packages/web-app-files/src/services/listing/loader/resourceLoader.ts new file mode 100644 index 00000000000..3ef665229dd --- /dev/null +++ b/packages/web-app-files/src/services/listing/loader/resourceLoader.ts @@ -0,0 +1,12 @@ +import { Resource } from '../../../types/resource' +import { DavProperty } from 'web-pkg/src/constants' +import OwnCloud from 'owncloud-sdk' + +export type ResourceNode = { + parent?: Resource + children: Resource[] +} + +export interface ResourceLoader { + loadResources(sdk: OwnCloud, path: string, davProperties: DavProperty[]): Promise +} diff --git a/packages/web-app-files/src/store/index.js b/packages/web-app-files/src/store/index.js index 107b2208575..6a5939cdf8f 100644 --- a/packages/web-app-files/src/store/index.js +++ b/packages/web-app-files/src/store/index.js @@ -2,8 +2,9 @@ import state from './state' import actions from './actions' import mutations from './mutations' import getters from './getters' -import sidebarModule from './modules/sidebar' +import listingModule from './modules/listing' import paginationModule from './modules/pagination' +import sidebarModule from './modules/sidebar' const namespaced = true export default { @@ -13,7 +14,8 @@ export default { actions, mutations, modules: { - sidebar: sidebarModule, - pagination: paginationModule + listing: listingModule, + pagination: paginationModule, + sidebar: sidebarModule } } diff --git a/packages/web-app-files/src/store/modules/listing.ts b/packages/web-app-files/src/store/modules/listing.ts new file mode 100644 index 00000000000..c0b15129de8 --- /dev/null +++ b/packages/web-app-files/src/store/modules/listing.ts @@ -0,0 +1,108 @@ +/** + * problem statement: + * our `files` array in the vuex store of the files app is only capable of representing one state. + * multiple PROPFINDs for different folders, fired at similar times, will override each other in that state. + * the longest running propfind wins. Cancelling the requests is not solving all edge cases, as the + * PROPFINDs might finish at similar times so that cancellation is not happening anymore. + * + * idea for a solution: + * loading resources in the different views should be converted into a two step process, each: + * 1) set the activeView and activePath immediately after each navigation (SET_ACTIVE_VIEW, SET_ACTIVE_PATH) + * 2) do the PROPFIND, convert the response into resources, and set them in a dedicated folder store module. + * That store module needs to get registered dynamically. The PROPFIND and conversion might be long running. + * It's really important that the activePath is set as early as possible! + * + * the views then render the files of the store module that matches the current `activePath`. + * as a bonus, requests could be cancelled so that we avoid unnecessary store module creation. + * + * reasons for dynamically registered store modules: we need the reactivity of the individual files. + * Which comes built in with the store modules. Another approach would be to build a cache of recent + * propfind results (= response converted into a file listing). but that's not reactive by default. + * + * TODO: think about when to unregister the folder store modules. they might be big so removal should be happening as soon as not needed anymore. + * TODO: further think about different views: propfind, fetch incoming shares, fetch outgoing shares, trashbin + * TODO: the different `loadResources` methods from the views should live in a service, which then also takes care of creating the respective store module. + */ +import { GetterTree, MutationTree } from 'vuex' +import crypto from 'crypto' +import { Resource } from '../../types/resource' +import { RuntimeError } from 'web-runtime/src/container/error' + +export const state = { + activeView: null, + activePath: null +} +export type State = typeof state + +export enum MutationType { + SET_ACTIVE_VIEW_AND_PATH = 'SET_ACTIVE_VIEW_AND_PATH' +} +export type Mutations = { + [MutationType.SET_ACTIVE_VIEW_AND_PATH]( + state: State, + params: { + activeView: string + activePath: string + } + ): void +} +export const mutations: MutationTree & Mutations = { + [MutationType.SET_ACTIVE_VIEW_AND_PATH]( + state: State, + params: { + activeView: string + activePath: string + } + ) { + state.activeView = params.activeView + state.activePath = params.activePath + } +} + +export type Getters = { + isLoading(state: State, getters: Getters, rootState: any): boolean + getCurrentFolder(state: State, getters: Getters, rootState: any, rootGetters: any): Resource + getFiles(state: State, getters: Getters, rootState: any, rootGetters: any): Resource[] +} + +export const getters: GetterTree & Getters = { + isLoading(state, getters, rootState): boolean { + const identifier = buildStoreModuleIdentifier(state.activeView, state.activePath) + if (!hasStoreModule(state.activeView, state.activePath, rootState)) { + return false + } + return rootState[`Files/${identifier}/loading`] + }, + getCurrentFolder(state, getters, rootState, rootGetters): Resource { + const identifier = buildStoreModuleIdentifier(state.activeView, state.activePath) + if (!hasStoreModule(state.activeView, state.activePath, rootState)) { + throw new RuntimeError('unknown resource') + } + return rootGetters[`Files/${identifier}/parent`] + }, + getFiles(state, getters, rootState, rootGetters): Resource[] { + const identifier = buildStoreModuleIdentifier(state.activeView, state.activePath) + if (!hasStoreModule(state.activeView, state.activePath, rootState)) { + throw new RuntimeError('unknown resource') + } + return rootGetters[`Files/${identifier}/children`] + } +} + +export const buildStoreModuleIdentifier = (activeView: string, activePath: string): string => { + const hash = crypto.createHash('sha256') + hash.update(activePath) + return `${activeView}_${hash.digest().toString('hex')}` +} + +const hasStoreModule = (activeView: string, activePath: string, rootState: any): boolean => { + const identifier = buildStoreModuleIdentifier(activeView, activePath) + return !!rootState[`Files/${identifier}`] +} + +export default { + namespaced: true, + state: () => state, + getters, + mutations +} diff --git a/packages/web-app-files/src/store/modules/resource.ts b/packages/web-app-files/src/store/modules/resource.ts new file mode 100644 index 00000000000..2bb18424f78 --- /dev/null +++ b/packages/web-app-files/src/store/modules/resource.ts @@ -0,0 +1,45 @@ +import { MutationTree } from 'vuex' +import { Resource } from '../../types/resource' + +export const state = { + loading: true, + path: '', + parent: null, + children: [] +} +export type State = typeof state + +export enum MutationType { + SET_LOADING = 'SET_LOADING', + SET_PATH = 'SET_PATH', + SET_PARENT = 'SET_PARENT', + SET_CHILDREN = 'SET_CHILDREN' +} + +export type Mutations = { + [MutationType.SET_LOADING](state: State, loading: boolean): void + [MutationType.SET_PATH](state: State, path: string): void + [MutationType.SET_PARENT](state: State, parent: Resource): void + [MutationType.SET_CHILDREN](state: State, children: Resource[]): void +} + +export const mutations: MutationTree & Mutations = { + [MutationType.SET_LOADING](state: State, loading: boolean): void { + state.loading = loading + }, + [MutationType.SET_PATH](state: State, path: string): void { + state.path = path + }, + [MutationType.SET_PARENT](state: State, parent: Resource): void { + state.parent = parent + }, + [MutationType.SET_CHILDREN](state: State, children: Resource[]): void { + state.children = children + } +} + +export default { + namespaced: true, + state: () => state, + mutations +} diff --git a/packages/web-app-files/src/helpers/resource/resource.ts b/packages/web-app-files/src/types/resource.ts similarity index 100% rename from packages/web-app-files/src/helpers/resource/resource.ts rename to packages/web-app-files/src/types/resource.ts diff --git a/packages/web-app-files/src/types/view.ts b/packages/web-app-files/src/types/view.ts new file mode 100644 index 00000000000..663d54d9935 --- /dev/null +++ b/packages/web-app-files/src/types/view.ts @@ -0,0 +1,9 @@ +export abstract class View { + static readonly Personal: string = 'personal' + static readonly Favorites: string = 'favorites' + static readonly SharedWithMe: string = 'shared-with-me' + static readonly SharedWithOthers: string = 'shared-with-others' + static readonly SharedViaLink: string = 'shared-via-link' + static readonly PublicFiles: string = 'public-files' + static readonly TrashBin: string = 'trash-bin' +} diff --git a/packages/web-app-files/src/views/Personal.vue b/packages/web-app-files/src/views/Personal.vue index 6bfd6c29ac7..db96832842d 100644 --- a/packages/web-app-files/src/views/Personal.vue +++ b/packages/web-app-files/src/views/Personal.vue @@ -1,6 +1,10 @@