数通智联化工云平台
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

668 lines
20 KiB

"use strict";
var _nonSecure = require("nanoid/non-secure");
var _companionClient = require("@uppy/companion-client");
var _RateLimitedQueue = require("@uppy/utils/lib/RateLimitedQueue");
const BasePlugin = require("@uppy/core/lib/BasePlugin");
const emitSocketProgress = require("@uppy/utils/lib/emitSocketProgress");
const getSocketHost = require("@uppy/utils/lib/getSocketHost");
const settle = require("@uppy/utils/lib/settle");
const EventTracker = require("@uppy/utils/lib/EventTracker");
const ProgressTimeout = require("@uppy/utils/lib/ProgressTimeout");
const NetworkError = require("@uppy/utils/lib/NetworkError");
const isNetworkError = require("@uppy/utils/lib/isNetworkError");
const packageJson = {
"version": "2.1.3"
};
const locale = require("./locale.js");
function buildResponseError(xhr, err) {
let error = err; // No error message
if (!error) error = new Error('Upload error'); // Got an error message string
if (typeof error === 'string') error = new Error(error); // Got something else
if (!(error instanceof Error)) {
error = Object.assign(new Error('Upload error'), {
data: error
});
}
if (isNetworkError(xhr)) {
error = new NetworkError(error, xhr);
return error;
}
error.request = xhr;
return error;
}
/**
* Set `data.type` in the blob to `file.meta.type`,
* because we might have detected a more accurate file type in Uppy
* https://stackoverflow.com/a/50875615
*
* @param {object} file File object with `data`, `size` and `meta` properties
* @returns {object} blob updated with the new `type` set from `file.meta.type`
*/
function setTypeInBlob(file) {
const dataWithUpdatedType = file.data.slice(0, file.data.size, file.meta.type);
return dataWithUpdatedType;
}
class XHRUpload extends BasePlugin {
// eslint-disable-next-line global-require
constructor(uppy, opts) {
super(uppy, opts);
this.type = 'uploader';
this.id = this.opts.id || 'XHRUpload';
this.title = 'XHRUpload';
this.defaultLocale = locale; // Default options
const defaultOptions = {
formData: true,
fieldName: opts.bundle ? 'files[]' : 'file',
method: 'post',
metaFields: null,
responseUrlFieldName: 'url',
bundle: false,
headers: {},
timeout: 30 * 1000,
limit: 5,
withCredentials: false,
responseType: '',
/**
* @param {string} responseText the response body string
*/
getResponseData(responseText) {
let parsedResponse = {};
try {
parsedResponse = JSON.parse(responseText);
} catch (err) {
uppy.log(err);
}
return parsedResponse;
},
/**
*
* @param {string} _ the response body string
* @param {XMLHttpRequest | respObj} response the response object (XHR or similar)
*/
getResponseError(_, response) {
let error = new Error('Upload error');
if (isNetworkError(response)) {
error = new NetworkError(error, response);
}
return error;
},
/**
* Check if the response from the upload endpoint indicates that the upload was successful.
*
* @param {number} status the response status code
*/
validateStatus(status) {
return status >= 200 && status < 300;
}
};
this.opts = { ...defaultOptions,
...opts
};
this.i18nInit();
this.handleUpload = this.handleUpload.bind(this); // Simultaneous upload limiting is shared across all uploads with this plugin.
if (_RateLimitedQueue.internalRateLimitedQueue in this.opts) {
this.requests = this.opts[_RateLimitedQueue.internalRateLimitedQueue];
} else {
this.requests = new _RateLimitedQueue.RateLimitedQueue(this.opts.limit);
}
if (this.opts.bundle && !this.opts.formData) {
throw new Error('`opts.formData` must be true when `opts.bundle` is enabled.');
}
this.uploaderEvents = Object.create(null);
}
getOptions(file) {
const overrides = this.uppy.getState().xhrUpload;
const {
headers
} = this.opts;
const opts = { ...this.opts,
...(overrides || {}),
...(file.xhrUpload || {}),
headers: {}
}; // Support for `headers` as a function, only in the XHRUpload settings.
// Options set by other plugins in Uppy state or on the files themselves are still merged in afterward.
//
// ```js
// headers: (file) => ({ expires: file.meta.expires })
// ```
if (typeof headers === 'function') {
opts.headers = headers(file);
} else {
Object.assign(opts.headers, this.opts.headers);
}
if (overrides) {
Object.assign(opts.headers, overrides.headers);
}
if (file.xhrUpload) {
Object.assign(opts.headers, file.xhrUpload.headers);
}
return opts;
} // eslint-disable-next-line class-methods-use-this
addMetadata(formData, meta, opts) {
const metaFields = Array.isArray(opts.metaFields) ? opts.metaFields : Object.keys(meta); // Send along all fields by default.
metaFields.forEach(item => {
formData.append(item, meta[item]);
});
}
createFormDataUpload(file, opts) {
const formPost = new FormData();
this.addMetadata(formPost, file.meta, opts);
const dataWithUpdatedType = setTypeInBlob(file);
if (file.name) {
formPost.append(opts.fieldName, dataWithUpdatedType, file.meta.name);
} else {
formPost.append(opts.fieldName, dataWithUpdatedType);
}
return formPost;
}
createBundledUpload(files, opts) {
const formPost = new FormData();
const {
meta
} = this.uppy.getState();
this.addMetadata(formPost, meta, opts);
files.forEach(file => {
const options = this.getOptions(file);
const dataWithUpdatedType = setTypeInBlob(file);
if (file.name) {
formPost.append(options.fieldName, dataWithUpdatedType, file.name);
} else {
formPost.append(options.fieldName, dataWithUpdatedType);
}
});
return formPost;
}
upload(file, current, total) {
const opts = this.getOptions(file);
this.uppy.log(`uploading ${current} of ${total}`);
return new Promise((resolve, reject) => {
this.uppy.emit('upload-started', file);
const data = opts.formData ? this.createFormDataUpload(file, opts) : file.data;
const xhr = new XMLHttpRequest();
this.uploaderEvents[file.id] = new EventTracker(this.uppy);
let queuedRequest;
const timer = new ProgressTimeout(opts.timeout, () => {
xhr.abort();
queuedRequest.done();
const error = new Error(this.i18n('timedOut', {
seconds: Math.ceil(opts.timeout / 1000)
}));
this.uppy.emit('upload-error', file, error);
reject(error);
});
const id = (0, _nonSecure.nanoid)();
xhr.upload.addEventListener('loadstart', () => {
this.uppy.log(`[XHRUpload] ${id} started`);
});
xhr.upload.addEventListener('progress', ev => {
this.uppy.log(`[XHRUpload] ${id} progress: ${ev.loaded} / ${ev.total}`); // Begin checking for timeouts when progress starts, instead of loading,
// to avoid timing out requests on browser concurrency queue
timer.progress();
if (ev.lengthComputable) {
this.uppy.emit('upload-progress', file, {
uploader: this,
bytesUploaded: ev.loaded,
bytesTotal: ev.total
});
}
});
xhr.addEventListener('load', () => {
this.uppy.log(`[XHRUpload] ${id} finished`);
timer.done();
queuedRequest.done();
if (this.uploaderEvents[file.id]) {
this.uploaderEvents[file.id].remove();
this.uploaderEvents[file.id] = null;
}
if (opts.validateStatus(xhr.status, xhr.responseText, xhr)) {
const body = opts.getResponseData(xhr.responseText, xhr);
const uploadURL = body[opts.responseUrlFieldName];
const uploadResp = {
status: xhr.status,
body,
uploadURL
};
this.uppy.emit('upload-success', file, uploadResp);
if (uploadURL) {
this.uppy.log(`Download ${file.name} from ${uploadURL}`);
}
return resolve(file);
}
const body = opts.getResponseData(xhr.responseText, xhr);
const error = buildResponseError(xhr, opts.getResponseError(xhr.responseText, xhr));
const response = {
status: xhr.status,
body
};
this.uppy.emit('upload-error', file, error, response);
return reject(error);
});
xhr.addEventListener('error', () => {
this.uppy.log(`[XHRUpload] ${id} errored`);
timer.done();
queuedRequest.done();
if (this.uploaderEvents[file.id]) {
this.uploaderEvents[file.id].remove();
this.uploaderEvents[file.id] = null;
}
const error = buildResponseError(xhr, opts.getResponseError(xhr.responseText, xhr));
this.uppy.emit('upload-error', file, error);
return reject(error);
});
xhr.open(opts.method.toUpperCase(), opts.endpoint, true); // IE10 does not allow setting `withCredentials` and `responseType`
// before `open()` is called.
xhr.withCredentials = opts.withCredentials;
if (opts.responseType !== '') {
xhr.responseType = opts.responseType;
}
queuedRequest = this.requests.run(() => {
this.uppy.emit('upload-started', file); // When using an authentication system like JWT, the bearer token goes as a header. This
// header needs to be fresh each time the token is refreshed so computing and setting the
// headers just before the upload starts enables this kind of authentication to work properly.
// Otherwise, half-way through the list of uploads the token could be stale and the upload would fail.
const currentOpts = this.getOptions(file);
Object.keys(currentOpts.headers).forEach(header => {
xhr.setRequestHeader(header, currentOpts.headers[header]);
});
xhr.send(data);
return () => {
timer.done();
xhr.abort();
};
});
this.onFileRemove(file.id, () => {
queuedRequest.abort();
reject(new Error('File removed'));
});
this.onCancelAll(file.id, _ref => {
let {
reason
} = _ref;
if (reason === 'user') {
queuedRequest.abort();
}
reject(new Error('Upload cancelled'));
});
});
}
uploadRemote(file) {
const opts = this.getOptions(file);
return new Promise((resolve, reject) => {
this.uppy.emit('upload-started', file);
const fields = {};
const metaFields = Array.isArray(opts.metaFields) ? opts.metaFields // Send along all fields by default.
: Object.keys(file.meta);
metaFields.forEach(name => {
fields[name] = file.meta[name];
});
const Client = file.remote.providerOptions.provider ? _companionClient.Provider : _companionClient.RequestClient;
const client = new Client(this.uppy, file.remote.providerOptions);
client.post(file.remote.url, { ...file.remote.body,
endpoint: opts.endpoint,
size: file.data.size,
fieldname: opts.fieldName,
metadata: fields,
httpMethod: opts.method,
useFormData: opts.formData,
headers: opts.headers
}).then(res => {
const {
token
} = res;
const host = getSocketHost(file.remote.companionUrl);
const socket = new _companionClient.Socket({
target: `${host}/api/${token}`,
autoOpen: false
});
this.uploaderEvents[file.id] = new EventTracker(this.uppy);
let queuedRequest;
this.onFileRemove(file.id, () => {
socket.send('cancel', {});
queuedRequest.abort();
resolve(`upload ${file.id} was removed`);
});
this.onCancelAll(file.id, function (_temp) {
let {
reason
} = _temp === void 0 ? {} : _temp;
if (reason === 'user') {
socket.send('cancel', {});
queuedRequest.abort();
}
resolve(`upload ${file.id} was canceled`);
});
this.onRetry(file.id, () => {
socket.send('pause', {});
socket.send('resume', {});
});
this.onRetryAll(file.id, () => {
socket.send('pause', {});
socket.send('resume', {});
});
socket.on('progress', progressData => emitSocketProgress(this, progressData, file));
socket.on('success', data => {
const body = opts.getResponseData(data.response.responseText, data.response);
const uploadURL = body[opts.responseUrlFieldName];
const uploadResp = {
status: data.response.status,
body,
uploadURL
};
this.uppy.emit('upload-success', file, uploadResp);
queuedRequest.done();
if (this.uploaderEvents[file.id]) {
this.uploaderEvents[file.id].remove();
this.uploaderEvents[file.id] = null;
}
return resolve();
});
socket.on('error', errData => {
const resp = errData.response;
const error = resp ? opts.getResponseError(resp.responseText, resp) : Object.assign(new Error(errData.error.message), {
cause: errData.error
});
this.uppy.emit('upload-error', file, error);
queuedRequest.done();
if (this.uploaderEvents[file.id]) {
this.uploaderEvents[file.id].remove();
this.uploaderEvents[file.id] = null;
}
reject(error);
});
queuedRequest = this.requests.run(() => {
socket.open();
if (file.isPaused) {
socket.send('pause', {});
}
return () => socket.close();
});
}).catch(err => {
this.uppy.emit('upload-error', file, err);
reject(err);
});
});
}
uploadBundle(files) {
return new Promise((resolve, reject) => {
const {
endpoint
} = this.opts;
const {
method
} = this.opts;
const optsFromState = this.uppy.getState().xhrUpload;
const formData = this.createBundledUpload(files, { ...this.opts,
...(optsFromState || {})
});
const xhr = new XMLHttpRequest();
const emitError = error => {
files.forEach(file => {
this.uppy.emit('upload-error', file, error);
});
};
const timer = new ProgressTimeout(this.opts.timeout, () => {
xhr.abort();
const error = new Error(this.i18n('timedOut', {
seconds: Math.ceil(this.opts.timeout / 1000)
}));
emitError(error);
reject(error);
});
xhr.upload.addEventListener('loadstart', () => {
this.uppy.log('[XHRUpload] started uploading bundle');
timer.progress();
});
xhr.upload.addEventListener('progress', ev => {
timer.progress();
if (!ev.lengthComputable) return;
files.forEach(file => {
this.uppy.emit('upload-progress', file, {
uploader: this,
bytesUploaded: ev.loaded / ev.total * file.size,
bytesTotal: file.size
});
});
});
xhr.addEventListener('load', ev => {
timer.done();
if (this.opts.validateStatus(ev.target.status, xhr.responseText, xhr)) {
const body = this.opts.getResponseData(xhr.responseText, xhr);
const uploadResp = {
status: ev.target.status,
body
};
files.forEach(file => {
this.uppy.emit('upload-success', file, uploadResp);
});
return resolve();
}
const error = this.opts.getResponseError(xhr.responseText, xhr) || new Error('Upload error');
error.request = xhr;
emitError(error);
return reject(error);
});
xhr.addEventListener('error', () => {
timer.done();
const error = this.opts.getResponseError(xhr.responseText, xhr) || new Error('Upload error');
emitError(error);
return reject(error);
});
this.uppy.on('cancel-all', function (_temp2) {
let {
reason
} = _temp2 === void 0 ? {} : _temp2;
if (reason !== 'user') return;
timer.done();
xhr.abort();
});
xhr.open(method.toUpperCase(), endpoint, true); // IE10 does not allow setting `withCredentials` and `responseType`
// before `open()` is called.
xhr.withCredentials = this.opts.withCredentials;
if (this.opts.responseType !== '') {
xhr.responseType = this.opts.responseType;
}
Object.keys(this.opts.headers).forEach(header => {
xhr.setRequestHeader(header, this.opts.headers[header]);
});
xhr.send(formData);
files.forEach(file => {
this.uppy.emit('upload-started', file);
});
});
}
uploadFiles(files) {
const promises = files.map((file, i) => {
const current = parseInt(i, 10) + 1;
const total = files.length;
if (file.error) {
return Promise.reject(new Error(file.error));
}
if (file.isRemote) {
return this.uploadRemote(file, current, total);
}
return this.upload(file, current, total);
});
return settle(promises);
}
onFileRemove(fileID, cb) {
this.uploaderEvents[fileID].on('file-removed', file => {
if (fileID === file.id) cb(file.id);
});
}
onRetry(fileID, cb) {
this.uploaderEvents[fileID].on('upload-retry', targetFileID => {
if (fileID === targetFileID) {
cb();
}
});
}
onRetryAll(fileID, cb) {
this.uploaderEvents[fileID].on('retry-all', () => {
if (!this.uppy.getFile(fileID)) return;
cb();
});
}
onCancelAll(fileID, eventHandler) {
var _this = this;
this.uploaderEvents[fileID].on('cancel-all', function () {
if (!_this.uppy.getFile(fileID)) return;
eventHandler(...arguments);
});
}
handleUpload(fileIDs) {
if (fileIDs.length === 0) {
this.uppy.log('[XHRUpload] No files to upload!');
return Promise.resolve();
} // No limit configured by the user, and no RateLimitedQueue passed in by a "parent" plugin
// (basically just AwsS3) using the internal symbol
if (this.opts.limit === 0 && !this.opts[_RateLimitedQueue.internalRateLimitedQueue]) {
this.uppy.log('[XHRUpload] When uploading multiple files at once, consider setting the `limit` option (to `10` for example), to limit the number of concurrent uploads, which helps prevent memory and network issues: https://uppy.io/docs/xhr-upload/#limit-0', 'warning');
}
this.uppy.log('[XHRUpload] Uploading...');
const files = fileIDs.map(fileID => this.uppy.getFile(fileID));
if (this.opts.bundle) {
// if bundle: true, we don’t support remote uploads
const isSomeFileRemote = files.some(file => file.isRemote);
if (isSomeFileRemote) {
throw new Error('Can’t upload remote files when the `bundle: true` option is set');
}
if (typeof this.opts.headers === 'function') {
throw new TypeError('`headers` may not be a function when the `bundle: true` option is set');
}
return this.uploadBundle(files);
}
return this.uploadFiles(files).then(() => null);
}
install() {
if (this.opts.bundle) {
const {
capabilities
} = this.uppy.getState();
this.uppy.setState({
capabilities: { ...capabilities,
individualCancellation: false
}
});
}
this.uppy.addUploader(this.handleUpload);
}
uninstall() {
if (this.opts.bundle) {
const {
capabilities
} = this.uppy.getState();
this.uppy.setState({
capabilities: { ...capabilities,
individualCancellation: true
}
});
}
this.uppy.removeUploader(this.handleUpload);
}
}
XHRUpload.VERSION = packageJson.version;
module.exports = XHRUpload;