12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562 |
- /**
- * @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 {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.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 = /WebKit/.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,
- target: '/',
- testChunks: true,
- generateUniqueIdentifier: null,
- maxChunkRetries: 0,
- chunkRetryInterval: null,
- permanentErrors: [404, 415, 500, 501],
- successStatuses: [200, 201, 202],
- onDropStopPropagation: false
- };
- /**
- * 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 {
- entry.createReader().readEntries(readSuccess, readError);
- }
- });
- function readSuccess(entries) {
- 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) {
- entry.createReader().readEntries(readSuccess, readError);
- }
- });
- decrement();
- }
- 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].preprocessState === 0) {
- 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[0].preprocessState === 0) {
- 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.preprocessState === 0) {
- 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'
- });
- // 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) {
- $.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) {
- // Uploading empty file IE10/IE11 hangs indefinitely
- // see https://connect.microsoft.com/IE/feedback/details/813443/uploading-empty-file-ie10-ie11-hangs-indefinitely
- // Directories have size `0` and name `.`
- // Ignore already added files
- if ((!ie10plus || ie10plus && file.size > 0) && !(file.size % 4096 === 0 && (file.name === '.' || file.fileName === '.')) &&
- !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;
- /**
- * 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 () {
- 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.file.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' || chunk.preprocessState === 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();
- }
- };
- /**
- * 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 size
- * @type {number}
- */
- this.fileObjSize = fileObj.size;
- /**
- * 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;
- /**
- * 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}
- */
- var chunkSize = this.flowObj.opts.chunkSize;
- /**
- * Chunk start byte in a file
- * @type {number}
- */
- this.startByte = this.offset * chunkSize;
- /**
- * Chunk end byte in a file
- * @type {number}
- */
- this.endByte = Math.min(this.fileObjSize, (this.offset + 1) * chunkSize);
- /**
- * XMLHttpRequest
- * @type {XMLHttpRequest}
- */
- this.xhr = null;
- if (this.fileObjSize - this.endByte < chunkSize &&
- !this.flowObj.opts.forceChunkSize) {
- // The last chunk will be bigger than the chunk size,
- // but less than 2*chunkSize
- this.endByte = this.fileObjSize;
- }
- 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') {
- $.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.fileObjSize,
- 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 () {
- this.preprocessState = 2;
- this.send();
- },
- /**
- * Uploads the actual data in a POST call
- * @function
- */
- send: function () {
- var preprocess = this.flowObj.opts.preprocess;
- if (typeof preprocess === 'function') {
- switch (this.preprocessState) {
- case 0:
- this.preprocessState = 1;
- preprocess(this);
- return;
- case 1:
- return;
- }
- }
- if (this.flowObj.opts.testChunks && !this.tested) {
- this.test();
- return;
- }
- this.loaded = 0;
- this.total = 0;
- this.pendingRetry = false;
- var func = (this.fileObj.file.slice ? 'slice' :
- (this.fileObj.file.mozSlice ? 'mozSlice' :
- (this.fileObj.file.webkitSlice ? 'webkitSlice' :
- 'slice')));
- var bytes = this.fileObj.file[func](this.startByte, this.endByte, this.fileObj.file.type);
- // 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, 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.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(this.getParams(), query);
- 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?
- 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.9.0';
- 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);
|