Ver Fonte

@uppy/utils: modernize `getDroppedFiles` (#3534)

`webkitGetAsEntry` is a non-standard/deprecated API, replacing it with
`getAsFileSystemHandle` when available.
This also work around a Chromium bug with symlinks.

Fixes: https://github.com/transloadit/uppy/issues/3505.
Antoine du Hamel há 2 anos atrás
pai
commit
65f2551645

+ 1 - 0
e2e/cypress/fixtures/images/cat-symbolic-link

@@ -0,0 +1 @@
+./cat.jpg

+ 1 - 0
e2e/cypress/fixtures/images/cat-symbolic-link.jpg

@@ -0,0 +1 @@
+./cat.jpg

+ 16 - 0
e2e/cypress/integration/dashboard-ui.spec.ts

@@ -2,6 +2,7 @@ describe('dashboard-ui', () => {
   beforeEach(() => {
     cy.visit('/dashboard-ui')
     cy.get('.uppy-Dashboard-input:first').as('file-input')
+    cy.get('.uppy-Dashboard-AddFiles').as('drop-target')
   })
 
   it('should not throw when calling uppy.close()', () => {
@@ -18,4 +19,19 @@ describe('dashboard-ui', () => {
       .should('have.length', 2)
       .each((element) => expect(element).attr('src').to.include('blob:'))
   })
+
+  it('should support drag&drop', () => {
+    cy.get('@drop-target').selectFile([
+      'cypress/fixtures/images/cat.jpg',
+      'cypress/fixtures/images/cat-symbolic-link',
+      'cypress/fixtures/images/cat-symbolic-link.jpg',
+      'cypress/fixtures/images/traffic.jpg',
+    ], { action: 'drag-drop' })
+
+    cy.get('.uppy-Dashboard-Item')
+      .should('have.length', 4)
+    cy.get('.uppy-Dashboard-Item-previewImg')
+      .should('have.length', 3)
+      .each((element) => expect(element).attr('src').to.include('blob:'))
+  })
 })

+ 9 - 4
packages/@uppy/utils/src/getDroppedFiles/index.js

@@ -15,11 +15,16 @@ import fallbackApi from './utils/fallbackApi.js'
  *
  * @returns {Promise} - Array<File>
  */
-export default function getDroppedFiles (dataTransfer, { logDropError = () => {} } = {}) {
+export default async function getDroppedFiles (dataTransfer, { logDropError = () => {} } = {}) {
   // Get all files from all subdirs. Works (at least) in Chrome, Mozilla, and Safari
-  if (dataTransfer.items?.[0] && 'webkitGetAsEntry' in dataTransfer.items[0]) {
-    return webkitGetAsEntryApi(dataTransfer, logDropError)
+  try {
+    const accumulator = []
+    for await (const file of webkitGetAsEntryApi(dataTransfer, logDropError)) {
+      accumulator.push(file)
+    }
+    return accumulator
   // Otherwise just return all first-order files
+  } catch {
+    return fallbackApi(dataTransfer)
   }
-  return fallbackApi(dataTransfer)
 }

+ 2 - 2
packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/getFilesAndDirectoriesFromDirectory.js

@@ -13,9 +13,9 @@ export default function getFilesAndDirectoriesFromDirectory (directoryReader, ol
       // According to the FileSystem API spec, getFilesAndDirectoriesFromDirectory()
       // must be called until it calls the onSuccess with an empty array.
       if (entries.length) {
-        setTimeout(() => {
+        queueMicrotask(() => {
           getFilesAndDirectoriesFromDirectory(directoryReader, newEntries, logDropError, { onSuccess })
-        }, 0)
+        })
       // Done iterating this particular directory
       } else {
         onSuccess(newEntries)

+ 52 - 47
packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.js

@@ -1,56 +1,61 @@
 import getRelativePath from './getRelativePath.js'
 import getFilesAndDirectoriesFromDirectory from './getFilesAndDirectoriesFromDirectory.js'
-import toArray from '../../../toArray.js'
 
-export default function webkitGetAsEntryApi (dataTransfer, logDropError) {
-  const files = []
-
-  const rootPromises = []
-
-  /**
-   * Returns a resolved promise, when :files array is enhanced
-   *
-   * @param {(FileSystemFileEntry|FileSystemDirectoryEntry)} entry
-   * @returns {Promise} - empty promise that resolves when :files is enhanced with a file
-   */
-  const createPromiseToAddFileOrParseDirectory = (entry) => new Promise((resolve) => {
-    // This is a base call
-    if (entry.isFile) {
-      // Creates a new File object which can be used to read the file.
-      entry.file(
-        (file) => {
-          // eslint-disable-next-line no-param-reassign
-          file.relativePath = getRelativePath(entry)
-          files.push(file)
-          resolve()
-        },
-        // Make sure we resolve on error anyway, it's fine if only one file couldn't be read!
-        (error) => {
-          logDropError(error)
-          resolve()
-        },
-      )
-      // This is a recursive call
-    } else if (entry.isDirectory) {
+/**
+ * Interop between deprecated webkitGetAsEntry and standard getAsFileSystemHandle.
+ */
+function getAsFileSystemHandleFromEntry (entry, logDropError) {
+  if (entry == null) return entry
+  return {
+    // eslint-disable-next-line no-nested-ternary
+    kind: entry.isFile ? 'file' : entry.isDirectory ? 'directory' : undefined,
+    getFile () {
+      return new Promise((resolve, reject) => entry.file(resolve, reject))
+    },
+    async* values () {
+      // If the file is a directory.
       const directoryReader = entry.createReader()
-      getFilesAndDirectoriesFromDirectory(directoryReader, [], logDropError, {
-        onSuccess: (entries) => resolve(Promise.all(
-          entries.map(createPromiseToAddFileOrParseDirectory),
-        )),
+      const entries = await new Promise(resolve => {
+        getFilesAndDirectoriesFromDirectory(directoryReader, [], logDropError, {
+          onSuccess: (dirEntries) => resolve(dirEntries.map(file => getAsFileSystemHandleFromEntry(file, logDropError))),
+        })
       })
-    }
-  })
+      yield* entries
+    },
+  }
+}
 
+async function* createPromiseToAddFileOrParseDirectory (entry) {
   // For each dropped item, - make sure it's a file/directory, and start deepening in!
-  toArray(dataTransfer.items)
-    .forEach((item) => {
-      const entry = item.webkitGetAsEntry()
-      // :entry can be null when we drop the url e.g.
-      if (entry) {
-        rootPromises.push(createPromiseToAddFileOrParseDirectory(entry))
-      }
-    })
+  if (entry.kind === 'file') {
+    const file = await entry.getFile()
+    if (file !== null) {
+      file.relativePath = getRelativePath(entry)
+      yield file
+    }
+  } else if (entry.kind === 'directory') {
+    for await (const handle of entry.values()) {
+      yield* createPromiseToAddFileOrParseDirectory(handle)
+    }
+  }
+}
 
-  return Promise.all(rootPromises)
-    .then(() => files)
+export default async function* getFilesFromDataTransfer (dataTransfer, logDropError) {
+  for (const item of dataTransfer.items) {
+    const lastResortFile = item.getAsFile() // Chromium bug, see https://github.com/transloadit/uppy/issues/3505.
+    const entry = await item.getAsFileSystemHandle?.()
+      ?? getAsFileSystemHandleFromEntry(item.webkitGetAsEntry(), logDropError)
+    // :entry can be null when we drop the url e.g.
+    if (entry != null) {
+      try {
+        yield* createPromiseToAddFileOrParseDirectory(entry, logDropError)
+      } catch (err) {
+        if (lastResortFile) {
+          yield lastResortFile
+        } else {
+          logDropError(err)
+        }
+      }
+    }
+  }
 }