Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/files/src/composables/useNavigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('Composables: useNavigation', () => {
it('should return already active navigation', async () => {
const view = new nextcloudFiles.View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 })
navigation.register(view)
navigation.setActive(view)
navigation.setActive(view.id)
// Now the navigation is already set it should take the active navigation
const wrapper = mount(TestComponent)
expect((wrapper.vm as unknown as { currentView: View | null }).currentView).toBe(view)
Expand All @@ -55,7 +55,7 @@ describe('Composables: useNavigation', () => {
// no active navigation
expect((wrapper.vm as unknown as { currentView: View | null }).currentView).toBe(null)

navigation.setActive(view)
navigation.setActive(view.id)
// Now the navigation is set it should take the active navigation
expect((wrapper.vm as unknown as { currentView: View | null }).currentView).toBe(view)
})
Expand Down
58 changes: 31 additions & 27 deletions apps/files/src/services/Favorites.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,49 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { ContentsWithRoot } from '@nextcloud/files'

import { getCurrentUser } from '@nextcloud/auth'
import { Folder, Permission } from '@nextcloud/files'
import { getFavoriteNodes, getRemoteURL, getRootPath } from '@nextcloud/files/dav'
import { CancelablePromise } from 'cancelable-promise'
import logger from '../logger.ts'
import { getContents as filesContents } from './Files.ts'
import { client } from './WebdavClient.ts'

/**
* Get the contents for the favorites view
*
* @param path
* @param path - The path to get the contents for
* @param options - Additional options
* @param options.signal - Optional AbortSignal to cancel the request
* @return A promise resolving to the contents with root folder
*/
export function getContents(path = '/'): CancelablePromise<ContentsWithRoot> {
export async function getContents(path = '/', options: { signal: AbortSignal }): Promise<ContentsWithRoot> {
// We only filter root files for favorites, for subfolders we can simply reuse the files contents
if (path !== '/') {
return filesContents(path)
if (path && path !== '/') {
return filesContents(path, options)
}

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: `${getRemoteURL()}${getRootPath()}`,
root: getRootPath(),
owner: getCurrentUser()?.uid || null,
permissions: Permission.READ,
}),
})
})
cancel(() => promise.cancel())
})
try {
const contents = await getFavoriteNodes({ client, signal: options.signal })
return {
contents,
folder: new Folder({
id: 0,
source: `${getRemoteURL()}${getRootPath()}`,
root: getRootPath(),
owner: getCurrentUser()?.uid || null,
permissions: Permission.READ,
}),
}
} catch (error) {
if (options.signal.aborted) {
logger.debug('Favorite nodes request was aborted')
throw new DOMException('Aborted', 'AbortError')
}
logger.error('Failed to load favorite nodes via WebDAV', { error })
throw error
}
}
78 changes: 33 additions & 45 deletions apps/files/src/services/Files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import type { ContentsWithRoot, File, Folder } from '@nextcloud/files'
import type { FileStat, ResponseDataDetailed } from 'webdav'

