123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- /**
- * @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>
- *
- * @author Ferdinand Thiessen <opensource@fthiessen.de>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @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 { RootDirectory } from './DropServiceUtils'
-
- import { Folder, Node, NodeStatus, davRootPath } from '@nextcloud/files'
- import { getUploader, hasConflict } from '@nextcloud/upload'
- import { join } from 'path'
- import { joinPaths } from '@nextcloud/paths'
- import { showError, showInfo, showSuccess, showWarning } from '@nextcloud/dialogs'
- import { translate as t } from '@nextcloud/l10n'
- import Vue from 'vue'
-
- import { Directory, traverseTree, resolveConflict, createDirectoryIfNotExists } from './DropServiceUtils'
- import { handleCopyMoveNodeTo } from '../actions/moveOrCopyAction'
- import { MoveCopyAction } from '../actions/moveOrCopyActionUtils'
- import logger from '../logger.js'
-
- /**
- * This function converts a list of DataTransferItems to a file tree.
- * It uses the Filesystem API if available, otherwise it falls back to the File API.
- * The File API will NOT be available if the browser is not in a secure context (e.g. HTTP).
- * ⚠️ When using this method, you need to use it as fast as possible, as the DataTransferItems
- * will be cleared after the first access to the props of one of the entries.
- *
- * @param items the list of DataTransferItems
- */
- export const dataTransferToFileTree = async (items: DataTransferItem[]): Promise<RootDirectory> => {
- // Check if the browser supports the Filesystem API
- // We need to cache the entries to prevent Blink engine bug that clears
- // the list (`data.items`) after first access props of one of the entries
- const entries = items
- .filter((item) => {
- if (item.kind !== 'file') {
- logger.debug('Skipping dropped item', { kind: item.kind, type: item.type })
- return false
- }
- return true
- }).map((item) => {
- // MDN recommends to try both, as it might be renamed in the future
- return (item as unknown as { getAsEntry?: () => FileSystemEntry|undefined })?.getAsEntry?.()
- ?? item?.webkitGetAsEntry?.()
- ?? item
- }) as (FileSystemEntry | DataTransferItem)[]
-
- let warned = false
- const fileTree = new Directory('root') as RootDirectory
-
- // Traverse the file tree
- for (const entry of entries) {
- // Handle browser issues if Filesystem API is not available. Fallback to File API
- if (entry instanceof DataTransferItem) {
- logger.warn('Could not get FilesystemEntry of item, falling back to file')
-
- const file = entry.getAsFile()
- if (file === null) {
- logger.warn('Could not process DataTransferItem', { type: entry.type, kind: entry.kind })
- showError(t('files', 'One of the dropped files could not be processed'))
- continue
- }
-
- // Warn the user that the browser does not support the Filesystem API
- // we therefore cannot upload directories recursively.
- if (file.type === 'httpd/unix-directory' || !file.type) {
- if (!warned) {
- logger.warn('Browser does not support Filesystem API. Directories will not be uploaded')
- showWarning(t('files', 'Your browser does not support the Filesystem API. Directories will not be uploaded'))
- warned = true
- }
- continue
- }
-
- fileTree.contents.push(file)
- continue
- }
-
- // Use Filesystem API
- try {
- fileTree.contents.push(await traverseTree(entry))
- } catch (error) {
- // Do not throw, as we want to continue with the other files
- logger.error('Error while traversing file tree', { error })
- }
- }
-
- return fileTree
- }
-
- export const onDropExternalFiles = async (root: RootDirectory, destination: Folder, contents: Node[]): Promise<Upload[]> => {
- const uploader = getUploader()
-
- // Check for conflicts on root elements
- if (await hasConflict(root.contents, contents)) {
- root.contents = await resolveConflict(root.contents, destination, contents)
- }
-
- if (root.contents.length === 0) {
- logger.info('No files to upload', { root })
- showInfo(t('files', 'No files to upload'))
- return []
- }
-
- // Let's process the files
- logger.debug(`Uploading files to ${destination.path}`, { root, contents: root.contents })
- const queue = [] as Promise<Upload>[]
-
- const uploadDirectoryContents = async (directory: Directory, path: string) => {
- for (const file of directory.contents) {
- // This is the relative path to the resource
- // from the current uploader destination
- const relativePath = join(path, file.name)
-
- // If the file is a directory, we need to create it first
- // then browse its tree and upload its contents.
- if (file instanceof Directory) {
- const absolutePath = joinPaths(davRootPath, destination.path, relativePath)
- try {
- console.debug('Processing directory', { relativePath })
- await createDirectoryIfNotExists(absolutePath)
- await uploadDirectoryContents(file, relativePath)
- } catch (error) {
- showError(t('files', 'Unable to create the directory {directory}', { directory: file.name }))
- logger.error('', { error, absolutePath, directory: file })
- }
- continue
- }
-
- // If we've reached a file, we can upload it
- logger.debug('Uploading file to ' + join(destination.path, relativePath), { file })
-
- // Overriding the root to avoid changing the current uploader context
- queue.push(uploader.upload(relativePath, file, destination.source))
- }
- }
-
- // Pause the uploader to prevent it from starting
- // while we compute the queue
- uploader.pause()
-
- // Upload the files. Using '/' as the starting point
- // as we already adjusted the uploader destination
- await uploadDirectoryContents(root, '/')
- uploader.start()
-
- // Wait for all promises to settle
- const results = await Promise.allSettled(queue)
-
- // Check for errors
- const errors = results.filter(result => result.status === 'rejected')
- if (errors.length > 0) {
- logger.error('Error while uploading files', { errors })
- showError(t('files', 'Some files could not be uploaded'))
- return []
- }
-
- logger.debug('Files uploaded successfully')
- showSuccess(t('files', 'Files uploaded successfully'))
-
- return Promise.all(queue)
- }
-
- export const onDropInternalFiles = async (nodes: Node[], destination: Folder, contents: Node[], isCopy = false) => {
- const queue = [] as Promise<void>[]
-
- // Check for conflicts on root elements
- if (await hasConflict(nodes, contents)) {
- nodes = await resolveConflict(nodes, destination, contents)
- }
-
- if (nodes.length === 0) {
- logger.info('No files to process', { nodes })
- showInfo(t('files', 'No files to process'))
- return
- }
-
- for (const node of nodes) {
- Vue.set(node, 'status', NodeStatus.LOADING)
- // TODO: resolve potential conflicts prior and force overwrite
- queue.push(handleCopyMoveNodeTo(node, destination, isCopy ? MoveCopyAction.COPY : MoveCopyAction.MOVE))
- }
-
- // Wait for all promises to settle
- const results = await Promise.allSettled(queue)
- nodes.forEach(node => Vue.set(node, 'status', undefined))
-
- // Check for errors
- const errors = results.filter(result => result.status === 'rejected')
- if (errors.length > 0) {
- logger.error('Error while copying or moving files', { errors })
- showError(isCopy ? t('files', 'Some files could not be copied') : t('files', 'Some files could not be moved'))
- return
- }
-
- logger.debug('Files copy/move successful')
- showSuccess(isCopy ? t('files', 'Files copied successfully') : t('files', 'Files moved successfully'))
- }
|