mirror of
https://github.com/nextcloud/server.git
synced 2026-03-02 21:41:12 -05:00
fix(files): Correctly handle dropping folders on file list
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
e1ecf798d9
commit
dd8897bb31
2 changed files with 145 additions and 21 deletions
|
|
@ -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
|
||||
|
|
@ -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',
|
||||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
133
apps/files/src/services/DropService.ts
Normal file
133
apps/files/src/services/DropService.ts
Normal file
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
Loading…
Reference in a new issue