import { getDefaultPropfind, getRootPath, resultToNode } from '@nextcloud/files/dav'
import { CancelablePromise } from 'cancelable-promise'
import { join } from 'path'
import logger from '../logger.ts'
import { useFilesStore } from '../store/files.ts'
Expand All @@ -20,66 +19,55 @@ import { searchNodes } from './WebDavSearch.ts'
* This also allows to fetch local search results when the user is currently filtering.
*
* @param path - The path to query
* @param options - Options
* @param options.signal - Abort signal to cancel the request
*/
export function getContents(path = '/'): CancelablePromise<ContentsWithRoot> {
const controller = new AbortController()
export async function getContents(path = '/', options?: { signal: AbortSignal }): Promise<ContentsWithRoot> {
const searchStore = useSearchStore(getPinia())

if (searchStore.query.length >= 3) {
return new CancelablePromise((resolve, reject, cancel) => {
cancel(() => controller.abort())
getLocalSearch(path, searchStore.query, controller.signal)
.then(resolve)
.catch(reject)
})
} else {
return defaultGetContents(path)
if (searchStore.query.length < 3) {
return await defaultGetContents(path, options)
}

return await getLocalSearch(path, searchStore.query, options?.signal)
}

/**
* Generic `getContents` implementation for the users files.
*
* @param path - The path to get the contents
* @param options - Options
* @param options.signal - Abort signal to cancel the request
*/
export function defaultGetContents(path: string): CancelablePromise<ContentsWithRoot> {
export async function defaultGetContents(path: string, options?: { signal: AbortSignal }): Promise<ContentsWithRoot> {
path = join(getRootPath(), path)
const controller = new AbortController()
const propfindPayload = getDefaultPropfind()

return new CancelablePromise(async (resolve, reject, onCancel) => {
onCancel(() => controller.abort())
const contentsResponse = await client.getDirectoryContents(path, {
details: true,
data: propfindPayload,
includeSelf: true,
signal: options?.signal,
}) as ResponseDataDetailed<FileStat[]>

try {
const contentsResponse = await client.getDirectoryContents(path, {
details: true,
data: propfindPayload,
includeSelf: true,
signal: controller.signal,
}) as ResponseDataDetailed<FileStat[]>
const root = contentsResponse.data[0]!
const contents = contentsResponse.data.slice(1)
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')
}

const root = contentsResponse.data[0]
const contents = contentsResponse.data.slice(1)
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')
return {
folder: resultToNode(root) as Folder,
contents: contents.map((result) => {
try {
return resultToNode(result)
} catch (error) {
logger.error(`Invalid node detected '${result.basename}'`, { error })
return null
}

resolve({
folder: resultToNode(root) as Folder,
contents: contents.map((result) => {
try {
return resultToNode(result)
} catch (error) {
logger.error(`Invalid node detected '${result.basename}'`, { error })
return null
}
}).filter(Boolean) as File[],
})
} catch (error) {
reject(error)
}
})
}).filter(Boolean) as File[],
}
}

/**
Expand All @@ -89,7 +77,7 @@ export function defaultGetContents(path: string): CancelablePromise<ContentsWith
* @param query - The current search query
* @param signal - The aboort signal
*/
async function getLocalSearch(path: string, query: string, signal: AbortSignal): Promise<ContentsWithRoot> {
async function getLocalSearch(path: string, query: string, signal?: AbortSignal): Promise<ContentsWithRoot> {
const filesStore = useFilesStore(getPinia())
let folder = filesStore.getDirectoryByPath('files', path)
if (!folder) {
Expand Down
21 changes: 12 additions & 9 deletions apps/files/src/services/FolderTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*/

import type { ContentsWithRoot } from '@nextcloud/files'
import type { CancelablePromise } from 'cancelable-promise'

import { getCurrentUser } from '@nextcloud/auth'
import axios from '@nextcloud/axios'
Expand Down Expand Up @@ -47,10 +46,11 @@ const collator = Intl.Collator(
const compareNodes = (a: TreeNodeData, b: TreeNodeData) => collator.compare(a.displayName ?? a.basename, b.displayName ?? b.basename)

/**
* Get all tree nodes recursively
*
* @param tree
* @param currentPath
* @param nodes
* @param tree - The tree to process
* @param currentPath - The current path
* @param nodes - The nodes collected so far
*/
function getTreeNodes(tree: Tree, currentPath: string = '/', nodes: TreeNode[] = []): TreeNode[] {
const sortedTree = tree.toSorted(compareNodes)
Expand All @@ -76,9 +76,10 @@ function getTreeNodes(tree: Tree, currentPath: string = '/', nodes: TreeNode[] =
}

/**
* Get folder tree nodes
*
* @param path
* @param depth
* @param path - The path to get the tree from
* @param depth - The depth to fetch
*/
export async function getFolderTreeNodes(path: string = '/', depth: number = 1): Promise<TreeNode[]> {
const { data: tree } = await axios.get<Tree>(generateOcsUrl('/apps/files/api/v1/folder-tree'), {
Expand All @@ -88,20 +89,22 @@ export async function getFolderTreeNodes(path: string = '/', depth: number = 1):
return nodes
}

export const getContents = (path: string): CancelablePromise<ContentsWithRoot> => getFiles(path)
export const getContents = (path: string, options: { signal: AbortSignal }): Promise<ContentsWithRoot> => getFiles(path, options)

/**
* Encode source URL
*
* @param source
* @param source - The source URL
*/
export function encodeSource(source: string): string {
const { origin } = new URL(source)
return origin + encodePath(source.slice(origin.length))
}

/**
* Get parent source URL
*
* @param source
* @param source - The source URL
*/
export function getSourceParent(source: string): string {
const parent = dirname(source)
Expand Down
11 changes: 7 additions & 4 deletions apps/files/src/services/PersonalFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*/

import type { ContentsWithRoot, Node } from '@nextcloud/files'
import type { CancelablePromise } from 'cancelable-promise'

import { getCurrentUser } from '@nextcloud/auth'
import { getContents as getFiles } from './Files.ts'
Expand All @@ -31,13 +30,17 @@ export function isPersonalFile(node: Node): boolean {
}

/**
* Get personal files from a given path
*
* @param path
* @param path - The path to get the personal files from
* @param options - Options
* @param options.signal - Abort signal to cancel the request
* @return A promise that resolves to the personal files
*/
export function getContents(path: string = '/'): CancelablePromise<ContentsWithRoot> {
export function getContents(path: string = '/', options: { signal: AbortSignal }): Promise<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)
return getFiles(path, options)
.then((content) => {
content.contents = content.contents.filter(isPersonalFile)
return content
Expand Down
23 changes: 13 additions & 10 deletions apps/files/src/services/Recent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { ResponseDataDetailed, SearchResult } from 'webdav'
import { getCurrentUser } from '@nextcloud/auth'
import { Folder, Permission } from '@nextcloud/files'
import { getRecentSearch, getRemoteURL, getRootPath, resultToNode } from '@nextcloud/files/dav'
import { CancelablePromise } from 'cancelable-promise'
import logger from '../logger.ts'
import { getPinia } from '../store/index.ts'
import { useUserConfigStore } from '../store/userconfig.ts'
import { client } from './WebdavClient.ts'
Expand All @@ -22,8 +22,10 @@ const lastTwoWeeksTimestamp = Math.round((Date.now() / 1000) - (60 * 60 * 24 * 1
* If hidden files are not shown, then also recently changed files *in* hidden directories are filtered.
*
* @param path Path to search for recent changes
* @param options Options including abort signal
* @param options.signal Abort signal to cancel the request
*/
export function getContents(path = '/'): CancelablePromise<ContentsWithRoot> {
export async function getContents(path = '/', options: { signal: AbortSignal }): Promise<ContentsWithRoot> {
const store = useUserConfigStore(getPinia())

/**
Expand All @@ -35,10 +37,9 @@ export function getContents(path = '/'): CancelablePromise<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 controller = new AbortController()
const handler = async () => {
try {
const contentsResponse = await client.search('/', {
signal: controller.signal,
signal: options.signal,
details: true,
data: getRecentSearch(lastTwoWeeksTimestamp),
}) as ResponseDataDetailed<SearchResult>
Expand All @@ -61,10 +62,12 @@ export function getContents(path = '/'): CancelablePromise<ContentsWithRoot> {
}),
contents,
}
} catch (error) {
if (options.signal.aborted) {
logger.info('Fetching recent files aborted')
throw new DOMException('Aborted', 'AbortError')
}
logger.error('Failed to fetch recent files', { error })
throw error
}

return new CancelablePromise(async (resolve, reject, cancel) => {
cancel(() => controller.abort())
resolve(handler())
})
}
9 changes: 5 additions & 4 deletions apps/files/src/services/Search.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ describe('Search service', () => {
searchNodes.mockImplementationOnce(() => {
throw new Error('expected error')
})
expect(getContents).rejects.toThrow('expected error')
expect(() => getContents('', { signal: new AbortController().signal })).rejects.toThrow('expected error')
})

it('returns the search results and a fake root', async () => {
searchNodes.mockImplementationOnce(() => [fakeFolder])
const { contents, folder } = await getContents()
const { contents, folder } = await getContents('', { signal: new AbortController().signal })

expect(searchNodes).toHaveBeenCalledOnce()
expect(contents).toHaveLength(1)
Expand All @@ -57,8 +57,9 @@ describe('Search service', () => {
return []
})

const content = getContents()
content.cancel()
const controller = new AbortController()
getContents('', { signal: controller.signal })
controller.abort()

// its cancelled thus the promise returns the event
const event = await promise
Expand Down
Loading
Loading