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,
readEntriesneeds 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 handler
async 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 readEntriesand for each entry callscanFilesrecursively.
- call directoryReader.readEntriesonce again. SincereadEntriesreturns 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 iteratorinstead ofPromisewith 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!
 
 