From 5c830878a71b365c36aecc8a3b0668c59b104775 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Fri, 4 Mar 2022 16:04:14 +0100 Subject: [PATCH] @uppy/utils: modernize `getDroppedFiles` `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. --- .../@uppy/utils/src/getDroppedFiles/index.js | 13 ++- .../getFilesAndDirectoriesFromDirectory.js | 4 +- .../utils/webkitGetAsEntryApi/index.js | 99 ++++++++++--------- 3 files changed, 63 insertions(+), 53 deletions(-) diff --git a/packages/@uppy/utils/src/getDroppedFiles/index.js b/packages/@uppy/utils/src/getDroppedFiles/index.js index 499bbc1841..7153ca629f 100644 --- a/packages/@uppy/utils/src/getDroppedFiles/index.js +++ b/packages/@uppy/utils/src/getDroppedFiles/index.js @@ -15,11 +15,16 @@ const fallbackApi = require('./utils/fallbackApi') * * @returns {Promise} - Array */ -module.exports = function getDroppedFiles (dataTransfer, { logDropError = () => {} } = {}) { +module.exports = 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) } diff --git a/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/getFilesAndDirectoriesFromDirectory.js b/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/getFilesAndDirectoriesFromDirectory.js index b3fee4091c..44b63e80c1 100644 --- a/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/getFilesAndDirectoriesFromDirectory.js +++ b/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/getFilesAndDirectoriesFromDirectory.js @@ -13,9 +13,9 @@ module.exports = function getFilesAndDirectoriesFromDirectory (directoryReader, // 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) diff --git a/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.js b/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.js index 4d05ee0e58..d7f0f9ad02 100644 --- a/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.js +++ b/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.js @@ -1,56 +1,61 @@ -const toArray = require('../../../toArray') const getRelativePath = require('./getRelativePath') const getFilesAndDirectoriesFromDirectory = require('./getFilesAndDirectoriesFromDirectory') -module.exports = 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) +module.exports = 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) + } + } + } + } }