Skip to content

Commit

Permalink
Merge pull request #42071 from nextcloud/fix/stable28/files-handle-dr…
Browse files Browse the repository at this point in the history
…op-folders-correctly

[stable28] fix(files): Correctly handle dropping folders on file list
  • Loading branch information
susnux authored Dec 6, 2023
2 parents 0577c9a + 05df9ac commit 465f216
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 24 deletions.
33 changes: 12 additions & 21 deletions apps/files/src/components/DragAndDropNotice.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -33,14 +34,14 @@
</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',
Expand Down Expand Up @@ -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,
},
Expand Down
133 changes: 133 additions & 0 deletions apps/files/src/services/DropService.ts
Original file line number Diff line number Diff line change
@@ -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()
})
}
4 changes: 2 additions & 2 deletions dist/files-main.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/files-main.js.map

Large diffs are not rendered by default.

0 comments on commit 465f216

Please sign in to comment.