- @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',
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,
},
--- /dev/null
+/**
+ * @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()
+ })
+}