]> source.dussan.org Git - nextcloud-server.git/commitdiff
fix(files): Correctly handle dropping folders on file list
authorFerdinand Thiessen <opensource@fthiessen.de>
Sat, 2 Dec 2023 02:05:30 +0000 (03:05 +0100)
committerFerdinand Thiessen <opensource@fthiessen.de>
Wed, 6 Dec 2023 15:45:29 +0000 (16:45 +0100)
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
apps/files/src/components/DragAndDropNotice.vue
apps/files/src/services/DropService.ts [new file with mode: 0644]

index 73f6ad13f47d727b7f791fd86784b8244ed58214..66cddcaff977a8175d5026fed1d8d7aa7ad2e1aa 100644 (file)
@@ -2,8 +2,9 @@
        - @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
        -
        - @author John Molakvoæ <skjnldsv@protonmail.com>
+       - @author Ferdinand Thiessen <opensource@fthiessen.de>
        -
-       - @license GNU AGPL version 3 or any later version
+       - @license AGPL-3.0-or-later
        -
        - This program is free software: you can redistribute it and/or modify
        - it under the terms of the GNU Affero General Public License as
 </template>
 
 <script lang="ts">
-import { showError, showSuccess } from '@nextcloud/dialogs'
 import { translate as t } from '@nextcloud/l10n'
-import { getUploader } from '@nextcloud/upload'
 import { defineComponent } from 'vue'
 
 import TrayArrowDownIcon from 'vue-material-design-icons/TrayArrowDown.vue'
 
 import logger from '../logger.js'
