UNPKG

@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
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) {