aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/src')
-rw-r--r--apps/files/src/services/Favorites.ts66
-rw-r--r--apps/files/src/services/Files.ts72
-rw-r--r--apps/files/src/services/PersonalFiles.ts45
-rw-r--r--apps/files/src/services/Recent.ts62
-rw-r--r--apps/files/src/services/WebdavClient.ts46
-rw-r--r--apps/files/src/views/favorites.spec.ts9
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')