Skip to content

Commit b6f43c1

Browse files
authored
Merge pull request #41975 from nextcloud/fix/files-handle-drop-folders-correctly
2 parents aa30452 + 0d75a41 commit b6f43c1

4 files changed

Lines changed: 148 additions & 24 deletions

File tree

apps/files/src/components/DragAndDropNotice.vue

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
- @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
33
-
44
- @author John Molakvoæ <skjnldsv@protonmail.com>
5+
- @author Ferdinand Thiessen <opensource@fthiessen.de>
56
-
6-
- @license GNU AGPL version 3 or any later version
7+
- @license AGPL-3.0-or-later
78
-
89
- This program is free software: you can redistribute it and/or modify
910
- it under the terms of the GNU Affero General Public License as
@@ -33,14 +34,14 @@
3334
</template>
3435

3536
<script lang="ts">
36-
import { showError, showSuccess } from '@nextcloud/dialogs'
3737
import { translate as t } from '@nextcloud/l10n'
38-
import { getUploader } from '@nextcloud/upload'
3938
import { defineComponent } from 'vue'
4039
4140
import TrayArrowDownIcon from 'vue-material-design-icons/TrayArrowDown.vue'
4241
4342
import logger from '../logger.js'
43+
import { handleDrop } from '../services/DropService'
44+
import { showSuccess } from '@nextcloud/dialogs'
4445
4546
export default defineComponent({
4647
name: 'DragAndDropNotice',
@@ -98,39 +99,29 @@ export default defineComponent({
9899
event.preventDefault()
99100
event.stopPropagation()
100101
101-
if (event.dataTransfer && event.dataTransfer.files?.length > 0) {
102-
const uploader = getUploader()
103-
uploader.destination = this.currentFolder
104-
102+
if (event.dataTransfer && event.dataTransfer.items.length > 0) {
105103
// Start upload
106104
logger.debug(`Uploading files to ${this.currentFolder.path}`)
107-
const promises = [...event.dataTransfer.files].map(async (file: File) => {
108-
try {
109-
return await uploader.upload(file.name, file)
110-
} catch (e) {
111-
showError(t('files', 'Uploading "{filename}" failed', { filename: file.name }))
112-
throw e
113-
}
114-
})
115-
116105
// Process finished uploads
117-
Promise.all(promises).then((uploads) => {
106+
handleDrop(event.dataTransfer).then((uploads) => {
118107
logger.debug('Upload terminated', { uploads })
119108
showSuccess(t('files', 'Upload successful'))
120109
121-
// Scroll to last upload if terminated
122-
const lastUpload = uploads[uploads.length - 1]
123-
if (lastUpload?.response?.headers?.['oc-fileid']) {
110+
// Scroll to last upload in current directory if terminated
111+
const lastUpload = uploads.findLast((upload) => !upload.file.webkitRelativePath.includes('/') && upload.response?.headers?.['oc-fileid'])
112+
if (lastUpload !== undefined) {
124113
this.$router.push({
125114
...this.$route,
126115
params: {
116+
view: this.$route.params?.view ?? 'files',
127117
// Remove instanceid from header response
128-
fileid: parseInt(lastUpload.response?.headers?.['oc-fileid']),
118+
fileid: parseInt(lastUpload.response!.headers['oc-fileid']),
129119
},
130120
})
131121
}
132122
})
133123
}
124+
this.dragover = false
134125
},
135126
t,
136127
},
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/**
2+
* @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>
3+
*
4+
* @author Ferdinand Thiessen <opensource@fthiessen.de>
5+
*
6+
* @license AGPL-3.0-or-later
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as
10+
* published by the Free Software Foundation, either version 3 of the
11+
* License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
*
21+
*/
22+
23+
import type { Upload } from '@nextcloud/upload'
24+
import type { FileStat, ResponseDataDetailed } from 'webdav'
25+
26+
import { showError } from '@nextcloud/dialogs'
27+
import { emit } from '@nextcloud/event-bus'
28+
import { davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files'
29+
import { translate as t } from '@nextcloud/l10n'
30+
import { getUploader } from '@nextcloud/upload'
31+
import logger from '../logger.js'
32+
33+
export const handleDrop = async (data: DataTransfer) => {
34+
// TODO: Maybe handle `getAsFileSystemHandle()` in the future
35+
36+
const uploads = [] as Upload[]
37+
for (const item of data.items) {
38+
if (item.kind !== 'file') {
39+
logger.debug('Skipping dropped item', { kind: item.kind, type: item.type })
40+
continue
41+
}
42+
43+
// MDN recommends to try both, as it might be renamed in the future
44+
const entry = (item as unknown as { getAsEntry?: () => FileSystemEntry|undefined})?.getAsEntry?.() ?? item.webkitGetAsEntry()
45+
46+
// Handle browser issues if Filesystem API is not available. Fallback to File API
47+
if (entry === null) {
48+
logger.debug('Could not get FilesystemEntry of item, falling back to file')
49+
const file = item.getAsFile()
50+
if (file === null) {
51+
logger.warn('Could not process DataTransferItem', { type: item.type, kind: item.kind })
52+
showError(t('files', 'One of the dropped files could not be processed'))
53+
} else {
54+
uploads.push(await handleFileUpload(file))
55+
}
56+
} else {
57+
logger.debug('Handle recursive upload', { entry: entry.name })
58+
// Use Filesystem API
59+
uploads.push(...await handleRecursiveUpload(entry))
60+
}
61+
}
62+
return uploads
63+
}
64+
65+
const handleFileUpload = async (file: File, path: string = '') => {
66+
const uploader = getUploader()
67+
68+
try {
69+
return await uploader.upload(`${path}${file.name}`, file)
70+
} catch (e) {
71+
showError(t('files', 'Uploading "{filename}" failed', { filename: file.name }))
72+
throw e
73+
}
74+
}
75+
76+
const handleRecursiveUpload = async (entry: FileSystemEntry, path: string = ''): Promise<Upload[]> => {
77+
if (entry.isFile) {
78+
return [
79+
await new Promise<Upload>((resolve, reject) => {
80+
(entry as FileSystemFileEntry).file(
81+
async (file) => resolve(await handleFileUpload(file, path)),
82+
(error) => reject(error),
83+
)
84+
}),
85+
]
86+
} else {
87+
const directory = entry as FileSystemDirectoryEntry
88+
logger.debug('Handle directory recursivly', { name: directory.name })
89+
90+
// TODO: Implement this on `@nextcloud/upload`
91+
const absolutPath = `${davRootPath}${getUploader().destination.path}${path}${directory.name}`
92+
const davClient = davGetClient()
93+
const dirExists = await davClient.exists(absolutPath)
94+
if (!dirExists) {
95+
logger.debug('Directory does not exist, creating it', { absolutPath })
96+
await davClient.createDirectory(absolutPath, { recursive: true })
97+
const stat = await davClient.stat(absolutPath, { details: true, data: davGetDefaultPropfind() }) as ResponseDataDetailed<FileStat>
98+
emit('files:node:created', davResultToNode(stat.data))
99+
}
100+
101+
const entries = await readDirectory(directory)
102+
// sorted so we upload files first before starting next level
103+
const promises = entries.sort((a) => a.isFile ? -1 : 1)
104+
.map((file) => handleRecursiveUpload(file, `${path}${directory.name}/`))
105+
return (await Promise.all(promises)).flat()
106+
}
107+
}
108+
109+
/**
110+
* Read a directory using Filesystem API
111+
* @param directory the directory to read
112+
*/
113+
function readDirectory(directory: FileSystemDirectoryEntry) {
114+
const dirReader = directory.createReader()
115+
116+
return new Promise<FileSystemEntry[]>((resolve, reject) => {
117+
const entries = [] as FileSystemEntry[]
118+
const getEntries = () => {
119+
dirReader.readEntries((results) => {
120+
if (results.length) {
121+
entries.push(...results)
122+
getEntries()
123+
} else {
124+
resolve(entries)
125+
}
126+
}, (error) => {
127+
reject(error)
128+
})
129+
}
130+
131+
getEntries()
132+
})
133+
}

dist/files-main.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/files-main.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)