123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635 |
- /**
- * @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) ||
- /Firefox/.test(window.navigator.userAgent) ||
- /Edge/.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, 413, 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),
- * fileRemoved(file), 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 (domNodes instanceof Element) {
- 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) {
- if (!file.isComplete()) {
- 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 === '.'))) {
- 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);
- }
- }
- }
- }, 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();
- this.fire('fileRemoved', file);
- }
- }
- },
- /**
- * 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
- * @param {string} uniqueIdentifier
- * @constructor
- */
- function FlowFile(flowObj, file, uniqueIdentifier) {
- /**
- * 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 = (uniqueIdentifier === undefined ? flowObj.generateUniqueIdentifier(file) : uniqueIdentifier);
- /**
- * 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, startByte, endByte, fileType, 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.fileObj.file.type, 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 413/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);
- });
- if (typeof blob !== "undefined") 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 = '<%= version %>';
- 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);
|