/**
* @license MIT
*/
(function(window, document, undefined) {'use strict';
// ie10+
var ie10plus = window.navigator.msPointerEnabled;
/**
* Flow.js is a library providing multiple simultaneous, stable and
* resumable uploads via the HTML5 File API.
* @param [opts]
* @param {number} [opts.chunkSize]
* @param {bool} [opts.forceChunkSize]
* @param {number} [opts.simultaneousUploads]
* @param {bool} [opts.singleFile]
* @param {string} [opts.fileParameterName]
* @param {number} [opts.progressCallbacksInterval]
* @param {number} [opts.speedSmoothingFactor]
* @param {Object|Function} [opts.query]
* @param {Object|Function} [opts.headers]
* @param {bool} [opts.withCredentials]
* @param {Function} [opts.preprocess]
* @param {string} [opts.method]
* @param {string|Function} [opts.testMethod]
* @param {string|Function} [opts.uploadMethod]
* @param {bool} [opts.prioritizeFirstAndLastChunk]
* @param {bool} [opts.allowDuplicateUploads]
* @param {string|Function} [opts.target]
* @param {number} [opts.maxChunkRetries]
* @param {number} [opts.chunkRetryInterval]
* @param {Array.<number>} [opts.permanentErrors]
* @param {Array.<number>} [opts.successStatuses]
* @param {Function} [opts.initFileFn]
* @param {Function} [opts.readFileFn]
* @param {Function} [opts.generateUniqueIdentifier]
* @constructor
*/
function Flow(opts) {
/**
* Supported by browser?
* @type {boolean}
*/
this.support = (
typeof File !== 'undefined' &&
typeof Blob !== 'undefined' &&
typeof FileList !== 'undefined' &&
(
!!Blob.prototype.slice || !!Blob.prototype.webkitSlice || !!Blob.prototype.mozSlice ||
false
) // slicing files support
);
if (!this.support) {
return ;
}
/**
* Check if directory upload is supported
* @type {boolean}
*/
this.supportDirectory = /Chrome/.test(window.navigator.userAgent);
/**
* List of FlowFile objects
* @type {Array.<FlowFile>}
*/
this.files = [];
/**
* Default options for flow.js
* @type {Object}
*/
this.defaults = {
chunkSize: 1024 * 1024,
forceChunkSize: false,
simultaneousUploads: 3,
singleFile: false,
fileParameterName: 'file',
progressCallbacksInterval: 500,
speedSmoothingFactor: 0.1,
query: {},
headers: {},
withCredentials: false,
preprocess: null,
method: 'multipart',
testMethod: 'GET',
uploadMethod: 'POST',
prioritizeFirstAndLastChunk: false,
allowDuplicateUploads: false,
target: '/',
testChunks: true,
generateUniqueIdentifier: null,
maxChunkRetries: 0,
chunkRetryInterval: null,
permanentErrors: [404, 415, 500, 501],
successStatuses: [200, 201, 202],
onDropStopPropagation: false,
initFileFn: null,
readFileFn: webAPIFileRead
};
/**
* Current options
* @type {Object}
*/
this.opts = {};
/**
* List of events:
* key stands for event name
* value array list of callbacks
* @type {}
*/
this.events = {};
var $ = this;
/**
* On drop event
* @function
* @param {MouseEvent} event
*/
this.onDrop = function (event) {
if ($.opts.onDropStopPropagation) {
event.stopPropagation();
}
event.preventDefault();
var dataTransfer = event.dataTransfer;
if (dataTransfer.items && dataTransfer.items[0] &&
dataTransfer.items[0].webkitGetAsEntry) {
$.webkitReadDataTransfer(event);
} else {
$.addFiles(dataTransfer.files, event);
}
};
/**
* Prevent default
* @function
* @param {MouseEvent} event
*/
this.preventEvent = function (event) {
event.preventDefault();
};
/**
* Current options
* @type {Object}
*/
this.opts = Flow.extend({}, this.defaults, opts || {});
}
Flow.prototype = {
/**
* Set a callback for an event, possible events:
* fileSuccess(file), fileProgress(file), fileAdded(file, event),
* fileRetry(file), fileError(file, message), complete(),
* progress(), error(message, file), pause()
* @function
* @param {string} event
* @param {Function} callback
*/
on: function (event, callback) {
event = event.toLowerCase();
if (!this.events.hasOwnProperty(event)) {
this.events[event] = [];
}
this.events[event].push(callback);
},
/**
* Remove event callback
* @function
* @param {string} [event] removes all events if not specified
* @param {Function} [fn] removes all callbacks of event if not specified
*/
off: function (event, fn) {
if (event !== undefined) {
event = event.toLowerCase();
if (fn !== undefined) {
if (this.events.hasOwnProperty(event)) {
arrayRemove(this.events[event], fn);
}
} else {
delete this.events[event];
}
} else {
this.events = {};
}
},
/**
* Fire an event
* @function
* @param {string} event event name
* @param {...} args arguments of a callback
* @return {bool} value is false if at least one of the event handlers which handled this event
* returned false. Otherwise it returns true.
*/
fire: function (event, args) {
// `arguments` is an object, not array, in FF, so:
args = Array.prototype.slice.call(arguments);
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);
}
if (event != 'catchall') {
args.unshift('catchAll');
preventDefault = this.fire.apply(this, args) === false || preventDefault;
}
return !preventDefault;
},
/**
* Read webkit dataTransfer object
* @param event
*/
webkitReadDataTransfer: function (event) {
var $ = this;
var queue = event.dataTransfer.items.length;
var files = [];
each(event.dataTransfer.items, function (item) {
var entry = item.webkitGetAsEntry();
if (!entry) {
decrement();
return ;
}
if (entry.isFile) {
// due to a bug in Chrome's File System API impl - #149735
fileReadSuccess(item.getAsFile(), entry.fullPath);
} else {
readDirectory(entry.createReader());
}
});
function readDirectory(reader) {
reader.readEntries(function (entries) {
if (entries.length) {
queue += entries.length;
each(entries, function(entry) {
if (entry.isFile) {
var fullPath = entry.fullPath;
entry.file(function (file) {
fileReadSuccess(file, fullPath);
}, readError);
} else if (entry.isDirectory) {
readDirectory(entry.createReader());
}
});
readDirectory(reader);
} else {
decrement();
}
}, readError);
}
function fileReadSuccess(file, fullPath) {
// relative path should not start with "/"
file.relativePath = fullPath.substring(1);
files.push(file);
decrement();
}
function readError(fileError) {
throw fileError;
}
function decrement() {
if (--queue == 0) {
$.addFiles(files, event);
}
}
},
/**
* Generate unique identifier for a file
* @function
* @param {FlowFile} file
* @returns {string}
*/
generateUniqueIdentifier: function (file) {
var custom = this.opts.generateUniqueIdentifier;
if (typeof custom === 'function') {
return custom(file);
}
// Some confusion in different versions of Firefox
var relativePath = file.relativePath || file.webkitRelativePath || file.fileName || file.name;
return file.size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, '');
},
/**
* Upload next chunk from the queue
* @function
* @returns {boolean}
* @private
*/
uploadNextChunk: function (preventEvents) {
// In some cases (such as videos) it's really handy to upload the first
// and last chunk of a file quickly; this let's the server check the file's
// metadata and determine if there's even a point in continuing.
var found = false;
if (this.opts.prioritizeFirstAndLastChunk) {
each(this.files, function (file) {
if (!file.paused && file.chunks.length &&
file.chunks[0].status() === 'pending') {
file.chunks[0].send();
found = true;
return false;
}
if (!file.paused && file.chunks.length > 1 &&
file.chunks[file.chunks.length - 1].status() === 'pending') {
file.chunks[file.chunks.length - 1].send();
found = true;
return false;
}
});
if (found) {
return found;
}
}
// Now, simply look for the next, best thing to upload
each(this.files, function (file) {
if (!file.paused) {
each(file.chunks, function (chunk) {
if (chunk.status() === 'pending') {
chunk.send();
found = true;
return false;
}
});
}
if (found) {
return false;
}
});
if (found) {
return true;
}
// The are no more outstanding chunks to upload, check is everything is done
var outstanding = false;
each(this.files, function (file) {
if (!file.isComplete()) {
outstanding = true;
return false;
}
});
if (!outstanding && !preventEvents) {
// All chunks have been uploaded, complete
async(function () {
this.fire('complete');
}, this);
}
return false;
},
/**
* Assign a browse action to one or more DOM nodes.
* @function
* @param {Element|Array.<Element>} domNodes
* @param {boolean} isDirectory Pass in true to allow directories to
* @param {boolean} singleFile prevent multi file upload
* @param {Object} attributes set custom attributes:
* http://www.w3.org/TR/html-markup/input.file.html#input.file-attributes
* eg: accept: 'image/*'
* be selected (Chrome only).
*/
assignBrowse: function (domNodes, isDirectory, singleFile, attributes) {
if (typeof domNodes.length === 'undefined') {
domNodes = [domNodes];
}
each(domNodes, function (domNode) {
var input;
if (domNode.tagName === 'INPUT' && domNode.type === 'file') {
input = domNode;
} else {
input = document.createElement('input');
input.setAttribute('type', 'file');
// display:none - not working in opera 12
extend(input.style, {
visibility: 'hidden',
position: 'absolute',
width: '1px',
height: '1px'
});
// for opera 12 browser, input must be assigned to a document
domNode.appendChild(input);
// https://developer.mozilla.org/en/using_files_from_web_applications)
// event listener is executed two times
// first one - original mouse click event
// second - input.click(), input is inside domNode
domNode.addEventListener('click', function() {
input.click();
}, false);
}
if (!this.opts.singleFile && !singleFile) {
input.setAttribute('multiple', 'multiple');
}
if (isDirectory) {
input.setAttribute('webkitdirectory', 'webkitdirectory');
}
each(attributes, function (value, key) {
input.setAttribute(key, value);
});
// When new files are added, simply append them to the overall list
var $ = this;
input.addEventListener('change', function (e) {
if (e.target.value) {
$.addFiles(e.target.files, e);
e.target.value = '';
}
}, false);
}, this);
},
/**
* Assign one or more DOM nodes as a drop target.
* @function
* @param {Element|Array.<Element>} domNodes
*/
assignDrop: function (domNodes) {
if (typeof domNodes.length === 'undefined') {
domNodes = [domNodes];
}
each(domNodes, function (domNode) {
domNode.addEventListener('dragover', this.preventEvent, false);
domNode.addEventListener('dragenter', this.preventEvent, false);
domNode.addEventListener('drop', this.onDrop, false);
}, this);
},
/**
* Un-assign drop event from DOM nodes
* @function
* @param domNodes
*/
unAssignDrop: function (domNodes) {
if (typeof domNodes.length === 'undefined') {
domNodes = [domNodes];
}
each(domNodes, function (domNode) {
domNode.removeEventListener('dragover', this.preventEvent);
domNode.removeEventListener('dragenter', this.preventEvent);
domNode.removeEventListener('drop', this.onDrop);
}, this);
},
/**
* Returns a boolean indicating whether or not the instance is currently
* uploading anything.
* @function
* @returns {boolean}
*/
isUploading: function () {
var uploading = false;
each(this.files, function (file) {
if (file.isUploading()) {
uploading = true;
return false;
}
});
return uploading;
},
/**
* should upload next chunk
* @function
* @returns {boolean|number}
*/
_shouldUploadNext: function () {
var num = 0;
var should = true;
var simultaneousUploads = this.opts.simultaneousUploads;
each(this.files, function (file) {
each(file.chunks, function(chunk) {
if (chunk.status() === 'uploading') {
num++;
if (num >= simultaneousUploads) {
should = false;
return false;
}
}
});
});
// if should is true then return uploading chunks's length
return should && num;
},
/**
* Start or resume uploading.
* @function
*/
upload: function () {
// Make sure we don't start too many uploads at once
var ret = this._shouldUploadNext();
if (ret === false) {
return;
}
// Kick off the queue
this.fire('uploadStart');
var started = false;
for (var num = 1; num <= this.opts.simultaneousUploads - ret; num++) {
started = this.uploadNextChunk(true) || started;
}
if (!started) {
async(function () {
this.fire('complete');
}, this);
}
},
/**
* Resume uploading.
* @function
*/
resume: function () {
each(this.files, function (file) {
file.resume();
});
},
/**
* Pause uploading.
* @function
*/
pause: function () {
each(this.files, function (file) {
file.pause();
});
},
/**
* Cancel upload of all FlowFile objects and remove them from the list.
* @function
*/
cancel: function () {
for (var i = this.files.length - 1; i >= 0; i--) {
this.files[i].cancel();
}
},
/**
* Returns a number between 0 and 1 indicating the current upload progress
* of all files.
* @function
* @returns {number}
*/
progress: function () {
var totalDone = 0;
var totalSize = 0;
// Resume all chunks currently being uploaded
each(this.files, function (file) {
totalDone += file.progress() * file.size;
totalSize += file.size;
});
return totalSize > 0 ? totalDone / totalSize : 0;
},
/**
* Add a HTML5 File object to the list of files.
* @function
* @param {File} file
* @param {Event} [event] event is optional
*/
addFile: function (file, event) {
this.addFiles([file], event);
},
/**
* Add a HTML5 File object to the list of files.
* @function
* @param {FileList|Array} fileList
* @param {Event} [event] event is optional
*/
addFiles: function (fileList, event) {
var files = [];
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 === '.')) &&
(this.opts.allowDuplicateUploads || !this.getFromUniqueIdentifier(this.generateUniqueIdentifier(file)))) {
var f = new FlowFile(this, file);
if (this.fire('fileAdded', f, event)) {
files.push(f);
}
}
}, 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);
},
/**
* Cancel upload of a specific FlowFile object from the list.
* @function
* @param {FlowFile} file
*/
removeFile: function (file) {
for (var i = this.files.length - 1; i >= 0; i--) {
if (this.files[i] === file) {
this.files.splice(i, 1);
file.abort();
}
}
},
/**
* Look up a FlowFile object by its unique identifier.
* @function
* @param {string} uniqueIdentifier
* @returns {boolean|FlowFile} false if file was not found
*/
getFromUniqueIdentifier: function (uniqueIdentifier) {
var ret = false;
each(this.files, function (file) {
if (file.uniqueIdentifier === uniqueIdentifier) {
ret = file;
}
});
return ret;
},
/**
* Returns the total size of all files in bytes.
* @function
* @returns {number}
*/
getSize: function () {
var totalSize = 0;
each(this.files, function (file) {
totalSize += file.size;
});
return totalSize;
},
/**
* Returns the total size uploaded of all files in bytes.
* @function
* @returns {number}
*/
sizeUploaded: function () {
var size = 0;
each(this.files, function (file) {
size += file.sizeUploaded();
});
return size;
},
/**
* Returns remaining time to upload all files in seconds. Accuracy is based on average speed.
* If speed is zero, time remaining will be equal to positive infinity `Number.POSITIVE_INFINITY`
* @function
* @returns {number}
*/
timeRemaining: function () {
var sizeDelta = 0;
var averageSpeed = 0;
each(this.files, function (file) {
if (!file.paused && !file.error) {
sizeDelta += file.size - file.sizeUploaded();
averageSpeed += file.averageSpeed;
}
});
if (sizeDelta && !averageSpeed) {
return Number.POSITIVE_INFINITY;
}
if (!sizeDelta && !averageSpeed) {
return 0;
}
return Math.floor(sizeDelta / averageSpeed);
}
};
/**
* FlowFile class
* @name FlowFile
* @param {Flow} flowObj
* @param {File} file
* @constructor
*/
function FlowFile(flowObj, file) {
/**
* Reference to parent Flow instance
* @type {Flow}
*/
this.flowObj = flowObj;
/**
* Used to store the bytes read
* @type {Blob|string}
*/
this.bytes = null;
/**
* Reference to file
* @type {File}
*/
this.file = file;
/**
* File name. Some confusion in different versions of Firefox
* @type {string}
*/
this.name = file.fileName || file.name;
/**
* File size
* @type {number}
*/
this.size = file.size;
/**
* Relative file path
* @type {string}
*/
this.relativePath = file.relativePath || file.webkitRelativePath || this.name;
/**
* File unique identifier
* @type {string}
*/
this.uniqueIdentifier = flowObj.generateUniqueIdentifier(file);
/**
* List of chunks
* @type {Array.<FlowChunk>}
*/
this.chunks = [];
/**
* Indicated if file is paused
* @type {boolean}
*/
this.paused = false;
/**
* Indicated if file has encountered an error
* @type {boolean}
*/
this.error = false;
/**
* Average upload speed
* @type {number}
*/
this.averageSpeed = 0;
/**
* Current upload speed
* @type {number}
*/
this.currentSpeed = 0;
/**
* Date then progress was called last time
* @type {number}
* @private
*/
this._lastProgressCallback = Date.now();
/**
* Previously uploaded file size
* @type {number}
* @private
*/
this._prevUploadedSize = 0;
/**
* Holds previous progress
* @type {number}
* @private
*/
this._prevProgress = 0;
this.bootstrap();
}
FlowFile.prototype = {
/**
* Update speed parameters
* @link http://stackoverflow.com/questions/2779600/how-to-estimate-download-time-remaining-accurately
* @function
*/
measureSpeed: function () {
var timeSpan = Date.now() - this._lastProgressCallback;
if (!timeSpan) {
return ;
}
var smoothingFactor = this.flowObj.opts.speedSmoothingFactor;
var uploaded = this.sizeUploaded();
// Prevent negative upload speed after file upload resume
this.currentSpeed = Math.max((uploaded - this._prevUploadedSize) / timeSpan * 1000, 0);
this.averageSpeed = smoothingFactor * this.currentSpeed + (1 - smoothingFactor) * this.averageSpeed;
this._prevUploadedSize = uploaded;
},
/**
* For internal usage only.
* Callback when something happens within the chunk.
* @function
* @param {FlowChunk} chunk
* @param {string} event can be 'progress', 'success', 'error' or 'retry'
* @param {string} [message]
*/
chunkEvent: function (chunk, event, message) {
switch (event) {
case 'progress':
if (Date.now() - this._lastProgressCallback <
this.flowObj.opts.progressCallbacksInterval) {
break;
}
this.measureSpeed();
this.flowObj.fire('fileProgress', this, chunk);
this.flowObj.fire('progress');
this._lastProgressCallback = Date.now();
break;
case 'error':
this.error = true;
this.abort(true);
this.flowObj.fire('fileError', this, message, chunk);
this.flowObj.fire('error', message, this, chunk);
break;
case 'success':
if (this.error) {
return;
}
this.measureSpeed();
this.flowObj.fire('fileProgress', this, chunk);
this.flowObj.fire('progress');
this._lastProgressCallback = Date.now();
if (this.isComplete()) {
this.currentSpeed = 0;
this.averageSpeed = 0;
this.flowObj.fire('fileSuccess', this, message, chunk);
}
break;
case 'retry':
this.flowObj.fire('fileRetry', this, chunk);
break;
}
},
/**
* Pause file upload
* @function
*/
pause: function() {
this.paused = true;
this.abort();
},
/**
* Resume file upload
* @function
*/
resume: function() {
this.paused = false;
this.flowObj.upload();
},
/**
* Abort current upload
* @function
*/
abort: function (reset) {
this.currentSpeed = 0;
this.averageSpeed = 0;
var chunks = this.chunks;
if (reset) {
this.chunks = [];
}
each(chunks, function (c) {
if (c.status() === 'uploading') {
c.abort();
this.flowObj.uploadNextChunk();
}
}, this);
},
/**
* Cancel current upload and remove from a list
* @function
*/
cancel: function () {
this.flowObj.removeFile(this);
},
/**
* Retry aborted file upload
* @function
*/
retry: function () {
this.bootstrap();
this.flowObj.upload();
},
/**
* Clear current chunks and slice file again
* @function
*/
bootstrap: function () {
if (typeof this.flowObj.opts.initFileFn === "function") {
this.flowObj.opts.initFileFn(this);
}
this.abort(true);
this.error = false;
// Rebuild stack of chunks from file
this._prevProgress = 0;
var round = this.flowObj.opts.forceChunkSize ? Math.ceil : Math.floor;
var chunks = Math.max(
round(this.size / this.flowObj.opts.chunkSize), 1
);
for (var offset = 0; offset < chunks; offset++) {
this.chunks.push(
new FlowChunk(this.flowObj, this, offset)
);
}
},
/**
* Get current upload progress status
* @function
* @returns {number} from 0 to 1
*/
progress: function () {
if (this.error) {
return 1;
}
if (this.chunks.length === 1) {
this._prevProgress = Math.max(this._prevProgress, this.chunks[0].progress());
return this._prevProgress;
}
// Sum up progress across everything
var bytesLoaded = 0;
each(this.chunks, function (c) {
// get chunk progress relative to entire file
bytesLoaded += c.progress() * (c.endByte - c.startByte);
});
var percent = bytesLoaded / this.size;
// We don't want to lose percentages when an upload is paused
this._prevProgress = Math.max(this._prevProgress, percent > 0.9999 ? 1 : percent);
return this._prevProgress;
},
/**
* Indicates if file is being uploaded at the moment
* @function
* @returns {boolean}
*/
isUploading: function () {
var uploading = false;
each(this.chunks, function (chunk) {
if (chunk.status() === 'uploading') {
uploading = true;
return false;
}
});
return uploading;
},
/**
* Indicates if file is has finished uploading and received a response
* @function
* @returns {boolean}
*/
isComplete: function () {
var outstanding = false;
each(this.chunks, function (chunk) {
var status = chunk.status();
if (status === 'pending' || status === 'uploading' || status === 'reading' || chunk.preprocessState === 1 || chunk.readState === 1) {
outstanding = true;
return false;
}
});
return !outstanding;
},
/**
* Count total size uploaded
* @function
* @returns {number}
*/
sizeUploaded: function () {
var size = 0;
each(this.chunks, function (chunk) {
size += chunk.sizeUploaded();
});
return size;
},
/**
* Returns remaining time to finish upload file in seconds. Accuracy is based on average speed.
* If speed is zero, time remaining will be equal to positive infinity `Number.POSITIVE_INFINITY`
* @function
* @returns {number}
*/
timeRemaining: function () {
if (this.paused || this.error) {
return 0;
}
var delta = this.size - this.sizeUploaded();
if (delta && !this.averageSpeed) {
return Number.POSITIVE_INFINITY;
}
if (!delta && !this.averageSpeed) {
return 0;
}
return Math.floor(delta / this.averageSpeed);
},
/**
* Get file type
* @function
* @returns {string}
*/
getType: function () {
return this.file.type && this.file.type.split('/')[1];
},
/**
* Get file extension
* @function
* @returns {string}
*/
getExtension: function () {
return this.name.substr((~-this.name.lastIndexOf(".") >>> 0) + 2).toLowerCase();
}
};
/**
* Default read function using the webAPI
*
* @function webAPIFileRead(fileObj, fileType, startByte, endByte, chunk)
*
*/
function webAPIFileRead(fileObj, startByte, endByte, fileType, chunk) {
var function_name = 'slice';
if (fileObj.file.slice)
function_name = 'slice';
else if (fileObj.file.mozSlice)
function_name = 'mozSlice';
else if (fileObj.file.webkitSlice)
function_name = 'webkitSlice';
chunk.readFinished(fileObj.file[function_name](startByte, endByte, fileType));
}
/**
* Class for storing a single chunk
* @name FlowChunk
* @param {Flow} flowObj
* @param {FlowFile} fileObj
* @param {number} offset
* @constructor
*/
function FlowChunk(flowObj, fileObj, offset) {
/**
* Reference to parent flow object
* @type {Flow}
*/
this.flowObj = flowObj;
/**
* Reference to parent FlowFile object
* @type {FlowFile}
*/
this.fileObj = fileObj;
/**
* File offset
* @type {number}
*/
this.offset = offset;
/**
* Indicates if chunk existence was checked on the server
* @type {boolean}
*/
this.tested = false;
/**
* Number of retries performed
* @type {number}
*/
this.retries = 0;
/**
* Pending retry
* @type {boolean}
*/
this.pendingRetry = false;
/**
* Preprocess state
* @type {number} 0 = unprocessed, 1 = processing, 2 = finished
*/
this.preprocessState = 0;
/**
* Read state
* @type {number} 0 = not read, 1 = reading, 2 = finished
*/
this.readState = 0;
/**
* Bytes transferred from total request size
* @type {number}
*/
this.loaded = 0;
/**
* Total request size
* @type {number}
*/
this.total = 0;
/**
* Size of a chunk
* @type {number}
*/
this.chunkSize = this.flowObj.opts.chunkSize;
/**
* Chunk start byte in a file
* @type {number}
*/
this.startByte = this.offset * this.chunkSize;
/**
* Compute the endbyte in a file
*
*/
this.computeEndByte = function() {
var endByte = Math.min(this.fileObj.size, (this.offset + 1) * this.chunkSize);
if (this.fileObj.size - endByte < this.chunkSize && !this.flowObj.opts.forceChunkSize) {
// The last chunk will be bigger than the chunk size,
// but less than 2 * this.chunkSize
endByte = this.fileObj.size;
}
return endByte;
}
/**
* Chunk end byte in a file
* @type {number}
*/
this.endByte = this.computeEndByte();
/**
* XMLHttpRequest
* @type {XMLHttpRequest}
*/
this.xhr = null;
var $ = this;
/**
* Send chunk event
* @param event
* @param {...} args arguments of a callback
*/
this.event = function (event, args) {
args = Array.prototype.slice.call(arguments);
args.unshift($);
$.fileObj.chunkEvent.apply($.fileObj, args);
};
/**
* Catch progress event
* @param {ProgressEvent} event
*/
this.progressHandler = function(event) {
if (event.lengthComputable) {
$.loaded = event.loaded ;
$.total = event.total;
}
$.event('progress', event);
};
/**
* Catch test event
* @param {Event} event
*/
this.testHandler = function(event) {
var status = $.status(true);
if (status === 'error') {
$.event(status, $.message());
$.flowObj.uploadNextChunk();
} else if (status === 'success') {
$.tested = true;
$.event(status, $.message());
$.flowObj.uploadNextChunk();
} else if (!$.fileObj.paused) {
// Error might be caused by file pause method
// Chunks does not exist on the server side
$.tested = true;
$.send();
}
};
/**
* Upload has stopped
* @param {Event} event
*/
this.doneHandler = function(event) {
var status = $.status();
if (status === 'success' || status === 'error') {
delete this.data;
$.event(status, $.message());
$.flowObj.uploadNextChunk();
} else {
$.event('retry', $.message());
$.pendingRetry = true;
$.abort();
$.retries++;
var retryInterval = $.flowObj.opts.chunkRetryInterval;
if (retryInterval !== null) {
setTimeout(function () {
$.send();
}, retryInterval);
} else {
$.send();
}
}
};
}
FlowChunk.prototype = {
/**
* Get params for a request
* @function
*/
getParams: function () {
return {
flowChunkNumber: this.offset + 1,
flowChunkSize: this.flowObj.opts.chunkSize,
flowCurrentChunkSize: this.endByte - this.startByte,
flowTotalSize: this.fileObj.size,
flowIdentifier: this.fileObj.uniqueIdentifier,
flowFilename: this.fileObj.name,
flowRelativePath: this.fileObj.relativePath,
flowTotalChunks: this.fileObj.chunks.length
};
},
/**
* Get target option with query params
* @function
* @param params
* @returns {string}
*/
getTarget: function(target, params){
if(target.indexOf('?') < 0) {
target += '?';
} else {
target += '&';
}
return target + params.join('&');
},
/**
* Makes a GET request without any data to see if the chunk has already
* been uploaded in a previous session
* @function
*/
test: function () {
// Set up request and listen for event
this.xhr = new XMLHttpRequest();
this.xhr.addEventListener("load", this.testHandler, false);
this.xhr.addEventListener("error", this.testHandler, false);
var testMethod = evalOpts(this.flowObj.opts.testMethod, this.fileObj, this);
var data = this.prepareXhrRequest(testMethod, true);
this.xhr.send(data);
},
/**
* Finish preprocess state
* @function
*/
preprocessFinished: function () {
// Re-compute the endByte after the preprocess function to allow an
// implementer of preprocess to set the fileObj size
this.endByte = this.computeEndByte();
this.preprocessState = 2;
this.send();
},
/**
* Finish read state
* @function
*/
readFinished: function (bytes) {
this.readState = 2;
this.bytes = bytes;
this.send();
},
/**
* Uploads the actual data in a POST call
* @function
*/
send: function () {
var preprocess = this.flowObj.opts.preprocess;
var read = this.flowObj.opts.readFileFn;
if (typeof preprocess === 'function') {
switch (this.preprocessState) {
case 0:
this.preprocessState = 1;
preprocess(this);
return;
case 1:
return;
}
}
switch (this.readState) {
case 0:
this.readState = 1;
read(this.fileObj, this.startByte, this.endByte, this.fileType, this);
return;
case 1:
return;
}
if (this.flowObj.opts.testChunks && !this.tested) {
this.test();
return;
}
this.loaded = 0;
this.total = 0;
this.pendingRetry = false;
// Set up request and listen for event
this.xhr = new XMLHttpRequest();
this.xhr.upload.addEventListener('progress', this.progressHandler, false);
this.xhr.addEventListener("load", this.doneHandler, false);
this.xhr.addEventListener("error", this.doneHandler, false);
var uploadMethod = evalOpts(this.flowObj.opts.uploadMethod, this.fileObj, this);
var data = this.prepareXhrRequest(uploadMethod, false, this.flowObj.opts.method, this.bytes);
this.xhr.send(data);
},
/**
* Abort current xhr request
* @function
*/
abort: function () {
// Abort and reset
var xhr = this.xhr;
this.xhr = null;
if (xhr) {
xhr.abort();
}
},
/**
* Retrieve current chunk upload status
* @function
* @returns {string} 'pending', 'uploading', 'success', 'error'
*/
status: function (isTest) {
if (this.readState === 1) {
return 'reading';
} else if (this.pendingRetry || this.preprocessState === 1) {
// if pending retry then that's effectively the same as actively uploading,
// there might just be a slight delay before the retry starts
return 'uploading';
} else if (!this.xhr) {
return 'pending';
} else if (this.xhr.readyState < 4) {
// Status is really 'OPENED', 'HEADERS_RECEIVED'
// or 'LOADING' - meaning that stuff is happening
return 'uploading';
} else {
if (this.flowObj.opts.successStatuses.indexOf(this.xhr.status) > -1) {
// HTTP 200, perfect
// HTTP 202 Accepted - The request has been accepted for processing, but the processing has not been completed.
return 'success';
} else if (this.flowObj.opts.permanentErrors.indexOf(this.xhr.status) > -1 ||
!isTest && this.retries >= this.flowObj.opts.maxChunkRetries) {
// HTTP 415/500/501, permanent error
return 'error';
} else {
// this should never happen, but we'll reset and queue a retry
// a likely case for this would be 503 service unavailable
this.abort();
return 'pending';
}
}
},
/**
* Get response from xhr request
* @function
* @returns {String}
*/
message: function () {
return this.xhr ? this.xhr.responseText : '';
},
/**
* Get upload progress
* @function
* @returns {number}
*/
progress: function () {
if (this.pendingRetry) {
return 0;
}
var s = this.status();
if (s === 'success' || s === 'error') {
return 1;
} else if (s === 'pending') {
return 0;
} else {
return this.total > 0 ? this.loaded / this.total : 0;
}
},
/**
* Count total size uploaded
* @function
* @returns {number}
*/
sizeUploaded: function () {
var size = this.endByte - this.startByte;
// can't return only chunk.loaded value, because it is bigger than chunk size
if (this.status() !== 'success') {
size = this.progress() * size;
}
return size;
},
/**
* Prepare Xhr request. Set query, headers and data
* @param {string} method GET or POST
* @param {bool} isTest is this a test request
* @param {string} [paramsMethod] octet or form
* @param {Blob} [blob] to send
* @returns {FormData|Blob|Null} data to send
*/
prepareXhrRequest: function(method, isTest, paramsMethod, blob) {
// Add data from the query options
var query = evalOpts(this.flowObj.opts.query, this.fileObj, this, isTest);
query = extend(query, this.getParams());
var target = evalOpts(this.flowObj.opts.target, this.fileObj, this, isTest);
var data = null;
if (method === 'GET' || paramsMethod === 'octet') {
// Add data from the query options
var params = [];
each(query, function (v, k) {
params.push([encodeURIComponent(k), encodeURIComponent(v)].join('='));
});
target = this.getTarget(target, params);
data = blob || null;
} else {
// Add data from the query options
data = new FormData();
each(query, function (v, k) {
data.append(k, v);
});
data.append(this.flowObj.opts.fileParameterName, blob, this.fileObj.file.name);
}
this.xhr.open(method, target, true);
this.xhr.withCredentials = this.flowObj.opts.withCredentials;
// Add data from header options
each(evalOpts(this.flowObj.opts.headers, this.fileObj, this, isTest), function (v, k) {
this.xhr.setRequestHeader(k, v);
}, this);
return data;
}
};
/**
* Remove value from array
* @param array
* @param value
*/
function arrayRemove(array, value) {
var index = array.indexOf(value);
if (index > -1) {
array.splice(index, 1);
}
}
/**
* If option is a function, evaluate it with given params
* @param {*} data
* @param {...} args arguments of a callback
* @returns {*}
*/
function evalOpts(data, args) {
if (typeof data === "function") {
// `arguments` is an object, not array, in FF, so:
args = Array.prototype.slice.call(arguments);
data = data.apply(null, args.slice(1));
}
return data;
}
Flow.evalOpts = evalOpts;
/**
* Execute function asynchronously
* @param fn
* @param context
*/
function async(fn, context) {
setTimeout(fn.bind(context), 0);
}
/**
* Extends the destination object `dst` by copying all of the properties from
* the `src` object(s) to `dst`. You can specify multiple `src` objects.
* @function
* @param {Object} dst Destination object.
* @param {...Object} src Source object(s).
* @returns {Object} Reference to `dst`.
*/
function extend(dst, src) {
each(arguments, function(obj) {
if (obj !== dst) {
each(obj, function(value, key){
dst[key] = value;
});
}
});
return dst;
}
Flow.extend = extend;
/**
* Iterate each element of an object
* @function
* @param {Array|Object} obj object or an array to iterate
* @param {Function} callback first argument is a value and second is a key.
* @param {Object=} context Object to become context (`this`) for the iterator function.
*/
function each(obj, callback, context) {
if (!obj) {
return ;
}
var key;
// Is Array?
// Array.isArray won't work, not only arrays can be iterated by index https://github.com/flowjs/ng-flow/issues/236#
if (typeof(obj.length) !== 'undefined') {
for (key = 0; key < obj.length; key++) {
if (callback.call(context, obj[key], key) === false) {
return ;
}
}
} else {
for (key in obj) {
if (obj.hasOwnProperty(key) && callback.call(context, obj[key], key) === false) {
return ;
}
}
}
}
Flow.each = each;
/**
* FlowFile constructor
* @type {FlowFile}
*/
Flow.FlowFile = FlowFile;
/**
* FlowFile constructor
* @type {FlowChunk}
*/
Flow.FlowChunk = FlowChunk;
/**
* Library version
* @type {string}
*/
Flow.version = '2.10.1';
if ( typeof module === "object" && module && typeof module.exports === "object" ) {
// Expose Flow as module.exports in loaders that implement the Node
// module pattern (including browserify). Do not create the global, since
// the user will be storing it themselves locally, and globals are frowned
// upon in the Node module world.
module.exports = Flow;
} else {
// Otherwise expose Flow to the global object as usual
window.Flow = Flow;
// Register as a named AMD module, since Flow can be concatenated with other
// files that may use define, but not via a proper concatenation script that
// understands anonymous AMD modules. A named AMD is safest and most robust
// way to register. Lowercase flow is used because AMD module names are
// derived from file names, and Flow is normally delivered in a lowercase
// file name. Do this after creating the global so that if an AMD module wants
// to call noConflict to hide this version of Flow, it will work.
if ( typeof define === "function" && define.amd ) {
define( "flow", [], function () { return Flow; } );
}
}
})(window, document);