aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src/services
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/src/services')
-rw-r--r--apps/files/src/services/DropService.ts27
-rw-r--r--apps/files/src/services/DropServiceUtils.spec.ts21
-rw-r--r--apps/files/src/services/DropServiceUtils.ts23
-rw-r--r--apps/files/src/services/Favorites.ts87
-rw-r--r--apps/files/src/services/FileInfo.js46
-rw-r--r--apps/files/src/services/FileInfo.ts36
-rw-r--r--apps/files/src/services/Files.ts144
-rw-r--r--apps/files/src/services/FolderTree.ts95
-rw-r--r--apps/files/src/services/LivePhotos.ts24
-rw-r--r--apps/files/src/services/PersonalFiles.ts66
-rw-r--r--apps/files/src/services/PreviewService.ts36
-rw-r--r--apps/files/src/services/Recent.ts92
-rw-r--r--apps/files/src/services/RouterService.ts56
-rw-r--r--apps/files/src/services/Search.spec.ts61
-rw-r--r--apps/files/src/services/Search.ts43
-rw-r--r--apps/files/src/services/ServiceWorker.js35
-rw-r--r--apps/files/src/services/Settings.js21
-rw-r--r--apps/files/src/services/Sidebar.js21
-rw-r--r--apps/files/src/services/Templates.js30
-rw-r--r--apps/files/src/services/WebDavSearch.ts83
-rw-r--r--apps/files/src/services/WebdavClient.ts73
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)
}