django-vue3-admin-web/node_modules/@uppy/xhr-upload/lib/index.js
2025-10-20 21:21:14 +08:00

668 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 dont support remote uploads
const isSomeFileRemote = files.some(file => file.isRemote);
if (isSomeFileRemote) {
throw new Error('Cant 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;