+import { handleDrop } from '../services/DropService'
+import { showSuccess } from '@nextcloud/dialogs'
 
 export default defineComponent({
        name: 'DragAndDropNotice',
@@ -98,39 +99,29 @@ export default defineComponent({
                        event.preventDefault()
                        event.stopPropagation()
 
-                       if (event.dataTransfer && event.dataTransfer.files?.length > 0) {
-                               const uploader = getUploader()
-                               uploader.destination = this.currentFolder
-
+                       if (event.dataTransfer && event.dataTransfer.items.length > 0) {
                                // Start upload
                                logger.debug(`Uploading files to ${this.currentFolder.path}`)
-                               const promises = [...event.dataTransfer.files].map(async (file: File) => {
-                                       try {
-                                               return await uploader.upload(file.name, file)
-                                       } catch (e) {
-                                               showError(t('files', 'Uploading "{filename}" failed', { filename: file.name }))
-                                               throw e
-                                       }
-                               })
-
                                // Process finished uploads
-                               Promise.all(promises).then((uploads) => {
+                               handleDrop(event.dataTransfer).then((uploads) => {
                                        logger.debug('Upload terminated', { uploads })
                                        showSuccess(t('files', 'Upload successful'))
 
-                                       // Scroll to last upload if terminated
-                                       const lastUpload = uploads[uploads.length - 1]
-                                       if (lastUpload?.response?.headers?.['oc-fileid']) {
+                                       // Scroll to last upload in current directory if terminated
+                                       const lastUpload = uploads.findLast((upload) => !upload.file.webkitRelativePath.includes('/') && upload.response?.headers?.['oc-fileid'])
+                                       if (lastUpload !== undefined) {
                                                this.$router.push({
                                                        ...this.$route,
                                                        params: {
+                                                               view: this.$route.params?.view ?? 'files',
                                                                // Remove instanceid from header response
-                                                               fileid: parseInt(lastUpload.response?.headers?.['oc-fileid']),
+                                                               fileid: parseInt(lastUpload.response!.headers['oc-fileid']),
                                                        },
                                                })
                                        }
                                })
                        }
+                       this.dragover = false
                },
                t,
        },
diff --git a/apps/files/src/services/DropService.ts b/apps/files/src/services/DropService.ts
new file mode 100644 (file)
index 0000000..4b4f98a
--- /dev/null
@@ -0,0 +1,133 @@
+/**
+ * @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>
+ *
+ * @author Ferdinand Thiessen <opensource@fthiessen.de>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import type { Upload } from '@nextcloud/upload'
+import type { FileStat, ResponseDataDetailed } from 'webdav'
+
+import { showError } from '@nextcloud/dialogs'
+import { emit } from '@nextcloud/event-bus'
+import { davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files'
+import { translate as t } from '@nextcloud/l10n'
+import { getUploader } from '@nextcloud/upload'
+import logger from '../logger.js'
+
+export const handleDrop = async (data: DataTransfer) => {
+       // TODO: Maybe handle `getAsFileSystemHandle()` in the future
+
+       const uploads = [] as Upload[]
+       for (const item of data.items) {
+               if (item.kind !== 'file') {
+                       logger.debug('Skipping dropped item', { kind: item.kind, type: item.type })
+                       continue
+               }
+
+               // MDN recommends to try both, as it might be renamed in the future
+               const entry = (item as unknown as { getAsEntry?: () => FileSystemEntry|undefined})?.getAsEntry?.() ?? item.webkitGetAsEntry()
+
+               // Handle browser issues if Filesystem API is not available. Fallback to File API
+               if (entry === null) {
+                       logger.debug('Could not get FilesystemEntry of item, falling back to file')
+                       const file = item.getAsFile()
+                       if (file === null) {
+                               logger.warn('Could not process DataTransferItem', { type: item.type, kind: item.kind })
+                               showError(t('files', 'One of the dropped files could not be processed'))
+                       } else {
+                               uploads.push(await handleFileUpload(file))
+                       }
+               } else {
+                       logger.debug('Handle recursive upload', { entry: entry.name })
+                       // Use Filesystem API
+                       uploads.push(...await handleRecursiveUpload(entry))
+               }
+       }
+       return uploads
+}
+
+const handleFileUpload = async (file: File, path: string = '') => {
+       const uploader = getUploader()
+
+       try {
+               return await uploader.upload(`${path}${file.name}`, file)
+       } catch (e) {
+               showError(t('files', 'Uploading "{filename}" failed', { filename: file.name }))
+               throw e
+       }
+}
+
+const handleRecursiveUpload = async (entry: FileSystemEntry, path: string = ''): Promise<Upload[]> => {
+       if (entry.isFile) {
+               return [
+                       await new Promise<Upload>((resolve, reject) => {
+                               (entry as FileSystemFileEntry).file(
+                                       async (file) => resolve(await handleFileUpload(file, path)),
+                                       (error) => reject(error),
+                               )
+                       }),
+               ]
+       } else {
+               const directory = entry as FileSystemDirectoryEntry
+               logger.debug('Handle directory recursivly', { name: directory.name })
+
+               // TODO: Implement this on `@nextcloud/upload`
+               const absolutPath = `${davRootPath}${getUploader().destination.path}${path}${directory.name}`
+               const davClient = davGetClient()
+               const dirExists = await davClient.exists(absolutPath)
+               if (!dirExists) {
+                       logger.debug('Directory does not exist, creating it', { absolutPath })
+                       await davClient.createDirectory(absolutPath, { recursive: true })
+                       const stat = await davClient.stat(absolutPath, { details: true, data: davGetDefaultPropfind() }) as ResponseDataDetailed<FileStat>
+                       emit('files:node:created', davResultToNode(stat.data))
+               }
+
+               const entries = await readDirectory(directory)
+               // sorted so we upload files first before starting next level
+               const promises = entries.sort((a) => a.isFile ? -1 : 1)
+                       .map((file) => handleRecursiveUpload(file, `${path}${directory.name}/`))
+               return (await Promise.all(promises)).flat()
+       }
+}
+
+/**
+ * Read a directory using Filesystem API
+ * @param directory the directory to read
+ */
+function readDirectory(directory: FileSystemDirectoryEntry) {
+       const dirReader = directory.createReader()
+
+       return new Promise<FileSystemEntry[]>((resolve, reject) => {
+               const entries = [] as FileSystemEntry[]
+               const getEntries = () => {
+                       dirReader.readEntries((results) => {
+                               if (results.length) {
+                                       entries.push(...results)
+                                       getEntries()
+                               } else {
+                                       resolve(entries)
+                               }
+                       }, (error) => {
+                               reject(error)
+                       })
+               }
+
+               getEntries()
+       })
+}