File: //home/arjun/projects/buyercall/node_modules/@ckeditor/ckeditor5-upload/src/filerepository.js
/**
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module upload/filerepository
*/
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import PendingActions from '@ckeditor/ckeditor5-core/src/pendingactions';
import CKEditorError, { logWarning } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import Collection from '@ckeditor/ckeditor5-utils/src/collection';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
import FileReader from './filereader.js';
import uid from '@ckeditor/ckeditor5-utils/src/uid.js';
/**
* File repository plugin. A central point for managing file upload.
*
* To use it, first you need an upload adapter. Upload adapter's job is to handle communication with the server
* (sending the file and handling server's response). You can use one of the existing plugins introducing upload adapters
* (e.g. {@link module:easy-image/cloudservicesuploadadapter~CloudServicesUploadAdapter} or
* {@link module:adapter-ckfinder/uploadadapter~CKFinderUploadAdapter}) or write your own one – see
* the {@glink framework/guides/deep-dive/upload-adapter Custom image upload adapter deep dive guide}.
*
* Then, you can use {@link module:upload/filerepository~FileRepository#createLoader `createLoader()`} and the returned
* {@link module:upload/filerepository~FileLoader} instance to load and upload files.
*
* @extends module:core/plugin~Plugin
*/
export default class FileRepository extends Plugin {
/**
* @inheritDoc
*/
static get pluginName() {
return 'FileRepository';
}
/**
* @inheritDoc
*/
static get requires() {
return [ PendingActions ];
}
/**
* @inheritDoc
*/
init() {
/**
* Collection of loaders associated with this repository.
*
* @member {module:utils/collection~Collection} #loaders
*/
this.loaders = new Collection();
// Keeps upload in a sync with pending actions.
this.loaders.on( 'add', () => this._updatePendingAction() );
this.loaders.on( 'remove', () => this._updatePendingAction() );
/**
* Loaders mappings used to retrieve loaders references.
*
* @private
* @member {Map<File|Promise, FileLoader>} #_loadersMap
*/
this._loadersMap = new Map();
/**
* Reference to a pending action registered in a {@link module:core/pendingactions~PendingActions} plugin
* while upload is in progress. When there is no upload then value is `null`.
*
* @private
* @member {Object} #_pendingAction
*/
this._pendingAction = null;
/**
* A factory function which should be defined before using `FileRepository`.
*
* It should return a new instance of {@link module:upload/filerepository~UploadAdapter} that will be used to upload files.
* {@link module:upload/filerepository~FileLoader} instance associated with the adapter
* will be passed to that function.
*
* For more information and example see {@link module:upload/filerepository~UploadAdapter}.
*
* @member {Function} #createUploadAdapter
*/
/**
* Number of bytes uploaded.
*
* @readonly
* @observable
* @member {Number} #uploaded
*/
this.set( 'uploaded', 0 );
/**
* Number of total bytes to upload.
*
* It might be different than the file size because of headers and additional data.
* It contains `null` if value is not available yet, so it's better to use {@link #uploadedPercent} to monitor
* the progress.
*
* @readonly
* @observable
* @member {Number|null} #uploadTotal
*/
this.set( 'uploadTotal', null );
/**
* Upload progress in percents.
*
* @readonly
* @observable
* @member {Number} #uploadedPercent
*/
this.bind( 'uploadedPercent' ).to( this, 'uploaded', this, 'uploadTotal', ( uploaded, total ) => {
return total ? ( uploaded / total * 100 ) : 0;
} );
}
/**
* Returns the loader associated with specified file or promise.
*
* To get loader by id use `fileRepository.loaders.get( id )`.
*
* @param {File|Promise.<File>} fileOrPromise Native file or promise handle.
* @returns {module:upload/filerepository~FileLoader|null}
*/
getLoader( fileOrPromise ) {
return this._loadersMap.get( fileOrPromise ) || null;
}
/**
* Creates a loader instance for the given file.
*
* Requires {@link #createUploadAdapter} factory to be defined.
*
* @param {File|Promise.<File>} fileOrPromise Native File object or native Promise object which resolves to a File.
* @returns {module:upload/filerepository~FileLoader|null}
*/
createLoader( fileOrPromise ) {
if ( !this.createUploadAdapter ) {
/**
* You need to enable an upload adapter in order to be able to upload files.
*
* This warning shows up when {@link module:upload/filerepository~FileRepository} is being used
* without {@link #createUploadAdapter defining an upload adapter}.
*
* **If you see this warning when using one of the {@glink builds/index CKEditor 5 Builds}**
* it means that you did not configure any of the upload adapters available by default in those builds.
*
* See the {@glink features/images/image-upload/image-upload comprehensive "Image upload overview"} to learn which upload
* adapters are available in the builds and how to configure them.
*
* **If you see this warning when using a custom build** there is a chance that you enabled
* a feature like {@link module:image/imageupload~ImageUpload},
* or {@link module:image/imageupload/imageuploadui~ImageUploadUI} but you did not enable any upload adapter.
* You can choose one of the existing upload adapters listed in the
* {@glink features/images/image-upload/image-upload "Image upload overview"}.
*
* You can also implement your {@glink framework/guides/deep-dive/upload-adapter own image upload adapter}.
*
* @error filerepository-no-upload-adapter
*/
logWarning( 'filerepository-no-upload-adapter' );
return null;
}
const loader = new FileLoader( Promise.resolve( fileOrPromise ), this.createUploadAdapter );
this.loaders.add( loader );
this._loadersMap.set( fileOrPromise, loader );
// Store also file => loader mapping so loader can be retrieved by file instance returned upon Promise resolution.
if ( fileOrPromise instanceof Promise ) {
loader.file
.then( file => {
this._loadersMap.set( file, loader );
} )
// Every then() must have a catch().
// File loader state (and rejections) are handled in read() and upload().
// Also, see the "does not swallow the file promise rejection" test.
.catch( () => {} );
}
loader.on( 'change:uploaded', () => {
let aggregatedUploaded = 0;
for ( const loader of this.loaders ) {
aggregatedUploaded += loader.uploaded;
}
this.uploaded = aggregatedUploaded;
} );
loader.on( 'change:uploadTotal', () => {
let aggregatedTotal = 0;
for ( const loader of this.loaders ) {
if ( loader.uploadTotal ) {
aggregatedTotal += loader.uploadTotal;
}
}
this.uploadTotal = aggregatedTotal;
} );
return loader;
}
/**
* Destroys the given loader.
*
* @param {File|Promise|module:upload/filerepository~FileLoader} fileOrPromiseOrLoader File or Promise associated
* with that loader or loader itself.
*/
destroyLoader( fileOrPromiseOrLoader ) {
const loader = fileOrPromiseOrLoader instanceof FileLoader ? fileOrPromiseOrLoader : this.getLoader( fileOrPromiseOrLoader );
loader._destroy();
this.loaders.remove( loader );
this._loadersMap.forEach( ( value, key ) => {
if ( value === loader ) {
this._loadersMap.delete( key );
}
} );
}
/**
* Registers or deregisters pending action bound with upload progress.
*
* @private
*/
_updatePendingAction() {
const pendingActions = this.editor.plugins.get( PendingActions );
if ( this.loaders.length ) {
if ( !this._pendingAction ) {
const t = this.editor.t;
const getMessage = value => `${ t( 'Upload in progress' ) } ${ parseInt( value ) }%.`;
this._pendingAction = pendingActions.add( getMessage( this.uploadedPercent ) );
this._pendingAction.bind( 'message' ).to( this, 'uploadedPercent', getMessage );
}
} else {
pendingActions.remove( this._pendingAction );
this._pendingAction = null;
}
}
}
mix( FileRepository, ObservableMixin );
/**
* File loader class.
*
* It is used to control the process of reading the file and uploading it using the specified upload adapter.
*/
class FileLoader {
/**
* Creates a new instance of `FileLoader`.
*
* @param {Promise.<File>} filePromise A promise which resolves to a file instance.
* @param {Function} uploadAdapterCreator The function which returns {@link module:upload/filerepository~UploadAdapter} instance.
*/
constructor( filePromise, uploadAdapterCreator ) {
/**
* Unique id of FileLoader instance.
*
* @readonly
* @member {Number}
*/
this.id = uid();
/**
* Additional wrapper over the initial file promise passed to this loader.
*
* @protected
* @member {module:upload/filerepository~FilePromiseWrapper}
*/
this._filePromiseWrapper = this._createFilePromiseWrapper( filePromise );
/**
* Adapter instance associated with this file loader.
*
* @private
* @member {module:upload/filerepository~UploadAdapter}
*/
this._adapter = uploadAdapterCreator( this );
/**
* FileReader used by FileLoader.
*
* @protected
* @member {module:upload/filereader~FileReader}
*/
this._reader = new FileReader();
/**
* Current status of FileLoader. It can be one of the following:
*
* * 'idle',
* * 'reading',
* * 'uploading',
* * 'aborted',
* * 'error'.
*
* When reading status can change in a following way:
*
* `idle` -> `reading` -> `idle`
* `idle` -> `reading -> `aborted`
* `idle` -> `reading -> `error`
*
* When uploading status can change in a following way:
*
* `idle` -> `uploading` -> `idle`
* `idle` -> `uploading` -> `aborted`
* `idle` -> `uploading` -> `error`
*
* @readonly
* @observable
* @member {String} #status
*/
this.set( 'status', 'idle' );
/**
* Number of bytes uploaded.
*
* @readonly
* @observable
* @member {Number} #uploaded
*/
this.set( 'uploaded', 0 );
/**
* Number of total bytes to upload.
*
* @readonly
* @observable
* @member {Number|null} #uploadTotal
*/
this.set( 'uploadTotal', null );
/**
* Upload progress in percents.
*
* @readonly
* @observable
* @member {Number} #uploadedPercent
*/
this.bind( 'uploadedPercent' ).to( this, 'uploaded', this, 'uploadTotal', ( uploaded, total ) => {
return total ? ( uploaded / total * 100 ) : 0;
} );
/**
* Response of the upload.
*
* @readonly
* @observable
* @member {Object|null} #uploadResponse
*/
this.set( 'uploadResponse', null );
}
/**
* A `Promise` which resolves to a `File` instance associated with this file loader.
*
* @type {Promise.<File|null>}
*/
get file() {
if ( !this._filePromiseWrapper ) {
// Loader was destroyed, return promise which resolves to null.
return Promise.resolve( null );
} else {
// The `this._filePromiseWrapper.promise` is chained and not simply returned to handle a case when:
//
// * The `loader.file.then( ... )` is called by external code (returned promise is pending).
// * Then `loader._destroy()` is called (call is synchronous) which destroys the `loader`.
// * Promise returned by the first `loader.file.then( ... )` call is resolved.
//
// Returning `this._filePromiseWrapper.promise` will still resolve to a `File` instance so there
// is an additional check needed in the chain to see if `loader` was destroyed in the meantime.
return this._filePromiseWrapper.promise.then( file => this._filePromiseWrapper ? file : null );
}
}
/**
* Returns the file data. To read its data, you need for first load the file
* by using the {@link module:upload/filerepository~FileLoader#read `read()`} method.
*
* @type {File|undefined}
*/
get data() {
return this._reader.data;
}
/**
* Reads file using {@link module:upload/filereader~FileReader}.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `filerepository-read-wrong-status` when status
* is different than `idle`.
*
* Example usage:
*
* fileLoader.read()
* .then( data => { ... } )
* .catch( err => {
* if ( err === 'aborted' ) {
* console.log( 'Reading aborted.' );
* } else {
* console.log( 'Reading error.', err );
* }
* } );
*
* @returns {Promise.<String>} Returns promise that will be resolved with read data. Promise will be rejected if error
* occurs or if read process is aborted.
*/
read() {
if ( this.status != 'idle' ) {
/**
* You cannot call read if the status is different than idle.
*
* @error filerepository-read-wrong-status
*/
throw new CKEditorError( 'filerepository-read-wrong-status', this );
}
this.status = 'reading';
return this.file
.then( file => this._reader.read( file ) )
.then( data => {
// Edge case: reader was aborted after file was read - double check for proper status.
// It can happen when image was deleted during its upload.
if ( this.status !== 'reading' ) {
throw this.status;
}
this.status = 'idle';
return data;
} )
.catch( err => {
if ( err === 'aborted' ) {
this.status = 'aborted';
throw 'aborted';
}
this.status = 'error';
throw this._reader.error ? this._reader.error : err;
} );
}
/**
* Reads file using the provided {@link module:upload/filerepository~UploadAdapter}.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `filerepository-upload-wrong-status` when status
* is different than `idle`.
* Example usage:
*
* fileLoader.upload()
* .then( data => { ... } )
* .catch( e => {
* if ( e === 'aborted' ) {
* console.log( 'Uploading aborted.' );
* } else {
* console.log( 'Uploading error.', e );
* }
* } );
*
* @returns {Promise.<Object>} Returns promise that will be resolved with response data. Promise will be rejected if error
* occurs or if read process is aborted.
*/
upload() {
if ( this.status != 'idle' ) {
/**
* You cannot call upload if the status is different than idle.
*
* @error filerepository-upload-wrong-status
*/
throw new CKEditorError( 'filerepository-upload-wrong-status', this );
}
this.status = 'uploading';
return this.file
.then( () => this._adapter.upload() )
.then( data => {
this.uploadResponse = data;
this.status = 'idle';
return data;
} )
.catch( err => {
if ( this.status === 'aborted' ) {
throw 'aborted';
}
this.status = 'error';
throw err;
} );
}
/**
* Aborts loading process.
*/
abort() {
const status = this.status;
this.status = 'aborted';
if ( !this._filePromiseWrapper.isFulfilled ) {
// Edge case: file loader is aborted before read() is called
// so it might happen that no one handled the rejection of this promise.
// See https://github.com/ckeditor/ckeditor5-upload/pull/100
this._filePromiseWrapper.promise.catch( () => {} );
this._filePromiseWrapper.rejecter( 'aborted' );
} else if ( status == 'reading' ) {
this._reader.abort();
} else if ( status == 'uploading' && this._adapter.abort ) {
this._adapter.abort();
}
this._destroy();
}
/**
* Performs cleanup.
*
* @private
*/
_destroy() {
this._filePromiseWrapper = undefined;
this._reader = undefined;
this._adapter = undefined;
this.uploadResponse = undefined;
}
/**
* Wraps a given file promise into another promise giving additional
* control (resolving, rejecting, checking if fulfilled) over it.
*
* @private
* @param filePromise The initial file promise to be wrapped.
* @returns {module:upload/filerepository~FilePromiseWrapper}
*/
_createFilePromiseWrapper( filePromise ) {
const wrapper = {};
wrapper.promise = new Promise( ( resolve, reject ) => {
wrapper.rejecter = reject;
wrapper.isFulfilled = false;
filePromise
.then( file => {
wrapper.isFulfilled = true;
resolve( file );
} )
.catch( err => {
wrapper.isFulfilled = true;
reject( err );
} );
} );
return wrapper;
}
}
mix( FileLoader, ObservableMixin );
/**
* Upload adapter interface used by the {@link module:upload/filerepository~FileRepository file repository}
* to handle file upload. An upload adapter is a bridge between the editor and server that handles file uploads.
* It should contain a logic necessary to initiate an upload process and monitor its progress.
*
* Learn how to develop your own upload adapter for CKEditor 5 in the
* {@glink framework/guides/deep-dive/upload-adapter "Custom upload adapter" guide}.
*
* @interface UploadAdapter
*/
/**
* Executes the upload process.
* This method should return a promise that will resolve when data will be uploaded to server. Promise should be
* resolved with an object containing information about uploaded file:
*
* {
* default: 'http://server/default-size.image.png'
* }
*
* Additionally, other image sizes can be provided:
*
* {
* default: 'http://server/default-size.image.png',
* '160': 'http://server/size-160.image.png',
* '500': 'http://server/size-500.image.png',
* '1000': 'http://server/size-1000.image.png',
* '1052': 'http://server/default-size.image.png'
* }
*
* You can also pass additional properties from the server. In this case you need to wrap URLs
* in the `urls` object and pass additional properties along the `urls` property.
*
* {
* myCustomProperty: 'foo',
* urls: {
* default: 'http://server/default-size.image.png',
* '160': 'http://server/size-160.image.png',
* '500': 'http://server/size-500.image.png',
* '1000': 'http://server/size-1000.image.png',
* '1052': 'http://server/default-size.image.png'
* }
* }
*
* NOTE: When returning multiple images, the widest returned one should equal the default one. It is essential to
* correctly set `width` attribute of the image. See this discussion:
* https://github.com/ckeditor/ckeditor5-easy-image/issues/4 for more information.
*
* Take a look at {@link module:upload/filerepository~UploadAdapter example Adapter implementation} and
* {@link module:upload/filerepository~FileRepository#createUploadAdapter createUploadAdapter method}.
*
* @method module:upload/filerepository~UploadAdapter#upload
* @returns {Promise.<Object>} Promise that should be resolved when data is uploaded.
*/
/**
* Aborts the upload process.
* After aborting it should reject promise returned from {@link #upload upload()}.
*
* Take a look at {@link module:upload/filerepository~UploadAdapter example Adapter implementation} and
* {@link module:upload/filerepository~FileRepository#createUploadAdapter createUploadAdapter method}.
*
* @method module:upload/filerepository~UploadAdapter#abort
*/
/**
* Object returned by {@link module:upload/filerepository~FileLoader#_createFilePromiseWrapper} method
* to add more control over the initial file promise passed to {@link module:upload/filerepository~FileLoader}.
*
* @protected
* @typedef {Object} module:upload/filerepository~FilePromiseWrapper
* @property {Promise.<File>} promise Wrapper promise which can be chained for further processing.
* @property {Function} rejecter Rejects the promise when called.
* @property {Boolean} isFulfilled Whether original promise is already fulfilled.
*/