From 3d918a59f38d48e4f4f1a76159087c835d395867 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Thu, 14 Mar 2024 14:08:46 +0100 Subject: [PATCH 1/2] feat: add proppatch to our webdav implementation --- .../web-client/src/webdav/client/builders.ts | 27 +++++++++++++++++++ packages/web-client/src/webdav/client/dav.ts | 11 +++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/web-client/src/webdav/client/builders.ts b/packages/web-client/src/webdav/client/builders.ts index c751ccf640a..bbc5b989108 100644 --- a/packages/web-client/src/webdav/client/builders.ts +++ b/packages/web-client/src/webdav/client/builders.ts @@ -35,6 +35,33 @@ export const buildPropFindBody = ( return builder.build(xmlObj) } +export const buildPropPatchBody = ( + properties: Partial> +): string => { + const props = Object.keys(properties).reduce>((acc, val) => { + const davNamespace = DavProperties.DavNamespace.includes(val as DavPropertyValue) + acc[davNamespace ? `d:${val}` : `oc:${val}`] = properties[val] + return acc + }, {}) + + const xmlObj = { + 'd:propertyupdate': { + 'd:set': { 'd:prop': props }, + '@@xmlns:d': 'DAV:', + '@@xmlns:oc': 'http://owncloud.org/ns' + } + } + + const builder = new XMLBuilder({ + format: true, + ignoreAttributes: false, + attributeNamePrefix: '@@', + suppressEmptyNode: true + }) + + return builder.build(xmlObj) +} + export const buildPublicLinkAuthHeader = (password: string) => { return 'Basic ' + Buffer.from('public:' + password).toString('base64') } diff --git a/packages/web-client/src/webdav/client/dav.ts b/packages/web-client/src/webdav/client/dav.ts index 43ef9be18f6..eb3ab1059b1 100644 --- a/packages/web-client/src/webdav/client/dav.ts +++ b/packages/web-client/src/webdav/client/dav.ts @@ -9,7 +9,7 @@ import { import { v4 as uuidV4 } from 'uuid' import { encodePath, urlJoin } from '../../utils' import { DavMethod, DavPropertyValue } from '../constants' -import { buildPropFindBody } from './builders' +import { buildPropFindBody, buildPropPatchBody } from './builders' import { parseError, parseMultiStatus, parseTusHeaders } from './parsers' import { WebDavResponseResource } from '../../helpers' import { HttpError } from '../../errors' @@ -146,6 +146,15 @@ export class DAV { return this.request(DavMethod.delete, path, { headers }) } + public propPatch( + path: string, + properties: Partial>, + { headers = {} }: { headers?: Headers } = {} + ) { + const body = buildPropPatchBody(properties) + return this.request(DavMethod.proppatch, path, { body, headers }) + } + public getFileUrl(path: string) { return urlJoin(this.davPath, encodePath(path)) } From 56b0a2cd341015b957a7db9c4420913aa4d172d9 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Thu, 14 Mar 2024 14:09:35 +0100 Subject: [PATCH 2/2] refactor: handle favorites via our webdav implementation instead of sdk --- .../src/services/folder/loaderFavorites.ts | 27 ++--------- .../web-client/src/webdav/client/builders.ts | 46 +++++++++++++------ packages/web-client/src/webdav/client/dav.ts | 9 ++-- packages/web-client/src/webdav/index.ts | 9 +++- .../src/webdav/listFavoriteFiles.ts | 19 ++++++++ packages/web-client/src/webdav/search.ts | 3 +- packages/web-client/src/webdav/setFavorite.ts | 17 +++++++ packages/web-client/src/webdav/types.ts | 4 ++ .../actions/files/useFileActionsFavorite.ts | 4 +- 9 files changed, 93 insertions(+), 45 deletions(-) create mode 100644 packages/web-client/src/webdav/listFavoriteFiles.ts create mode 100644 packages/web-client/src/webdav/setFavorite.ts diff --git a/packages/web-app-files/src/services/folder/loaderFavorites.ts b/packages/web-app-files/src/services/folder/loaderFavorites.ts index 38e010f3318..f46188ba66a 100644 --- a/packages/web-app-files/src/services/folder/loaderFavorites.ts +++ b/packages/web-app-files/src/services/folder/loaderFavorites.ts @@ -1,7 +1,6 @@ import { FolderLoader, FolderLoaderTask, TaskContext } from '../folder' import { Router } from 'vue-router' import { useTask } from 'vue-concurrency' -import { DavProperties } from '@ownclouders/web-client/src/webdav/constants' import { buildResource } from '@ownclouders/web-client/src/helpers' import { isLocationCommonActive } from '@ownclouders/web-pkg' @@ -15,36 +14,16 @@ export class FolderLoaderFavorites implements FolderLoader { } public getTask(context: TaskContext): FolderLoaderTask { - const { - userStore, - resourcesStore, - clientService: { owncloudSdk: client } - } = context + const { resourcesStore, clientService } = context // eslint-disable-next-line @typescript-eslint/no-unused-vars return useTask(function* (signal1, signal2) { resourcesStore.clearResourceList() resourcesStore.setAncestorMetaData({}) - // favorite implementation is going to change soon, so we are not implementing it - // as a proper factory in our new WebDAV client for now - const legacyPropertyNames = DavProperties.Default.map((propertyName) => { - const prefix = DavProperties.DavNamespace.includes(propertyName) - ? '{DAV:}' - : '{http://owncloud.org/ns}' + let resources = yield clientService.webdav.listFavoriteFiles() - return `${prefix}${propertyName}` - }) - - let resources = yield client.files.getFavoriteFiles(legacyPropertyNames) - resources = resources.map((f) => { - const resource = buildResource(f) - // info: in oc10 we have no storageId in resources. All resources are mounted into the personal space. - if (!resource.storageId) { - resource.storageId = userStore.user.onPremisesSamAccountName - } - return resource - }) + resources = resources.map(buildResource) resourcesStore.initResourceList({ currentFolder: null, resources }) resourcesStore.loadIndicators('/') }) diff --git a/packages/web-client/src/webdav/client/builders.ts b/packages/web-client/src/webdav/client/builders.ts index bbc5b989108..2d51e9e9944 100644 --- a/packages/web-client/src/webdav/client/builders.ts +++ b/packages/web-client/src/webdav/client/builders.ts @@ -3,16 +3,37 @@ import { DavProperties, DavPropertyValue } from '../constants' import { SpaceResource, isPublicSpaceResource } from '../../helpers' import { Headers } from 'webdav' +const getNamespacedDavProps = (obj: Partial>) => { + return Object.keys(obj).reduce>((acc, val) => { + const davNamespace = DavProperties.DavNamespace.includes(val as DavPropertyValue) + acc[davNamespace ? `d:${val}` : `oc:${val}`] = obj[val] || '' + return acc + }, {}) +} + export const buildPropFindBody = ( properties: DavPropertyValue[] = [], - { pattern, limit = 0 }: { pattern?: string; limit?: number } = {} + { + pattern, + filterRules, + limit = 0 + }: { + pattern?: string + filterRules?: Partial> + limit?: number + } = {} ): string => { - const bodyType = pattern ? 'oc:search-files' : 'd:propfind' - const props = properties.reduce>((acc, val) => { - const davNamespace = DavProperties.DavNamespace.includes(val) - acc[davNamespace ? `d:${val}` : `oc:${val}`] = '' - return acc - }, {}) + let bodyType = 'd:propfind' + if (pattern) { + bodyType = 'oc:search-files' + } + + if (filterRules) { + bodyType = 'oc:filter-files' + } + + const object = properties.reduce((obj, item) => Object.assign(obj, { [item]: null }), {}) + const props = getNamespacedDavProps(object) const xmlObj = { [bodyType]: { @@ -21,6 +42,9 @@ export const buildPropFindBody = ( '@@xmlns:oc': 'http://owncloud.org/ns', ...(pattern && { 'oc:search': { 'oc:pattern': pattern, 'oc:limit': limit } + }), + ...(filterRules && { + 'oc:filter-rules': getNamespacedDavProps(filterRules) }) } } @@ -38,15 +62,9 @@ export const buildPropFindBody = ( export const buildPropPatchBody = ( properties: Partial> ): string => { - const props = Object.keys(properties).reduce>((acc, val) => { - const davNamespace = DavProperties.DavNamespace.includes(val as DavPropertyValue) - acc[davNamespace ? `d:${val}` : `oc:${val}`] = properties[val] - return acc - }, {}) - const xmlObj = { 'd:propertyupdate': { - 'd:set': { 'd:prop': props }, + 'd:set': { 'd:prop': getNamespacedDavProps(properties) }, '@@xmlns:d': 'DAV:', '@@xmlns:oc': 'http://owncloud.org/ns' } diff --git a/packages/web-client/src/webdav/client/dav.ts b/packages/web-client/src/webdav/client/dav.ts index eb3ab1059b1..d50950d1190 100644 --- a/packages/web-client/src/webdav/client/dav.ts +++ b/packages/web-client/src/webdav/client/dav.ts @@ -65,21 +65,24 @@ export class DAV { return body } - public async search( - pattern: string, + public async report( path: string, { + pattern = '', + filterRules = null, limit = 30, properties, headers = {} }: { + pattern?: string + filterRules?: Partial> limit?: number properties?: DavPropertyValue[] headers?: Headers } = {} ) { const { body, result } = await this.request(DavMethod.report, path, { - body: buildPropFindBody(properties, { pattern, limit }), + body: buildPropFindBody(properties, { pattern, filterRules, limit }), headers }) diff --git a/packages/web-client/src/webdav/index.ts b/packages/web-client/src/webdav/index.ts index ef069416165..b6c280e9f7c 100644 --- a/packages/web-client/src/webdav/index.ts +++ b/packages/web-client/src/webdav/index.ts @@ -17,6 +17,8 @@ import { GetPathForFileIdFactory } from './getPathForFileId' import { DAV } from './client/dav' import { ListFileVersionsFactory } from './listFileVersions' import { ListFilesByIdFactory } from './listFilesById' +import { SetFavoriteFactory } from './setFavorite' +import { ListFavoriteFilesFactory } from './listFavoriteFiles' export * from './constants' export * from './types' @@ -61,6 +63,9 @@ export const webdav = (options: WebDavOptions): WebDAV => { const { search } = SearchFactory(dav, options) + const { listFavoriteFiles } = ListFavoriteFilesFactory(dav, options) + const { setFavorite } = SetFavoriteFactory(dav, options) + return { copyFiles, createFolder, @@ -79,6 +84,8 @@ export const webdav = (options: WebDavOptions): WebDAV => { putFileContents, revokeUrl, clearTrashBin, - search + search, + listFavoriteFiles, + setFavorite } } diff --git a/packages/web-client/src/webdav/listFavoriteFiles.ts b/packages/web-client/src/webdav/listFavoriteFiles.ts new file mode 100644 index 00000000000..7bdb472c34b --- /dev/null +++ b/packages/web-client/src/webdav/listFavoriteFiles.ts @@ -0,0 +1,19 @@ +import { urlJoin } from '../utils' +import { WebDavOptions } from './types' +import { DAV, buildAuthHeader } from './client' +import { unref } from 'vue' +import { DavProperties } from './constants' + +export const ListFavoriteFilesFactory = (dav: DAV, { accessToken, user }: WebDavOptions) => { + return { + listFavoriteFiles({ davProperties = DavProperties.Default } = {}) { + const headers = buildAuthHeader(unref(accessToken)) + + return dav.report(urlJoin('files', unref(user).onPremisesSamAccountName), { + headers, + properties: davProperties, + filterRules: { favorite: 1 } + }) + } + } +} diff --git a/packages/web-client/src/webdav/search.ts b/packages/web-client/src/webdav/search.ts index d64fbfdc8e1..cdec83ae9ac 100644 --- a/packages/web-client/src/webdav/search.ts +++ b/packages/web-client/src/webdav/search.ts @@ -26,7 +26,8 @@ export const SearchFactory = (dav: DAV, { accessToken }: WebDavOptions) => { ): Promise { const path = '/spaces/' const headers = buildAuthHeader(unref(accessToken)) - const { range, results } = await dav.search(term, path, { + const { range, results } = await dav.report(path, { + pattern: term, limit: searchLimit, properties: davProperties, headers diff --git a/packages/web-client/src/webdav/setFavorite.ts b/packages/web-client/src/webdav/setFavorite.ts new file mode 100644 index 00000000000..e858c78808b --- /dev/null +++ b/packages/web-client/src/webdav/setFavorite.ts @@ -0,0 +1,17 @@ +import { urlJoin } from '../utils' +import { SpaceResource } from '../helpers' +import { WebDavOptions } from './types' +import { DAV, buildAuthHeader } from './client' +import { unref } from 'vue' +import { DavProperty } from './constants' + +export const SetFavoriteFactory = (dav: DAV, { accessToken }: WebDavOptions) => { + return { + setFavorite(space: SpaceResource, { path }: { path: string }, value: unknown) { + const headers = buildAuthHeader(unref(accessToken), space) + const properties = { [DavProperty.IsFavorite]: value ? 'true' : 'false' } + + return dav.propPatch(urlJoin(space.webDavPath, path), properties, { headers }) + } + } +} diff --git a/packages/web-client/src/webdav/types.ts b/packages/web-client/src/webdav/types.ts index 89a6ce024bc..8d36695b81b 100644 --- a/packages/web-client/src/webdav/types.ts +++ b/packages/web-client/src/webdav/types.ts @@ -19,6 +19,8 @@ import { Capabilities } from '../ocs' import { Ref } from 'vue' import { ListFilesByIdFactory } from './listFilesById' import { User } from '../generated' +import { SetFavoriteFactory } from './setFavorite' +import { ListFavoriteFilesFactory } from './listFavoriteFiles' export interface WebDavOptions { sdk: OwnCloudSdk @@ -49,4 +51,6 @@ export interface WebDAV { restoreFileVersion: ReturnType['restoreFileVersion'] clearTrashBin: ReturnType['clearTrashBin'] search: ReturnType['search'] + listFavoriteFiles: ReturnType['listFavoriteFiles'] + setFavorite: ReturnType['setFavorite'] } diff --git a/packages/web-pkg/src/composables/actions/files/useFileActionsFavorite.ts b/packages/web-pkg/src/composables/actions/files/useFileActionsFavorite.ts index 6bbce4e101e..6d26cc4babe 100644 --- a/packages/web-pkg/src/composables/actions/files/useFileActionsFavorite.ts +++ b/packages/web-pkg/src/composables/actions/files/useFileActionsFavorite.ts @@ -19,10 +19,10 @@ export const useFileActionsFavorite = () => { const resourcesStore = useResourcesStore() const eventBus = useEventBus() - const handler = async ({ resources }: FileActionOptions) => { + const handler = async ({ space, resources }: FileActionOptions) => { try { const newValue = !resources[0].starred - await clientService.owncloudSdk.files.favorite(resources[0].webDavPath, newValue) + await clientService.webdav.setFavorite(space, resources[0], newValue) resourcesStore.updateResourceField({ id: resources[0].id, field: 'starred', value: newValue }) if (!newValue) {