diff options
Diffstat (limited to 'apps/files/src/services')
21 files changed, 582 insertions, 538 deletions
diff --git a/apps/files/src/services/DropService.ts b/apps/files/src/services/DropService.ts index d3711741753..1013baeda6c 100644 --- a/apps/files/src/services/DropService.ts +++ b/apps/files/src/services/DropService.ts @@ -1,24 +1,6 @@ /** - * @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de> - * - * @author Ferdinand Thiessen <opensource@fthiessen.de> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ import type { Upload } from '@nextcloud/upload' @@ -35,7 +17,7 @@ import Vue from 'vue' import { Directory, traverseTree, resolveConflict, createDirectoryIfNotExists } from './DropServiceUtils' import { handleCopyMoveNodeTo } from '../actions/moveOrCopyAction' import { MoveCopyAction } from '../actions/moveOrCopyActionUtils' -import logger from '../logger.js' +import logger from '../logger.ts' /** * This function converts a list of DataTransferItems to a file tree. @@ -196,8 +178,7 @@ export const onDropInternalFiles = async (nodes: Node[], destination: Folder, co for (const node of nodes) { Vue.set(node, 'status', NodeStatus.LOADING) - // TODO: resolve potential conflicts prior and force overwrite - queue.push(handleCopyMoveNodeTo(node, destination, isCopy ? MoveCopyAction.COPY : MoveCopyAction.MOVE)) + queue.push(handleCopyMoveNodeTo(node, destination, isCopy ? MoveCopyAction.COPY : MoveCopyAction.MOVE, true)) } // Wait for all promises to settle diff --git a/apps/files/src/services/DropServiceUtils.spec.ts b/apps/files/src/services/DropServiceUtils.spec.ts index 1502d83d9ce..5f4370c7894 100644 --- a/apps/files/src/services/DropServiceUtils.spec.ts +++ b/apps/files/src/services/DropServiceUtils.spec.ts @@ -1,4 +1,8 @@ -import { describe, it, expect } from '@jest/globals' +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { beforeAll, describe, expect, it, vi } from 'vitest' import { FileSystemDirectoryEntry, FileSystemFileEntry, fileSystemEntryToDataTransferItem, DataTransferItem as DataTransferItemMock } from '../../../../__tests__/FileSystemAPIUtils' import { join } from 'node:path' @@ -84,20 +88,17 @@ describe('Filesystem API traverseTree', () => { describe('DropService dataTransferToFileTree', () => { beforeAll(() => { + // @ts-expect-error jsdom doesn't have DataTransferItem + delete window.DataTransferItem // DataTransferItem doesn't exists in jsdom, let's mock // a dumb one so we can check the instanceof // @ts-expect-error jsdom doesn't have DataTransferItem window.DataTransferItem = DataTransferItemMock }) - afterAll(() => { - // @ts-expect-error jsdom doesn't have DataTransferItem - delete window.DataTransferItem - }) - it('Should return a RootDirectory with Filesystem API', async () => { - jest.spyOn(logger, 'error').mockImplementation(() => jest.fn()) - jest.spyOn(logger, 'warn').mockImplementation(() => jest.fn()) + vi.spyOn(logger, 'error').mockImplementation(() => vi.fn()) + vi.spyOn(logger, 'warn').mockImplementation(() => vi.fn()) const dataTransferItems = buildDataTransferItemArray('root', dataTree) const fileTree = await dataTransferToFileTree(dataTransferItems as unknown as DataTransferItem[]) @@ -117,8 +118,8 @@ describe('DropService dataTransferToFileTree', () => { }) it('Should return a RootDirectory with legacy File API ignoring recursive directories', async () => { - jest.spyOn(logger, 'error').mockImplementation(() => jest.fn()) - jest.spyOn(logger, 'warn').mockImplementation(() => jest.fn()) + vi.spyOn(logger, 'error').mockImplementation(() => vi.fn()) + vi.spyOn(logger, 'warn').mockImplementation(() => vi.fn()) const dataTransferItems = buildDataTransferItemArray('root', dataTree, false) diff --git a/apps/files/src/services/DropServiceUtils.ts b/apps/files/src/services/DropServiceUtils.ts index 6fd051f9dae..f10a09cfe27 100644 --- a/apps/files/src/services/DropServiceUtils.ts +++ b/apps/files/src/services/DropServiceUtils.ts @@ -1,23 +1,6 @@ /** - * @copyright Copyright (c) 2024 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ import type { FileStat, ResponseDataDetailed } from 'webdav' @@ -27,7 +10,7 @@ import { openConflictPicker } from '@nextcloud/upload' import { showError, showInfo } from '@nextcloud/dialogs' import { translate as t } from '@nextcloud/l10n' -import logger from '../logger.js' +import logger from '../logger.ts' /** * This represents a Directory in the file tree diff --git a/apps/files/src/services/Favorites.ts b/apps/files/src/services/Favorites.ts index 83577f9a75e..e156c92c511 100644 --- a/apps/files/src/services/Favorites.ts +++ b/apps/files/src/services/Favorites.ts @@ -1,63 +1,40 @@ /** - * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * 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/FileInfo.js b/apps/files/src/services/FileInfo.js deleted file mode 100644 index 64b12d8594b..00000000000 --- a/apps/files/src/services/FileInfo.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ - -import axios from '@nextcloud/axios' -import { davGetDefaultPropfind } from '@nextcloud/files' - -/** - * @param {any} url - - */ -export default async function(url) { - const response = await axios({ - method: 'PROPFIND', - url, - data: davGetDefaultPropfind(), - }) - - // TODO: create new parser or use cdav-lib when available - const file = OC.Files.getClient()._client.parseMultiStatus(response.data) - // TODO: create new parser or use cdav-lib when available - const fileInfo = OC.Files.getClient()._parseFileInfo(file[0]) - - // TODO remove when no more legacy backbone is used - fileInfo.get = (key) => fileInfo[key] - fileInfo.isDirectory = () => fileInfo.mimetype === 'httpd/unix-directory' - - return fileInfo -} diff --git a/apps/files/src/services/FileInfo.ts b/apps/files/src/services/FileInfo.ts new file mode 100644 index 00000000000..318236f1677 --- /dev/null +++ b/apps/files/src/services/FileInfo.ts @@ -0,0 +1,36 @@ +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/* eslint-disable jsdoc/require-jsdoc */ + +import type { Node } from '@nextcloud/files' + +export default function(node: Node) { + const fileInfo = new OC.Files.FileInfo({ + id: node.fileid, + path: node.dirname, + name: node.basename, + mtime: node.mtime?.getTime(), + etag: node.attributes.etag, + size: node.size, + hasPreview: node.attributes.hasPreview, + isEncrypted: node.attributes.isEncrypted === 1, + isFavourited: node.attributes.favorite === 1, + mimetype: node.mime, + permissions: node.permissions, + mountType: node.attributes['mount-type'], + sharePermissions: node.attributes['share-permissions'], + shareAttributes: JSON.parse(node.attributes['share-attributes'] || '[]'), + type: node.type === 'file' ? 'file' : 'dir', + attributes: node.attributes, + }) + + // TODO remove when no more legacy backbone is used + fileInfo.get = (key) => fileInfo[key] + fileInfo.isDirectory = () => fileInfo.mimetype === 'httpd/unix-directory' + fileInfo.canEdit = () => Boolean(fileInfo.permissions & OC.PERMISSION_UPDATE) + + return fileInfo +} diff --git a/apps/files/src/services/Files.ts b/apps/files/src/services/Files.ts index bcfb368882d..080ce91e538 100644 --- a/apps/files/src/services/Files.ts +++ b/apps/files/src/services/Files.ts @@ -1,89 +1,60 @@ /** - * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { ContentsWithRoot } from '@nextcloud/files' -import type { FileStat, ResponseDataDetailed, DAVResultResponseProps } from 'webdav' +import type { ContentsWithRoot, File, Folder, Node } from '@nextcloud/files' +import type { FileStat, ResponseDataDetailed } from 'webdav' +import { defaultRootPath, getDefaultPropfind, resultToNode as davResultToNode } from '@nextcloud/files/dav' import { CancelablePromise } from 'cancelable-promise' -import { File, Folder, davParsePermissions, davGetDefaultPropfind } from '@nextcloud/files' -import { generateRemoteUrl } from '@nextcloud/router' -import { getCurrentUser } from '@nextcloud/auth' - -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() +import { join } from 'path' +import { client } from './WebdavClient.ts' +import { searchNodes } from './WebDavSearch.ts' +import { getPinia } from '../store/index.ts' +import { useFilesStore } from '../store/files.ts' +import { useSearchStore } from '../store/search.ts' +import logger from '../logger.ts' +/** + * Slim wrapper over `@nextcloud/files` `davResultToNode` to allow using the function with `Array.map` + * @param stat The result returned by the webdav library + */ +export const resultToNode = (stat: FileStat): Node => davResultToNode(stat) - const source = generateRemoteUrl('dav' + rootPath + node.filename) - const id = props?.fileid < 0 - ? hashCode(source) - : props?.fileid as number || 0 +/** + * Get contents implementation for the files view. + * This also allows to fetch local search results when the user is currently filtering. + * + * @param path - The path to query + */ +export function getContents(path = '/'): CancelablePromise<ContentsWithRoot> { + const controller = new AbortController() + const searchStore = useSearchStore(getPinia()) - 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, - }, + 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) } - - delete nodeData.attributes.props - - return node.type === 'file' - ? new File(nodeData) - : new Folder(nodeData) } -export const getContents = (path = '/'): Promise<ContentsWithRoot> => { +/** + * Generic `getContents` implementation for the users files. + * + * @param path - The path to get the contents + */ +export function defaultGetContents(path: string): CancelablePromise<ContentsWithRoot> { + path = join(defaultRootPath, path) const controller = new AbortController() - const propfindPayload = davGetDefaultPropfind() + const propfindPayload = getDefaultPropfind() return new CancelablePromise(async (resolve, reject, onCancel) => { onCancel(() => controller.abort()) + try { const contentsResponse = await client.getDirectoryContents(path, { details: true, @@ -94,13 +65,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) { @@ -114,3 +86,25 @@ export const getContents = (path = '/'): Promise<ContentsWithRoot> => { } }) } + +/** + * Get the local search results for the current folder. + * + * @param path - The path + * @param query - The current search query + * @param signal - The aboort signal + */ +async function getLocalSearch(path: string, query: string, signal: AbortSignal): Promise<ContentsWithRoot> { + const filesStore = useFilesStore(getPinia()) + let folder = filesStore.getDirectoryByPath('files', path) + if (!folder) { + const rootPath = join(defaultRootPath, path) + const stat = await client.stat(rootPath, { details: true }) as ResponseDataDetailed<FileStat> + folder = resultToNode(stat.data) as Folder + } + const contents = await searchNodes(query, { dir: path, signal }) + return { + folder, + contents, + } +} diff --git a/apps/files/src/services/FolderTree.ts b/apps/files/src/services/FolderTree.ts new file mode 100644 index 00000000000..82f0fb392e5 --- /dev/null +++ b/apps/files/src/services/FolderTree.ts @@ -0,0 +1,95 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { ContentsWithRoot } from '@nextcloud/files' + +import { CancelablePromise } from 'cancelable-promise' +import { davRemoteURL } from '@nextcloud/files' +import axios from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' +import { getCurrentUser } from '@nextcloud/auth' +import { dirname, encodePath, joinPaths } from '@nextcloud/paths' +import { getCanonicalLocale, getLanguage } from '@nextcloud/l10n' + +import { getContents as getFiles } from './Files.ts' + +// eslint-disable-next-line no-use-before-define +type Tree = TreeNodeData[] + +interface TreeNodeData { + id: number, + basename: string, + displayName?: string, + children: Tree, +} + +export interface TreeNode { + source: string, + encodedSource: string, + path: string, + fileid: number, + basename: string, + displayName?: string, +} + +export const folderTreeId = 'folders' + +export const sourceRoot = `${davRemoteURL}/files/${getCurrentUser()?.uid}` + +const collator = Intl.Collator( + [getLanguage(), getCanonicalLocale()], + { + numeric: true, + usage: 'sort', + }, +) + +const compareNodes = (a: TreeNodeData, b: TreeNodeData) => collator.compare(a.displayName ?? a.basename, b.displayName ?? b.basename) + +const getTreeNodes = (tree: Tree, currentPath: string = '/', nodes: TreeNode[] = []): TreeNode[] => { + const sortedTree = tree.toSorted(compareNodes) + for (const { id, basename, displayName, children } of sortedTree) { + const path = joinPaths(currentPath, basename) + const source = `${sourceRoot}${path}` + const node: TreeNode = { + source, + encodedSource: encodeSource(source), + path, + fileid: id, + basename, + } + if (displayName) { + node.displayName = displayName + } + nodes.push(node) + if (children.length > 0) { + getTreeNodes(children, path, nodes) + } + } + return nodes +} + +export const getFolderTreeNodes = async (path: string = '/', depth: number = 1): Promise<TreeNode[]> => { + const { data: tree } = await axios.get<Tree>(generateOcsUrl('/apps/files/api/v1/folder-tree'), { + params: new URLSearchParams({ path, depth: String(depth) }), + }) + const nodes = getTreeNodes(tree, path) + return nodes +} + +export const getContents = (path: string): CancelablePromise<ContentsWithRoot> => getFiles(path) + +export const encodeSource = (source: string): string => { + const { origin } = new URL(source) + return origin + encodePath(source.slice(origin.length)) +} + +export const getSourceParent = (source: string): string => { + const parent = dirname(source) + if (parent === sourceRoot) { + return folderTreeId + } + return encodeSource(parent) +} diff --git a/apps/files/src/services/LivePhotos.ts b/apps/files/src/services/LivePhotos.ts index ce333f31b0a..10be42444e2 100644 --- a/apps/files/src/services/LivePhotos.ts +++ b/apps/files/src/services/LivePhotos.ts @@ -1,26 +1,12 @@ /** - * @copyright Copyright (c) 2023 Louis Chmn <louis@chmn.me> - * - * @author Louis Chmn <louis@chmn.me> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ import { Node, registerDavProperty } from '@nextcloud/files' +/** + * + */ export function initLivePhotos(): void { registerDavProperty('nc:metadata-files-live-photo', { nc: 'http://nextcloud.org/ns' }) } diff --git a/apps/files/src/services/PersonalFiles.ts b/apps/files/src/services/PersonalFiles.ts index cb65800898d..6d86bd3bae2 100644 --- a/apps/files/src/services/PersonalFiles.ts +++ b/apps/files/src/services/PersonalFiles.ts @@ -1,57 +1,39 @@ /** - * @copyright Copyright (c) 2024 Eduardo Morales <emoral435@gmail.com> - * - * @author Eduardo Morales <emoral435@gmail.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * 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/PreviewService.ts b/apps/files/src/services/PreviewService.ts index e581257760a..6dbb67f30b6 100644 --- a/apps/files/src/services/PreviewService.ts +++ b/apps/files/src/services/PreviewService.ts @@ -1,23 +1,6 @@ /** - * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ // The preview service worker cache name (see webpack config) @@ -25,17 +8,14 @@ const SWCacheName = 'previews' /** * Check if the preview is already cached by the service worker + * @param previewUrl URL to check */ -export const isCachedPreview = function(previewUrl: string): Promise<boolean> { +export async function isCachedPreview(previewUrl: string): Promise<boolean> { if (!window?.caches?.open) { - return Promise.resolve(false) + return false } - return window?.caches?.open(SWCacheName) - .then(function(cache) { - return cache.match(previewUrl) - .then(function(response) { - return !!response - }) - }) + const cache = await window.caches.open(SWCacheName) + const response = await cache.match(previewUrl) + return response !== undefined } diff --git a/apps/files/src/services/Recent.ts b/apps/files/src/services/Recent.ts index 47f31466dd2..d0ca285b05c 100644 --- a/apps/files/src/services/Recent.ts +++ b/apps/files/src/services/Recent.ts @@ -1,38 +1,29 @@ /** - * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Ferdinand Thiessen <opensource@fthiessen.de> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ import type { ContentsWithRoot, Node } from '@nextcloud/files' -import type { FileStat, ResponseDataDetailed } from 'webdav' +import type { FileStat, 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, davResultToNode } from '@nextcloud/files' +import { CancelablePromise } from 'cancelable-promise' import { useUserConfigStore } from '../store/userconfig.ts' -import { pinia } from '../store/index.ts' - -const client = davGetClient() +import { getPinia } from '../store/index.ts' +import { client } from './WebdavClient.ts' +import { getBaseUrl } from '@nextcloud/router' const lastTwoWeeksTimestamp = Math.round((Date.now() / 1000) - (60 * 60 * 24 * 14)) /** + * Helper to map a WebDAV result to a Nextcloud node + * The search endpoint already includes the dav remote URL so we must not include it in the source + * + * @param stat the WebDAV result + */ +const resultToNode = (stat: FileStat) => davResultToNode(stat, davRootPath, getBaseUrl()) + +/** * Get recently changed nodes * * This takes the users preference about hidden files into account. @@ -40,8 +31,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> => { - const store = useUserConfigStore(pinia) +export const getContents = (path = '/'): CancelablePromise<ContentsWithRoot> => { + const store = useUserConfigStore(getPinia()) + /** * Filter function that returns only the visible nodes - or hidden if explicitly configured * @param node The node to check @@ -51,28 +43,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 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 + 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: contents.map((r) => davResultToNode(r)).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/RouterService.ts b/apps/files/src/services/RouterService.ts index 7e3bd854e71..4e2999b1d29 100644 --- a/apps/files/src/services/RouterService.ts +++ b/apps/files/src/services/RouterService.ts @@ -1,46 +1,38 @@ /** - * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { Route } from 'vue-router' +import type { Route, Location } from 'vue-router' import type VueRouter from 'vue-router' -import type { Dictionary, Location } from 'vue-router/types/router' export default class RouterService { - private _router: VueRouter + // typescript compiles this to `#router` to make it private even in JS, + // but in TS it needs to be called without the visibility specifier + private router: VueRouter constructor(router: VueRouter) { - this._router = router + this.router = router } get name(): string | null | undefined { - return this._router.currentRoute.name + return this.router.currentRoute.name } - get query(): Dictionary<string | (string | null)[] | null | undefined> { - return this._router.currentRoute.query || {} + get query(): Record<string, string | (string | null)[] | null | undefined> { + return this.router.currentRoute.query || {} } - get params(): Dictionary<string> { - return this._router.currentRoute.params || {} + get params(): Record<string, string> { + return this.router.currentRoute.params || {} + } + + /** + * This is a protected getter only for internal use + * @private + */ + get _router() { + return this.router } /** @@ -51,7 +43,7 @@ export default class RouterService { * @see https://router.vuejs.org/guide/essentials/navigation.html#navigate-to-a-different-location */ goTo(path: string, replace = false): Promise<Route> { - return this._router.push({ + return this.router.push({ path, replace, }) @@ -68,11 +60,11 @@ export default class RouterService { */ goToRoute( name?: string, - params?: Dictionary<string>, - query?: Dictionary<string | (string | null)[] | null | undefined>, + params?: Record<string, string>, + query?: Record<string, string | (string | null)[] | null | undefined>, replace?: boolean, ): Promise<Route> { - return this._router.push({ + return this.router.push({ name, query, params, diff --git a/apps/files/src/services/Search.spec.ts b/apps/files/src/services/Search.spec.ts new file mode 100644 index 00000000000..c2840521a15 --- /dev/null +++ b/apps/files/src/services/Search.spec.ts @@ -0,0 +1,61 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { createPinia, setActivePinia } from 'pinia' +import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { getContents } from './Search.ts' +import { Folder, Permission } from '@nextcloud/files' + +const searchNodes = vi.hoisted(() => vi.fn()) +vi.mock('./WebDavSearch.ts', () => ({ searchNodes })) +vi.mock('@nextcloud/auth') + +describe('Search service', () => { + const fakeFolder = new Folder({ owner: 'owner', source: 'https://cloud.example.com/remote.php/dav/files/owner/folder', root: '/files/owner' }) + + beforeAll(() => { + window.OCP ??= {} + window.OCP.Files ??= {} + window.OCP.Files.Router ??= { params: {}, query: {} } + vi.spyOn(window.OCP.Files.Router, 'params', 'get').mockReturnValue({ view: 'files' }) + }) + + beforeEach(() => { + vi.restoreAllMocks() + setActivePinia(createPinia()) + }) + + it('rejects on error', async () => { + searchNodes.mockImplementationOnce(() => { throw new Error('expected error') }) + expect(getContents).rejects.toThrow('expected error') + }) + + it('returns the search results and a fake root', async () => { + searchNodes.mockImplementationOnce(() => [fakeFolder]) + const { contents, folder } = await getContents() + + expect(searchNodes).toHaveBeenCalledOnce() + expect(contents).toHaveLength(1) + expect(contents).toEqual([fakeFolder]) + // read only root + expect(folder.permissions).toBe(Permission.READ) + }) + + it('can be cancelled', async () => { + const { promise, resolve } = Promise.withResolvers<Event>() + searchNodes.mockImplementationOnce(async (_, { signal }: { signal: AbortSignal}) => { + signal.addEventListener('abort', resolve) + await promise + return [] + }) + + const content = getContents() + content.cancel() + + // its cancelled thus the promise returns the event + const event = await promise + expect(event.type).toBe('abort') + }) +}) diff --git a/apps/files/src/services/Search.ts b/apps/files/src/services/Search.ts new file mode 100644 index 00000000000..f1d7c30a94e --- /dev/null +++ b/apps/files/src/services/Search.ts @@ -0,0 +1,43 @@ +/*! + * SPDX-FileCopyrightText: 2025 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 { defaultRemoteURL } from '@nextcloud/files/dav' +import { CancelablePromise } from 'cancelable-promise' +import { searchNodes } from './WebDavSearch.ts' +import logger from '../logger.ts' +import { useSearchStore } from '../store/search.ts' +import { getPinia } from '../store/index.ts' + +/** + * Get the contents for a search view + */ +export function getContents(): CancelablePromise<ContentsWithRoot> { + const controller = new AbortController() + + const searchStore = useSearchStore(getPinia()) + + return new CancelablePromise<ContentsWithRoot>(async (resolve, reject, cancel) => { + cancel(() => controller.abort()) + try { + const contents = await searchNodes(searchStore.query, { signal: controller.signal }) + resolve({ + contents, + folder: new Folder({ + id: 0, + source: `${defaultRemoteURL}#search`, + owner: getCurrentUser()!.uid, + permissions: Permission.READ, + }), + }) + } catch (error) { + logger.error('Failed to fetch search results', { error }) + reject(error) + } + }) +} diff --git a/apps/files/src/services/ServiceWorker.js b/apps/files/src/services/ServiceWorker.js index b89d5af4040..cc13db44009 100644 --- a/apps/files/src/services/ServiceWorker.js +++ b/apps/files/src/services/ServiceWorker.js @@ -1,26 +1,9 @@ /** - * @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { generateUrl } from '@nextcloud/router' -import logger from '../logger.js' +import { generateUrl, getRootUrl } from '@nextcloud/router' +import logger from '../logger.ts' export default () => { if ('serviceWorker' in navigator) { @@ -28,7 +11,15 @@ export default () => { window.addEventListener('load', async () => { try { const url = generateUrl('/apps/files/preview-service-worker.js', {}, { noRewrite: true }) - const registration = await navigator.serviceWorker.register(url, { scope: '/' }) + let scope = getRootUrl() + // If the instance is not in a subfolder an empty string will be returned. + // The service worker registration will use the current path if it receives an empty string, + // which will result in a service worker registration for every single path the user visits. + if (scope === '') { + scope = '/' + } + + const registration = await navigator.serviceWorker.register(url, { scope }) logger.debug('SW registered: ', { registration }) } catch (error) { logger.error('SW registration failed: ', { error }) diff --git a/apps/files/src/services/Settings.js b/apps/files/src/services/Settings.js index 323a2499a78..7f04aa82fda 100644 --- a/apps/files/src/services/Settings.js +++ b/apps/files/src/services/Settings.js @@ -1,23 +1,6 @@ /** - * @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ export default class Settings { diff --git a/apps/files/src/services/Sidebar.js b/apps/files/src/services/Sidebar.js index e87ee71a4b1..0f5c275e532 100644 --- a/apps/files/src/services/Sidebar.js +++ b/apps/files/src/services/Sidebar.js @@ -1,23 +1,6 @@ /** - * @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ export default class Sidebar { diff --git a/apps/files/src/services/Templates.js b/apps/files/src/services/Templates.js index c242f9ae82d..d7f25846ceb 100644 --- a/apps/files/src/services/Templates.js +++ b/apps/files/src/services/Templates.js @@ -1,23 +1,6 @@ /** - * @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ import { generateOcsUrl } from '@nextcloud/router' @@ -28,18 +11,25 @@ export const getTemplates = async function() { return response.data.ocs.data } +export const getTemplateFields = async function(fileId) { + const response = await axios.get(generateOcsUrl(`apps/files/api/v1/templates/fields/${fileId}`)) + return response.data.ocs.data +} + /** * Create a new file from a specified template * * @param {string} filePath The new file destination path * @param {string} templatePath The template source path * @param {string} templateType The template type e.g 'user' + * @param {object} templateFields The template fields to fill in (if any) */ -export const createFromTemplate = async function(filePath, templatePath, templateType) { +export const createFromTemplate = async function(filePath, templatePath, templateType, templateFields) { const response = await axios.post(generateOcsUrl('apps/files/api/v1/templates/create'), { filePath, templatePath, templateType, + templateFields, }) return response.data.ocs.data } diff --git a/apps/files/src/services/WebDavSearch.ts b/apps/files/src/services/WebDavSearch.ts new file mode 100644 index 00000000000..feb7f30b357 --- /dev/null +++ b/apps/files/src/services/WebDavSearch.ts @@ -0,0 +1,83 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { INode } from '@nextcloud/files' +import type { ResponseDataDetailed, SearchResult } from 'webdav' + +import { getCurrentUser } from '@nextcloud/auth' +import { defaultRootPath, getDavNameSpaces, getDavProperties, resultToNode } from '@nextcloud/files/dav' +import { getBaseUrl } from '@nextcloud/router' +import { client } from './WebdavClient.ts' +import logger from '../logger.ts' + +export interface SearchNodesOptions { + dir?: string, + signal?: AbortSignal +} + +/** + * Search for nodes matching the given query. + * + * @param query - Search query + * @param options - Options + * @param options.dir - The base directory to scope the search to + * @param options.signal - Abort signal for the request + */ +export async function searchNodes(query: string, { dir, signal }: SearchNodesOptions): Promise<INode[]> { + const user = getCurrentUser() + if (!user) { + // the search plugin only works for user roots + return [] + } + + query = query.trim() + if (query.length < 3) { + // the search plugin only works with queries of at least 3 characters + return [] + } + + if (dir && !dir.startsWith('/')) { + dir = `/${dir}` + } + + logger.debug('Searching for nodes', { query, dir }) + const { data } = await client.search('/', { + details: true, + signal, + data: ` +<d:searchrequest ${getDavNameSpaces()}> + <d:basicsearch> + <d:select> + <d:prop> + ${getDavProperties()} + </d:prop> + </d:select> + <d:from> + <d:scope> + <d:href>/files/${user.uid}${dir || ''}</d:href> + <d:depth>infinity</d:depth> + </d:scope> + </d:from> + <d:where> + <d:like> + <d:prop> + <d:displayname/> + </d:prop> + <d:literal>%${query.replace('%', '')}%</d:literal> + </d:like> + </d:where> + <d:orderby/> + </d:basicsearch> +</d:searchrequest>`, + }) as ResponseDataDetailed<SearchResult> + + // check if the request was aborted + if (signal?.aborted) { + return [] + } + + // otherwise return the result mapped to Nextcloud nodes + return data.results.map((result) => resultToNode(result, defaultRootPath, getBaseUrl())) +} diff --git a/apps/files/src/services/WebdavClient.ts b/apps/files/src/services/WebdavClient.ts index 6c98b299703..2b92deba9b4 100644 --- a/apps/files/src/services/WebdavClient.ts +++ b/apps/files/src/services/WebdavClient.ts @@ -1,66 +1,19 @@ /** - * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ +import type { FileStat, ResponseDataDetailed } from 'webdav' +import type { Node } from '@nextcloud/files' -import { createClient, getPatcher } from 'webdav' -import { generateRemoteUrl } from '@nextcloud/router' -import { getCurrentUser, getRequestToken, onRequestTokenUpdate } from '@nextcloud/auth' +import { getClient, getDefaultPropfind, getRootPath, resultToNode } from '@nextcloud/files/dav' -export const rootPath = `/files/${getCurrentUser()?.uid}` -export const defaultRootUrl = generateRemoteUrl('dav' + rootPath) +export const client = getClient() -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 fetchNode = async (path: string): Promise<Node> => { + const propfindPayload = getDefaultPropfind() + const result = await client.stat(`${getRootPath()}${path}`, { + details: true, + data: propfindPayload, + }) as ResponseDataDetailed<FileStat> + return resultToNode(result.data) } |