@@ -64,6 +64,14 @@ export default class FileRepository extends Plugin {
6464 this . loaders . on ( 'add' , ( ) => this . _updatePendingAction ( ) ) ;
6565 this . loaders . on ( 'remove' , ( ) => this . _updatePendingAction ( ) ) ;
6666
67+ /**
68+ * Loaders mappings used to retrieve loaders references.
69+ *
70+ * @private
71+ * @member {Map<File|Promise, FileLoader>} #_loadersMap
72+ */
73+ this . _loadersMap = new Map ( ) ;
74+
6775 /**
6876 * Reference to a pending action registered in a {@link module:core/pendingactions~PendingActions} plugin
6977 * while upload is in progress. When there is no upload then value is `null`.
@@ -120,32 +128,26 @@ export default class FileRepository extends Plugin {
120128 }
121129
122130 /**
123- * Returns the loader associated with specified file.
131+ * Returns the loader associated with specified file or promise .
124132 *
125133 * To get loader by id use `fileRepository.loaders.get( id )`.
126134 *
127- * @param {File } file Native file handle.
135+ * @param {File|Promise.<File> } fileOrPromise Native file or promise handle.
128136 * @returns {module:upload/filerepository~FileLoader|null }
129137 */
130- getLoader ( file ) {
131- for ( const loader of this . loaders ) {
132- if ( loader . file == file ) {
133- return loader ;
134- }
135- }
136-
137- return null ;
138+ getLoader ( fileOrPromise ) {
139+ return this . _loadersMap . get ( fileOrPromise ) || null ;
138140 }
139141
140142 /**
141143 * Creates a loader instance for the given file.
142144 *
143145 * Requires {@link #createUploadAdapter} factory to be defined.
144146 *
145- * @param {File } file Native File object.
147+ * @param {File|Promise.<File> } fileOrPromise Native File object or native Promise object which resolves to a File .
146148 * @returns {module:upload/filerepository~FileLoader|null }
147149 */
148- createLoader ( file ) {
150+ createLoader ( fileOrPromise ) {
149151 if ( ! this . createUploadAdapter ) {
150152 /**
151153 * You need to enable an upload adapter in order to be able to upload files.
@@ -174,10 +176,17 @@ export default class FileRepository extends Plugin {
174176 return null ;
175177 }
176178
177- const loader = new FileLoader ( file ) ;
178- loader . _adapter = this . createUploadAdapter ( loader ) ;
179+ const loader = new FileLoader ( Promise . resolve ( fileOrPromise ) , this . createUploadAdapter ) ;
179180
180181 this . loaders . add ( loader ) ;
182+ this . _loadersMap . set ( fileOrPromise , loader ) ;
183+
184+ // Store also file => loader mapping so loader can be retrieved by file instance returned upon Promise resolution.
185+ if ( fileOrPromise instanceof Promise ) {
186+ loader . file . then ( file => {
187+ this . _loadersMap . set ( file , loader ) ;
188+ } ) ;
189+ }
181190
182191 loader . on ( 'change:uploaded' , ( ) => {
183192 let aggregatedUploaded = 0 ;
@@ -207,15 +216,21 @@ export default class FileRepository extends Plugin {
207216 /**
208217 * Destroys the given loader.
209218 *
210- * @param {File|module:upload/filerepository~FileLoader } fileOrLoader File associated with that loader or loader
211- * itself.
219+ * @param {File|Promise| module:upload/filerepository~FileLoader } fileOrPromiseOrLoader File or Promise associated
220+ * with that loader or loader itself.
212221 */
213- destroyLoader ( fileOrLoader ) {
214- const loader = fileOrLoader instanceof FileLoader ? fileOrLoader : this . getLoader ( fileOrLoader ) ;
222+ destroyLoader ( fileOrPromiseOrLoader ) {
223+ const loader = fileOrPromiseOrLoader instanceof FileLoader ? fileOrPromiseOrLoader : this . getLoader ( fileOrPromiseOrLoader ) ;
215224
216225 loader . _destroy ( ) ;
217226
218227 this . loaders . remove ( loader ) ;
228+
229+ this . _loadersMap . forEach ( ( value , key ) => {
230+ if ( value === loader ) {
231+ this . _loadersMap . delete ( key ) ;
232+ }
233+ } ) ;
219234 }
220235
221236 /**
@@ -252,10 +267,10 @@ class FileLoader {
252267 /**
253268 * Creates a new instance of `FileLoader`.
254269 *
255- * @param {File } file A native file instance.
256- * @param {module:upload/filerepository~UploadAdapter } adapter
270+ * @param {Promise.< File> } filePromise A promise which resolves to a file instance.
271+ * @param {Function } uploadAdapterCreator The function which returns { @link module:upload/filerepository~UploadAdapter} instance.
257272 */
258- constructor ( file , adapter ) {
273+ constructor ( filePromise , uploadAdapterCreator ) {
259274 /**
260275 * Unique id of FileLoader instance.
261276 *
@@ -265,20 +280,20 @@ class FileLoader {
265280 this . id = uid ( ) ;
266281
267282 /**
268- * A `File` instance associated with this file loader.
283+ * Additional wrapper over the initial file promise passed to this loader.
269284 *
270- * @readonly
271- * @member {File }
285+ * @private
286+ * @member {module:upload/filerepository~FilePromiseWrapper }
272287 */
273- this . file = file ;
288+ this . _filePromiseWrapper = this . _createFilePromiseWrapper ( filePromise ) ;
274289
275290 /**
276291 * Adapter instance associated with this file loader.
277292 *
278293 * @private
279294 * @member {module:upload/filerepository~UploadAdapter}
280295 */
281- this . _adapter = adapter ;
296+ this . _adapter = uploadAdapterCreator ( this ) ;
282297
283298 /**
284299 * FileReader used by FileLoader.
@@ -354,6 +369,28 @@ class FileLoader {
354369 this . set ( 'uploadResponse' , null ) ;
355370 }
356371
372+ /**
373+ * A `Promise` which resolves to a `File` instance associated with this file loader.
374+ *
375+ * @type {Promise.<File|null> }
376+ */
377+ get file ( ) {
378+ if ( ! this . _filePromiseWrapper ) {
379+ // Loader was destroyed, return promise which resolves to null.
380+ return Promise . resolve ( null ) ;
381+ } else {
382+ // The `this._filePromiseWrapper.promise` is chained and not simply returned to handle a case when:
383+ //
384+ // * The `loader.file.then( ... )` is called by external code (returned promise is pending).
385+ // * Then `loader._destroy()` is called (call is synchronous) which destroys the `loader`.
386+ // * Promise returned by the first `loader.file.then( ... )` call is resolved.
387+ //
388+ // Returning `this._filePromiseWrapper.promise` will still resolve to a `File` instance so there
389+ // is an additional check needed in the chain to see if `loader` was destroyed in the meantime.
390+ return this . _filePromiseWrapper . promise . then ( file => this . _filePromiseWrapper ? file : null ) ;
391+ }
392+ }
393+
357394 /**
358395 * Reads file using {@link module:upload/filereader~FileReader}.
359396 *
@@ -372,7 +409,7 @@ class FileLoader {
372409 * }
373410 * } );
374411 *
375- * @returns {Promise } Returns promise that will be resolved with read data. Promise will be rejected if error
412+ * @returns {Promise.<String> } Returns promise that will be resolved with read data. Promise will be rejected if error
376413 * occurs or if read process is aborted.
377414 */
378415 read ( ) {
@@ -382,7 +419,8 @@ class FileLoader {
382419
383420 this . status = 'reading' ;
384421
385- return this . _reader . read ( this . file )
422+ return this . _filePromiseWrapper . promise
423+ . then ( file => this . _reader . read ( file ) )
386424 . then ( data => {
387425 this . status = 'idle' ;
388426
@@ -395,7 +433,7 @@ class FileLoader {
395433 }
396434
397435 this . status = 'error' ;
398- throw this . _reader . error ;
436+ throw this . _reader . error ? this . _reader . error : err ;
399437 } ) ;
400438 }
401439
@@ -416,7 +454,7 @@ class FileLoader {
416454 * }
417455 * } );
418456 *
419- * @returns {Promise } Returns promise that will be resolved with response data. Promise will be rejected if error
457+ * @returns {Promise.<Object> } Returns promise that will be resolved with response data. Promise will be rejected if error
420458 * occurs or if read process is aborted.
421459 */
422460 upload ( ) {
@@ -426,7 +464,8 @@ class FileLoader {
426464
427465 this . status = 'uploading' ;
428466
429- return this . _adapter . upload ( )
467+ return this . _filePromiseWrapper . promise
468+ . then ( ( ) => this . _adapter . upload ( ) )
430469 . then ( data => {
431470 this . uploadResponse = data ;
432471 this . status = 'idle' ;
@@ -450,11 +489,11 @@ class FileLoader {
450489 const status = this . status ;
451490 this . status = 'aborted' ;
452491
453- if ( status == 'reading' ) {
492+ if ( ! this . _filePromiseWrapper . isFulfilled ) {
493+ this . _filePromiseWrapper . rejecter ( 'aborted' ) ;
494+ } else if ( status == 'reading' ) {
454495 this . _reader . abort ( ) ;
455- }
456-
457- if ( status == 'uploading' && this . _adapter . abort ) {
496+ } else if ( status == 'uploading' && this . _adapter . abort ) {
458497 this . _adapter . abort ( ) ;
459498 }
460499
@@ -467,11 +506,41 @@ class FileLoader {
467506 * @private
468507 */
469508 _destroy ( ) {
509+ this . _filePromiseWrapper = undefined ;
470510 this . _reader = undefined ;
471511 this . _adapter = undefined ;
472512 this . data = undefined ;
473513 this . uploadResponse = undefined ;
474- this . file = undefined ;
514+ }
515+
516+ /**
517+ * Wraps a given file promise into another promise giving additional
518+ * control (resolving, rejecting, checking if fulfilled) over it.
519+ *
520+ * @private
521+ * @param filePromise The initial file promise to be wrapped.
522+ * @returns {module:upload/filerepository~FilePromiseWrapper }
523+ */
524+ _createFilePromiseWrapper ( filePromise ) {
525+ const wrapper = { } ;
526+
527+ wrapper . promise = new Promise ( ( resolve , reject ) => {
528+ wrapper . resolver = resolve ;
529+ wrapper . rejecter = reject ;
530+ wrapper . isFulfilled = false ;
531+
532+ filePromise
533+ . then ( file => {
534+ wrapper . isFulfilled = true ;
535+ resolve ( file ) ;
536+ } )
537+ . catch ( err => {
538+ wrapper . isFulfilled = true ;
539+ reject ( err ) ;
540+ } ) ;
541+ } ) ;
542+
543+ return wrapper ;
475544 }
476545}
477546
@@ -515,7 +584,7 @@ mix( FileLoader, ObservableMixin );
515584 * {@link module:upload/filerepository~FileRepository#createUploadAdapter createUploadAdapter method}.
516585 *
517586 * @method module:upload/filerepository~UploadAdapter#upload
518- * @returns {Promise } Promise that should be resolved when data is uploaded.
587+ * @returns {Promise.<Object> } Promise that should be resolved when data is uploaded.
519588 */
520589
521590/**
@@ -527,3 +596,14 @@ mix( FileLoader, ObservableMixin );
527596 *
528597 * @method module:upload/filerepository~UploadAdapter#abort
529598 */
599+
600+ /**
601+ * Object returned by {@link module:upload/filerepository~FileLoader#_createFilePromiseWrapper} method
602+ * to add more control over the initial file promise passed to {@link module:upload/filerepository~FileLoader}.
603+ *
604+ * @typedef {Object } module:upload/filerepository~FilePromiseWrapper
605+ * @property {Promise.<File> } promise Wrapper promise which can be chained for further processing.
606+ * @property {Function } resolver Resolves the promise when called.
607+ * @property {Function } rejecter Rejects the promise when called.
608+ * @property {Boolean } isFulfilled Whether original promise is already fulfilled.
609+ */
0 commit comments