Search code examples
reactjstypescriptfileupload

React - File selector component that can manage VERY large file count selections


I'm looking for a component that can get a list of files from the user. It just needs to get the files, not upload. The uploading process is already implemented, it just needs a list of files. The component needs to fulfill the following requirements:

  • Restrictable to only directory selection
  • Supports drag and drop on top of selection via the file dialogue
  • Captures all selected files in a single callback, meaning just a single state update
    • The file list should also be accessible before uploading so that they can be used in a preview
  • Returns the webkitRelativePath for all files

The closest I've gotten to achieving this is with Antd's Upload component. The limitation here was that the only way to capture the file list is with its onChange callback, which gets called once for every selected file. This means if a user is selecting thousands of files, which is a regular circumstance in my case, it will update the file list state thousands of times, causing thousands of rerenders and ultimately crashing the site.

const uploadProps = {
    accept: '*',
    multiple: true,
    customRequest: () => {},
    onRemove: (file: UploadFile) => {
        const index = props.fileList.indexOf(file)
        const newFileList = [...props.fileList]
        newFileList.splice(index, 1)
        props.setFileList(newFileList)
    },
    beforeUpload: () => {
        return false
    },
    onChange: (info: UploadChangeParam<UploadFile<any>>) => {
        if (JSON.stringify(info.fileList) !== JSON.stringify(props.fileList)) {
            console.log(info.fileList)
            props.setFileList(info.fileList)
        }
        if (info.fileList.length === 0 && props.progress !== 0) props.setProgress(0)
    },
    directory: true
}
<Dragger
    {...uploadProps}
    fileList={props.fileList.slice(fileListIndex, fileListIndex + 10)}
>
    <p className='ant-upload-text'>
        <b>Uploading to:</b> {S3_BUCKET.split('/').slice(1).join('/')}
    </p>
    <br></br>
    <p className='ant-upload-drag-icon'>
        <InboxOutlined />
    </p>
    <p className='ant-upload-text'>
        Browse or drag folder to upload
        <br />
        <strong>Uploading {props.fileList.length} files</strong>
        <br />
        Showing files {props.fileList.length ? fileListIndex + 1 : 0}-
        {Math.min(fileListIndex + 10, props.fileList.length)}
    </p>
</Dragger>

I tried a couple other libraries, but the second closest I've gotten was with the @rpldy/uploady library. I wrapped Antd's Dragger component to utilize its visual aspects with rpldy's Uploady and UploadDropZone components for the functional aspects. The Dropzone component fulfills the first three criteria, however it does not return the webkitRelativePath of the files in the file list.

<Uploady autoUpload={false} accept={'*'} webkitdirectory>
    <UploadDropZone
        onDragOverClassName='drag-over'
        htmlDirContentParams={{ recursive: true }}
        dropHandler={async (e, getFiles) => {
            let fileList = await getFiles()
            props.setFileList(fileList)
            fileList.map((file) => console.log(file.webkitRelativePath)) // Empty log
            return fileList
        }}
    >
        <Dragger
            openFileDialogOnClick={false}
            customRequest={() => {}}
            onRemove={(file: UploadFile) => {
                const index = props.fileList.indexOf(file as unknown as File)
                const newFileList = [...props.fileList]
                newFileList.splice(index, 1)
                props.setFileList(newFileList)
            }}
            fileList={
                props.fileList.slice(
                    fileListIndex,
                    fileListIndex + 10
                ) as unknown as UploadFile[]
            }
        >
            <p className='ant-upload-text'>
                <b>Uploading to:</b> {S3_BUCKET.split('/').slice(1).join('/')}
            </p>
            <br></br>
            <p className='ant-upload-drag-icon'>
                <InboxOutlined />
            </p>
            <p className='ant-upload-text'>
                <>
                    Browse or drag folder to upload
                    <br />
                    <UploadButton text='Browse' />
                    <br />
                    <strong>Uploading {props.fileList.length} files</strong>
                    <br />
                    Showing files{' '}
                    {props.fileList.length
                        ? fileListIndex + 1 > props.fileList.length
                            ? setFileListIndex(fileListIndex - 10)
                            : fileListIndex + 1
                        : 0}
                    -{Math.min(fileListIndex + 10, props.fileList.length)}
                </>
            </p>
        </Dragger>
    </UploadDropZone>
</Uploady>

Solution

  • Unfortunately, this is due to browser implementation decisions.

    I re-created a simple version of your solution in this code sandbox.

    In Firefox, I get the path as expected while in Chrome, the path is empty as you reported.

    The MDN definition for the webkitRelativePath property is: "the file's path relative to the directory selected by the user in an element with its webkitdirectory attribute set. "

    So it means it should only be available when actually using the file-selection dialog when the webkitdirectory

    However, you're in luck, as I've faced this before and built in the capability to retain the path in the file name. You can activate this by passing: withFullPath to the html-dir-content package used by Rpldy's upload-drop-zone:

    
    htmlDirContentParams={{ recursive: true, withFullPath: true }}
    
    

    The resulting file data will look like this:

    enter image description here