diff options
Diffstat (limited to 'apps/files/src/actions/downloadAction.ts')
-rw-r--r-- | apps/files/src/actions/downloadAction.ts | 116 |
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) }, |