@uppy/core
Version:
Core module for the 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:
1,476 lines (1,428 loc) • 64.1 kB
JavaScript
function _classPrivateFieldLooseBase(e, t) { if (!{}.hasOwnProperty.call(e, t)) throw new TypeError("attempted to use private field on non-instance"); return e; }
var id = 0;
function _classPrivateFieldLooseKey(e) { return "__private_" + id++ + "_" + e; }
/* eslint-disable max-classes-per-file */
/* global AggregateError */
import Translator from '@uppy/utils/lib/Translator';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore untyped
import ee from 'namespace-emitter';
import { nanoid } from 'nanoid/non-secure';
import throttle from 'lodash/throttle.js';
import DefaultStore from '@uppy/store-default';
import getFileType from '@uppy/utils/lib/getFileType';
import getFileNameAndExtension from '@uppy/utils/lib/getFileNameAndExtension';
import { getSafeFileId } from '@uppy/utils/lib/generateFileID';
import supportsUploadProgress from './supportsUploadProgress.js';
import getFileName from './getFileName.js';
import { justErrorsLogger, debugLogger } from './loggers.js';
import { Restricter, defaultOptions as defaultRestrictionOptions, RestrictionError } from './Restricter.js';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore We don't want TS to generate types for the package.json
const packageJson = {
"version": "4.4.7"
};
import locale from './locale.js';
/**
* ids are always `string`s, except the root folder's id can be `null`
*/
/**
* PartialTree has the following structure.
*
* FolderRoot
* ┌─────┴─────┐
* FolderNode File
* ┌─────┴────┐
* File File
*
* Root folder is called `PartialTreeFolderRoot`,
* all other folders are called `PartialTreeFolderNode`, because they are "internal nodes".
*
* It's possible for `PartialTreeFolderNode` to be a leaf node if it doesn't contain any files.
*/
/**
* This is a base for a provider that does not necessarily use the Companion-assisted OAuth2 flow
*/
/*
* UnknownProviderPlugin can be any Companion plugin (such as Google Drive)
* that uses the Companion-assisted OAuth flow.
* As the plugins are passed around throughout Uppy we need a generic type for this.
* It may seems like duplication, but this type safe. Changing the type of `storage`
* will error in the `Provider` class of @uppy/companion-client and vice versa.
*
* Note that this is the *plugin* class, not a version of the `Provider` class.
* `Provider` does operate on Companion plugins with `uppy.getPlugin()`.
*/
/*
* UnknownSearchProviderPlugin can be any search Companion plugin (such as Unsplash).
* As the plugins are passed around throughout Uppy we need a generic type for this.
* It may seems like duplication, but this type safe. Changing the type of `title`
* will error in the `SearchProvider` class of @uppy/companion-client and vice versa.
*
* Note that this is the *plugin* class, not a version of the `SearchProvider` class.
* `SearchProvider` does operate on Companion plugins with `uppy.getPlugin()`.
*/
// TODO: can we use namespaces in other plugins to populate this?
// eslint-disable-next-line @typescript-eslint/no-empty-interface
// The user facing type for UppyOptions used in uppy.setOptions()
/** `OmitFirstArg<typeof someArray>` is the type of the returned value of `someArray.slice(1)`. */
const defaultUploadState = {
totalProgress: 0,
allowNewUpload: true,
error: null,
recoveredState: null
};
/**
* Uppy Core module.
* Manages plugins, state updates, acts as an event bus,
* adds/removes files and metadata.
*/
var _plugins = /*#__PURE__*/_classPrivateFieldLooseKey("plugins");
var _restricter = /*#__PURE__*/_classPrivateFieldLooseKey("restricter");
var _storeUnsubscribe = /*#__PURE__*/_classPrivateFieldLooseKey("storeUnsubscribe");
var _emitter = /*#__PURE__*/_classPrivateFieldLooseKey("emitter");
var _preProcessors = /*#__PURE__*/_classPrivateFieldLooseKey("preProcessors");
var _uploaders = /*#__PURE__*/_classPrivateFieldLooseKey("uploaders");
var _postProcessors = /*#__PURE__*/_classPrivateFieldLooseKey("postProcessors");
var _informAndEmit = /*#__PURE__*/_classPrivateFieldLooseKey("informAndEmit");
var _checkRequiredMetaFieldsOnFile = /*#__PURE__*/_classPrivateFieldLooseKey("checkRequiredMetaFieldsOnFile");
var _checkRequiredMetaFields = /*#__PURE__*/_classPrivateFieldLooseKey("checkRequiredMetaFields");
var _assertNewUploadAllowed = /*#__PURE__*/_classPrivateFieldLooseKey("assertNewUploadAllowed");
var _transformFile = /*#__PURE__*/_classPrivateFieldLooseKey("transformFile");
var _startIfAutoProceed = /*#__PURE__*/_classPrivateFieldLooseKey("startIfAutoProceed");
var _checkAndUpdateFileState = /*#__PURE__*/_classPrivateFieldLooseKey("checkAndUpdateFileState");
var _getFilesToRetry = /*#__PURE__*/_classPrivateFieldLooseKey("getFilesToRetry");
var _doRetryAll = /*#__PURE__*/_classPrivateFieldLooseKey("doRetryAll");
var _handleUploadProgress = /*#__PURE__*/_classPrivateFieldLooseKey("handleUploadProgress");
var _updateTotalProgress = /*#__PURE__*/_classPrivateFieldLooseKey("updateTotalProgress");
var _updateTotalProgressThrottled = /*#__PURE__*/_classPrivateFieldLooseKey("updateTotalProgressThrottled");
var _calculateTotalProgress = /*#__PURE__*/_classPrivateFieldLooseKey("calculateTotalProgress");
var _addListeners = /*#__PURE__*/_classPrivateFieldLooseKey("addListeners");
var _updateOnlineStatus = /*#__PURE__*/_classPrivateFieldLooseKey("updateOnlineStatus");
var _requestClientById = /*#__PURE__*/_classPrivateFieldLooseKey("requestClientById");
var _createUpload = /*#__PURE__*/_classPrivateFieldLooseKey("createUpload");
var _getUpload = /*#__PURE__*/_classPrivateFieldLooseKey("getUpload");
var _removeUpload = /*#__PURE__*/_classPrivateFieldLooseKey("removeUpload");
var _runUpload = /*#__PURE__*/_classPrivateFieldLooseKey("runUpload");
export class Uppy {
/**
* Instantiate Uppy
*/
constructor(_opts) {
/**
* Run an upload. This picks up where it left off in case the upload is being restored.
*/
Object.defineProperty(this, _runUpload, {
value: _runUpload2
});
/**
* Remove an upload, eg. if it has been canceled or completed.
*
*/
Object.defineProperty(this, _removeUpload, {
value: _removeUpload2
});
Object.defineProperty(this, _getUpload, {
value: _getUpload2
});
/**
* Create an upload for a bunch of files.
*
*/
Object.defineProperty(this, _createUpload, {
value: _createUpload2
});
/**
* Registers listeners for all global actions, like:
* `error`, `file-removed`, `upload-progress`
*/
Object.defineProperty(this, _addListeners, {
value: _addListeners2
});
Object.defineProperty(this, _calculateTotalProgress, {
value: _calculateTotalProgress2
});
Object.defineProperty(this, _updateTotalProgress, {
value: _updateTotalProgress2
});
Object.defineProperty(this, _doRetryAll, {
value: _doRetryAll2
});
Object.defineProperty(this, _getFilesToRetry, {
value: _getFilesToRetry2
});
Object.defineProperty(this, _checkAndUpdateFileState, {
value: _checkAndUpdateFileState2
});
// Schedule an upload if `autoProceed` is enabled.
Object.defineProperty(this, _startIfAutoProceed, {
value: _startIfAutoProceed2
});
/**
* Create a file state object based on user-provided `addFile()` options.
*/
Object.defineProperty(this, _transformFile, {
value: _transformFile2
});
Object.defineProperty(this, _assertNewUploadAllowed, {
value: _assertNewUploadAllowed2
});
Object.defineProperty(this, _checkRequiredMetaFields, {
value: _checkRequiredMetaFields2
});
Object.defineProperty(this, _checkRequiredMetaFieldsOnFile, {
value: _checkRequiredMetaFieldsOnFile2
});
Object.defineProperty(this, _informAndEmit, {
value: _informAndEmit2
});
Object.defineProperty(this, _plugins, {
writable: true,
value: Object.create(null)
});
Object.defineProperty(this, _restricter, {
writable: true,
value: void 0
});
Object.defineProperty(this, _storeUnsubscribe, {
writable: true,
value: void 0
});
Object.defineProperty(this, _emitter, {
writable: true,
value: ee()
});
Object.defineProperty(this, _preProcessors, {
writable: true,
value: new Set()
});
Object.defineProperty(this, _uploaders, {
writable: true,
value: new Set()
});
Object.defineProperty(this, _postProcessors, {
writable: true,
value: new Set()
});
this.scheduledAutoProceed = null;
this.wasOffline = false;
Object.defineProperty(this, _handleUploadProgress, {
writable: true,
value: (file, progress) => {
const fileInState = file ? this.getFile(file.id) : undefined;
if (file == null || !fileInState) {
this.log(`Not setting progress for a file that has been removed: ${file == null ? void 0 : file.id}`);
return;
}
if (fileInState.progress.percentage === 100) {
this.log(`Not setting progress for a file that has been already uploaded: ${file.id}`);
return;
}
const newProgress = {
bytesTotal: progress.bytesTotal,
// bytesTotal may be null or zero; in that case we can't divide by it
percentage: progress.bytesTotal != null && Number.isFinite(progress.bytesTotal) && progress.bytesTotal > 0 ? Math.round(progress.bytesUploaded / progress.bytesTotal * 100) : undefined
};
if (fileInState.progress.uploadStarted != null) {
this.setFileState(file.id, {
progress: {
...fileInState.progress,
...newProgress,
bytesUploaded: progress.bytesUploaded
}
});
} else {
this.setFileState(file.id, {
progress: {
...fileInState.progress,
...newProgress
}
});
}
_classPrivateFieldLooseBase(this, _updateTotalProgressThrottled)[_updateTotalProgressThrottled]();
}
});
// ___Why throttle at 500ms?
// - We must throttle at >250ms for superfocus in Dashboard to work well
// (because animation takes 0.25s, and we want to wait for all animations to be over before refocusing).
// [Practical Check]: if thottle is at 100ms, then if you are uploading a file,
// and click 'ADD MORE FILES', - focus won't activate in Firefox.
// - We must throttle at around >500ms to avoid performance lags.
// [Practical Check] Firefox, try to upload a big file for a prolonged period of time. Laptop will start to heat up.
Object.defineProperty(this, _updateTotalProgressThrottled, {
writable: true,
value: throttle(() => _classPrivateFieldLooseBase(this, _updateTotalProgress)[_updateTotalProgress](), 500, {
leading: true,
trailing: true
})
});
Object.defineProperty(this, _updateOnlineStatus, {
writable: true,
value: this.updateOnlineStatus.bind(this)
});
// We need to store request clients by a unique ID, so we can share RequestClient instances across files
// this allows us to do rate limiting and synchronous operations like refreshing provider tokens
// example: refreshing tokens: if each file has their own requestclient,
// we don't have any way to synchronize all requests in order to
// - block all requests
// - refresh the token
// - unblock all requests and allow them to run with a the new access token
// back when we had a requestclient per file, once an access token expired,
// all 6 files would go ahead and refresh the token at the same time
// (calling /refresh-token up to 6 times), which will probably fail for some providers
Object.defineProperty(this, _requestClientById, {
writable: true,
value: new Map()
});
this.defaultLocale = locale;
const defaultOptions = {
id: 'uppy',
autoProceed: false,
allowMultipleUploadBatches: true,
debug: false,
restrictions: defaultRestrictionOptions,
meta: {},
onBeforeFileAdded: (file, files) => !Object.hasOwn(files, file.id),
onBeforeUpload: files => files,
store: new DefaultStore(),
logger: justErrorsLogger,
infoTimeout: 5000
};
const merged = {
...defaultOptions,
..._opts
};
// Merge default options with the ones set by user,
// making sure to merge restrictions too
this.opts = {
...merged,
restrictions: {
...defaultOptions.restrictions,
...(_opts && _opts.restrictions)
}
};
// Support debug: true for backwards-compatability, unless logger is set in opts
// opts instead of this.opts to avoid comparing objects — we set logger: justErrorsLogger in defaultOptions
if (_opts && _opts.logger && _opts.debug) {
this.log('You are using a custom `logger`, but also set `debug: true`, which uses built-in logger to output logs to console. Ignoring `debug: true` and using your custom `logger`.', 'warning');
} else if (_opts && _opts.debug) {
this.opts.logger = debugLogger;
}
this.log(`Using Core v${Uppy.VERSION}`);
this.i18nInit();
this.store = this.opts.store;
this.setState({
...defaultUploadState,
plugins: {},
files: {},
currentUploads: {},
capabilities: {
uploadProgress: supportsUploadProgress(),
individualCancellation: true,
resumableUploads: false
},
meta: {
...this.opts.meta
},
info: []
});
_classPrivateFieldLooseBase(this, _restricter)[_restricter] = new Restricter(() => this.opts, () => this.i18n);
_classPrivateFieldLooseBase(this, _storeUnsubscribe)[_storeUnsubscribe] = this.store.subscribe((prevState, nextState, patch) => {
this.emit('state-update', prevState, nextState, patch);
this.updateAll(nextState);
});
// Exposing uppy object on window for debugging and testing
if (this.opts.debug && typeof window !== 'undefined') {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Mutating the global object for debug purposes
window[this.opts.id] = this;
}
_classPrivateFieldLooseBase(this, _addListeners)[_addListeners]();
}
emit(event) {
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
_classPrivateFieldLooseBase(this, _emitter)[_emitter].emit(event, ...args);
}
on(event, callback) {
_classPrivateFieldLooseBase(this, _emitter)[_emitter].on(event, callback);
return this;
}
once(event, callback) {
_classPrivateFieldLooseBase(this, _emitter)[_emitter].once(event, callback);
return this;
}
off(event, callback) {
_classPrivateFieldLooseBase(this, _emitter)[_emitter].off(event, callback);
return this;
}
/**
* Iterate on all plugins and run `update` on them.
* Called each time state changes.
*
*/
updateAll(state) {
this.iteratePlugins(plugin => {
plugin.update(state);
});
}
/**
* Updates state with a patch
*/
setState(patch) {
this.store.setState(patch);
}
/**
* Returns current state.
*/
getState() {
return this.store.getState();
}
patchFilesState(filesWithNewState) {
const existingFilesState = this.getState().files;
this.setState({
files: {
...existingFilesState,
...Object.fromEntries(Object.entries(filesWithNewState).map(_ref => {
let [fileID, newFileState] = _ref;
return [fileID, {
...existingFilesState[fileID],
...newFileState
}];
}))
}
});
}
/**
* Shorthand to set state for a specific file.
*/
setFileState(fileID, state) {
if (!this.getState().files[fileID]) {
throw new Error(`Can’t set state for ${fileID} (the file could have been removed)`);
}
this.patchFilesState({
[fileID]: state
});
}
i18nInit() {
const onMissingKey = key => this.log(`Missing i18n string: ${key}`, 'error');
const translator = new Translator([this.defaultLocale, this.opts.locale], {
onMissingKey
});
this.i18n = translator.translate.bind(translator);
this.i18nArray = translator.translateArray.bind(translator);
this.locale = translator.locale;
}
setOptions(newOpts) {
this.opts = {
...this.opts,
...newOpts,
restrictions: {
...this.opts.restrictions,
...(newOpts == null ? void 0 : newOpts.restrictions)
}
};
if (newOpts.meta) {
this.setMeta(newOpts.meta);
}
this.i18nInit();
if (newOpts.locale) {
this.iteratePlugins(plugin => {
plugin.setOptions(newOpts);
});
}
// Note: this is not the preact `setState`, it's an internal function that has the same name.
this.setState(undefined); // so that UI re-renders with new options
}
resetProgress() {
const defaultProgress = {
percentage: 0,
bytesUploaded: false,
uploadComplete: false,
uploadStarted: null
};
const files = {
...this.getState().files
};
const updatedFiles = Object.create(null);
Object.keys(files).forEach(fileID => {
updatedFiles[fileID] = {
...files[fileID],
progress: {
...files[fileID].progress,
...defaultProgress
},
// @ts-expect-error these typed are inserted
// into the namespace in their respective packages
// but core isn't ware of those
tus: undefined,
transloadit: undefined
};
});
this.setState({
files: updatedFiles,
...defaultUploadState
});
}
clear() {
const {
capabilities,
currentUploads
} = this.getState();
if (Object.keys(currentUploads).length > 0 && !capabilities.individualCancellation) {
throw new Error('The installed uploader plugin does not allow removing files during an upload.');
}
this.setState({
...defaultUploadState,
files: {}
});
}
addPreProcessor(fn) {
_classPrivateFieldLooseBase(this, _preProcessors)[_preProcessors].add(fn);
}
removePreProcessor(fn) {
return _classPrivateFieldLooseBase(this, _preProcessors)[_preProcessors].delete(fn);
}
addPostProcessor(fn) {
_classPrivateFieldLooseBase(this, _postProcessors)[_postProcessors].add(fn);
}
removePostProcessor(fn) {
return _classPrivateFieldLooseBase(this, _postProcessors)[_postProcessors].delete(fn);
}
addUploader(fn) {
_classPrivateFieldLooseBase(this, _uploaders)[_uploaders].add(fn);
}
removeUploader(fn) {
return _classPrivateFieldLooseBase(this, _uploaders)[_uploaders].delete(fn);
}
setMeta(data) {
const updatedMeta = {
...this.getState().meta,
...data
};
const updatedFiles = {
...this.getState().files
};
Object.keys(updatedFiles).forEach(fileID => {
updatedFiles[fileID] = {
...updatedFiles[fileID],
meta: {
...updatedFiles[fileID].meta,
...data
}
};
});
this.log('Adding metadata:');
this.log(data);
this.setState({
meta: updatedMeta,
files: updatedFiles
});
}
setFileMeta(fileID, data) {
const updatedFiles = {
...this.getState().files
};
if (!updatedFiles[fileID]) {
this.log(`Was trying to set metadata for a file that has been removed: ${fileID}`);
return;
}
const newMeta = {
...updatedFiles[fileID].meta,
...data
};
updatedFiles[fileID] = {
...updatedFiles[fileID],
meta: newMeta
};
this.setState({
files: updatedFiles
});
}
/**
* Get a file object.
*/
getFile(fileID) {
return this.getState().files[fileID];
}
/**
* Get all files in an array.
*/
getFiles() {
const {
files
} = this.getState();
return Object.values(files);
}
getFilesByIds(ids) {
return ids.map(id => this.getFile(id));
}
getObjectOfFilesPerState() {
const {
files: filesObject,
totalProgress,
error
} = this.getState();
const files = Object.values(filesObject);
const inProgressFiles = [];
const newFiles = [];
const startedFiles = [];
const uploadStartedFiles = [];
const pausedFiles = [];
const completeFiles = [];
const erroredFiles = [];
const inProgressNotPausedFiles = [];
const processingFiles = [];
for (const file of files) {
const {
progress
} = file;
if (!progress.uploadComplete && progress.uploadStarted) {
inProgressFiles.push(file);
if (!file.isPaused) {
inProgressNotPausedFiles.push(file);
}
}
if (!progress.uploadStarted) {
newFiles.push(file);
}
if (progress.uploadStarted || progress.preprocess || progress.postprocess) {
startedFiles.push(file);
}
if (progress.uploadStarted) {
uploadStartedFiles.push(file);
}
if (file.isPaused) {
pausedFiles.push(file);
}
if (progress.uploadComplete) {
completeFiles.push(file);
}
if (file.error) {
erroredFiles.push(file);
}
if (progress.preprocess || progress.postprocess) {
processingFiles.push(file);
}
}
return {
newFiles,
startedFiles,
uploadStartedFiles,
pausedFiles,
completeFiles,
erroredFiles,
inProgressFiles,
inProgressNotPausedFiles,
processingFiles,
isUploadStarted: uploadStartedFiles.length > 0,
isAllComplete: totalProgress === 100 && completeFiles.length === files.length && processingFiles.length === 0,
isAllErrored: !!error && erroredFiles.length === files.length,
isAllPaused: inProgressFiles.length !== 0 && pausedFiles.length === inProgressFiles.length,
isUploadInProgress: inProgressFiles.length > 0,
isSomeGhost: files.some(file => file.isGhost)
};
}
validateRestrictions(file, files) {
if (files === void 0) {
files = this.getFiles();
}
try {
_classPrivateFieldLooseBase(this, _restricter)[_restricter].validate(files, [file]);
} catch (err) {
return err;
}
return null;
}
validateSingleFile(file) {
try {
_classPrivateFieldLooseBase(this, _restricter)[_restricter].validateSingleFile(file);
} catch (err) {
return err.message;
}
return null;
}
validateAggregateRestrictions(files) {
const existingFiles = this.getFiles();
try {
_classPrivateFieldLooseBase(this, _restricter)[_restricter].validateAggregateRestrictions(existingFiles, files);
} catch (err) {
return err.message;
}
return null;
}
checkIfFileAlreadyExists(fileID) {
const {
files
} = this.getState();
if (files[fileID] && !files[fileID].isGhost) {
return true;
}
return false;
}
/**
* Add a new file to `state.files`. This will run `onBeforeFileAdded`,
* try to guess file type in a clever way, check file against restrictions,
* and start an upload if `autoProceed === true`.
*/
addFile(file) {
_classPrivateFieldLooseBase(this, _assertNewUploadAllowed)[_assertNewUploadAllowed](file);
const {
nextFilesState,
validFilesToAdd,
errors
} = _classPrivateFieldLooseBase(this, _checkAndUpdateFileState)[_checkAndUpdateFileState]([file]);
const restrictionErrors = errors.filter(error => error.isRestriction);
_classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit](restrictionErrors);
if (errors.length > 0) throw errors[0];
this.setState({
files: nextFilesState
});
const [firstValidFileToAdd] = validFilesToAdd;
this.emit('file-added', firstValidFileToAdd);
this.emit('files-added', validFilesToAdd);
this.log(`Added file: ${firstValidFileToAdd.name}, ${firstValidFileToAdd.id}, mime type: ${firstValidFileToAdd.type}`);
_classPrivateFieldLooseBase(this, _startIfAutoProceed)[_startIfAutoProceed]();
return firstValidFileToAdd.id;
}
/**
* Add multiple files to `state.files`. See the `addFile()` documentation.
*
* If an error occurs while adding a file, it is logged and the user is notified.
* This is good for UI plugins, but not for programmatic use.
* Programmatic users should usually still use `addFile()` on individual files.
*/
addFiles(fileDescriptors) {
_classPrivateFieldLooseBase(this, _assertNewUploadAllowed)[_assertNewUploadAllowed]();
const {
nextFilesState,
validFilesToAdd,
errors
} = _classPrivateFieldLooseBase(this, _checkAndUpdateFileState)[_checkAndUpdateFileState](fileDescriptors);
const restrictionErrors = errors.filter(error => error.isRestriction);
_classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit](restrictionErrors);
const nonRestrictionErrors = errors.filter(error => !error.isRestriction);
if (nonRestrictionErrors.length > 0) {
let message = 'Multiple errors occurred while adding files:\n';
nonRestrictionErrors.forEach(subError => {
message += `\n * ${subError.message}`;
});
this.info({
message: this.i18n('addBulkFilesFailed', {
smart_count: nonRestrictionErrors.length
}),
details: message
}, 'error', this.opts.infoTimeout);
if (typeof AggregateError === 'function') {
throw new AggregateError(nonRestrictionErrors, message);
} else {
const err = new Error(message);
// @ts-expect-error fallback when AggregateError is not available
err.errors = nonRestrictionErrors;
throw err;
}
}
// OK, we haven't thrown an error, we can start updating state and emitting events now:
this.setState({
files: nextFilesState
});
validFilesToAdd.forEach(file => {
this.emit('file-added', file);
});
this.emit('files-added', validFilesToAdd);
if (validFilesToAdd.length > 5) {
this.log(`Added batch of ${validFilesToAdd.length} files`);
} else {
Object.values(validFilesToAdd).forEach(file => {
this.log(`Added file: ${file.name}\n id: ${file.id}\n type: ${file.type}`);
});
}
if (validFilesToAdd.length > 0) {
_classPrivateFieldLooseBase(this, _startIfAutoProceed)[_startIfAutoProceed]();
}
}
removeFiles(fileIDs) {
const {
files,
currentUploads
} = this.getState();
const updatedFiles = {
...files
};
const updatedUploads = {
...currentUploads
};
const removedFiles = Object.create(null);
fileIDs.forEach(fileID => {
if (files[fileID]) {
removedFiles[fileID] = files[fileID];
delete updatedFiles[fileID];
}
});
// Remove files from the `fileIDs` list in each upload.
function fileIsNotRemoved(uploadFileID) {
return removedFiles[uploadFileID] === undefined;
}
Object.keys(updatedUploads).forEach(uploadID => {
const newFileIDs = currentUploads[uploadID].fileIDs.filter(fileIsNotRemoved);
// Remove the upload if no files are associated with it anymore.
if (newFileIDs.length === 0) {
delete updatedUploads[uploadID];
return;
}
const {
capabilities
} = this.getState();
if (newFileIDs.length !== currentUploads[uploadID].fileIDs.length && !capabilities.individualCancellation) {
throw new Error('The installed uploader plugin does not allow removing files during an upload.');
}
updatedUploads[uploadID] = {
...currentUploads[uploadID],
fileIDs: newFileIDs
};
});
const stateUpdate = {
currentUploads: updatedUploads,
files: updatedFiles
};
// If all files were removed - allow new uploads,
// and clear recoveredState
if (Object.keys(updatedFiles).length === 0) {
stateUpdate.allowNewUpload = true;
stateUpdate.error = null;
stateUpdate.recoveredState = null;
}
this.setState(stateUpdate);
_classPrivateFieldLooseBase(this, _updateTotalProgressThrottled)[_updateTotalProgressThrottled]();
const removedFileIDs = Object.keys(removedFiles);
removedFileIDs.forEach(fileID => {
this.emit('file-removed', removedFiles[fileID]);
});
if (removedFileIDs.length > 5) {
this.log(`Removed ${removedFileIDs.length} files`);
} else {
this.log(`Removed files: ${removedFileIDs.join(', ')}`);
}
}
removeFile(fileID) {
this.removeFiles([fileID]);
}
pauseResume(fileID) {
if (!this.getState().capabilities.resumableUploads || this.getFile(fileID).progress.uploadComplete) {
return undefined;
}
const file = this.getFile(fileID);
const wasPaused = file.isPaused || false;
const isPaused = !wasPaused;
this.setFileState(fileID, {
isPaused
});
this.emit('upload-pause', file, isPaused);
return isPaused;
}
pauseAll() {
const updatedFiles = {
...this.getState().files
};
const inProgressUpdatedFiles = Object.keys(updatedFiles).filter(file => {
return !updatedFiles[file].progress.uploadComplete && updatedFiles[file].progress.uploadStarted;
});
inProgressUpdatedFiles.forEach(file => {
const updatedFile = {
...updatedFiles[file],
isPaused: true
};
updatedFiles[file] = updatedFile;
});
this.setState({
files: updatedFiles
});
this.emit('pause-all');
}
resumeAll() {
const updatedFiles = {
...this.getState().files
};
const inProgressUpdatedFiles = Object.keys(updatedFiles).filter(file => {
return !updatedFiles[file].progress.uploadComplete && updatedFiles[file].progress.uploadStarted;
});
inProgressUpdatedFiles.forEach(file => {
const updatedFile = {
...updatedFiles[file],
isPaused: false,
error: null
};
updatedFiles[file] = updatedFile;
});
this.setState({
files: updatedFiles
});
this.emit('resume-all');
}
async retryAll() {
const result = await _classPrivateFieldLooseBase(this, _doRetryAll)[_doRetryAll]();
this.emit('complete', result);
return result;
}
cancelAll() {
this.emit('cancel-all');
const {
files
} = this.getState();
const fileIDs = Object.keys(files);
if (fileIDs.length) {
this.removeFiles(fileIDs);
}
this.setState(defaultUploadState);
}
retryUpload(fileID) {
this.setFileState(fileID, {
error: null,
isPaused: false
});
this.emit('upload-retry', this.getFile(fileID));
const uploadID = _classPrivateFieldLooseBase(this, _createUpload)[_createUpload]([fileID], {
forceAllowNewUpload: true // create new upload even if allowNewUpload: false
});
return _classPrivateFieldLooseBase(this, _runUpload)[_runUpload](uploadID);
}
logout() {
this.iteratePlugins(plugin => {
var _provider;
;
(_provider = plugin.provider) == null || _provider.logout == null || _provider.logout();
});
}
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/explicit-module-boundary-types
[Symbol.for('uppy test: updateTotalProgress')]() {
return _classPrivateFieldLooseBase(this, _updateTotalProgress)[_updateTotalProgress]();
}
updateOnlineStatus() {
var _window$navigator$onL;
const online = (_window$navigator$onL = window.navigator.onLine) != null ? _window$navigator$onL : true;
if (!online) {
this.emit('is-offline');
this.info(this.i18n('noInternetConnection'), 'error', 0);
this.wasOffline = true;
} else {
this.emit('is-online');
if (this.wasOffline) {
this.emit('back-online');
this.info(this.i18n('connectedToInternet'), 'success', 3000);
this.wasOffline = false;
}
}
}
getID() {
return this.opts.id;
}
/**
* Registers a plugin with Core.
*/
use(Plugin) {
if (typeof Plugin !== 'function') {
const msg = `Expected a plugin class, but got ${Plugin === null ? 'null' : typeof Plugin}.` + ' Please verify that the plugin was imported and spelled correctly.';
throw new TypeError(msg);
}
// Instantiate
for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
args[_key2 - 1] = arguments[_key2];
}
const plugin = new Plugin(this, ...args);
const pluginId = plugin.id;
if (!pluginId) {
throw new Error('Your plugin must have an id');
}
if (!plugin.type) {
throw new Error('Your plugin must have a type');
}
const existsPluginAlready = this.getPlugin(pluginId);
if (existsPluginAlready) {
const msg = `Already found a plugin named '${existsPluginAlready.id}'. ` + `Tried to use: '${pluginId}'.\n` + 'Uppy plugins must have unique `id` options.';
throw new Error(msg);
}
// @ts-expect-error does exist
if (Plugin.VERSION) {
// @ts-expect-error does exist
this.log(`Using ${pluginId} v${Plugin.VERSION}`);
}
if (plugin.type in _classPrivateFieldLooseBase(this, _plugins)[_plugins]) {
_classPrivateFieldLooseBase(this, _plugins)[_plugins][plugin.type].push(plugin);
} else {
_classPrivateFieldLooseBase(this, _plugins)[_plugins][plugin.type] = [plugin];
}
plugin.install();
this.emit('plugin-added', plugin);
return this;
}
/**
* Find one Plugin by name.
*/
getPlugin(id) {
for (const plugins of Object.values(_classPrivateFieldLooseBase(this, _plugins)[_plugins])) {
const foundPlugin = plugins.find(plugin => plugin.id === id);
if (foundPlugin != null) return foundPlugin;
}
return undefined;
}
[Symbol.for('uppy test: getPlugins')](type) {
return _classPrivateFieldLooseBase(this, _plugins)[_plugins][type];
}
/**
* Iterate through all `use`d plugins.
*
*/
iteratePlugins(method) {
Object.values(_classPrivateFieldLooseBase(this, _plugins)[_plugins]).flat(1).forEach(method);
}
/**
* Uninstall and remove a plugin.
*
* @param {object} instance The plugin instance to remove.
*/
removePlugin(instance) {
this.log(`Removing plugin ${instance.id}`);
this.emit('plugin-remove', instance);
if (instance.uninstall) {
instance.uninstall();
}
const list = _classPrivateFieldLooseBase(this, _plugins)[_plugins][instance.type];
// list.indexOf failed here, because Vue3 converted the plugin instance
// to a Proxy object, which failed the strict comparison test:
// obj !== objProxy
const index = list.findIndex(item => item.id === instance.id);
if (index !== -1) {
list.splice(index, 1);
}
const state = this.getState();
const updatedState = {
plugins: {
...state.plugins,
[instance.id]: undefined
}
};
this.setState(updatedState);
}
/**
* Uninstall all plugins and close down this Uppy instance.
*/
destroy() {
this.log(`Closing Uppy instance ${this.opts.id}: removing all files and uninstalling plugins`);
this.cancelAll();
_classPrivateFieldLooseBase(this, _storeUnsubscribe)[_storeUnsubscribe]();
this.iteratePlugins(plugin => {
this.removePlugin(plugin);
});
if (typeof window !== 'undefined' && window.removeEventListener) {
window.removeEventListener('online', _classPrivateFieldLooseBase(this, _updateOnlineStatus)[_updateOnlineStatus]);
window.removeEventListener('offline', _classPrivateFieldLooseBase(this, _updateOnlineStatus)[_updateOnlineStatus]);
}
}
hideInfo() {
const {
info
} = this.getState();
this.setState({
info: info.slice(1)
});
this.emit('info-hidden');
}
/**
* Set info message in `state.info`, so that UI plugins like `Informer`
* can display the message.
*/
info(message, type, duration) {
if (type === void 0) {
type = 'info';
}
if (duration === void 0) {
duration = 3000;
}
const isComplexMessage = typeof message === 'object';
this.setState({
info: [...this.getState().info, {
type,
message: isComplexMessage ? message.message : message,
details: isComplexMessage ? message.details : null
}]
});
setTimeout(() => this.hideInfo(), duration);
this.emit('info-visible');
}
/**
* Passes messages to a function, provided in `opts.logger`.
* If `opts.logger: Uppy.debugLogger` or `opts.debug: true`, logs to the browser console.
*/
log(message, type) {
const {
logger
} = this.opts;
switch (type) {
case 'error':
logger.error(message);
break;
case 'warning':
logger.warn(message);
break;
default:
logger.debug(message);
break;
}
}
registerRequestClient(id, client) {
_classPrivateFieldLooseBase(this, _requestClientById)[_requestClientById].set(id, client);
}
/** @protected */
getRequestClientForFile(file) {
if (!file.remote) throw new Error(`Tried to get RequestClient for a non-remote file ${file.id}`);
const requestClient = _classPrivateFieldLooseBase(this, _requestClientById)[_requestClientById].get(file.remote.requestClientId);
if (requestClient == null) throw new Error(`requestClientId "${file.remote.requestClientId}" not registered for file "${file.id}"`);
return requestClient;
}
/**
* Restore an upload by its ID.
*/
restore(uploadID) {
this.log(`Core: attempting to restore upload "${uploadID}"`);
if (!this.getState().currentUploads[uploadID]) {
_classPrivateFieldLooseBase(this, _removeUpload)[_removeUpload](uploadID);
return Promise.reject(new Error('Nonexistent upload'));
}
return _classPrivateFieldLooseBase(this, _runUpload)[_runUpload](uploadID);
}
[Symbol.for('uppy test: createUpload')]() {
// @ts-expect-error https://github.com/microsoft/TypeScript/issues/47595
return _classPrivateFieldLooseBase(this, _createUpload)[_createUpload](...arguments);
}
/**
* Add data to an upload's result object.
*/
addResultData(uploadID, data) {
if (!_classPrivateFieldLooseBase(this, _getUpload)[_getUpload](uploadID)) {
this.log(`Not setting result for an upload that has been removed: ${uploadID}`);
return;
}
const {
currentUploads
} = this.getState();
const currentUpload = {
...currentUploads[uploadID],
result: {
...currentUploads[uploadID].result,
...data
}
};
this.setState({
currentUploads: {
...currentUploads,
[uploadID]: currentUpload
}
});
}
/**
* Start an upload for all the files that are not currently being uploaded.
*/
async upload() {
var _classPrivateFieldLoo;
if (!((_classPrivateFieldLoo = _classPrivateFieldLooseBase(this, _plugins)[_plugins]['uploader']) != null && _classPrivateFieldLoo.length)) {
this.log('No uploader type plugins are used', 'warning');
}
let {
files
} = this.getState();
// retry any failed files from a previous upload() call
const filesToRetry = _classPrivateFieldLooseBase(this, _getFilesToRetry)[_getFilesToRetry]();
if (filesToRetry.length > 0) {
const retryResult = await _classPrivateFieldLooseBase(this, _doRetryAll)[_doRetryAll](); // we don't want the complete event to fire
const hasNewFiles = this.getFiles().filter(file => file.progress.uploadStarted == null).length > 0;
// if no new files, make it idempotent and return
if (!hasNewFiles) {
this.emit('complete', retryResult);
return retryResult;
}
// reload files which might have changed after retry
;
({
files
} = this.getState());
}
// If no files to retry, proceed with original upload() behavior for new files
const onBeforeUploadResult = this.opts.onBeforeUpload(files);
if (onBeforeUploadResult === false) {
return Promise.reject(new Error('Not starting the upload because onBeforeUpload returned false'));
}
if (onBeforeUploadResult && typeof onBeforeUploadResult === 'object') {
files = onBeforeUploadResult;
// Updating files in state, because uploader plugins receive file IDs,
// and then fetch the actual file object from state
this.setState({
files
});
}
return Promise.resolve().then(() => _classPrivateFieldLooseBase(this, _restricter)[_restricter].validateMinNumberOfFiles(files)).catch(err => {
_classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit]([err]);
throw err;
}).then(() => {
if (!_classPrivateFieldLooseBase(this, _checkRequiredMetaFields)[_checkRequiredMetaFields](files)) {
throw new RestrictionError(this.i18n('missingRequiredMetaField'));
}
}).catch(err => {
// Doing this in a separate catch because we already emited and logged
// all the errors in `checkRequiredMetaFields` so we only throw a generic
// missing fields error here.
throw err;
}).then(async () => {
const {
currentUploads
} = this.getState();
// get a list of files that are currently assigned to uploads
const currentlyUploadingFiles = Object.values(currentUploads).flatMap(curr => curr.fileIDs);
const waitingFileIDs = [];
Object.keys(files).forEach(fileID => {
const file = this.getFile(fileID);
// if the file hasn't started uploading and hasn't already been assigned to an upload..
if (!file.progress.uploadStarted && currentlyUploadingFiles.indexOf(fileID) === -1) {
waitingFileIDs.push(file.id);
}
});
const uploadID = _classPrivateFieldLooseBase(this, _createUpload)[_createUpload](waitingFileIDs);
const result = await _classPrivateFieldLooseBase(this, _runUpload)[_runUpload](uploadID);
this.emit('complete', result);
return result;
}).catch(err => {
this.emit('error', err);
this.log(err, 'error');
throw err;
});
}
}
function _informAndEmit2(errors) {
for (const error of errors) {
if (error.isRestriction) {
this.emit('restriction-failed', error.file, error);
} else {
this.emit('error', error, error.file);
}
this.log(error, 'warning');
}
const userFacingErrors = errors.filter(error => error.isUserFacing);
// don't flood the user: only show the first 4 toasts
const maxNumToShow = 4;
const firstErrors = userFacingErrors.slice(0, maxNumToShow);
const additionalErrors = userFacingErrors.slice(maxNumToShow);
firstErrors.forEach(_ref2 => {
let {
message,
details = ''
} = _ref2;
this.info({
message,
details
}, 'error', this.opts.infoTimeout);
});
if (additionalErrors.length > 0) {
this.info({
message: this.i18n('additionalRestrictionsFailed', {
count: additionalErrors.length
})
});
}
}
function _checkRequiredMetaFieldsOnFile2(file) {
const {
missingFields,
error
} = _classPrivateFieldLooseBase(this, _restricter)[_restricter].getMissingRequiredMetaFields(file);
if (missingFields.length > 0) {
this.setFileState(file.id, {
missingRequiredMetaFields: missingFields
});
this.log(error.message);
this.emit('restriction-failed', file, error);
return false;
}
if (missingFields.length === 0 && file.missingRequiredMetaFields) {
this.setFileState(file.id, {
missingRequiredMetaFields: []
});
}
return true;
}
function _checkRequiredMetaFields2(files) {
let success = true;
for (const file of Object.values(files)) {
if (!_classPrivateFieldLooseBase(this, _checkRequiredMetaFieldsOnFile)[_checkRequiredMetaFieldsOnFile](file)) {
success = false;
}
}
return success;
}
function _assertNewUploadAllowed2(file) {
const {
allowNewUpload
} = this.getState();
if (allowNewUpload === false) {
const error = new RestrictionError(this.i18n('noMoreFilesAllowed'), {
file
});
_classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit]([error]);
throw error;
}
}
function _transformFile2(fileDescriptorOrFile) {
// Uppy expects files in { name, type, size, data } format.
// If the actual File object is passed from input[type=file] or drag-drop,
// we normalize it to match Uppy file object
const file = fileDescriptorOrFile instanceof File ? {
name: fileDescriptorOrFile.name,
type: fileDescriptorOrFile.type,
size: fileDescriptorOrFile.size,
data: fileDescriptorOrFile
} : fileDescriptorOrFile;
const fileType = getFileType(file);
const fileName = getFileName(fileType, file);
const fileExtension = getFileNameAndExtension(fileName).extension;
const id = getSafeFileId(file, this.getID());
const meta = file.meta || {};
meta.name = fileName;
meta.type = fileType;
// `null` means the size is unknown.
const size = Number.isFinite(file.data.size) ? file.data.size : null;
return {
source: file.source || '',
id,
name: fileName,
extension: fileExtension || '',
meta: {
...this.getState().meta,
...meta
},
type: fileType,
data: file.data,
progress: {
percentage: 0,
bytesUploaded: false,
bytesTotal: size,
uploadComplete: false,
uploadStarted: null
},
size,
isGhost: false,
isRemote: file.isRemote || false,
remote: file.remote,
preview: file.preview
};
}
function _startIfAutoProceed2() {
if (this.opts.autoProceed && !this.scheduledAutoProceed) {
this.scheduledAutoProceed = setTimeout(() => {
this.scheduledAutoProceed = null;
this.upload().catch(err => {
if (!err.isRestriction) {
this.log(err.stack || err.message || err);
}
});
}, 4);
}
}
function _checkAndUpdateFileState2(filesToAdd) {
const {
files: existingFiles
} = this.getState();
// create a copy of the files object only once
const nextFilesState = {
...existingFiles
};
const validFilesToAdd = [];
const errors = [];
for (const fileToAdd of filesToAdd) {
try {
var _existingFiles$newFil;
let newFile = _classPrivateFieldLooseBase(this, _transformFile)[_transformFile](fileToAdd);
// If a file has been recovered (Golden Retriever), but we were unable to recover its data (probably too large),
// users are asked to re-select these half-recovered files and then this method will be called again.
// In order to keep the progress, meta and everything else, we keep the existing file,
// but we replace `data`, and we remove `isGhost`, because the file is no longer a ghost now
const isGhost = (_existingFiles$newFil = existingFiles[newFile.id]) == null ? void 0 : _existingFiles$newFil.isGhost;
if (isGhost) {
const existingFileState = existingFiles[newFile.id];
newFile = {
...existingFileState,
isGhost: false,
data: fileToAdd.data
};
this.log(`Replaced the blob in the restored ghost file: ${newFile.name}, ${newFile.id}`);
}
const onBeforeFileAddedResult = this.opts.onBeforeFileAdded(newFile, nextFilesState);
if (!onBeforeFileAddedResult && this.checkIfFileAlreadyExists(newFile.id)) {
var _newFile$name;
throw new RestrictionError(this.i18n('noDuplicates', {
fileName: (_newFile$name = newFile.name) != null ? _newFile$name : this.i18n('unnamed')
}), {
file: fileToAdd
});
}
// Pass through reselected files from Golden Retriever
if (onBeforeFileAddedResult === false && !isGhost) {
// Don’t show UI info for this error, as it should be done by the developer
throw new RestrictionError('Cannot add the file because onBeforeFileAdded returned false.', {
isUserFacing: false,
file: fileToAdd
});
} else if (typeof onBeforeFileAddedResult === 'object' && onBeforeFileAddedResult !== null) {
newFile = onBeforeFileAddedResult;
}
_classPrivateFieldLooseBase(this, _restricter)[_restricter].validateSingleFile(newFile);
// need to add it to the new local state immediately, so we can use the state to validate the next files too
nextFilesState[newFile.id] = newFile;
validFilesToAdd.push(newFile);
} catch (err) {
errors.push(err);
}
}
try {
// need to run this separately because it's much more slow, so if we run it inside the for-loop it will be very slow
// when many files are added
_classPrivateFieldLooseBase(this, _restricter)[_restricter].validateAggregateRestrictions(Object.values(existingFiles), validFilesToAdd);
} catch (err) {
errors.push(err);
// If we have any aggregate error, don't allow adding this batch
return {
nextFilesState: existingFiles,
validFilesToAdd: [],
errors
};
}
return {
nextFilesState,
validFilesToAdd,
errors
};
}
function _getFilesToRetry2() {
const {
files
} = this.getState();
return Object.keys(files).filter(file => {
return files[file].error;
});
}
async function _doRetryAll2() {
const filesToRetry = _classPrivateFieldLooseBase(this, _getFilesToRetry)[_getFilesToRetry]();
const updatedFiles = {
...this.getState().files
};
filesToRetry.forEach(fileID => {
updatedFiles[fileID] = {
...updatedFiles[fileID],
isPaused: false,
error: null
};
});
this.setState({
files: updatedFiles,
error: null
});
this.emit('retry-all', this.getFilesByIds(filesToRetry));
if (filesToRetry.length === 0) {