uppy
Version:
Extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:
520 lines (418 loc) • 16.5 kB
JavaScript
;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var _Promise = typeof Promise === 'undefined' ? require('es6-promise').Promise : Promise;
var Plugin = require('../core/Plugin');
var cuid = require('cuid');
var Translator = require('../core/Translator');
var UppySocket = require('../core/UppySocket');
var _require = require('../core/Utils'),
emitSocketProgress = _require.emitSocketProgress,
getSocketHost = _require.getSocketHost,
settle = _require.settle,
limitPromises = _require.limitPromises;
function buildResponseError(xhr, error) {
// 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 = _extends(new Error('Upload error'), { data: error });
}
error.request = xhr;
return error;
}
module.exports = function (_Plugin) {
_inherits(XHRUpload, _Plugin);
function XHRUpload(uppy, opts) {
_classCallCheck(this, XHRUpload);
var _this = _possibleConstructorReturn(this, _Plugin.call(this, uppy, opts));
_this.type = 'uploader';
_this.id = 'XHRUpload';
_this.title = 'XHRUpload';
var defaultLocale = {
strings: {
timedOut: 'Upload stalled for %{seconds} seconds, aborting.'
}
// Default options
};var defaultOptions = {
formData: true,
fieldName: 'files[]',
method: 'post',
metaFields: null,
responseUrlFieldName: 'url',
bundle: false,
headers: {},
locale: defaultLocale,
timeout: 30 * 1000,
limit: 0,
/**
* @typedef respObj
* @property {string} responseText
* @property {number} status
* @property {string} statusText
* @property {Object.<string, string>} headers
*
* @param {string} responseContent the response body
* @param {XMLHttpRequest | respObj} responseObject the response object
*/
getResponseData: function getResponseData(responseContent, responseObject) {
var response = {};
try {
response = JSON.parse(responseContent);
} catch (err) {
console.log(err);
}
return response;
},
/**
*
* @param {string} responseContent the response body
* @param {XMLHttpRequest | respObj} responseObject the response object
*/
getResponseError: function getResponseError(responseContent, responseObject) {
return new Error('Upload error');
}
};
// Merge default options with the ones set by user
_this.opts = _extends({}, defaultOptions, opts);
_this.locale = _extends({}, defaultLocale, _this.opts.locale);
_this.locale.strings = _extends({}, defaultLocale.strings, _this.opts.locale.strings);
// i18n
_this.translator = new Translator({ locale: _this.locale });
_this.i18n = _this.translator.translate.bind(_this.translator);
_this.handleUpload = _this.handleUpload.bind(_this);
// Simultaneous upload limiting is shared across all uploads with this plugin.
if (typeof _this.opts.limit === 'number' && _this.opts.limit !== 0) {
_this.limitUploads = limitPromises(_this.opts.limit);
} else {
_this.limitUploads = function (fn) {
return fn;
};
}
if (_this.opts.bundle && !_this.opts.formData) {
throw new Error('`opts.formData` must be true when `opts.bundle` is enabled.');
}
return _this;
}
XHRUpload.prototype.getOptions = function getOptions(file) {
var opts = _extends({}, this.opts, this.uppy.state.xhrUpload || {}, file.xhrUpload || {});
opts.headers = {};
_extends(opts.headers, this.opts.headers);
if (this.uppy.state.xhrUpload) {
_extends(opts.headers, this.uppy.state.xhrUpload.headers);
}
if (file.xhrUpload) {
_extends(opts.headers, file.xhrUpload.headers);
}
return opts;
};
// Helper to abort upload requests if there has not been any progress for `timeout` ms.
// Create an instance using `timer = createProgressTimeout(10000, onTimeout)`
// Call `timer.progress()` to signal that there has been progress of any kind.
// Call `timer.done()` when the upload has completed.
XHRUpload.prototype.createProgressTimeout = function createProgressTimeout(timeout, timeoutHandler) {
var uppy = this.uppy;
var self = this;
function onTimedOut() {
uppy.log('[XHRUpload] timed out');
var error = new Error(self.i18n('timedOut', { seconds: Math.ceil(timeout / 1000) }));
timeoutHandler(error);
}
var aliveTimer = null;
function progress() {
if (timeout > 0) {
done();
aliveTimer = setTimeout(onTimedOut, timeout);
}
}
function done() {
if (aliveTimer) {
clearTimeout(aliveTimer);
aliveTimer = null;
}
}
return {
progress: progress,
done: done
};
};
XHRUpload.prototype.createFormDataUpload = function createFormDataUpload(file, opts) {
var formPost = new FormData();
var metaFields = Array.isArray(opts.metaFields) ? opts.metaFields
// Send along all fields by default.
: Object.keys(file.meta);
metaFields.forEach(function (item) {
formPost.append(item, file.meta[item]);
});
formPost.append(opts.fieldName, file.data);
return formPost;
};
XHRUpload.prototype.createBareUpload = function createBareUpload(file, opts) {
return file.data;
};
XHRUpload.prototype.upload = function upload(file, current, total) {
var _this2 = this;
var opts = this.getOptions(file);
this.uppy.log('uploading ' + current + ' of ' + total);
return new _Promise(function (resolve, reject) {
var data = opts.formData ? _this2.createFormDataUpload(file, opts) : _this2.createBareUpload(file, opts);
var timer = _this2.createProgressTimeout(opts.timeout, function (error) {
xhr.abort();
_this2.uppy.emit('upload-error', file, error);
reject(error);
});
var xhr = new XMLHttpRequest();
var id = cuid();
xhr.upload.addEventListener('loadstart', function (ev) {
_this2.uppy.log('[XHRUpload] ' + id + ' started');
// Begin checking for timeouts when loading starts.
timer.progress();
});
xhr.upload.addEventListener('progress', function (ev) {
_this2.uppy.log('[XHRUpload] ' + id + ' progress: ' + ev.loaded + ' / ' + ev.total);
timer.progress();
if (ev.lengthComputable) {
_this2.uppy.emit('upload-progress', file, {
uploader: _this2,
bytesUploaded: ev.loaded,
bytesTotal: ev.total
});
}
});
xhr.addEventListener('load', function (ev) {
_this2.uppy.log('[XHRUpload] ' + id + ' finished');
timer.done();
if (ev.target.status >= 200 && ev.target.status < 300) {
var body = opts.getResponseData(xhr.responseText, xhr);
var uploadURL = body[opts.responseUrlFieldName];
var response = {
status: ev.target.status,
body: body,
uploadURL: uploadURL
};
_this2.uppy.setFileState(file.id, { response: response });
_this2.uppy.emit('upload-success', file, body, uploadURL);
if (uploadURL) {
_this2.uppy.log('Download ' + file.name + ' from ' + file.uploadURL);
}
return resolve(file);
} else {
var _body = opts.getResponseData(xhr.responseText, xhr);
var error = buildResponseError(xhr, opts.getResponseError(xhr.responseText, xhr));
var _response = {
status: ev.target.status,
body: _body
};
_this2.uppy.setFileState(file.id, { response: _response });
_this2.uppy.emit('upload-error', file, error);
return reject(error);
}
});
xhr.addEventListener('error', function (ev) {
_this2.uppy.log('[XHRUpload] ' + id + ' errored');
timer.done();
var error = buildResponseError(xhr, opts.getResponseError(xhr.responseText, xhr));
_this2.uppy.emit('upload-error', file, error);
return reject(error);
});
xhr.open(opts.method.toUpperCase(), opts.endpoint, true);
Object.keys(opts.headers).forEach(function (header) {
xhr.setRequestHeader(header, opts.headers[header]);
});
xhr.send(data);
_this2.uppy.on('file-removed', function (removedFile) {
if (removedFile.id === file.id) {
timer.done();
xhr.abort();
}
});
_this2.uppy.on('upload-cancel', function (fileID) {
if (fileID === file.id) {
timer.done();
xhr.abort();
}
});
_this2.uppy.on('cancel-all', function () {
// const files = this.uppy.getState().files
// if (!files[file.id]) return
xhr.abort();
});
});
};
XHRUpload.prototype.uploadRemote = function uploadRemote(file, current, total) {
var _this3 = this;
var opts = this.getOptions(file);
return new _Promise(function (resolve, reject) {
var fields = {};
var metaFields = Array.isArray(opts.metaFields) ? opts.metaFields
// Send along all fields by default.
: Object.keys(file.meta);
metaFields.forEach(function (name) {
fields[name] = file.meta[name];
});
fetch(file.remote.url, {
method: 'post',
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(_extends({}, file.remote.body, {
endpoint: opts.endpoint,
size: file.data.size,
fieldname: opts.fieldName,
metadata: fields,
headers: opts.headers
}))
}).then(function (res) {
if (res.status < 200 && res.status > 300) {
return reject(res.statusText);
}
res.json().then(function (data) {
var token = data.token;
var host = getSocketHost(file.remote.host);
var socket = new UppySocket({ target: host + '/api/' + token });
socket.on('progress', function (progressData) {
return emitSocketProgress(_this3, progressData, file);
});
socket.on('success', function (data) {
var resp = opts.getResponseData(data.response.responseText, data.response);
var uploadURL = resp[opts.responseUrlFieldName];
_this3.uppy.emit('upload-success', file, resp, uploadURL);
socket.close();
return resolve();
});
socket.on('error', function (errData) {
var resp = errData.response;
var error = resp ? opts.getResponseError(resp.responseText, resp) : new Error(errData.error);
_this3.uppy.emit('upload-error', file, error);
reject(new Error(errData.error));
});
});
});
});
};
XHRUpload.prototype.uploadBundle = function uploadBundle(files) {
var _this4 = this;
return new _Promise(function (resolve, reject) {
var endpoint = _this4.opts.endpoint;
var method = _this4.opts.method;
var formData = new FormData();
files.forEach(function (file, i) {
var opts = _this4.getOptions(file);
formData.append(opts.fieldName, file.data);
});
var xhr = new XMLHttpRequest();
var timer = _this4.createProgressTimeout(_this4.opts.timeout, function (error) {
xhr.abort();
emitError(error);
reject(error);
});
var emitError = function emitError(error) {
files.forEach(function (file) {
_this4.uppy.emit('upload-error', file, error);
});
};
xhr.upload.addEventListener('loadstart', function (ev) {
_this4.uppy.log('[XHRUpload] started uploading bundle');
timer.progress();
});
xhr.upload.addEventListener('progress', function (ev) {
timer.progress();
if (!ev.lengthComputable) return;
files.forEach(function (file) {
_this4.uppy.emit('upload-progress', file, {
uploader: _this4,
bytesUploaded: ev.loaded,
bytesTotal: ev.total
});
});
});
xhr.addEventListener('load', function (ev) {
timer.done();
if (ev.target.status >= 200 && ev.target.status < 300) {
var resp = _this4.opts.getResponseData(xhr.responseText, xhr);
files.forEach(function (file) {
_this4.uppy.emit('upload-success', file, resp);
});
return resolve();
}
var error = _this4.opts.getResponseError(xhr.responseText, xhr) || new Error('Upload error');
error.request = xhr;
emitError(error);
return reject(error);
});
xhr.addEventListener('error', function (ev) {
timer.done();
var error = _this4.opts.getResponseError(xhr.responseText, xhr) || new Error('Upload error');
emitError(error);
return reject(error);
});
_this4.uppy.on('cancel-all', function () {
xhr.abort();
});
xhr.open(method.toUpperCase(), endpoint, true);
Object.keys(_this4.opts.headers).forEach(function (header) {
xhr.setRequestHeader(header, _this4.opts.headers[header]);
});
xhr.send(formData);
files.forEach(function (file) {
_this4.uppy.emit('upload-started', file);
});
});
};
XHRUpload.prototype.uploadFiles = function uploadFiles(files) {
var _this5 = this;
var actions = files.map(function (file, i) {
var current = parseInt(i, 10) + 1;
var total = files.length;
if (file.error) {
return function () {
return _Promise.reject(new Error(file.error));
};
} else if (file.isRemote) {
// We emit upload-started here, so that it's also emitted for files
// that have to wait due to the `limit` option.
_this5.uppy.emit('upload-started', file);
return _this5.uploadRemote.bind(_this5, file, current, total);
} else {
_this5.uppy.emit('upload-started', file);
return _this5.upload.bind(_this5, file, current, total);
}
});
var promises = actions.map(function (action) {
var limitedAction = _this5.limitUploads(action);
return limitedAction();
});
return settle(promises);
};
XHRUpload.prototype.handleUpload = function handleUpload(fileIDs) {
var _this6 = this;
if (fileIDs.length === 0) {
this.uppy.log('[XHRUpload] No files to upload!');
return _Promise.resolve();
}
this.uppy.log('[XHRUpload] Uploading...');
var files = fileIDs.map(function (fileID) {
return _this6.uppy.getFile(fileID);
});
if (this.opts.bundle) {
return this.uploadBundle(files);
}
return this.uploadFiles(files).then(function () {
return null;
});
};
XHRUpload.prototype.install = function install() {
this.uppy.addUploader(this.handleUpload);
};
XHRUpload.prototype.uninstall = function uninstall() {
this.uppy.removeUploader(this.handleUpload);
};
return XHRUpload;
}(Plugin);
//# sourceMappingURL=XHRUpload.js.map