diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index e88c291..c8d4b9a 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -66,7 +66,7 @@ class AjaxUploader extends Component { } }; - onFileDrop = (e: React.DragEvent) => { + onFileDrop = async (e: React.DragEvent) => { const { multiple } = this.props; e.preventDefault(); @@ -76,11 +76,11 @@ class AjaxUploader extends Component { } if (this.props.directory) { - traverseFileTree( + const files = await traverseFileTree( Array.prototype.slice.call(e.dataTransfer.items), - this.uploadFiles, (_file: RcFile) => attrAccept(_file, this.props.accept), ); + this.uploadFiles(files); } else { let files = [...e.dataTransfer.files].filter((file: RcFile) => attrAccept(file, this.props.accept), diff --git a/src/traverseFileTree.ts b/src/traverseFileTree.ts index 7b74c0b..6544ac5 100644 --- a/src/traverseFileTree.ts +++ b/src/traverseFileTree.ts @@ -10,36 +10,35 @@ interface InternalDataTransferItem extends DataTransferItem { path: string; } -const traverseFileTree = (files: InternalDataTransferItem[], callback, isAccepted) => { +// https://github.com/ant-design/ant-design/issues/50080 +const traverseFileTree = async (files: InternalDataTransferItem[], isAccepted) => { const flattenFileList = []; const progressFileList = []; files.forEach(file => progressFileList.push(file.webkitGetAsEntry() as any)); - function loopFiles(item: InternalDataTransferItem) { - const dirReader = item.createReader(); - function sequence() { - dirReader.readEntries((entries: InternalDataTransferItem[]) => { - const entryList = Array.prototype.slice.apply(entries); + async function readDirectory(directory: InternalDataTransferItem) { + const dirReader = directory.createReader(); + const entries = []; - progressFileList.push(...entryList); - // Check if all the file has been viewed - const isFinished = !entryList.length; - if (!isFinished) { - sequence(); - } + while (true) { + const results = await new Promise((resolve) => { + dirReader.readEntries(resolve, () => resolve([])); }); - } + const n = results.length; + + if (!n) { + break; + } - sequence(); - } - // eslint-disable-next-line @typescript-eslint/naming-convention - const _traverseFileTree = (item: InternalDataTransferItem, path?: string) => { - if (!item) { - return; + for (let i = 0; i < n; i++) { + entries.push(results[i]); + } } - // eslint-disable-next-line no-param-reassign - item.path = path || ''; - if (item.isFile) { + return entries; + } + + async function readFile(item: InternalDataTransferItem) { + return new Promise(reslove => { item.file(file => { if (isAccepted(file)) { // https://github.com/ant-design/ant-design/issues/16426 @@ -57,23 +56,39 @@ const traverseFileTree = (files: InternalDataTransferItem[], callback, isAccepte }, }); } - flattenFileList.push(file); + reslove(file); + } else { + reslove(null); } }); + }); + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + const _traverseFileTree = async (item: InternalDataTransferItem, path?: string) => { + if (!item) { + return; + } + // eslint-disable-next-line no-param-reassign + item.path = path || ''; + if (item.isFile) { + const file = await readFile(item); + if (file) { + flattenFileList.push(file); + } } else if (item.isDirectory) { - loopFiles(item); + const entries = await readDirectory(item); + progressFileList.push(...entries); } }; - function walkFiles() { - let wipIndex = 0; - while (wipIndex < progressFileList.length) { - _traverseFileTree(progressFileList[wipIndex]); - wipIndex++; - } - callback(flattenFileList); + let wipIndex = 0; + while (wipIndex < progressFileList.length) { + await _traverseFileTree(progressFileList[wipIndex]); + wipIndex++; } - walkFiles(); + + return flattenFileList; }; export default traverseFileTree; diff --git a/tests/uploader.spec.tsx b/tests/uploader.spec.tsx index ba3e47d..efcb70d 100644 --- a/tests/uploader.spec.tsx +++ b/tests/uploader.spec.tsx @@ -37,12 +37,49 @@ const makeFileSystemEntry = item => { return ret; }; +const makeFileSystemEntryAsync = item => { + const isDirectory = Array.isArray(item.children); + const ret = { + isDirectory, + isFile: !isDirectory, + file: handle => { + handle(new Item(item.name)); + }, + createReader: () => { + let first = true; + return { + async readEntries(handle, error) { + await sleep(100); + + if (!first) { + return handle([]); + } + + if (item.error && first) { + return error && error(new Error('read file error')) + } + + first = false; + return handle(item.children.map(makeFileSystemEntryAsync)); + }, + }; + }, + }; + return ret; +}; + const makeDataTransferItem = item => { return { webkitGetAsEntry: () => makeFileSystemEntry(item), }; }; +const makeDataTransferItemAsync = item => { + return { + webkitGetAsEntry: () => makeFileSystemEntryAsync(item), + }; +}; + describe('uploader', () => { let requests; let xhr; @@ -462,6 +499,95 @@ describe('uploader', () => { }, 100); }); + it('dragging and dropping files to upload through asynchronous file reading is run normal', done => { + const input = uploader.container.querySelector('input')!; + + const files = { + name: 'foo', + children: [ + { + name: 'bar', + children: [ + { + name: '1.png', + }, + { + name: '2.png', + }, + { + name: 'rc', + children: [ + { + name: '5.webp', + }, + { + name: '4.webp', + }, + ], + }, + ], + }, + ], + }; + fireEvent.drop(input, { dataTransfer: { items: [makeDataTransferItemAsync(files)] } }); + const mockStart = jest.fn(); + handlers.onStart = mockStart; + + setTimeout(() => { + expect(mockStart.mock.calls.length).toBe(2); + done(); + }, 1000); + }); + + it('dragging and dropping files to upload through asynchronous file reading with some readEntries method throw error', (done) => { + const input = uploader.container.querySelector('input')!; + + const files = { + name: 'foo', + children: [ + { + name: 'bar', + error: true, + children: [ + { + name: '1.png', + }, + { + name: 'ffc', + children: [ + { + name: '7.png', + }, + { + name: '8.png', + }, + ], + } + ], + }, + { + name: 'rc', + children: [ + { + name: '3.png', + }, + { + name: '4.webp', + }, + ], + }, + ], + }; + fireEvent.drop(input, { dataTransfer: { items: [makeDataTransferItemAsync(files)] } }); + const mockStart = jest.fn(); + handlers.onStart = mockStart; + + setTimeout(() => { + expect(mockStart.mock.calls.length).toBe(1); + done(); + }, 1000); + }); + it('unaccepted type files to upload will not trigger onStart when select directory', done => { const input = uploader.container.querySelector('input')!; const files = [