diff options
Diffstat (limited to 'apps/files/src')
-rw-r--r-- | apps/files/src/services/Favorites.ts | 66 | ||||
-rw-r--r-- | apps/files/src/services/Files.ts | 72 | ||||
-rw-r--r-- | apps/files/src/services/PersonalFiles.ts | 45 | ||||
-rw-r--r-- | apps/files/src/services/Recent.ts | 62 | ||||
-rw-r--r-- | apps/files/src/services/WebdavClient.ts | 46 | ||||
-rw-r--r-- | apps/files/src/views/favorites.spec.ts | 9 |
6 files changed, 108 insertions, 192 deletions
diff --git a/apps/files/src/services/Favorites.ts b/apps/files/src/services/Favorites.ts index 23f93751135..e156c92c511 100644 --- a/apps/files/src/services/Favorites.ts +++ b/apps/files/src/services/Favorites.ts @@ -3,44 +3,38 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import type { ContentsWithRoot } from '@nextcloud/files' -import type { FileStat, ResponseDataDetailed } from 'webdav' -import { Folder, davGetDefaultPropfind, davGetFavoritesReport } from '@nextcloud/files' +import { getCurrentUser } from '@nextcloud/auth' +import { Folder, Permission, davRemoteURL, davRootPath, getFavoriteNodes } from '@nextcloud/files' +import { CancelablePromise } from 'cancelable-promise' +import { getContents as filesContents } from './Files.ts' +import { client } from './WebdavClient.ts' -import { getClient } from './WebdavClient' -import { resultToNode } from './Files' - -const client = getClient() - -export const getContents = async (path = '/'): Promise<ContentsWithRoot> => { - const propfindPayload = davGetDefaultPropfind() - const reportPayload = davGetFavoritesReport() - - // Get root folder - let rootResponse - if (path === '/') { - rootResponse = await client.stat(path, { - details: true, - data: propfindPayload, - }) as ResponseDataDetailed<FileStat> +export const getContents = (path = '/'): CancelablePromise<ContentsWithRoot> => { + // We only filter root files for favorites, for subfolders we can simply reuse the files contents + if (path !== '/') { + return filesContents(path) } - const contentsResponse = await client.getDirectoryContents(path, { - details: true, - // Only filter favorites if we're at the root - data: path === '/' ? reportPayload : propfindPayload, - headers: { - // Patched in WebdavClient.ts - method: path === '/' ? 'REPORT' : 'PROPFIND', - }, - includeSelf: true, - }) as ResponseDataDetailed<FileStat[]> - - const root = rootResponse?.data || contentsResponse.data[0] - const contents = contentsResponse.data.filter(node => node.filename !== path) - - return { - folder: resultToNode(root) as Folder, - contents: contents.map(resultToNode), - } + return new CancelablePromise((resolve, reject, cancel) => { + const promise = getFavoriteNodes(client) + .catch(reject) + .then((contents) => { + if (!contents) { + reject() + return + } + resolve({ + contents, + folder: new Folder({ + id: 0, + source: `${davRemoteURL}${davRootPath}`, + root: davRootPath, + owner: getCurrentUser()?.uid || null, + permissions: Permission.READ, + }), + }) + }) + cancel(() => promise.cancel()) + }) } diff --git a/apps/files/src/services/Files.ts b/apps/files/src/services/Files.ts index 1fcd9d7fee1..dc83f16187b 100644 --- a/apps/files/src/services/Files.ts +++ b/apps/files/src/services/Files.ts @@ -3,68 +3,25 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import type { ContentsWithRoot } from '@nextcloud/files' -import type { FileStat, ResponseDataDetailed, DAVResultResponseProps } from 'webdav' +import type { FileStat, ResponseDataDetailed } from 'webdav' import { CancelablePromise } from 'cancelable-promise' -import { File, Folder, davParsePermissions, davGetDefaultPropfind } from '@nextcloud/files' -import { generateRemoteUrl } from '@nextcloud/router' -import { getCurrentUser } from '@nextcloud/auth' +import { File, Folder, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files' +import { client } from './WebdavClient.ts' +import logger from '../logger.js' -import { getClient, rootPath } from './WebdavClient' -import { hashCode } from '../utils/hashUtils' -import logger from '../logger' - -const client = getClient() - -interface ResponseProps extends DAVResultResponseProps { - permissions: string, - fileid: number, - size: number, -} - -export const resultToNode = function(node: FileStat): File | Folder { - const userId = getCurrentUser()?.uid - if (!userId) { - throw new Error('No user id found') - } - - const props = node.props as ResponseProps - const permissions = davParsePermissions(props?.permissions) - const owner = (props['owner-id'] || userId).toString() - - const source = generateRemoteUrl('dav' + rootPath + node.filename) - const id = props?.fileid < 0 - ? hashCode(source) - : props?.fileid as number || 0 - - const nodeData = { - id, - source, - mtime: new Date(node.lastmod), - mime: node.mime || 'application/octet-stream', - size: props?.size as number || 0, - permissions, - owner, - root: rootPath, - attributes: { - ...node, - ...props, - hasPreview: props?.['has-preview'], - failed: props?.fileid < 0, - }, - } - - delete nodeData.attributes.props - - return node.type === 'file' - ? new File(nodeData) - : new Folder(nodeData) -} +/** + * Slim wrapper over `@nextcloud/files` `davResultToNode` to allow using the function with `Array.map` + * @param node The node returned by the webdav library + */ +export const resultToNode = (node: FileStat): File | Folder => davResultToNode(node) -export const getContents = (path = '/'): Promise<ContentsWithRoot> => { +export const getContents = (path = '/'): CancelablePromise<ContentsWithRoot> => { const controller = new AbortController() const propfindPayload = davGetDefaultPropfind() + path = `${davRootPath}${path}` + return new CancelablePromise(async (resolve, reject, onCancel) => { onCancel(() => controller.abort()) try { @@ -77,13 +34,14 @@ export const getContents = (path = '/'): Promise<ContentsWithRoot> => { const root = contentsResponse.data[0] const contents = contentsResponse.data.slice(1) - if (root.filename !== path) { + if (root.filename !== path && `${root.filename}/` !== path) { + logger.debug(`Exepected "${path}" but got filename "${root.filename}" instead.`) throw new Error('Root node does not match requested path') } resolve({ folder: resultToNode(root) as Folder, - contents: contents.map(result => { + contents: contents.map((result) => { try { return resultToNode(result) } catch (error) { diff --git a/apps/files/src/services/PersonalFiles.ts b/apps/files/src/services/PersonalFiles.ts index 3d6ef7c7430..6d86bd3bae2 100644 --- a/apps/files/src/services/PersonalFiles.ts +++ b/apps/files/src/services/PersonalFiles.ts @@ -2,39 +2,38 @@ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { File, type ContentsWithRoot } from '@nextcloud/files' -import { getCurrentUser } from '@nextcloud/auth'; +import type { Node, ContentsWithRoot } from '@nextcloud/files' +import type { CancelablePromise } from 'cancelable-promise' +import { getCurrentUser } from '@nextcloud/auth' -import { getContents as getFiles } from './Files'; +import { getContents as getFiles } from './Files' -const currUserID = getCurrentUser()?.uid +const currentUserId = getCurrentUser()?.uid /** - * NOTE MOVE TO @nextcloud/files - * @brief filters each file/folder on its shared status - * A personal file is considered a file that has all of the following properties: - * a.) the current user owns - * b.) the file is not shared with anyone - * c.) the file is not a group folder - * @param {FileStat} node that contains - * @return {Boolean} + * Filters each file/folder on its shared status + * + * A personal file is considered a file that has all of the following properties: + * 1. the current user owns + * 2. the file is not shared with anyone + * 3. the file is not a group folder + * @todo Move to `@nextcloud/files` + * @param node The node to check */ -export const isPersonalFile = function(node: File): Boolean { +export const isPersonalFile = function(node: Node): boolean { // the type of mounts that determine whether the file is shared - const sharedMountTypes = ["group", "shared"] + const sharedMountTypes = ['group', 'shared'] const mountType = node.attributes['mount-type'] - // the check to determine whether the current logged in user is the owner / creator of the node - const currUserCreated = currUserID ? node.owner === currUserID : true - return currUserCreated && !sharedMountTypes.includes(mountType) + return currentUserId === node.owner && !sharedMountTypes.includes(mountType) } -export const getContents = (path: string = "/"): Promise<ContentsWithRoot> => { +export const getContents = (path: string = '/'): CancelablePromise<ContentsWithRoot> => { // get all the files from the current path as a cancellable promise // then filter the files that the user does not own, or has shared / is a group folder - return getFiles(path) - .then(c => { - c.contents = c.contents.filter(isPersonalFile) as File[] - return c + return getFiles(path) + .then((content) => { + content.contents = content.contents.filter(isPersonalFile) + return content }) -}
\ No newline at end of file +} diff --git a/apps/files/src/services/Recent.ts b/apps/files/src/services/Recent.ts index 68e66ed121c..c8cde136069 100644 --- a/apps/files/src/services/Recent.ts +++ b/apps/files/src/services/Recent.ts @@ -3,14 +3,15 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import type { ContentsWithRoot, Node } from '@nextcloud/files' -import type { FileStat, ResponseDataDetailed } from 'webdav' +import type { ResponseDataDetailed, SearchResult } from 'webdav' import { getCurrentUser } from '@nextcloud/auth' -import { Folder, Permission, davGetRecentSearch, davGetClient, davResultToNode, davRootPath, davRemoteURL } from '@nextcloud/files' +import { Folder, Permission, davGetRecentSearch, davRootPath, davRemoteURL } from '@nextcloud/files' +import { CancelablePromise } from 'cancelable-promise' import { useUserConfigStore } from '../store/userconfig.ts' import { pinia } from '../store/index.ts' - -const client = davGetClient() +import { client } from './WebdavClient.ts' +import { resultToNode } from './Files.ts' const lastTwoWeeksTimestamp = Math.round((Date.now() / 1000) - (60 * 60 * 24 * 14)) @@ -22,8 +23,9 @@ const lastTwoWeeksTimestamp = Math.round((Date.now() / 1000) - (60 * 60 * 24 * 1 * * @param path Path to search for recent changes */ -export const getContents = async (path = '/'): Promise<ContentsWithRoot> => { +export const getContents = (path = '/'): CancelablePromise<ContentsWithRoot> => { const store = useUserConfigStore(pinia) + /** * Filter function that returns only the visible nodes - or hidden if explicitly configured * @param node The node to check @@ -33,28 +35,32 @@ export const getContents = async (path = '/'): Promise<ContentsWithRoot> => { || store.userConfig.show_hidden // If configured to show hidden files we can early return || !node.dirname.split('/').some((dir) => dir.startsWith('.')) // otherwise only include the file if non of the parent directories is hidden - const contentsResponse = await client.getDirectoryContents(path, { - details: true, - data: davGetRecentSearch(lastTwoWeeksTimestamp), - headers: { - // Patched in WebdavClient.ts - method: 'SEARCH', - // Somehow it's needed to get the correct response - 'Content-Type': 'application/xml; charset=utf-8', - }, - deep: true, - }) as ResponseDataDetailed<FileStat[]> - - const contents = contentsResponse.data - - return { - folder: new Folder({ - id: 0, - source: `${davRemoteURL}${davRootPath}`, - root: davRootPath, - owner: getCurrentUser()?.uid || null, - permissions: Permission.READ, - }), - contents: contents.map((r) => davResultToNode(r)).filter(filterHidden), + const controller = new AbortController() + const handler = async () => { + const contentsResponse = await client.search('/', { + signal: controller.signal, + details: true, + data: davGetRecentSearch(lastTwoWeeksTimestamp), + }) as ResponseDataDetailed<SearchResult> + + const contents = contentsResponse.data.results + .map(resultToNode) + .filter(filterHidden) + + return { + folder: new Folder({ + id: 0, + source: `${davRemoteURL}${davRootPath}`, + root: davRootPath, + owner: getCurrentUser()?.uid || null, + permissions: Permission.READ, + }), + contents, + } } + + return new CancelablePromise(async (resolve, reject, cancel) => { + cancel(() => controller.abort()) + resolve(handler()) + }) } diff --git a/apps/files/src/services/WebdavClient.ts b/apps/files/src/services/WebdavClient.ts index 506f3f1e07e..5563508e2c7 100644 --- a/apps/files/src/services/WebdavClient.ts +++ b/apps/files/src/services/WebdavClient.ts @@ -2,48 +2,6 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ +import { davGetClient } from '@nextcloud/files' -import { createClient, getPatcher } from 'webdav' -import { generateRemoteUrl } from '@nextcloud/router' -import { getCurrentUser, getRequestToken, onRequestTokenUpdate } from '@nextcloud/auth' - -export const rootPath = `/files/${getCurrentUser()?.uid}` -export const defaultRootUrl = generateRemoteUrl('dav' + rootPath) - -export const getClient = (rootUrl = defaultRootUrl) => { - const client = createClient(rootUrl) - - // set CSRF token header - const setHeaders = (token: string | null) => { - client?.setHeaders({ - // Add this so the server knows it is an request from the browser - 'X-Requested-With': 'XMLHttpRequest', - // Inject user auth - requesttoken: token ?? '', - }); - } - - // refresh headers when request token changes - onRequestTokenUpdate(setHeaders) - setHeaders(getRequestToken()) - - /** - * Allow to override the METHOD to support dav REPORT - * - * @see https://github.com/perry-mitchell/webdav-client/blob/8d9694613c978ce7404e26a401c39a41f125f87f/source/request.ts - */ - const patcher = getPatcher() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // https://github.com/perry-mitchell/hot-patcher/issues/6 - patcher.patch('fetch', (url: string, options: RequestInit): Promise<Response> => { - const headers = options.headers as Record<string, string> - if (headers?.method) { - options.method = headers.method - delete headers.method - } - return fetch(url, options) - }) - - return client; -} +export const client = davGetClient() diff --git a/apps/files/src/views/favorites.spec.ts b/apps/files/src/views/favorites.spec.ts index 58e60b2ee06..7dbb0dbc551 100644 --- a/apps/files/src/views/favorites.spec.ts +++ b/apps/files/src/views/favorites.spec.ts @@ -6,6 +6,7 @@ import { basename } from 'path' import { expect } from '@jest/globals' import { Folder, Navigation, getNavigation } from '@nextcloud/files' +import { CancelablePromise } from 'cancelable-promise' import eventBus from '@nextcloud/event-bus' import * as initialState from '@nextcloud/initial-state' @@ -40,7 +41,7 @@ describe('Favorites view definition', () => { test('Default empty favorite view', () => { jest.spyOn(eventBus, 'subscribe') - jest.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as Folder, contents: [] })) + jest.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as Folder, contents: [] })) registerFavoritesView() const favoritesView = Navigation.views.find(view => view.id === 'favorites') @@ -71,7 +72,7 @@ describe('Favorites view definition', () => { { fileid: 3, path: '/foo/bar' }, ] jest.spyOn(initialState, 'loadState').mockReturnValue(favoriteFolders) - jest.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as Folder, contents: [] })) + jest.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as Folder, contents: [] })) registerFavoritesView() const favoritesView = Navigation.views.find(view => view.id === 'favorites') @@ -114,7 +115,7 @@ describe('Dynamic update of favourite folders', () => { test('Add a favorite folder creates a new entry in the navigation', async () => { jest.spyOn(eventBus, 'emit') jest.spyOn(initialState, 'loadState').mockReturnValue([]) - jest.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as Folder, contents: [] })) + jest.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as Folder, contents: [] })) registerFavoritesView() const favoritesView = Navigation.views.find(view => view.id === 'favorites') @@ -143,7 +144,7 @@ describe('Dynamic update of favourite folders', () => { jest.spyOn(eventBus, 'emit') jest.spyOn(eventBus, 'subscribe') jest.spyOn(initialState, 'loadState').mockReturnValue([{ fileid: 42, path: '/Foo/Bar' }]) - jest.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as Folder, contents: [] })) + jest.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as Folder, contents: [] })) registerFavoritesView() let favoritesView = Navigation.views.find(view => view.id === 'favorites') |