'use strict'; import React from 'react/addons'; import fineUploader from 'fineUploader'; import Q from 'q'; import S3Fetcher from '../../fetchers/s3_fetcher'; import FileDragAndDrop from './ascribe_file_drag_and_drop/file_drag_and_drop'; import ErrorQueueStore from '../../stores/error_queue_store'; import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationActions from '../../actions/global_notification_actions'; import AppConstants from '../../constants/application_constants'; import { ErrorClasses, testErrorAgainstAll } from '../../constants/error_constants'; import { RETRY_ATTEMPT_TO_SHOW_CONTACT_US } from '../../constants/uploader_constants'; import { displayValidFilesFilter, FileStatus, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils'; import { getCookie } from '../../utils/fetch_api_utils'; import { computeHashOfFile, extractFileExtensionFromString } from '../../utils/file_utils'; import { getLangText } from '../../utils/lang_utils'; const { shape, string, oneOfType, number, func, bool, any, object, oneOf, element, arrayOf } = React.PropTypes; const ReactS3FineUploader = React.createClass({ propTypes: { areAssetsDownloadable: bool, areAssetsEditable: bool, errorNotificationMessage: string, handleChangedFile: func, // for when a file is dropped or selected, TODO: rename to onChangedFile submitFile: func, // for when a file has been successfully uploaded, TODO: rename to onSubmitFile onValidationFailed: func, setWarning: func, // for when the parent component wants to be notified of uploader warnings (ie. upload failed) showErrorPrompt: bool, // Handle form validation setIsUploadReady: func, //TODO: rename to setIsUploaderValidated isReadyForFormSubmission: func, // We encountered some cases where people had difficulties to upload their // works to ascribe due to a slow internet connection. // One solution we found in the process of tackling this problem was to hash // the file in the browser using md5 and then uploading the resulting text document instead // of the actual file. // // This boolean and string essentially enable that behavior. // Right now, we determine which upload method to use by appending a query parameter, // which should be passed into 'uploadMethod': // 'hash': upload using the hash // 'upload': upload full file (default if not specified) enableLocalHashing: bool, uploadMethod: oneOf(['hash', 'upload']), // A class of a file the user has to upload // Needs to be defined both in singular as well as in plural fileClassToUpload: shape({ singular: string, plural: string }), // Uploading functionality of react fineuploader is disconnected from its UI // layer, which means that literally every (properly adjusted) react element // can handle the UI handling. fileInputElement: oneOfType([ func, element ]), // S3 helpers createBlobRoutine: shape({ url: string, pieceId: number }), keyRoutine: shape({ url: string, fileClass: string, pieceId: number }), // FineUploader options debug: bool, autoUpload: bool, chunking: shape({ enabled: bool }), cors: shape({ expected: bool }), deleteFile: shape({ enabled: bool, method: string, endpoint: string, customHeaders: object }).isRequired, formatFileName: func, messages: shape({ unsupportedBrowser: string }), multiple: bool, objectProperties: shape({ acl: string }), request: shape({ endpoint: string, accessKey: string, params: shape({ csrfmiddlewaretoken: string }) }), resume: shape({ enabled: bool }), retry: shape({ enableAuto: bool }), session: shape({ customHeaders: object, endpoint: string, params: object, refreshOnRequests: bool }), signature: shape({ endpoint: string }).isRequired, uploadSuccess: shape({ method: string, endpoint: string, params: shape({ isBrowserPreviewCapable: any, // maybe fix this later bitcoin_ID_noPrefix: string }) }), validation: shape({ itemLimit: number, sizeLimit: number, allowedExtensions: arrayOf(string) }) }, getDefaultProps() { return { errorNotificationMessage: getLangText('Oops, we had a problem uploading your file. Please contact us if this happens repeatedly.'), showErrorPrompt: false, fileClassToUpload: { singular: getLangText('file'), plural: getLangText('files') }, fileInputElement: FileDragAndDrop, // FineUploader options autoUpload: true, debug: false, multiple: false, objectProperties: { acl: 'public-read', bucket: 'ascribe0' }, request: { //endpoint: 'https://www.ascribe.io.global.prod.fastly.net', endpoint: 'https://ascribe0.s3.amazonaws.com', accessKey: 'AKIAIVCZJ33WSCBQ3QDA' }, uploadSuccess: { params: { isBrowserPreviewCapable: fineUploader.supportedFeatures.imagePreviews } }, cors: { expected: true, sendCredentials: true }, chunking: { enabled: true, concurrent: { enabled: true } }, resume: { enabled: true }, retry: { enableAuto: false }, session: { endpoint: null }, messages: { unsupportedBrowser: '

' + getLangText('Upload is not functional in IE7 as IE7 has no support for CORS!') + '

' }, formatFileName: function(name) { // fix maybe if (name !== undefined && name.length > 26) { name = name.slice(0, 15) + '...' + name.slice(-15); } return name; } }; }, getInitialState() { return { filesToUpload: [], uploader: this.createNewFineUploader(), csrfToken: getCookie(AppConstants.csrftoken), errorState: { manualRetryAttempt: 0, errorClass: null }, uploadInProgress: false, // -1: aborted // -2: uninitialized hashingProgress: -2, // this is for logging chunks: {} }; }, componentWillUpdate() { // since the csrf header is defined in this component's props, // everytime the csrf cookie is changed we'll need to reinitalize // fineuploader and update the actual csrf token let potentiallyNewCSRFToken = getCookie(AppConstants.csrftoken); if(this.state.csrfToken !== potentiallyNewCSRFToken) { this.setState({ uploader: this.createNewFineUploader(), csrfToken: potentiallyNewCSRFToken }); } }, componentWillUnmount() { // Without this method, fineuploader will continue to try to upload artworks // even though this component is not mounted any more. // Therefore we cancel all uploads this.state.uploader.cancelAll(); }, createNewFineUploader() { return new fineUploader.s3.FineUploaderBasic(this.propsToConfig()); }, propsToConfig() { const objectProperties = Object.assign({}, this.props.objectProperties); objectProperties.key = this.requestKey; return { autoUpload: this.props.autoUpload, debug: this.props.debug, objectProperties: objectProperties, // do a special key handling here request: this.props.request, signature: this.props.signature, uploadSuccess: this.props.uploadSuccess, cors: this.props.cors, chunking: this.props.chunking, resume: this.props.resume, deleteFile: this.props.deleteFile, session: this.props.session, validation: this.props.validation, messages: this.props.messages, formatFileName: this.props.formatFileName, multiple: this.props.multiple, retry: this.props.retry, callbacks: { onAllComplete: this.onAllComplete, onComplete: this.onComplete, onCancel: this.onCancel, onProgress: this.onProgress, onDeleteComplete: this.onDeleteComplete, onSessionRequestComplete: this.onSessionRequestComplete, onError: this.onError, onUploadChunk: this.onUploadChunk, onUploadChunkSuccess: this.onUploadChunkSuccess } }; }, // Resets the whole react fineuploader component to its initial state reset() { // Cancel all currently ongoing uploads this.cancelUploads(); // and reset component in general this.state.uploader.reset(); // proclaim that upload is not ready this.props.setIsUploadReady(false); // reset any warnings propagated to parent this.setWarning(false); // reset internal data structures of component this.setState(this.getInitialState()); }, requestKey(fileId) { let filename = this.state.uploader.getName(fileId); let uuid = this.state.uploader.getUuid(fileId); return Q.Promise((resolve, reject) => { window.fetch(this.props.keyRoutine.url, { method: 'post', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'X-CSRFToken': getCookie(AppConstants.csrftoken) }, credentials: 'include', body: JSON.stringify({ 'filename': filename, 'category': this.props.keyRoutine.fileClass, 'uuid': uuid, 'piece_id': this.props.keyRoutine.pieceId }) }) .then((res) => { return res.json(); }) .then((res) =>{ resolve(res.key); }) .catch((err) => { this.onErrorPromiseProxy(err); reject(err); }); }); }, createBlob(file) { const { createBlobRoutine } = this.props; return Q.Promise((resolve, reject) => { // if createBlobRoutine is not defined, // we're progressing right away without posting to S3 // so that this can be done manually by the form if (!createBlobRoutine) { // still we warn the user of this component console.warn('createBlobRoutine was not defined for ReactS3FineUploader. Continuing without creating the blob on the server.'); resolve(); return; } window.fetch(createBlobRoutine.url, { method: 'post', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'X-CSRFToken': getCookie(AppConstants.csrftoken) }, credentials: 'include', body: JSON.stringify({ 'filename': file.name, 'key': file.key, 'piece_id': createBlobRoutine.pieceId }) }) .then((res) => { return res.json(); }) .then((res) => { if(res.otherdata) { file.s3Url = res.otherdata.url_safe; file.s3UrlSafe = res.otherdata.url_safe; } else if(res.digitalwork) { file.s3Url = res.digitalwork.url_safe; file.s3UrlSafe = res.digitalwork.url_safe; } else if(res.contractblob) { file.s3Url = res.contractblob.url_safe; file.s3UrlSafe = res.contractblob.url_safe; } else if(res.thumbnail) { file.s3Url = res.thumbnail.url_safe; file.s3UrlSafe = res.thumbnail.url_safe; } else { throw new Error(getLangText('Could not find a url to download.')); } resolve(res); }) .catch((err) => { this.onErrorPromiseProxy(err); reject(err); }); }); }, // Cancel uploads and clear previously selected files on the input element cancelUploads(id) { typeof id !== 'undefined' ? this.state.uploader.cancel(id) : this.state.uploader.cancelAll(); // Reset the file input element to clear the previously selected files so that // the user can reselect them again. this.clearFileSelection(); }, checkFormSubmissionReady() { const { isReadyForFormSubmission, setIsUploadReady } = this.props; // since the form validation props isReadyForFormSubmission and setIsUploadReady // are optional, we'll only trigger them when they're actually defined if (typeof isReadyForFormSubmission === 'function' && typeof setIsUploadReady === 'function') { // set uploadReady to true if the uploader's ready for submission setIsUploadReady(isReadyForFormSubmission(this.state.filesToUpload)); } else { console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader'); } }, clearFileSelection() { const { fileInput } = this.refs; if (fileInput && typeof fileInput.clearSelection === 'function') { fileInput.clearSelection(); } }, getAllowedExtensions() { const { validation: { allowedExtensions } = {} } = this.props; if (allowedExtensions && allowedExtensions.length) { return transformAllowedExtensionsToInputAcceptProp(allowedExtensions); } else { return null; } }, getUploadErrorClass({ type = 'upload', reason, xhr }) { const { manualRetryAttempt } = this.state.errorState; let matchedErrorClass; if ('onLine' in window.navigator && !window.navigator.onLine) { // If the user's offline, this is definitely the most important error to show. // TODO: use a better mechanism for checking network state, ie. offline.js matchedErrorClass = ErrorClasses.upload.offline; } else if (manualRetryAttempt === RETRY_ATTEMPT_TO_SHOW_CONTACT_US) { // Use the contact us error class if they've retried a number of times // and are still unsuccessful matchedErrorClass = ErrorClasses.upload.contactUs; } else { matchedErrorClass = testErrorAgainstAll({ type, reason, xhr }); if (!matchedErrorClass) { // If none found, show the next error message in the queue for upload errors matchedErrorClass = ErrorQueueStore.getNextError('upload'); } } return matchedErrorClass; }, getXhrErrorComment(xhr) { if (xhr) { return { response: xhr.response, url: xhr.responseURL, status: xhr.status, statusText: xhr.statusText }; } }, isDropzoneInactive() { const { areAssetsEditable, enableLocalHashing, multiple, showErrorPrompt, uploadMethod } = this.props; const { errorState, filesToUpload } = this.state; const filesToDisplay = filesToUpload.filter((file) => { return file.status !== FileStatus.DELETED && file.status !== FileStatus.CANCELED && file.status !== FileStatus.UPLOAD_FAILED && file.size !== -1; }); return (enableLocalHashing && !uploadMethod) || !areAssetsEditable || (showErrorPrompt && errorState.errorClass) || (!multiple && filesToDisplay.length > 0); }, isFileValid(file) { const { validation: { allowedExtensions, sizeLimit = 0 }, onValidationFailed } = this.props; const fileExt = extractFileExtensionFromString(file.name); if (file.size > sizeLimit) { const fileSizeInMegaBytes = sizeLimit / 1000000; const notification = new GlobalNotificationModel(getLangText('A file you submitted is bigger than ' + fileSizeInMegaBytes + 'MB.'), 'danger', 5000); GlobalNotificationActions.appendGlobalNotification(notification); if (typeof onValidationFailed === 'function') { onValidationFailed(file); } return false; } else if (allowedExtensions && !allowedExtensions.includes(fileExt)) { const notification = new GlobalNotificationModel(getLangText(`The file you've submitted is of an invalid file format: Valid format(s): ${allowedExtensions.join(', ')}`), 'danger', 5000); GlobalNotificationActions.appendGlobalNotification(notification); return false; } else { return true; } }, selectValidFiles(files) { return Array.from(files).reduce((validFiles, file) => { if (this.isFileValid(file)) { validFiles.push(file); } return validFiles; }, []); }, // This method has been made promise-based to immediately afterwards // call a callback function (instantly after this.setState went through) // This is e.g. needed when showing/hiding the optional thumbnail upload // field in the registration form setStatusOfFile(fileId, status) { return Q.Promise((resolve) => { let changeSet = {}; if (status === FileStatus.DELETED || status === FileStatus.CANCELED || status === FileStatus.UPLOAD_FAILED) { changeSet.progress = { $set: 0 }; } changeSet.status = { $set: status }; let filesToUpload = React.addons.update(this.state.filesToUpload, { [fileId]: changeSet }); this.setState({ filesToUpload }, resolve); }); }, setThumbnailForFileId(fileId, url) { const { filesToUpload } = this.state; if(fileId < filesToUpload.length) { const changeSet = { $set: url }; const newFilesToUpload = React.addons.update(filesToUpload, { [fileId]: { thumbnailUrl: changeSet } }); this.setState({ filesToUpload: newFilesToUpload }); } else { throw new Error('Accessing an index out of range of filesToUpload'); } }, setWarning(hasWarning) { if (typeof this.props.setWarning === 'function') { this.props.setWarning(hasWarning); } }, /* FineUploader specific callback function handlers */ onUploadChunk(id, name, chunkData) { let chunks = this.state.chunks; chunks[id + '-' + chunkData.startByte + '-' + chunkData.endByte] = { id, name, chunkData, completed: false }; let startedChunks = React.addons.update(this.state.startedChunks, { $set: chunks }); this.setState({ startedChunks }); }, onUploadChunkSuccess(id, chunkData, responseJson, xhr) { let chunks = this.state.chunks; let chunkKey = id + '-' + chunkData.startByte + '-' + chunkData.endByte; if(chunks[chunkKey]) { chunks[chunkKey].completed = true; chunks[chunkKey].responseJson = responseJson; chunks[chunkKey].xhr = xhr; let startedChunks = React.addons.update(this.state.startedChunks, { $set: chunks }); this.setState({ startedChunks }); } }, onAllComplete(succeed, failed) { if (this.state.uploadInProgress) { this.setState({ uploadInProgress: false }); } }, onComplete(id, name, res, xhr) { // There has been an issue with the server's connection if (xhr && xhr.status === 0 && res.success) { console.logGlobal(new Error('Upload succeeded with a status code 0'), { files: this.state.filesToUpload, chunks: this.state.chunks, xhr: this.getXhrErrorComment(xhr) }); // onError will catch any errors, so we can ignore them here } else if (!res.error && res.success) { let files = this.state.filesToUpload; // Set the state of the completed file to 'upload successful' in order to // remove it from the GUI files[id].status = FileStatus.UPLOAD_SUCCESSFUL; files[id].key = this.state.uploader.getKey(id); let filesToUpload = React.addons.update(this.state.filesToUpload, { $set: files }); this.setState({ filesToUpload }); // Only after the blob has been created server-side, we can make the form submittable. this.createBlob(files[id]) .then(() => { if (typeof this.props.submitFile === 'function') { this.props.submitFile(files[id]); } else { console.warn('You didn\'t define submitFile as a prop in react-s3-fine-uploader'); } this.checkFormSubmissionReady(); }); } }, /** * We want to channel all errors in this component through one single method. * As fineuploader's `onError` method cannot handle the callback parameters of * a promise we define this proxy method to crunch them into the correct form. * * @param {error} err a plain Javascript error */ onErrorPromiseProxy(err) { this.onError(null, null, err.message); }, onError(id, name, errorReason, xhr) { const { errorNotificationMessage, showErrorPrompt } = this.props; const { chunks, filesToUpload } = this.state; console.logGlobal(errorReason, { files: filesToUpload, chunks: chunks, xhr: this.getXhrErrorComment(xhr) }); let notificationMessage; if (showErrorPrompt) { this.setStatusOfFile(id, FileStatus.UPLOAD_FAILED); // If we've already found an error on this upload, just ignore other errors // that pop up. They'll likely pop up again when the user retries. if (!this.state.errorState.errorClass) { notificationMessage = errorNotificationMessage; const errorState = React.addons.update(this.state.errorState, { errorClass: { $set: this.getUploadErrorClass({ reason: errorReason, xhr }) } }); this.setState({ errorState }); this.setWarning(true); } } else { notificationMessage = errorReason || errorNotificationMessage; this.cancelUploads(); } if (notificationMessage) { const notification = new GlobalNotificationModel(notificationMessage, 'danger', 5000); GlobalNotificationActions.appendGlobalNotification(notification); } }, onCancel(id) { // when a upload is canceled, we need to update this components file array this.setStatusOfFile(id, FileStatus.CANCELED) .then(() => { if(typeof this.props.handleChangedFile === 'function') { this.props.handleChangedFile(this.state.filesToUpload[id]); } }); let notification = new GlobalNotificationModel(getLangText('File upload canceled'), 'success', 5000); GlobalNotificationActions.appendGlobalNotification(notification); this.checkFormSubmissionReady(); // FineUploader's onAllComplete event doesn't fire if all files are cancelled // so we need to double check if this is the last file getting cancelled. // // Because we're calling FineUploader.getInProgress() in a cancel callback, // the current file getting cancelled is still considered to be in progress // so there will be one file left in progress when we're cancelling the last file. if (this.state.uploader.getInProgress() === 1) { this.setState({ uploadInProgress: false }); } return true; }, onProgress(id, name, uploadedBytes, totalBytes) { let filesToUpload = React.addons.update(this.state.filesToUpload, { [id]: { progress: { $set: (uploadedBytes / totalBytes) * 100} } }); this.setState({ filesToUpload }); }, onSessionRequestComplete(response, success) { if(success) { // fetch blobs for images response = response.map((file) => { file.url = file.s3UrlSafe; file.status = FileStatus.ONLINE; file.progress = 100; return file; }); // add file to filesToUpload let updatedFilesToUpload = this.state.filesToUpload.concat(response); // refresh all files ids, updatedFilesToUpload = updatedFilesToUpload.map((file, i) => { file.id = i; return file; }); let filesToUpload = React.addons.update(this.state.filesToUpload, {$set: updatedFilesToUpload}); this.setState({filesToUpload }); } else { // server has to respond with 204 //let notification = new GlobalNotificationModel('Could not load attached files (Further data)', 'danger', 10000); //GlobalNotificationActions.appendGlobalNotification(notification); // //throw new Error('The session request failed', response); } }, onDeleteComplete(id, xhr, isError) { if(isError) { this.setStatusOfFile(id, FileStatus.ONLINE); let notification = new GlobalNotificationModel(getLangText('There was an error deleting your file.'), 'danger', 10000); GlobalNotificationActions.appendGlobalNotification(notification); } else { let notification = new GlobalNotificationModel(getLangText('File deleted'), 'success', 5000); GlobalNotificationActions.appendGlobalNotification(notification); } this.checkFormSubmissionReady(); }, handleDeleteFile(fileId) { // We set the files state to 'deleted' immediately, so that the user is not confused with // the unresponsiveness of the UI // // If there is an error during the deletion, we will just change the status back to FileStatus.ONLINE // and display an error message this.setStatusOfFile(fileId, FileStatus.DELETED) .then(() => { if(typeof this.props.handleChangedFile === 'function') { this.props.handleChangedFile(this.state.filesToUpload[fileId]); } }); // In some instances (when the file was already uploaded and is just displayed to the user // - for example in the contract or additional files dialog) // fineuploader does not register an id on the file (we do, don't be confused by this!). // Since you can only delete a file by its id, we have to implement this method ourselves // // So, if an id is not present, we delete the file manually // To check which files are already uploaded from previous sessions we check their status. // If they are, it is "online" if(this.state.filesToUpload[fileId].status !== FileStatus.ONLINE) { // delete file from server this.state.uploader.deleteFile(fileId); // this is being continued in onDeleteFile, as // fineuploaders deleteFile does not return a correct callback or // promise } else { let fileToDelete = this.state.filesToUpload[fileId]; S3Fetcher .deleteFile(fileToDelete.s3Key, fileToDelete.s3Bucket) .then(() => this.onDeleteComplete(fileToDelete.id, null, false)) .catch(() => this.onDeleteComplete(fileToDelete.id, null, true)); } }, handleCancelFile(fileId) { this.cancelUploads(fileId); }, handleCancelHashing() { // Every progress tick of the hashing function in handleUploadFile there is a // check if this.state.hashingProgress is -1. If so, there is an error thrown that cancels // the hashing of all files immediately. this.setState({ hashingProgress: -1 }); }, handlePauseFile(fileId) { if(this.state.uploader.pauseUpload(fileId)) { this.setStatusOfFile(fileId, FileStatus.PAUSED); } else { throw new Error(getLangText('File upload could not be paused.')); } }, handleResumeFile(fileId) { if(this.state.uploader.continueUpload(fileId)) { this.setStatusOfFile(fileId, FileStatus.UPLOADING); } else { throw new Error(getLangText('File upload could not be resumed.')); } }, handleRetryFiles(fileIds) { let filesToUpload = this.state.filesToUpload; if (fileIds.constructor !== Array) { fileIds = [ fileIds ]; } fileIds.forEach((fileId) => { this.state.uploader.retry(fileId); filesToUpload = React.addons.update(filesToUpload, { [fileId]: { status: { $set: FileStatus.UPLOADING } } }); }); this.setState({ // Reset the error class along with the retry errorState: { manualRetryAttempt: this.state.errorState.manualRetryAttempt + 1 }, filesToUpload }); this.setWarning(false); }, handleUploadFile(files) { // While files are being uploaded, the form cannot be ready // for submission this.props.setIsUploadReady(false); // If multiple set and user already uploaded its work, // cancel upload if(!this.props.multiple && this.state.filesToUpload.filter(displayValidFilesFilter).length > 0) { this.clearFileSelection(); return; } // Select only the submitted files that fit the file size and allowed extensions files = this.selectValidFiles(files); // if multiple is set to false and user drops multiple files into the dropzone, // take the first one and notify user that only one file can be submitted if(!this.props.multiple && files.length > 1) { let tempFilesList = []; tempFilesList.push(files[0]); // replace filelist with first-element file list files = tempFilesList; // TOOD translate? let notification = new GlobalNotificationModel(getLangText('Only one file allowed (took first one)'), 'danger', 10000); GlobalNotificationActions.appendGlobalNotification(notification); } // As mentioned already in the propTypes declaration, in some instances we need to calculate the // md5 hash of a file locally and just upload a txt file containing that hash. // // In the view this only happens when the user is allowed to do local hashing as well // as when the correct method prop is present ('hash' and not 'upload') if (this.props.enableLocalHashing && this.props.uploadMethod === 'hash') { const convertedFilePromises = []; let overallFileSize = 0; // "files" is not a classical Javascript array but a Javascript FileList, therefore // we can not use map to convert values for(let i = 0; i < files.length; i++) { // for calculating the overall progress of all submitted files // we'll need to calculate the overall sum of all files' sizes overallFileSize += files[i].size; // also, we need to set the files' initial progress value files[i].progress = 0; // since the actual computation of a file's hash is an async task , // we're using promises to handle that let hashedFilePromise = computeHashOfFile(files[i]); convertedFilePromises.push(hashedFilePromise); } // To react after the computation of all files, we define the resolvement // with the all function for iterables and essentially replace all original files // with their txt representative Q.all(convertedFilePromises) .progress(({index, value: {progress, reject}}) => { // hashing progress has been aborted from outside // To get out of the executing, we need to call reject from the // inside of the promise's execution. // This is why we're passing (along with value) a function that essentially // just does that (calling reject(err)) // // In the promises catch method, we're then checking if the interruption // was due to that error or another generic one. if(this.state.hashingProgress === -1) { reject(new Error(getLangText('Hashing canceled'))); } // update file's progress files[index].progress = progress; // calculate weighted average for overall progress of all // currently hashing files let overallHashingProgress = 0; for(let i = 0; i < files.length; i++) { let filesSliceOfOverall = files[i].size / overallFileSize; overallHashingProgress += filesSliceOfOverall * files[i].progress; } // Multiply by 100, since react-progressbar expects decimal numbers this.setState({ hashingProgress: overallHashingProgress * 100}); }) .then((convertedFiles) => { // clear hashing progress, since its done this.setState({ hashingProgress: -2}); // actually replacing all files with their txt-hash representative files = convertedFiles; // routine for adding all the files submitted to fineuploader for actual uploading them // to the server this.state.uploader.addFiles(files); this.synchronizeFileLists(files); }) .catch((err) => { // If the error is that hashing has been canceled, we want to display a success // message instead of a danger message let typeOfMessage = 'danger'; if(err.message === getLangText('Hashing canceled')) { typeOfMessage = 'success'; this.setState({ hashingProgress: -2 }); } else { // if there was a more generic error, we also log it console.logGlobal(err); } let notification = new GlobalNotificationModel(err.message, typeOfMessage, 5000); GlobalNotificationActions.appendGlobalNotification(notification); }); // if we're not hashing the files locally, we're just going to hand them over to fineuploader // to upload them to the server } else { if(files.length > 0) { this.state.uploader.addFiles(files); this.synchronizeFileLists(files); this.setState({ uploadInProgress: true }); } } }, // ReactFineUploader is essentially just a react layer around s3 fineuploader. // However, since we need to display the status of a file (progress, uploading) as well as // be able to execute actions on a currently uploading file we need to exactly sync the file list // fineuploader is keeping internally. // // Unfortunately though fineuploader is not keeping all of a File object's properties after // submitting them via .addFiles (it deletes the type, key as well as the ObjectUrl (which we need for // displaying a thumbnail)), we need to readd them manually after each file that gets submitted // to the dropzone. // This method is essentially taking care of all these steps. synchronizeFileLists(files) { let oldFiles = this.state.filesToUpload; let oldAndNewFiles = this.state.uploader.getUploads(); // Add fineuploader specific information to new files for(let i = 0; i < oldAndNewFiles.length; i++) { for(let j = 0; j < files.length; j++) { if(oldAndNewFiles[i].originalName === files[j].name) { oldAndNewFiles[i].progress = 0; oldAndNewFiles[i].type = files[j].type; oldAndNewFiles[i].url = URL.createObjectURL(files[j]); } } } // and re-add fineuploader specific information for old files as well for(let i = 0; i < oldAndNewFiles.length; i++) { for(let j = 0; j < oldFiles.length; j++) { // EXCEPTION: // // Files do not necessarily come from the user's hard drive but can also be fetched // from Amazon S3. This is handled in onSessionRequestComplete. // // If the user deletes one of those files, then fineuploader will still keep it in his // files array but with key, progress undefined and size === -1 but // status === FileStatus.UPLOAD_SUCCESSFUL. // This poses a problem as we depend on the amount of files that have // status === FileStatus.UPLOAD_SUCCESSFUL, therefore once the file is synced, // we need to tag its status as FileStatus.DELETED (which basically happens here) if(oldAndNewFiles[i].size === -1 && (!oldAndNewFiles[i].progress || oldAndNewFiles[i].progress === 0)) { oldAndNewFiles[i].status = FileStatus.DELETED; } if(oldAndNewFiles[i].originalName === oldFiles[j].name) { oldAndNewFiles[i].progress = oldFiles[j].progress; oldAndNewFiles[i].type = oldFiles[j].type; oldAndNewFiles[i].url = oldFiles[j].url; oldAndNewFiles[i].key = oldFiles[j].key; } } } // set the new file array let filesToUpload = React.addons.update(this.state.filesToUpload, { $set: oldAndNewFiles }); this.setState({ filesToUpload }, () => { // when files have been dropped or selected by a user, we want to propagate that // information to the outside components, so they can act on it (in our case, because // we want the user to define a thumbnail when the actual work is not renderable // (like e.g. a .zip file)) if(typeof this.props.handleChangedFile === 'function') { // its save to assume that the last file in `filesToUpload` is always // the latest file added this.props.handleChangedFile(this.state.filesToUpload.slice(-1)[0]); } }); }, render() { const { errorState: { errorClass }, filesToUpload, uploadInProgress } = this.state; const { areAssetsDownloadable, areAssetsEditable, enableLocalHashing, fileClassToUpload, fileInputElement: FileInputElement, multiple, showErrorPrompt, uploadMethod } = this.props; // Only show the error state once all files are finished const showError = !uploadInProgress && showErrorPrompt && errorClass != null; const props = { multiple, areAssetsDownloadable, areAssetsEditable, enableLocalHashing, uploadMethod, fileClassToUpload, filesToUpload, uploadInProgress, errorClass, showError, onDrop: this.handleUploadFile, handleDeleteFile: this.handleDeleteFile, handleCancelFile: this.handleCancelFile, handlePauseFile: this.handlePauseFile, handleResumeFile: this.handleResumeFile, handleRetryFiles: this.handleRetryFiles, handleCancelHashing: this.handleCancelHashing, dropzoneInactive: this.isDropzoneInactive(), hashingProgress: this.state.hashingProgress, allowedExtensions: this.getAllowedExtensions() }; return ( ); } }); export default ReactS3FineUploader;