Skip to content

Commit

Permalink
Ability to use asynchronous initFileFn.
Browse files Browse the repository at this point in the history
This is a follow-up (and proper implementation) of flowjs#296 (reverted in e867d54)

This could be use to initialize a stream, a reader, fetch a remote resource, ... during the FlowFile initialization.
`asyncAddFile` and `asyncAddFiles` are introduced which support this mechanism.
These function return promises of one (or multiple) FlowFile(s).

To implement this:
- An AsyncFlowFile class extending a FlowFile is created, overriding the bootstrap part
- In order to keep code-duplication low, a filterFileList generator yield files
  in way common to async and non-async addFiles() functions.

The possibly async' nature of initializing file hit back events firing.
(flowjs#319) in general and `fileAdded`
in particular (flowjs#271) and the current
confusión between hooks altering bootstraping and simple events.

- fileAdded is now assumed an event. If we detect a return value, a warning is sent.
- preFilterFile is introduced and allow filtering the raw file before bootstrap()
- postFilterFile is introduced and allow filtering the FlowFile after a (possibly async) bootstrap()
- filesAdded is preserved

About parameters:
- `initFileFn` is only used during bootstrap
- If such a helper exists, it's tightly related to addFile* functions.
As such, it was deemed adequate to provide it as a parameter to `*addFile*()`
 (but `opts.initFileFn` is still supported)
  • Loading branch information
Raphaël Droz committed Jan 13, 2021
1 parent 7d84d31 commit 7a81c7b
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 30 deletions.
36 changes: 36 additions & 0 deletions src/AsyncFlowFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import FlowFile from './FlowFile';

/**
* AsyncFlowFile class
* @name AsyncFlowFile
*/
export default class AsyncFlowFile extends FlowFile {

/**
* Retry aborted file upload
* @function
*/
async retry() {
await this.bootstrap('retry');
return this.flowObj.upload();
}

async bootstrap(event = null, initFileFn = this.flowObj.opts.initFileFn) {
/**
* Asynchronous initialization function, if defined, is run
* Then _bootstrap follow-up occurs
* And, optionally (in case of initial FlowFile creation), the `fileAdded` event is fired.
*/
if (typeof initFileFn === 'function') {
await initFileFn(this, event);
}

this._bootstrap();
if (event !== 'retry') {
this.flowObj.fire('fileAdded', this, event);
}

// console.log("Flowfile returns [async]", this._bootstrapped);
return this;
}
}
165 changes: 135 additions & 30 deletions src/Flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

import FlowFile from './FlowFile';
import AsyncFlowFile from './AsyncFlowFile';
import {each, async, arrayRemove, extend, webAPIFileRead} from './tools';

/**
Expand Down Expand Up @@ -156,7 +157,8 @@ export default class Flow {

/**
* Set a callback for an event, possible events:
* fileSuccess(file), fileProgress(file), fileAdded(file, event),
* fileSuccess(file), fileProgress(file),
* preFilterFile(file, event), postFilterFile(flowFile, event),
* fileRemoved(file), fileRetry(file), fileError(file, message),
* complete(), progress(), error(message, file), pause()
* @function
Expand Down Expand Up @@ -206,9 +208,15 @@ export default class Flow {
event = event.toLowerCase();
var preventDefault = false;
if (this.events.hasOwnProperty(event)) {
each(this.events[event], function (callback) {
preventDefault = callback.apply(this, args.slice(1)) === false || preventDefault;
}, this);
for (let callback of this.events[event]) {
var ret = callback.apply(this, args.slice(1));
// v3 compatibility warning
if (event === 'fileadded' && ret !== null) {
console.warn("Warning: In Flow.js v3, fileAdded hook has been replaced by preFilterFile and postFilterFile hooks.");
}

preventDefault = ret === false || preventDefault;
}
}
if (event != 'catchall') {
args.unshift('catchAll');
Expand Down Expand Up @@ -565,14 +573,44 @@ export default class Flow {
return totalSize > 0 ? totalDone / totalSize : 0;
}

/**
* A generator to yield files and factor the sync part of the filtering logic used by both
* addFiles & asyncAddFiles
*/
*filterFileList(fileList, event) {
// ie10+
var ie10plus = window.navigator.msPointerEnabled;

for (let file of fileList) {
// https://github.com/flowjs/flow.js/issues/55
if ((ie10plus && file.size === 0) || (file.size % 4096 === 0 && (file.name === '.' || file.fileName === '.'))) {
// console.log(`file ${file.name} empty. skipping`);
continue;
}

var uniqueIdentifier = this.generateUniqueIdentifier(file);
if (!this.opts.allowDuplicateUploads && this.getFromUniqueIdentifier(uniqueIdentifier)) {
// console.log(`file ${file.name} non-unique. skipping`);
continue;
}

if (this.fire('preFilterFile', file, event) === false) {
// console.log(`file ${file.name} filtered-out. skipping`);
continue;
}

yield [file, uniqueIdentifier];
}
}

/**
* Add a HTML5 File object to the list of files.
* @function
* @param {File} file
* @param {Event} [event] event is optional
* @param Any other parameters supported by addFiles
*/
addFile(file, event) {
this.addFiles([file], event);
addFile(file, ...args) {
this.addFiles([file], ...args);
}

/**
Expand All @@ -581,34 +619,101 @@ export default class Flow {
* @param {FileList|Array} fileList
* @param {Event} [event] event is optional
*/
addFiles(fileList, event) {
var files = [];
// ie10+
var ie10plus = window.navigator.msPointerEnabled;
addFiles(fileList, event = null, initFileFn = this.opts.initFileFn) {
let item, file, flowfile, uniqueIdentifier, files = [];
const iterator = this.filterFileList(fileList, event);

each(fileList, function (file) {
// https://github.com/flowjs/flow.js/issues/55
if ((!ie10plus || ie10plus && file.size > 0) && !(file.size % 4096 === 0 && (file.name === '.' || file.fileName === '.'))) {
var uniqueIdentifier = this.generateUniqueIdentifier(file);
if (this.opts.allowDuplicateUploads || !this.getFromUniqueIdentifier(uniqueIdentifier)) {
var f = new FlowFile(this, file, uniqueIdentifier);
if (this.fire('fileAdded', f, event)) {
files.push(f);
}
}
while ((item = iterator.next()) && item.value) {
[file, uniqueIdentifier] = item.value;

var f = new FlowFile(this, file, uniqueIdentifier);
f.bootstrap(event, initFileFn);

if (this.fire('postFilterFile', f, event) === false) {
// console.log(`file ${file.name}. Filter-out by postFilterFile.`);
continue;
}
}, this);
if (this.fire('filesAdded', files, event)) {
each(files, function (file) {
if (this.opts.singleFile && this.files.length > 0) {
this.removeFile(this.files[0]);
}
this.files.push(file);
}, this);
this.fire('filesSubmitted', files, event);

this.fire('fileAdded', f, event);
files.push(f);
}

if (this.fire('filesAdded', files, event) === false) {
return;
}

for (file of files) {
if (this.opts.singleFile && this.files.length > 0) {
this.removeFile(this.files[0]);
}
this.files.push(file);
// console.log(`enqueue file ${file.name} of ${file.chunks.length} chunks`);
}

this.fire('filesSubmitted', files, event);
}


/**
* Add a HTML5 File object to the list of files.
* @function
* @param {File} file
* @param Any other parameters supported by asyncAddFiles.
*
* @return (async) An initialized <AsyncFlowFile>.
*/
async asyncAddFile(file, ...args) {
return (await this.asyncAddFiles([file], ...args))[0];
}

/**
* Add a HTML5 File object to the list of files.
* @function
* @param {FileList|Array} fileList
* @param {Event} [event] event is optional
*
* @return Promise{[<AsyncFlowFile>,...]} The promise of getting an array of AsyncFlowFile.
*/
asyncAddFiles(fileList, event = null, initFileFn = this.opts.initFileFn) {
let item, file, flowfile, uniqueIdentifier, files = {}, states = [];
const iterator = this.filterFileList(fileList, event);

while ((item = iterator.next()) && item.value) {
[file, uniqueIdentifier] = item.value;
var flowFile = new AsyncFlowFile(this, file, uniqueIdentifier),
state = flowFile.bootstrap(event, initFileFn);
files[uniqueIdentifier] = flowFile;
state.then(e => {
if (this.fire('postFilterFile', flowFile, event) === false) {
delete files[uniqueIdentifier];
return;
}

this.fire('fileAdded', flowFile, event);
});

states.push(state);
}

files = Object.values(files);
return Promise.all(states)
.then(e => {
if (! this.fire('filesAdded', files, event)) {
return [];
}

for (let file of files) {
if (this.opts.singleFile && this.files.length > 0) {
this.removeFile(this.files[0]);
}
this.files.push(file);
}

this.fire('filesSubmitted', files, event);

return files;
});
}

/**
* Cancel upload of a specific FlowFile object from the list.
Expand Down

0 comments on commit 7a81c7b

Please sign in to comment.