Skip to content

Commit d6bf698

Browse files
committed
chore(files): adjust getContents to use AbortController
Signed-off-by: Ferdinand Thiessen <[email protected]>
1 parent 4bf7874 commit d6bf698

File tree

11 files changed

+184
-179
lines changed

11 files changed

+184
-179
lines changed
Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,49 @@
1-
/**
1+
/*!
22
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
5+
56
import type { ContentsWithRoot } from '@nextcloud/files'
67

78
import { getCurrentUser } from '@nextcloud/auth'
89
import { Folder, Permission } from '@nextcloud/files'
910
import { getFavoriteNodes, getRemoteURL, getRootPath } from '@nextcloud/files/dav'
10-
import { CancelablePromise } from 'cancelable-promise'
11+
import logger from '../logger.ts'
1112
import { getContents as filesContents } from './Files.ts'
1213
import { client } from './WebdavClient.ts'
1314

1415
/**
16+
* Get the contents for the favorites view
1517
*
16-
* @param path
18+
* @param path - The path to get the contents for
19+
* @param options - Additional options
20+
* @param options.signal - Optional AbortSignal to cancel the request
21+
* @return A promise resolving to the contents with root folder
1722
*/
18-
export function getContents(path = '/'): CancelablePromise<ContentsWithRoot> {
23+
export async function getContents(path = '/', { signal }: { signal?: AbortSignal } = {}): Promise<ContentsWithRoot> {
1924
// We only filter root files for favorites, for subfolders we can simply reuse the files contents
20-
if (path !== '/') {
25+
if (path && path !== '/') {
2126
return filesContents(path)
2227
}
2328

24-
return new CancelablePromise((resolve, reject, cancel) => {
25-
const promise = getFavoriteNodes(client)
26-
.catch(reject)
27-
.then((contents) => {
28-
if (!contents) {
29-
reject()
30-
return
31-
}
32-
resolve({
33-
contents,
34-
folder: new Folder({
35-
id: 0,
36-
source: `${getRemoteURL()}${getRootPath()}`,
37-
root: getRootPath(),
38-
owner: getCurrentUser()?.uid || null,
39-
permissions: Permission.READ,
40-
}),
41-
})
42-
})
43-
cancel(() => promise.cancel())
44-
})
29+
try {
30+
const contents = await getFavoriteNodes({ client, signal })
31+
return {
32+
contents,
33+
folder: new Folder({
34+
id: 0,
35+
source: `${getRemoteURL()}${getRootPath()}`,
36+
root: getRootPath(),
37+
owner: getCurrentUser()?.uid || null,
38+
permissions: Permission.READ,
39+
}),
40+
}
41+
} catch (error) {
42+
if (signal?.aborted) {
43+
logger.debug('Favorite nodes request was aborted')
44+
throw new DOMException('Aborted', 'AbortError')
45+
}
46+
logger.error('Failed to load favorite nodes via WebDAV', { error })
47+
throw error
48+
}
4549
}

apps/files/src/services/Files.ts

Lines changed: 32 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import type { ContentsWithRoot, File, Folder } from '@nextcloud/files'
66
import type { FileStat, ResponseDataDetailed } from 'webdav'
77

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

28-
if (searchStore.query.length >= 3) {
29-
return new CancelablePromise((resolve, reject, cancel) => {
30-
cancel(() => controller.abort())
31-
getLocalSearch(path, searchStore.query, controller.signal)
32-
.then(resolve)
33-
.catch(reject)
34-
})
35-
} else {
36-
return defaultGetContents(path)
28+
if (searchStore.query.length < 3) {
29+
return await defaultGetContents(path, options)
3730
}
31+
32+
return await getLocalSearch(path, searchStore.query, options.signal)
3833
}
3934

4035
/**
4136
* Generic `getContents` implementation for the users files.
4237
*
4338
* @param path - The path to get the contents
39+
* @param options - Options
40+
* @param options.signal - Abort signal to cancel the request
4441
*/
45-
export function defaultGetContents(path: string): CancelablePromise<ContentsWithRoot> {
42+
export async function defaultGetContents(path: string, options: { signal: AbortSignal }): Promise<ContentsWithRoot> {
4643
path = join(getRootPath(), path)
47-
const controller = new AbortController()
4844
const propfindPayload = getDefaultPropfind()
4945

50-
return new CancelablePromise(async (resolve, reject, onCancel) => {
51-
onCancel(() => controller.abort())
46+
const contentsResponse = await client.getDirectoryContents(path, {
47+
details: true,
48+
data: propfindPayload,
49+
includeSelf: true,
50+
signal: options.signal,
51+
}) as ResponseDataDetailed<FileStat[]>
5252

53-
try {
54-
const contentsResponse = await client.getDirectoryContents(path, {
55-
details: true,
56-
data: propfindPayload,
57-
includeSelf: true,
58-
signal: controller.signal,
59-
}) as ResponseDataDetailed<FileStat[]>
53+
const root = contentsResponse.data[0]
54+
const contents = contentsResponse.data.slice(1)
55+
if (root?.filename !== path && `${root?.filename}/` !== path) {
56+
logger.debug(`Exepected "${path}" but got filename "${root.filename}" instead.`)
57+
throw new Error('Root node does not match requested path')
58+
}
6059

61-
const root = contentsResponse.data[0]
62-
const contents = contentsResponse.data.slice(1)
63-
if (root?.filename !== path && `${root?.filename}/` !== path) {
64-
logger.debug(`Exepected "${path}" but got filename "${root.filename}" instead.`)
65-
throw new Error('Root node does not match requested path')
60+
return {
61+
folder: resultToNode(root) as Folder,
62+
contents: contents.map((result) => {
63+
try {
64+
return resultToNode(result)
65+
} catch (error) {
66+
logger.error(`Invalid node detected '${result.basename}'`, { error })
67+
return null
6668
}
67-
68-
resolve({
69-
folder: resultToNode(root) as Folder,
70-
contents: contents.map((result) => {
71-
try {
72-
return resultToNode(result)
73-
} catch (error) {
74-
logger.error(`Invalid node detected '${result.basename}'`, { error })
75-
return null
76-
}
77-
}).filter(Boolean) as File[],
78-
})
79-
} catch (error) {
80-
reject(error)
81-
}
82-
})
69+
}).filter(Boolean) as File[],
70+
}
8371
}
8472

8573
/**

apps/files/src/services/FolderTree.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
*/
55

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

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

4948
/**
49+
* Get all tree nodes recursively
5050
*
51-
* @param tree
52-
* @param currentPath
53-
* @param nodes
51+
* @param tree - The tree to process
52+
* @param currentPath - The current path
53+
* @param nodes - The nodes collected so far
5454
*/
5555
function getTreeNodes(tree: Tree, currentPath: string = '/', nodes: TreeNode[] = []): TreeNode[] {
5656
const sortedTree = tree.toSorted(compareNodes)
@@ -76,9 +76,10 @@ function getTreeNodes(tree: Tree, currentPath: string = '/', nodes: TreeNode[] =
7676
}
7777

7878
/**
79+
* Get folder tree nodes
7980
*
80-
* @param path
81-
* @param depth
81+
* @param path - The path to get the tree from
82+
* @param depth - The depth to fetch
8283
*/
8384
export async function getFolderTreeNodes(path: string = '/', depth: number = 1): Promise<TreeNode[]> {
8485
const { data: tree } = await axios.get<Tree>(generateOcsUrl('/apps/files/api/v1/folder-tree'), {
@@ -88,20 +89,22 @@ export async function getFolderTreeNodes(path: string = '/', depth: number = 1):
8889
return nodes
8990
}
9091

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

9394
/**
95+
* Encode source URL
9496
*
95-
* @param source
97+
* @param source - The source URL
9698
*/
9799
export function encodeSource(source: string): string {
98100
const { origin } = new URL(source)
99101
return origin + encodePath(source.slice(origin.length))
100102
}
101103

102104
/**
105+
* Get parent source URL
103106
*
104-
* @param source
107+
* @param source - The source URL
105108
*/
106109
export function getSourceParent(source: string): string {
107110
const parent = dirname(source)

apps/files/src/services/PersonalFiles.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
*/
55

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

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

3332
/**
33+
* Get personal files from a given path
3434
*
35-
* @param path
35+
* @param path - The path to get the personal files from
36+
* @param options - Options
37+
* @param options.signal - Abort signal to cancel the request
38+
* @return A promise that resolves to the personal files
3639
*/
37-
export function getContents(path: string = '/'): CancelablePromise<ContentsWithRoot> {
40+
export function getContents(path: string = '/', options: { signal: AbortSignal }): Promise<ContentsWithRoot> {
3841
// get all the files from the current path as a cancellable promise
3942
// then filter the files that the user does not own, or has shared / is a group folder
40-
return getFiles(path)
43+
return getFiles(path, options)
4144
.then((content) => {
4245
content.contents = content.contents.filter(isPersonalFile)
4346
return content

apps/files/src/services/Recent.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { ResponseDataDetailed, SearchResult } from 'webdav'
88
import { getCurrentUser } from '@nextcloud/auth'
99
import { Folder, Permission } from '@nextcloud/files'
1010
import { getRecentSearch, getRemoteURL, getRootPath, resultToNode } from '@nextcloud/files/dav'
11-
import { CancelablePromise } from 'cancelable-promise'
11+
import logger from '../logger.ts'
1212
import { getPinia } from '../store/index.ts'
1313
import { useUserConfigStore } from '../store/userconfig.ts'
1414
import { client } from './WebdavClient.ts'
@@ -22,8 +22,10 @@ const lastTwoWeeksTimestamp = Math.round((Date.now() / 1000) - (60 * 60 * 24 * 1
2222
* If hidden files are not shown, then also recently changed files *in* hidden directories are filtered.
2323
*
2424
* @param path Path to search for recent changes
25+
* @param options Options including abort signal
26+
* @param options.signal Abort signal to cancel the request
2527
*/
26-
export function getContents(path = '/'): CancelablePromise<ContentsWithRoot> {
28+
export async function getContents(path = '/', options: { signal: AbortSignal }): Promise<ContentsWithRoot> {
2729
const store = useUserConfigStore(getPinia())
2830

2931
/**
@@ -35,10 +37,9 @@ export function getContents(path = '/'): CancelablePromise<ContentsWithRoot> {
3537
|| store.userConfig.show_hidden // If configured to show hidden files we can early return
3638
|| !node.dirname.split('/').some((dir) => dir.startsWith('.')) // otherwise only include the file if non of the parent directories is hidden
3739

38-
const controller = new AbortController()
39-
const handler = async () => {
40+
try {
4041
const contentsResponse = await client.search('/', {
41-
signal: controller.signal,
42+
signal: options.signal,
4243
details: true,
4344
data: getRecentSearch(lastTwoWeeksTimestamp),
4445
}) as ResponseDataDetailed<SearchResult>
@@ -61,10 +62,12 @@ export function getContents(path = '/'): CancelablePromise<ContentsWithRoot> {
6162
}),
6263
contents,
6364
}
65+
} catch (error) {
66+
if (options.signal.aborted) {
67+
logger.info('Fetching recent files aborted')
68+
throw new DOMException('Aborted', 'AbortError')
69+
}
70+
logger.error('Failed to fetch recent files', { error })
71+
throw error
6472
}
65-
66-
return new CancelablePromise(async (resolve, reject, cancel) => {
67-
cancel(() => controller.abort())
68-
resolve(handler())
69-
})
7073
}

apps/files/src/services/Search.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ describe('Search service', () => {
5757
return []
5858
})
5959

60-
const content = getContents()
61-
content.cancel()
60+
const controller = new AbortController()
61+
const content = getContents('', { signal: controller.signal })
62+
controller.abort()
6263

6364
// its cancelled thus the promise returns the event
6465
const event = await promise

apps/files/src/services/Search.ts

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,39 @@ import type { ContentsWithRoot } from '@nextcloud/files'
88
import { getCurrentUser } from '@nextcloud/auth'
99
import { Folder, Permission } from '@nextcloud/files'
1010
import { defaultRemoteURL, getRootPath } from '@nextcloud/files/dav'
11-
import { CancelablePromise } from 'cancelable-promise'
1211
import logger from '../logger.ts'
1312
import { getPinia } from '../store/index.ts'
1413
import { useSearchStore } from '../store/search.ts'
1514
import { searchNodes } from './WebDavSearch.ts'
1615

1716
/**
1817
* Get the contents for a search view
18+
*
19+
* @param path - (not used)
20+
* @param options - Options including abort signal
21+
* @param options.signal - Abort signal to cancel the request
1922
*/
20-
export function getContents(): CancelablePromise<ContentsWithRoot> {
21-
const controller = new AbortController()
22-
23+
export async function getContents(path, options: { signal: AbortSignal }): Promise<ContentsWithRoot> {
2324
const searchStore = useSearchStore(getPinia())
2425

25-
return new CancelablePromise<ContentsWithRoot>(async (resolve, reject, cancel) => {
26-
cancel(() => controller.abort())
27-
try {
28-
const contents = await searchNodes(searchStore.query, { signal: controller.signal })
29-
resolve({
30-
contents,
31-
folder: new Folder({
32-
id: 0,
33-
source: `${defaultRemoteURL}${getRootPath()}}#search`,
34-
owner: getCurrentUser()!.uid,
35-
permissions: Permission.READ,
36-
root: getRootPath(),
37-
}),
38-
})
39-
} catch (error) {
40-
logger.error('Failed to fetch search results', { error })
41-
reject(error)
26+
try {
27+
const contents = await searchNodes(searchStore.query, { signal: options.signal })
28+
return {
29+
contents,
30+
folder: new Folder({
31+
id: 0,
32+
source: `${defaultRemoteURL}${getRootPath()}}#search`,
33+
owner: getCurrentUser()!.uid,
34+
permissions: Permission.READ,
35+
root: getRootPath(),
36+
}),
37+
}
38+
} catch (error) {
39+
if (options.signal.aborted) {
40+
logger.info('Fetching search results aborted')
41+
throw new DOMException('Aborted', 'AbortError')
4242
}
43-
})
43+
logger.error('Failed to fetch search results', { error })
44+
throw error
45+
}
4446
}

0 commit comments

Comments
 (0)