odysseyscience / react-s3-uploader

React component that renders an <input type="file"/> and automatically uploads to an S3 bucket
MIT License
826 stars 240 forks source link

Document support for using with react-dropzone #43

Open seanadkinson opened 8 years ago

seanadkinson commented 8 years ago

I've personally integrated this with react-dropzone, but the code was too specific to my use case to release.

We should add some code and documentation for the preferred way to integrate with react-dropzone.

For those wondering, I essentially bypassed the ReactS3Uploader react component and just included s3upload.js directly. Using the S3Upload class, I just wrapped each dropped file in a new instance, which kicks off the same upload process as using the component.

ttyao commented 8 years ago

I am essentially looking for doing the same thing! @seanadkinson do you mind share your solution (after remove all personal credentials of course)?

seanadkinson commented 8 years ago

So I basically wrapped the s3uploader in a class that fires events and returns a promise.

Here is the class I called S3FileUploader:

import Promise from 'bluebird';
import _ from 'lodash';
import klass from 'klass';
import Events from 'events';
import S3Upload from 'react-s3-uploader/s3upload.js';

const EventEmitter = Events.EventEmitter;

const S3FileUploader = klass({

    initialize(file, options) {
        this.file = file;
        this.preview = file.preview || URL.createObjectURL(file);
        this.emitter = new EventEmitter();
        this.opts = _.defaults(options || {}, {
            signingUrl: '/s3/signUpload'
        });
    },

    getFile() {
        return this.file;
    },

    getFileType() {
        return this.getFile().type;
    },

    getPreview() {
        return this.preview;
    },

    getFilename() {
        return this.getFile().name;
    },

    isImage() {
        return /^image\//.exec(this.getFileType());
    },

    getResultS3Filename() {
        return this.result && this.result.filename;
    },

    getPublicUrl() {
        return this.result && this.result.publicUrl;
    },

    on(event, callback) {
        this.emitter.addListener(event, callback);
    },

    off(event, callback) {
        this.emitter.removeListener(event, callback);
    },

    cancel() {
        if (this.upload) {
            this.upload.abortUpload();
            this.emitter.emit('abort');
            this.upload = null;
        }
    },

    markFailed() {
        this.failed = true;
    },

    isFailed() {
        return !!this.failed;
    },

    getContentDisposition() {
        let isPdf = this.getFileType() == 'application/pdf';
        return isPdf ? 'inline' : 'auto';
    },

    start() {
        return new Promise((resolve, reject) => {
            this.upload = new S3Upload({
                files: [this.file],
                signingUrl: this.opts.signingUrl,
                signingUrlQueryParams: this.opts.signingUrlQueryParams,
                contentDisposition: this.getContentDisposition(),
                onProgress: (percent, status) => {
                    this.emitter.emit('progress', percent, status);
                },
                onFinishS3Put: (result) => {
                    this.result = result;
                    this.emitter.emit('complete', result);
                    resolve(result);
                },
                onError: (status) => {
                    this.emitter.emit('error', status);
                    this.markFailed();
                    reject(status);
                }
            });
            this.emitter.once('abort', resolve);
        });
    }

});

export default S3FileUploader;

And then I'm using this in the onDrop function called from react-dropzone:

        onDrop(files) {
            let existingUploaders = this.state.uploaders;
            let existingUploadersByName = _.indexBy(existingUploaders, uploader => uploader.getFilename());

            let uploaders = _.map(files, (file) => {
                let filename = file.name;
                if (existingUploadersByName[filename] || !this.acceptsType(file.type)) {
                    return null;
                }

                let uploader = new S3FileUploader(file);

                uploader.on('progress', (percent) => {
                    this.setState({
                        [`progress-${filename}`]: {
                            percent: percent,
                            message: m('common.attachments.uploading')
                        }
                    });
                });

                uploader.on('complete', () => {
                    this.setState({
                        [`progress-${filename}`]: {
                            isComplete: true,
                            message: m('common.attachments.complete')
                        }
                    });
                });

                uploader.on('error', (status) => {
                    this.setState({
                        hasFailedUpload: true,
                        [`progress-${filename}`]: {
                            hidePercent: true,
                            message: status || m('common.attachments.error')
                        }
                    });
                });

                return uploader;
            });

            uploaders = _.compact(uploaders);

            if (!this.props.allowMultiple && uploaders.length) {
                _.each(existingUploaders, up => this.removeUpload(up));
                existingUploaders = [];
            }

            this.setState({
                uploaders: existingUploaders.concat(uploaders),
                uploading: true
            });

            return Promise.resolve()
                .then(() => {
                    return Promise.each(uploaders, uploader => uploader.start());
                })
                .finally(() => {
                    this.setState({ uploading: false });
                });
        },
ttyao commented 8 years ago

Thank you so much for the explanation!

talolard commented 8 years ago

This is great. Thanks!

sniepoort commented 7 years ago

@seanadkinson Could I ask how you are using the progress state from the onProgress event? What I assumed it was for was being able to show for example a progress bar for each file, until it has completed uploading. So wouldn't it make sense to have an array in the state with progress objects?

seanadkinson commented 7 years ago

@sniepoort With this code, I instantiate a new S3FileUploader to wrap each uploaded file. That means I actually have an array of uploaders (which I keep in this.state.uploaders), instead of an array of progress objects.

sniepoort commented 7 years ago

Thanks for getting back to me :) I can see that, but as far as I can tell, the array of uploaders don't have a progress state on them? They don't get updated onProgress, or am I missing something?

seanadkinson commented 7 years ago

Sorry, its been awhile since I've looked at this code. It looks like each S3FileUploader has an emitter that subscribes to the upload events when you call start(). And there is an on() method that let's you subscribe to events on the uploader.

So I'd expect you to be able to get updates from the progress of the uploader by doing something like:

    let uploader = new S3FileUploader(...);
    uploader.on('progress', this.updateProgressBar);
    uploader.start();

Does that help?

In my usage, I'm definitely rendering a progress bar per file upload, so let me know if you'd like me to go dig up some code to help.

sniepoort commented 7 years ago

Ah yes, I was also going in that direction. But how do you bind the updateProgressBar with each uploader? I'm guessing in the render you would loop over the uploaders then? Would be really great if you had a moment to dig up some code :) Always nice to save some time!

seanadkinson commented 7 years ago

@sniepoort Actually the code I was thinking about is already pasted above: https://github.com/odysseyscience/react-s3-uploader/issues/43#issuecomment-181953402

Here is a snippet of how it is used. We actually render a percent, not a progress bar, but it should be easy to modify.

        getUploaderStatus(uploader) {
            let filename = uploader.getFilename();
            let progress = this.state[`progress-${filename}`];

            if (progress && progress.isComplete) {
                return (
                    <a className="upload-link" href={uploader.getPublicUrl()} target="_blank">
                        {filename}
                    </a>
                );
            }

            let progressMessage = progress ? progress.message : m('common.attachments.waiting');
            let progressPercent = progress && !progress.hidePercent ? progress.percent + '%' : '';
            return `${filename}: ${progressMessage} ${progressPercent}`;
        }