You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

DropService.ts 4.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. /**
  2. * @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>
  3. *
  4. * @author Ferdinand Thiessen <opensource@fthiessen.de>
  5. *
  6. * @license AGPL-3.0-or-later
  7. *
  8. * This program is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU Affero General Public License as
  10. * published by the Free Software Foundation, either version 3 of the
  11. * License, or (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License
  19. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. *
  21. */
  22. import type { Upload } from '@nextcloud/upload'
  23. import type { FileStat, ResponseDataDetailed } from 'webdav'
  24. import { davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files'
  25. import { emit } from '@nextcloud/event-bus'
  26. import { getUploader } from '@nextcloud/upload'
  27. import { joinPaths } from '@nextcloud/paths'
  28. import { showError } from '@nextcloud/dialogs'
  29. import { translate as t } from '@nextcloud/l10n'
  30. import logger from '../logger.js'
  31. export const handleDrop = async (data: DataTransfer): Promise<Upload[]> => {
  32. // TODO: Maybe handle `getAsFileSystemHandle()` in the future
  33. const uploads = [] as Upload[]
  34. // 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
  35. const entries = [...data.items]
  36. .filter((item) => {
  37. if (item.kind !== 'file') {
  38. logger.debug('Skipping dropped item', { kind: item.kind, type: item.type })
  39. return false
  40. }
  41. return true
  42. })
  43. .map((item) => {
  44. // MDN recommends to try both, as it might be renamed in the future
  45. return (item as unknown as { getAsEntry?: () => FileSystemEntry|undefined})?.getAsEntry?.() ?? item.webkitGetAsEntry() ?? item
  46. })
  47. for (const entry of entries) {
  48. // Handle browser issues if Filesystem API is not available. Fallback to File API
  49. if (entry instanceof DataTransferItem) {
  50. logger.debug('Could not get FilesystemEntry of item, falling back to file')
  51. const file = entry.getAsFile()
  52. if (file === null) {
  53. logger.warn('Could not process DataTransferItem', { type: entry.type, kind: entry.kind })
  54. showError(t('files', 'One of the dropped files could not be processed'))
  55. } else {
  56. uploads.push(await handleFileUpload(file))
  57. }
  58. } else {
  59. logger.debug('Handle recursive upload', { entry: entry.name })
  60. // Use Filesystem API
  61. uploads.push(...await handleRecursiveUpload(entry))
  62. }
  63. }
  64. return uploads
  65. }
  66. const handleFileUpload = async (file: File, path: string = '') => {
  67. const uploader = getUploader()
  68. try {
  69. return await uploader.upload(`${path}${file.name}`, file)
  70. } catch (e) {
  71. showError(t('files', 'Uploading "{filename}" failed', { filename: file.name }))
  72. throw e
  73. }
  74. }
  75. const handleRecursiveUpload = async (entry: FileSystemEntry, path: string = ''): Promise<Upload[]> => {
  76. if (entry.isFile) {
  77. return [
  78. await new Promise<Upload>((resolve, reject) => {
  79. (entry as FileSystemFileEntry).file(
  80. async (file) => resolve(await handleFileUpload(file, path)),
  81. (error) => reject(error),
  82. )
  83. }),
  84. ]
  85. } else {
  86. const directory = entry as FileSystemDirectoryEntry
  87. // TODO: Implement this on `@nextcloud/upload`
  88. const absolutPath = joinPaths(davRootPath, getUploader().destination.path, path, directory.name)
  89. logger.debug('Handle directory recursively', { name: directory.name, absolutPath })
  90. const davClient = davGetClient()
  91. const dirExists = await davClient.exists(absolutPath)
  92. if (!dirExists) {
  93. logger.debug('Directory does not exist, creating it', { absolutPath })
  94. await davClient.createDirectory(absolutPath, { recursive: true })
  95. const stat = await davClient.stat(absolutPath, { details: true, data: davGetDefaultPropfind() }) as ResponseDataDetailed<FileStat>
  96. emit('files:node:created', davResultToNode(stat.data))
  97. }
  98. const entries = await readDirectory(directory)
  99. // sorted so we upload files first before starting next level
  100. const promises = entries.sort((a) => a.isFile ? -1 : 1)
  101. .map((file) => handleRecursiveUpload(file, `${path}${directory.name}/`))
  102. return (await Promise.all(promises)).flat()
  103. }
  104. }
  105. /**
  106. * Read a directory using Filesystem API
  107. * @param directory the directory to read
  108. */
  109. function readDirectory(directory: FileSystemDirectoryEntry) {
  110. const dirReader = directory.createReader()
  111. return new Promise<FileSystemEntry[]>((resolve, reject) => {
  112. const entries = [] as FileSystemEntry[]
  113. const getEntries = () => {
  114. dirReader.readEntries((results) => {
  115. if (results.length) {
  116. entries.push(...results)
  117. getEntries()
  118. } else {
  119. resolve(entries)
  120. }
  121. }, (error) => {
  122. reject(error)
  123. })
  124. }
  125. getEntries()
  126. })
  127. }