1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
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'
/**
* 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 = name ?? ''
hiddenElement.href = url
hiddenElement.click()
}
/**
* 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 downloadNodes = function(nodes: Node[]) {
let url: URL
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 triggerDownload(url.href)
}
export const action = new FileAction({
id: 'download',
default: DefaultType.DEFAULT,
displayName: () => t('files', 'Download'),
iconSvgInline: () => ArrowDownSvg,
enabled(nodes: Node[], view: View) {
if (nodes.length === 0) {
return false
}
// 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) {
downloadNodes([node])
return null
},
async execBatch(nodes: Node[]) {
downloadNodes(nodes)
return new Array(nodes.length).fill(null)
},
order: 30,
})
|