*/
import { action } from './downloadAction'
import { expect } from '@jest/globals'
-import { File, Folder, Permission, View, FileAction, DefaultType } from '@nextcloud/files'
+import {
+ File,
+ Folder,
+ Permission,
+ View,
+ FileAction,
+ DefaultType,
+} from '@nextcloud/files'
const view = {
id: 'files',
// Silent action
expect(exec).toBe(null)
expect(link.download).toEqual('')
- expect(link.href).toEqual('https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt')
+ expect(link.href).toEqual(
+ 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
+ )
expect(link.click).toHaveBeenCalledTimes(1)
})
// Silent action
expect(exec).toStrictEqual([null])
expect(link.download).toEqual('')
- expect(link.href).toEqual('https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt')
+ expect(link.href).toEqual(
+ 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
+ )
expect(link.click).toHaveBeenCalledTimes(1)
})
// Silent action
expect(exec).toBe(null)
expect(link.download).toEqual('')
- expect(link.href.startsWith('/index.php/apps/files/ajax/download.php?dir=%2F&files=%5B%22FooBar%22%5D&downloadStartSecret=')).toBe(true)
+ expect(
+ link.href.startsWith(
+ '/index.php/apps/files/ajax/download.php?dir=%2F&files=%5B%22FooBar%22%5D&downloadStartSecret=',
+ ),
+ ).toBe(true)
expect(link.click).toHaveBeenCalledTimes(1)
})
// Silent action
expect(exec).toStrictEqual([null, null])
expect(link.download).toEqual('')
- expect(link.href.startsWith('/index.php/apps/files/ajax/download.php?dir=%2FDir&files=%5B%22foo.txt%22%2C%22bar.txt%22%5D&downloadStartSecret=')).toBe(true)
expect(link.click).toHaveBeenCalledTimes(1)
+
+ expect(link.href).toMatch(
+ '/index.php/apps/files/ajax/download.php?dir=%2F&files=%5B%22foo.txt%22%2C%22bar.txt%22%5D&downloadStartSecret=',
+ )
+ })
+
+ test('Download multiple nodes from different sources', async () => {
+ const files = [
+ new File({
+ id: 1,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/Folder 1/foo.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.READ,
+ }),
+ new File({
+ id: 2,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/Folder 2/bar.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.READ,
+ }),
+ new File({
+ id: 3,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/Folder 2/baz.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.READ,
+ }),
+ ]
+
+ const exec = await action.execBatch!(files, view, '/Dir')
+
+ // Silent action
+ expect(exec).toStrictEqual([null, null, null])
+ expect(link.download).toEqual('')
+ expect(link.click).toHaveBeenCalledTimes(1)
+
+ expect(link.href).toMatch(
+ '/index.php/apps/files/ajax/download.php?dir=%2F&files=%5B%22foo.txt%22%2C%22bar.txt%22%2C%22baz.txt%22%5D&downloadStartSecret=',
+ )
+ })
+
+ test('Download node and parent folder', async () => {
+ const files = [
+ new File({
+ id: 1,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/Folder 1/foo.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.READ,
+ }),
+ new Folder({
+ id: 2,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/Folder 1',
+ owner: 'admin',
+ permissions: Permission.READ,
+ }),
+ ]
+
+ const exec = await action.execBatch!(files, view, '/Dir')
+
+ // Silent action
+ expect(exec).toStrictEqual([null, null])
+ expect(link.download).toEqual('')
+ expect(link.click).toHaveBeenCalledTimes(1)
+
+ expect(link.href).toMatch(
+ '/index.php/apps/files/ajax/download.php?dir=%2F&files=%5B%22foo.txt%22%2C%22Folder%201%22%5D&downloadStartSecret=',
+ )
})
})
hiddenElement.click()
}
-const downloadNodes = function(dir: string, nodes: Node[]) {
+/**
+ * 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
+}
+
+/**
+ * Handle downloading multiple nodes
+ * @param nodes The nodes to download
+ */
+function downloadNodes(nodes: Node[]): void {
+ // Remove nodes that are already included in parent folders
+ // Example: Download A/foo.txt and A will only return A as A/foo.txt is already included
+ const filteredNodes = nodes.filter((node) => {
+ const parent = nodes.find((other) => (
+ other.type === FileType.Folder
+ && node.path.startsWith(`${other.path}/`)
+ ))
+ return parent === undefined
+ })
+
+ let base = filteredNodes[0].dirname
+ for (const node of filteredNodes.slice(1)) {
+ base = longestCommonPath(base, node.dirname)
+ }
+ base = base || '/'
+
+ // Remove the common prefix
+ const filenames = filteredNodes.map((node) => node.path.slice(base === '/' ? 1 : (base.length + 1)))
+
const secret = Math.random().toString(36).substring(2)
- const url = generateUrl('/apps/files/ajax/download.php?dir={dir}&files={files}&downloadStartSecret={secret}', {
- dir,
+ const url = generateUrl('/apps/files/ajax/download.php?dir={base}&files={files}&downloadStartSecret={secret}', {
+ base,
secret,
- files: JSON.stringify(nodes.map(node => node.basename)),
+ files: JSON.stringify(filenames),
})
triggerDownload(url)
}
async exec(node: Node, view: View, dir: string) {
if (node.type === FileType.Folder) {
- downloadNodes(dir, [node])
+ downloadNodes([node])
return null
}
return [null]
}
- downloadNodes(dir, nodes)
+ downloadNodes(nodes)
return new Array(nodes.length).fill(null)
},