mirror of
https://github.com/ascribe/onion.git
synced 2025-01-05 11:25:09 +01:00
342 lines
13 KiB
JavaScript
342 lines
13 KiB
JavaScript
import React from 'react';
|
|
|
|
import { AscribeFileHashUploaderFactory, AscribeUploaderFactory } from 'ascribe-react-components/es6/uploader/ascribe_uploader_factory';
|
|
import { uploaderCreateBlobParamsShapeSpec } from 'ascribe-react-components/es6/prop_types/uploader_create_blob_params_shape';
|
|
import { uploaderRequestKeyParamsShapeSpec } from 'ascribe-react-components/es6/prop_types/uploader_request_key_params_shape';
|
|
import ValidationErrors from 'react-utility-belt/es6/uploader/constants/validation_errors';
|
|
import uploaderSpecExtender from 'react-utility-belt/es6/uploader/utils/uploader_spec_extender';
|
|
|
|
import { safeInvoke } from 'js-utility-belt/es6';
|
|
|
|
import S3Fetcher from '../../fetchers/s3_fetcher';
|
|
|
|
import GlobalNotificationModel from '../../models/global_notification_model';
|
|
import GlobalNotificationActions from '../../actions/global_notification_actions';
|
|
|
|
import { S3_ACCESS_KEY, S3_ACL, S3_BUCKET } from '../../constants/uploader_constants';
|
|
|
|
import { makeCsrfHeader as createCsrfHeader } from '../../utils/csrf';
|
|
import { kbToMb } from '../../utils/file';
|
|
import { getLangText } from '../../utils/lang';
|
|
import request from '../../utils/request';
|
|
import { resolveUrl } from '../../utils/url_resolver';
|
|
|
|
|
|
const { bool, func, number, shape, string } = React.PropTypes;
|
|
|
|
const Uploader = React.createClass(uploaderSpecExtender({
|
|
displayName: 'Uploader',
|
|
|
|
propTypes: {
|
|
// Whether the selected files should be hashed locally before being uploaded
|
|
hashLocally: bool,
|
|
|
|
/**
|
|
* Override to control the notification (if any) that is shown when an upload fails.
|
|
*
|
|
* @param {File} file File that errored
|
|
* @param {string} errorReason Reason for the error
|
|
* @param {Xhr|Xdr} xhr The xhr used to make the request
|
|
*/
|
|
showErrorNotification: func,
|
|
|
|
// AscribeBlobUploader props
|
|
// Extend the base createBlobParamsShape with piece details
|
|
// eslint-disable-next-line react/sort-prop-types
|
|
createBlobParams: shape({
|
|
...uploaderCreateBlobParamsShapeSpec,
|
|
body: shape({
|
|
'piece_id': number
|
|
}).isRequired
|
|
}).isRequired,
|
|
|
|
onCreateBlobError: func,
|
|
onCreateBlobSuccess: func,
|
|
|
|
// AscribeRequestKeyUploader props
|
|
// Extend the base requestKeyParamsShape with category and piece details
|
|
// eslint-disable-next-line react/sort-prop-types
|
|
requestKeyParams: shape({
|
|
...uploaderRequestKeyParamsShapeSpec,
|
|
body: shape({
|
|
'category': string.isRequired,
|
|
'piece_id': number,
|
|
}).isRequired
|
|
}).isRequired,
|
|
|
|
onRequestKeyError: func,
|
|
onRequestKeySuccess: func,
|
|
|
|
// All other props are passed through to AscribeUploader
|
|
},
|
|
|
|
getDefaultProps() {
|
|
return {
|
|
onCreateBlobError: (err, file) => this.onError(file, err && err.message),
|
|
onCreateBlobSuccess: this.defaultOnCreateBlobSuccess,
|
|
onDeleteOnlineFile: this.defaultOnDeleteOnlineFile,
|
|
onRequestKeyError: (err, file) => this.onError(file, err && err.message),
|
|
onRequestKeySuccess: this.defaultOnRequestKeySuccess,
|
|
onSessionRequestComplete: this.defaultOnSessionRequestComplete,
|
|
showErrorNotification: (file, errorReason) => {
|
|
const message = errorReason || getLangText('Oops, we had a problem uploading ' +
|
|
'your file. Please contact us if this ' +
|
|
'happens repeatedly.');
|
|
|
|
const notification = new GlobalNotificationModel(message, 'danger', 5000);
|
|
GlobalNotificationActions.appendGlobalNotification(notification);
|
|
}
|
|
};
|
|
},
|
|
|
|
componentWillMount() {
|
|
// Create uploaders based on current app settings
|
|
// Let's assume that these settings won't change without forcing a remounting of this
|
|
// component.
|
|
const [AscribeUploader, AscribeFileHashUploader] = [
|
|
AscribeUploaderFactory, AscribeFileHashUploaderFactory
|
|
].map((factory) => factory({
|
|
createCsrfHeader,
|
|
// Note that our request module already includes the CSRF token on every call, so we
|
|
// don't have to add it to the blob and request key headers ourselves
|
|
request,
|
|
S3_ACCESS_KEY,
|
|
S3_ACL,
|
|
S3_BUCKET,
|
|
Urls: {
|
|
S3_DELETE: resolveUrl('s3_delete_file'),
|
|
S3_SIGNATUURE: resolveUrl('s3_signature')
|
|
}
|
|
}));
|
|
|
|
// Cache uploaders on this component
|
|
this.AscribeUploader = AscribeUploader;
|
|
this.AscribeFileHashUploader = AscribeFileHashUploader;
|
|
},
|
|
|
|
getXhrErrorComment(xhr) {
|
|
return xhr && {
|
|
response: xhr.response,
|
|
url: xhr.responseURL,
|
|
status: xhr.status,
|
|
statusText: xhr.statusText
|
|
};
|
|
},
|
|
|
|
/** DEFAULT EVENT HANDLERS (CAN BE OVERRIDDEN COMPLETELY THROUGH PROPS) **/
|
|
defaultOnCreateBlobSuccess(res, file) {
|
|
// `res` should contain one of these file types as a property
|
|
const fileType = res.otherdata || res.digitalwork || res.contractblob || res.thumbnail;
|
|
|
|
if (!fileType) {
|
|
const errorMsg = getLangText(
|
|
'Could not find a s3 url as the download location of the file: %s',
|
|
file.name
|
|
);
|
|
|
|
throw new Error(errorMsg);
|
|
}
|
|
|
|
const changeSet = { $set: fileType.url_safe };
|
|
return {
|
|
s3Url: changeSet,
|
|
s3UrlSafe: changeSet
|
|
};
|
|
},
|
|
|
|
defaultOnDeleteOnlineFile(file) {
|
|
return S3Fetcher.deleteFile(file.s3Key, file.s3Bucket);
|
|
},
|
|
|
|
defaultOnRequestKeySuccess(res) {
|
|
return res.key;
|
|
},
|
|
|
|
defaultOnSessionRequestComplete(response, success) {
|
|
if (!success) {
|
|
return undefined;
|
|
}
|
|
|
|
response.forEach((file) => {
|
|
file.url = file.s3UrlSafe;
|
|
});
|
|
|
|
return response;
|
|
},
|
|
|
|
/** EXTENDED EVENT HANDLERS (ADDS ADDITIONAL BEHAVIOUR TO CALLBACK) **/
|
|
onCanceled(file, ...args) {
|
|
const notification = new GlobalNotificationModel(
|
|
getLangText('Upload of "%s" cancelled', file.name),
|
|
'success',
|
|
5000
|
|
);
|
|
GlobalNotificationActions.appendGlobalNotification(notification);
|
|
|
|
safeInvoke(this.props.onCanceled, file, ...args);
|
|
},
|
|
|
|
onDeleteComplete(file, xhr, isError, ...args) {
|
|
const notificationTemplate = isError ? 'There was an error deleting "${name}"'
|
|
: '"${name}" deleted';
|
|
|
|
const notification = new GlobalNotificationModel(
|
|
getLangText(notificationTemplate, file),
|
|
isError ? 'danger' : 'success',
|
|
5000
|
|
);
|
|
|
|
GlobalNotificationActions.appendGlobalNotification(notification);
|
|
|
|
safeInvoke(this.props.onDeleteComplete, file, xhr, isError, ...args);
|
|
},
|
|
|
|
onError(file, errorReason, xhr, ...args) {
|
|
const { onError, showErrorNotification } = this.props;
|
|
const { uploader } = this.refs;
|
|
|
|
console.logGlobal(errorReason, {
|
|
files: uploader.getFiles(),
|
|
chunks: uploader.getChunks(),
|
|
xhr: this.getXhrErrorComment(xhr)
|
|
});
|
|
|
|
safeInvoke(showErrorNotification, file, errorReason, xhr, ...args);
|
|
safeInvoke(onError, file, errorReason, xhr, ...args);
|
|
},
|
|
|
|
onFileHashError(error, ...args) {
|
|
let notification;
|
|
|
|
if (error && error.message && error.message.toLowerCase().includes('cancel')) {
|
|
notification = new GlobalNotificationModel(error.message, 'success', 5000);
|
|
} else {
|
|
notification = new GlobalNotificationModel(
|
|
'Failed to hash files. Please contact us if this problem persists.',
|
|
'danger',
|
|
5000
|
|
);
|
|
}
|
|
|
|
GlobalNotificationActions.appendGlobalNotification(notification);
|
|
|
|
const {
|
|
invoked,
|
|
result: errorResult
|
|
} = safeInvoke(this.props.onFileHashError, error, ...args);
|
|
|
|
// If `onFileHashError` doesn't throw its own error or isn't invoked, rethrow the original
|
|
// error back to the uploader
|
|
if (invoked) {
|
|
return errorResult;
|
|
} else {
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
onValidationError(errors, validFiles, ...args) {
|
|
const numFileLimitErrors = errors
|
|
.filter((error) => error.validationError.type === ValidationErrors.FILE_LIMIT)
|
|
.length;
|
|
|
|
if (numFileLimitErrors === errors.length) {
|
|
// Validation failed because number of files submitted was over the limit
|
|
const { limit, remaining } = errors[0].validationError.description;
|
|
let notification;
|
|
|
|
// If we are currently under the limit, just select as many files from the files as
|
|
// possible
|
|
if (remaining) {
|
|
const firstSubmittedFiles = errors
|
|
.slice(0, remaining)
|
|
.map((error) => error.file);
|
|
|
|
validFiles.push(...firstSubmittedFiles);
|
|
|
|
notification = new GlobalNotificationModel(
|
|
getLangText(
|
|
'Only %s were allowed (took first %s)',
|
|
`${remaining} ${getLangText(remaining === 1 ? 'file' : 'files')}`,
|
|
remaining
|
|
),
|
|
'danger',
|
|
10000
|
|
);
|
|
} else {
|
|
notification = new GlobalNotificationModel(
|
|
getLangText(
|
|
"Oops, you've already uploaded the maximum number of items (%s)!",
|
|
limit
|
|
),
|
|
'danger',
|
|
10000
|
|
);
|
|
}
|
|
|
|
GlobalNotificationActions.appendGlobalNotification(notification);
|
|
} else {
|
|
// Validation failed because of only some items; ignore those and notify the user
|
|
// If a lot of submitted files fail validation, this might be spammy
|
|
errors
|
|
.map(({ file, validationError: { description, type } }) => {
|
|
switch (type) {
|
|
case ValidationErrors.SIZE:
|
|
// eslint-disable-next-line max-len
|
|
return getLangText('Cancelled upload of "${name}" as it was bigger than ${size}MB', {
|
|
name: file.name,
|
|
size: kbToMb(description.limit)
|
|
});
|
|
case ValidationErrors.EXTENSION:
|
|
return getLangText(
|
|
// eslint-disable-next-line max-len
|
|
'Cancelled upload of "${name}" as it has an invalid file format (valid formats: ${allowedExt})', {
|
|
allowedExt: description.allowedExtensions.join(', '),
|
|
name: file.name
|
|
}
|
|
);
|
|
default:
|
|
return getLangText(
|
|
'Cancelled upload of "${name}" as it failed file validation',
|
|
file
|
|
);
|
|
}
|
|
})
|
|
.forEach((errorMsg) => {
|
|
const notification = new GlobalNotificationModel(errorMsg, 'danger', 5000);
|
|
GlobalNotificationActions.appendGlobalNotification(notification);
|
|
});
|
|
}
|
|
|
|
const {
|
|
invoked,
|
|
result
|
|
} = safeInvoke(this.props.onValidationError, errors, validFiles, ...args);
|
|
|
|
return invoked ? result : validFiles;
|
|
},
|
|
|
|
render() {
|
|
const { AscribeUploader, AscribeFileHashUploader } = this;
|
|
const { hashLocally, ...props } = this.props;
|
|
|
|
const uploaderProps = {
|
|
...props,
|
|
onCanceled: this.onCanceled,
|
|
onDeleteComplete: this.onDeleteComplete,
|
|
onError: this.onError,
|
|
onValidationError: this.onValidationError,
|
|
|
|
// Mandatory for uploaderSpecExtender
|
|
ref: 'uploader'
|
|
};
|
|
|
|
return hashLocally ? (
|
|
<AscribeFileHashUploader
|
|
{...uploaderProps}
|
|
onFileHashError={this.onFileHashError} />
|
|
) : (<AscribeUploader {...uploaderProps} />);
|
|
}
|
|
}));
|
|
|
|
export default Uploader;
|