aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src/actions/downloadAction.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/src/actions/downloadAction.ts')
-rw-r--r--apps/files/src/actions/downloadAction.ts116
1 files changed, 72 insertions, 44 deletions
diff --git a/apps/files/src/actions/downloadAction.ts b/apps/files/src/actions/downloadAction.ts
index 28a52551d22..8abd87972ee 100644
--- a/apps/files/src/actions/downloadAction.ts
+++ b/apps/files/src/actions/downloadAction.ts
@@ -2,83 +2,111 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { generateUrl } from '@nextcloud/router'
-import { FileAction, Permission, Node, FileType, View } from '@nextcloud/files'
-import { translate as t } from '@nextcloud/l10n'
+import type { Node, View } from '@nextcloud/files'
+import { FileAction, FileType, DefaultType } from '@nextcloud/files'
+import { t } from '@nextcloud/l10n'
+import { isDownloadable } from '../utils/permissions'
+
import ArrowDownSvg from '@mdi/svg/svg/arrow-down.svg?raw'
-const triggerDownload = function(url: string) {
+/**
+ * Trigger downloading a file.
+ *
+ * @param url The url of the asset to download
+ * @param name Optionally the recommended name of the download (browsers might ignore it)
+ */
+function triggerDownload(url: string, name?: string) {
const hiddenElement = document.createElement('a')
- hiddenElement.download = ''
+ hiddenElement.download = name ?? ''
hiddenElement.href = url
hiddenElement.click()
}
-const downloadNodes = function(dir: string, nodes: Node[]) {
- const secret = Math.random().toString(36).substring(2)
- const url = generateUrl('/apps/files/ajax/download.php?dir={dir}&files={files}&downloadStartSecret={secret}', {
- dir,
- secret,
- files: JSON.stringify(nodes.map(node => node.basename)),
- })
- triggerDownload(url)
+/**
+ * Find the longest common path prefix of both input paths
+ * @param first The first path
+ * @param second The second path
+ */
+function longestCommonPath(first: string, second: string): string {
+ const firstSegments = first.split('/').filter(Boolean)
+ const secondSegments = second.split('/').filter(Boolean)
+ let base = ''
+ for (const [index, segment] of firstSegments.entries()) {
+ if (index >= second.length) {
+ break
+ }
+ if (segment !== secondSegments[index]) {
+ break
+ }
+ const sep = base === '' ? '' : '/'
+ base = `${base}${sep}${segment}`
+ }
+ return base
}
-const isDownloadable = function(node: Node) {
- if ((node.permissions & Permission.READ) === 0) {
- return false
- }
+const downloadNodes = function(nodes: Node[]) {
+ let url: URL
- // If the mount type is a share, ensure it got download permissions.
- if (node.attributes['mount-type'] === 'shared') {
- const shareAttributes = JSON.parse(node.attributes['share-attributes'] ?? 'null')
- const downloadAttribute = shareAttributes?.find?.((attribute: { scope: string; key: string }) => attribute.scope === 'permissions' && attribute.key === 'download')
- if (downloadAttribute !== undefined && downloadAttribute.enabled === false) {
- return false
+ if (nodes.length === 1) {
+ if (nodes[0].type === FileType.File) {
+ return triggerDownload(nodes[0].encodedSource, nodes[0].displayname)
+ } else {
+ url = new URL(nodes[0].encodedSource)
+ url.searchParams.append('accept', 'zip')
+ }
+ } else {
+ url = new URL(nodes[0].encodedSource)
+ let base = url.pathname
+ for (const node of nodes.slice(1)) {
+ base = longestCommonPath(base, (new URL(node.encodedSource).pathname))
}
+ url.pathname = base
+
+ // The URL contains the path encoded so we need to decode as the query.append will re-encode it
+ const filenames = nodes.map((node) => decodeURIComponent(node.encodedSource.slice(url.href.length + 1)))
+ url.searchParams.append('accept', 'zip')
+ url.searchParams.append('files', JSON.stringify(filenames))
+ }
+
+ if (url.pathname.at(-1) !== '/') {
+ url.pathname = `${url.pathname}/`
}
- return true
+ return triggerDownload(url.href)
}
export const action = new FileAction({
id: 'download',
+ default: DefaultType.DEFAULT,
+
displayName: () => t('files', 'Download'),
iconSvgInline: () => ArrowDownSvg,
- enabled(nodes: Node[]) {
+ enabled(nodes: Node[], view: View) {
if (nodes.length === 0) {
return false
}
- // We can download direct dav files. But if we have
- // some folders, we need to use the /apps/files/ajax/download.php
- // endpoint, which only supports user root folder.
- if (nodes.some(node => node.type === FileType.Folder)
- && nodes.some(node => !node.root?.startsWith('/files'))) {
+ // We can only download dav files and folders.
+ if (nodes.some(node => !node.isDavResource)) {
+ return false
+ }
+
+ // Trashbin does not allow batch download
+ if (nodes.length > 1 && view.id === 'trashbin') {
return false
}
return nodes.every(isDownloadable)
},
- async exec(node: Node, view: View, dir: string) {
- if (node.type === FileType.Folder) {
- downloadNodes(dir, [node])
- return null
- }
-
- triggerDownload(node.encodedSource)
+ async exec(node: Node) {
+ downloadNodes([node])
return null
},
- async execBatch(nodes: Node[], view: View, dir: string) {
- if (nodes.length === 1) {
- this.exec(nodes[0], view, dir)
- return [null]
- }
-
- downloadNodes(dir, nodes)
+ async execBatch(nodes: Node[]) {
+ downloadNodes(nodes)
return new Array(nodes.length).fill(null)
},