diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2023-12-02 03:05:30 +0100 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2023-12-06 16:45:29 +0100 |
commit | dd8897bb3155b6b4e56f10ca7a8e33578fbdd1ea (patch) | |
tree | 5ee8f874ed645d48a30a4d24b66638777a826cf2 /apps/files/src/services | |
parent | e1ecf798d9ccce0b5d0c1b90d6cd93e22c2d9588 (diff) | |
download | nextcloud-server-dd8897bb3155b6b4e56f10ca7a8e33578fbdd1ea.tar.gz nextcloud-server-dd8897bb3155b6b4e56f10ca7a8e33578fbdd1ea.zip |
fix(files): Correctly handle dropping folders on file list
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Diffstat (limited to 'apps/files/src/services')
-rw-r--r-- | apps/files/src/services/DropService.ts | 133 |
1 files changed, 133 insertions, 0 deletions
diff --git a/apps/files/src/services/DropService.ts b/apps/files/src/services/DropService.ts new file mode 100644 index 00000000000..4b4f98a01d6 --- /dev/null +++ b/apps/files/src/services/DropService.ts @@ -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() + }) +} |