Context
As a part of the exercise i was wondering - “is it possible to attach all files for particular folder?”. And here is a blog post how it was achieved.
TLDR: Yes it is possible. Scroll to the end of the article to see full example.
Part 1. Input component
Let’s design <input />
component to add support DND(drag and drop) support, since folder uploader works only with DND functionality.
export type Props = { onFilesSelect: (files: File[]) => void;};
export default function FileUploader({onFilesSelect}: Props){ // Drag'n Drop const handleDrop = () => {};
return ( <div id="dropzone" onDragEnter={handleDrop} onDragOver={handleDrop} onDrop={handleDrop} > <div id="boxtitle"> Drop Folders from explorer Here </div> </div> );}
Add Some Styles:
#dropzone { text-align: center; width: 300px; height: 100px; margin: 10px; padding: 10px; border: 4px dashed red; border-radius: 10px;}
#boxtitle { display: table-cell; vertical-align: middle; text-align: center; width: 300px; height: 100px;}
Stylish result will be looks like this 💅:
Result of the component
Part 2. DND handler
As you may see in previous part - we left handleDrop
empty. Now we put some code and see the results:
const handleDrop = (e:React.DragEvent<HTMLInputElement>) => { // stop default behavior e.preventDefault(); e.stopPropagation();
if(e.type !== 'drop' || e.type !== 'dragend') { // if unexpected event - return empty files array return []; } const items = e.dataTransfer.items; return Promise.all( Array.from(items).map(async (item) => { const entry = item.webkitGetAsEntry(); if (!entry) { return []; } // transform File Or Directory - to File[] const files = await scanFiles(entry); return files; }) ).then(files => { // call prop with file results. onFilesSelect(files); });
}
Part 3. Directory Reader
To convert files - we ‘ll write scanFiles
function. It’s returns Promise
since Reader.readEntries
returns callback. Also, this function will be recursive, since when we read the directory - we should call a reader to get all files/folders inside.
⚠️ From MDN. Note: To read all files in a directory,
readEntries
needs to be called repeatedly until it returns an empty array. In Chromium-based browsers, the following example will only return a max of 100 entries.
⚠️ webkitGetAsEntry API has no official W3C or WHATWG specification. It will works only in chromium-based browsers.
export default async function scanFiles(item: FileSystemEntry | FileSystemDirectoryEntry | FileSystemFileEntry, files: File[] = []): Promise<File[]> { if (item.isFile) { // return an array of files return [...files, await scanFile(item)]; } else if (item.isDirectory) { // create a reader const directoryReader = (item as FileSystemDirectoryEntry).createReader(); // make readEntries as a promise const res = await new Promise<File[]>((resolve, reject) => { directoryReader.readEntries(async (entries) => { // read direactories const result = await Promise.all( entries.map(async (entry) => await scanFiles(entry, files)) ); // NOTE: result will contains only first 100 elements, so me make a hack // we recall readEntries again and get more results. // NOTE 2: this hack allows you to get an 100^100 results? directoryReader.readEntries(async (entries) => { const result2 = await Promise.all( entries.map(async (entry) => await scanFiles(entry, files)) ); // NOTE 3: merge 2 arrays resolve([...result.flat(), ...result2.flat()]); }, reject); }, reject); }); return res; } // typescript was thoughts that this code is reachable. throw new Error("Unknown Error");}// file handlerasync function scanFile(item: FileSystemEntry): Promise<File> { const file = await new Promise<File>((resolve, reject) => { (item as FileSystemFileEntry).file( (fileResult) => { resolve(fileResult); }, (error) => { reject(error); } ); }); return file;}
Let me explain this piece of sh*t line by line.
- If item is file - return file (as array)
- if item is directory:
- create a
directoryReader
. - create a
Promise
.- call
readEntries
and for each entry callscanFiles
recursively. - call
directoryReader.readEntries
once again. SincereadEntries
returns only first 100 elements, but folders can contains more than 100 files. - resolve the promise with all collected entries. (I do
flat()
call since File handler also returns array.)
- call
- Return Promise result.
- create a
Performance
Performance can be slow since we doesn’t know about how many items inside directories (if items less than 100 - performance is good enough.) But the problem is - when folder contains thousands of files like in screenshot below
9k items - 9k function calls as a microtask.
Thoughts
- It’s better to use
iterator
instead ofPromise
with full result. It helps with performance issue. - Maybe WASM module will be better but not sure about support of the FS system - need to research.
Have a good day 👋. See you soon!