Skip to content

Commit

Permalink
@uppy/utils: modernize getDroppedFiles (#3534)
Browse files Browse the repository at this point in the history
`webkitGetAsEntry` is a non-standard/deprecated API, replacing it with
`getAsFileSystemHandle` when available.
This also work around a Chromium bug with symlinks.

Fixes: #3505.
  • Loading branch information
aduh95 committed Aug 4, 2022
1 parent 0d482cc commit 65f2551
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 53 deletions.
1 change: 1 addition & 0 deletions e2e/cypress/fixtures/images/cat-symbolic-link
1 change: 1 addition & 0 deletions e2e/cypress/fixtures/images/cat-symbolic-link.jpg
16 changes: 16 additions & 0 deletions e2e/cypress/integration/dashboard-ui.spec.ts
Expand Up @@ -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()', () => {
Expand All @@ -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:'))
})
})
13 changes: 9 additions & 4 deletions packages/@uppy/utils/src/getDroppedFiles/index.js
Expand Up @@ -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)
}
Expand Up @@ -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)
Expand Down
@@ -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)
}
}
}
}
}

0 comments on commit 65f2551

Please sign in to comment.