diff --git a/README.md b/README.md index 0e90663..766b0c3 100644 --- a/README.md +++ b/README.md @@ -1,282 +1,3 @@ -# Flow.js [![Build Status](https://travis-ci.org/flowjs/flow.js.svg)](https://travis-ci.org/flowjs/flow.js) [![Test Coverage](https://codeclimate.com/github/flowjs/flow.js/badges/coverage.svg)](https://codeclimate.com/github/flowjs/flow.js/coverage) [![Saucelabs Test Status](https://saucelabs.com/browser-matrix/flowjs.svg)](https://saucelabs.com/u/flowjs) Buy Me A Coffee +# Flow.js -Flow.js is a JavaScript library providing multiple simultaneous, stable and resumable uploads via the HTML5 File API. [(Demo)](http://flowjs.github.io/ng-flow/) - -The library is designed to introduce fault-tolerance into the upload of large files through HTTP. This is done by splitting each file into small chunks. Then, whenever the upload of a chunk fails, uploading is retried until the procedure completes. This allows uploads to automatically resume uploading after a network connection is lost either locally or to the server. Additionally, it allows for users to pause, resume and even recover uploads without losing state because only the currently uploading chunks will be aborted, not the entire upload. - -Flow.js does not have any external dependencies other than the `HTML5 File API`. This is relied on for the ability to chunk files into smaller pieces. Currently, this means that support is limited to Firefox 4+, Chrome 11+, Safari 6+ and Internet Explorer 10+. - -Samples and examples are available in the `samples/` folder. Please push your own as Markdown to help document the project. - -## Can I see a demo? -[Flow.js + angular.js file upload demo](http://flowjs.github.io/ng-flow/) - ng-flow extension page https://github.com/flowjs/ng-flow - -JQuery and node.js backend demo https://github.com/flowjs/flow.js/tree/master/samples/Node.js - -## How can I install it? - -Download a latest build from https://github.com/flowjs/flow.js/releases -it contains development and minified production files in `dist/` folder. - -or use npm: -```console -npm install @flowjs/flow.js -``` - -or use bower: -```console -bower install flow.js#~2 -``` -or use git clone -```console -git clone https://github.com/flowjs/flow.js -``` -## How can I use it? - -A new `Flow` object is created with information of what and where to post: -```javascript -var flow = new Flow({ - target:'/api/photo/redeem-upload-token', - query:{upload_token:'my_token'} -}); -// Flow.js isn't supported, fall back on a different method -if(!flow.support) location.href = '/some-old-crappy-uploader'; -``` -To allow files to be either selected and drag-dropped, you'll assign drop target and a DOM item to be clicked for browsing: -```javascript -flow.assignBrowse(document.getElementById('browseButton')); -flow.assignDrop(document.getElementById('dropTarget')); -``` -After this, interaction with Flow.js is done by listening to events: -```javascript -flow.on('fileAdded', function(file, event){ - console.log(file, event); -}); -flow.on('fileSuccess', function(file,message){ - console.log(file,message); -}); -flow.on('fileError', function(file, message){ - console.log(file, message); -}); -``` -## How do I set it up with my server? - -Most of the magic for Flow.js happens in the user's browser, but files still need to be reassembled from chunks on the server side. This should be a fairly simple task and can be achieved in any web framework or language, which is able to receive file uploads. - -To handle the state of upload chunks, a number of extra parameters are sent along with all requests: - -* `flowChunkNumber`: The index of the chunk in the current upload. First chunk is `1` (no base-0 counting here). -* `flowTotalChunks`: The total number of chunks. -* `flowChunkSize`: The general chunk size. Using this value and `flowTotalSize` you can calculate the total number of chunks. Please note that the size of the data received in the HTTP might be lower than `flowChunkSize` of this for the last chunk for a file. -* `flowTotalSize`: The total file size. -* `flowIdentifier`: A unique identifier for the file contained in the request. -* `flowFilename`: The original file name (since a bug in Firefox results in the file name not being transmitted in chunk multipart posts). -* `flowRelativePath`: The file's relative path when selecting a directory (defaults to file name in all browsers except Chrome). - -You should allow for the same chunk to be uploaded more than once; this isn't standard behaviour, but on an unstable network environment it could happen, and this case is exactly what Flow.js is designed for. - -For every request, you can confirm reception in HTTP status codes (can be change through the `permanentErrors` option): - -* `200`, `201`, `202`: The chunk was accepted and correct. No need to re-upload. -* `404`, `415`. `500`, `501`: The file for which the chunk was uploaded is not supported, cancel the entire upload. -* _Anything else_: Something went wrong, but try reuploading the file. - -## Handling GET (or `test()` requests) - -Enabling the `testChunks` option will allow uploads to be resumed after browser restarts and even across browsers (in theory you could even run the same file upload across multiple tabs or different browsers). The `POST` data requests listed are required to use Flow.js to receive data, but you can extend support by implementing a corresponding `GET` request with the same parameters: - -* If this request returns a `200`, `201` or `202` HTTP code, the chunks is assumed to have been completed. -* If request returns a permanent error status, upload is stopped. -* If request returns anything else, the chunk will be uploaded in the standard fashion. - -After this is done and `testChunks` enabled, an upload can quickly catch up even after a browser restart by simply verifying already uploaded chunks that do not need to be uploaded again. - -## Full documentation - -### Flow -#### Configuration - -The object is loaded with a configuration options: -```javascript -var r = new Flow({opt1:'val', ...}); -``` -Available configuration options are: - -* `target` The target URL for the multipart POST request. This can be a string or a function. If a -function, it will be passed a FlowFile, a FlowChunk and isTest boolean (Default: `/`) -* `singleFile` Enable single file upload. Once one file is uploaded, second file will overtake existing one, first one will be canceled. (Default: false) -* `chunkSize` The size in bytes of each uploaded chunk of data. This can be a number or a function. If a function, it will be passed a FlowFile. The last uploaded chunk will be at least this size and up to two the size, see [Issue #51](https://github.com/23/resumable.js/issues/51) for details and reasons. (Default: `1*1024*1024`, 1MB) -* `forceChunkSize` Force all chunks to be less or equal than chunkSize. Otherwise, the last chunk will be greater than or equal to `chunkSize`. (Default: `false`) -* `simultaneousUploads` Number of simultaneous uploads (Default: `3`) -* `fileParameterName` The name of the multipart POST parameter to use for the file chunk (Default: `file`) -* `query` Extra parameters to include in the multipart POST with data. This can be an object or a - function. If a function, it will be passed a FlowFile, a FlowChunk object and a isTest boolean - (Default: `{}`) -* `headers` Extra headers to include in the multipart POST with data. If a function, it will be passed a FlowFile, a FlowChunk object and a isTest boolean (Default: `{}`) -* `withCredentials` Standard CORS requests do not send or set any cookies by default. In order to - include cookies as part of the request, you need to set the `withCredentials` property to true. -(Default: `false`) -* `method` Method to use when POSTing chunks to the server (`multipart` or `octet`) (Default: `multipart`) -* `testMethod` HTTP method to use when chunks are being tested. If set to a function, it will be passed a FlowFile and a FlowChunk arguments. (Default: `GET`) -* `uploadMethod` HTTP method to use when chunks are being uploaded. If set to a function, it will be passed a FlowFile and a FlowChunk arguments. (Default: `POST`) -* `allowDuplicateUploads ` Once a file is uploaded, allow reupload of the same file. By default, if a file is already uploaded, it will be skipped unless the file is removed from the existing Flow object. (Default: `false`) -* `prioritizeFirstAndLastChunk` Prioritize first and last chunks of all files. This can be handy if you can determine if a file is valid for your service from only the first or last chunk. For example, photo or video meta data is usually located in the first part of a file, making it easy to test support from only the first chunk. (Default: `false`) -* `testChunks` Make a GET request to the server for each chunks to see if it already exists. If implemented on the server-side, this will allow for upload resumes even after a browser crash or even a computer restart. (Default: `true`) -* `preprocess` Optional function to process each chunk before testing & sending. To the function it will be passed the chunk as parameter, and should call the `preprocessFinished` method on the chunk when finished. (Default: `null`) -* `changeRawDataBeforeSend` Optional function to change Raw Data just before the XHR Request can be sent for each chunk. To the function, it will be passed the chunk and the data as a Parameter. Return the data which will be then sent to the XHR request without further modification. (Default: `null`). This is helpful when using FlowJS with [Google Cloud Storage](https://cloud.google.com/storage/docs/json_api/v1/how-tos/multipart-upload). Usage example can be seen [#276](https://github.com/flowjs/flow.js/pull/276). (For more, check issue [#170](https://github.com/flowjs/flow.js/issues/170)). -* `initFileFn` Optional function to initialize the fileObject. To the function it will be passed a FlowFile and a FlowChunk arguments. -* `readFileFn` Optional function wrapping reading operation from the original file. To the function it will be passed the FlowFile, the startByte and endByte, the fileType and the FlowChunk. -* `generateUniqueIdentifier` Override the function that generates unique identifiers for each file. (Default: `null`) -* `maxChunkRetries` The maximum number of retries for a chunk before the upload is failed. Valid values are any positive integer and `undefined` for no limit. (Default: `0`) -* `chunkRetryInterval` The number of milliseconds to wait before retrying a chunk on a non-permanent error. Valid values are any positive integer and `undefined` for immediate retry. (Default: `undefined`) -* `progressCallbacksInterval` The time interval in milliseconds between progress reports. Set it -to 0 to handle each progress callback. (Default: `500`) -* `speedSmoothingFactor` Used for calculating average upload speed. Number from 1 to 0. Set to 1 -and average upload speed wil be equal to current upload speed. For longer file uploads it is -better set this number to 0.02, because time remaining estimation will be more accurate. This -parameter must be adjusted together with `progressCallbacksInterval` parameter. (Default 0.1) -* `successStatuses` Response is success if response status is in this list (Default: `[200,201, -202]`) -* `permanentErrors` Response fails if response status is in this list (Default: `[404, 415, 500, 501]`) - - -#### Properties - -* `.support` A boolean value indicator whether or not Flow.js is supported by the current browser. -* `.supportDirectory` A boolean value, which indicates if browser supports directory uploads. -* `.opts` A hash object of the configuration of the Flow.js instance. -* `.files` An array of `FlowFile` file objects added by the user (see full docs for this object type below). - -#### Methods - -* `.assignBrowse(domNodes, isDirectory, singleFile, attributes)` Assign a browse action to one or more DOM nodes. - * `domNodes` array of dom nodes or a single node. - * `isDirectory` Pass in `true` to allow directories to be selected (Chrome only, support can be checked with `supportDirectory` property). - * `singleFile` To prevent multiple file uploads set this to true. Also look at config parameter `singleFile`. - * `attributes` Pass object of keys and values to set custom attributes on input fields. - For example, you can set `accept` attribute to `image/*`. This means that user will be able to select only images. - Full list of attributes: https://www.w3.org/wiki/HTML/Elements/input/file - - Note: avoid using `a` and `button` tags as file upload buttons, use span instead. -* `.assignDrop(domNodes)` Assign one or more DOM nodes as a drop target. -* `.unAssignDrop(domNodes)` Unassign one or more DOM nodes as a drop target. -* `.on(event, callback)` Listen for event from Flow.js (see below) -* `.off([event, [callback]])`: - * `.off()` All events are removed. - * `.off(event)` Remove all callbacks of specific event. - * `.off(event, callback)` Remove specific callback of event. `callback` should be a `Function`. -* `.upload()` Start or resume uploading. -* `.pause()` Pause uploading. -* `.resume()` Resume uploading. -* `.cancel()` Cancel upload of all `FlowFile` objects and remove them from the list. -* `.progress()` Returns a float between 0 and 1 indicating the current upload progress of all files. -* `.isUploading()` Returns a boolean indicating whether or not the instance is currently uploading anything. -* `.addFile(file)` Add a HTML5 File object to the list of files. -* `.removeFile(file)` Cancel upload of a specific `FlowFile` object on the list from the list. -* `.getFromUniqueIdentifier(uniqueIdentifier)` Look up a `FlowFile` object by its unique identifier. -* `.getSize()` Returns the total size of the upload in bytes. -* `.sizeUploaded()` Returns the total size uploaded of all files in bytes. -* `.timeRemaining()` 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` - -#### Events - -* `.fileSuccess(file, message, chunk)` A specific file was completed. First argument `file` is instance of `FlowFile`, second argument `message` contains server response. Response is always a string. -Third argument `chunk` is instance of `FlowChunk`. You can get response status by accessing xhr -object `chunk.xhr.status`. -* `.fileProgress(file, chunk)` Uploading progressed for a specific file. -* `.fileAdded(file, event)` This event is used for file validation. To reject this file return false. -This event is also called before file is added to upload queue, -this means that calling `flow.upload()` function will not start current file upload. -Optionally, you can use the browser `event` object from when the file was -added. -* `.filesAdded(array, event)` Same as fileAdded, but used for multiple file validation. -* `.filesSubmitted(array, event)` Same as filesAdded, but happens after the file is added to upload queue. Can be used to start upload of currently added files. -* `.fileRemoved(file)` The specific file was removed from the upload queue. Combined with filesSubmitted, can be used to notify UI to update its state to match the upload queue. -* `.fileRetry(file, chunk)` Something went wrong during upload of a specific file, uploading is being -retried. -* `.fileError(file, message, chunk)` An error occurred during upload of a specific file. -* `.uploadStart()` Upload has been started on the Flow object. -* `.complete()` Uploading completed. -* `.progress()` Uploading progress. -* `.error(message, file, chunk)` An error, including fileError, occurred. -* `.catchAll(event, ...)` Listen to all the events listed above with the same callback function. - -### FlowFile -FlowFile constructor can be accessed in `Flow.FlowFile`. -#### Properties - -* `.flowObj` A back-reference to the parent `Flow` object. -* `.file` The correlating HTML5 `File` object. -* `.name` The name of the file. -* `.relativePath` The relative path to the file (defaults to file name if relative path doesn't exist) -* `.size` Size in bytes of the file. -* `.uniqueIdentifier` A unique identifier assigned to this file object. This value is included in uploads to the server for reference, but can also be used in CSS classes etc when building your upload UI. -* `.averageSpeed` Average upload speed, bytes per second. -* `.currentSpeed` Current upload speed, bytes per second. -* `.chunks` An array of `FlowChunk` items. You shouldn't need to dig into these. -* `.paused` Indicated if file is paused. -* `.error` Indicated if file has encountered an error. - -#### Methods - -* `.progress(relative)` Returns a float between 0 and 1 indicating the current upload progress of the file. If `relative` is `true`, the value is returned relative to all files in the Flow.js instance. -* `.pause()` Pause uploading the file. -* `.resume()` Resume uploading the file. -* `.cancel()` Abort uploading the file and delete it from the list of files to upload. -* `.retry()` Retry uploading the file. -* `.bootstrap()` Rebuild the state of a `FlowFile` object, including reassigning chunks and XMLHttpRequest instances. -* `.isUploading()` Returns a boolean indicating whether file chunks is uploading. -* `.isComplete()` Returns a boolean indicating whether the file has completed uploading and received a server response. -* `.sizeUploaded()` Returns size uploaded in bytes. -* `.timeRemaining()` 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` -* `.getExtension()` Returns file extension in lowercase. -* `.getType()` Returns file type. - -## Contribution - -To ensure consistency throughout the source code, keep these rules in mind as you are working: - -* All features or bug fixes must be tested by one or more specs. - -* We follow the rules contained in [Google's JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html) with an exception we wrap all code at 100 characters. - - -## Installation Dependencies -1. To clone your Github repository, run: -```console -git clone git@github.com:/flow.js.git -``` -2. To go to the Flow.js directory, run: -```console -cd flow.js -``` -3. To add node.js dependencies -```console -npm install -``` -## Testing - -Our unit and integration tests are written with Jasmine and executed with Karma. To run all of the -tests on Chrome run: -```console -grunt karma:watch -``` -Or choose other browser -```console -grunt karma:watch --browsers=Firefox,Chrome -``` -Browsers should be comma separated and case sensitive. - -To re-run tests just change any source or test file. - -Automated tests is running after every commit at travis-ci. - -### Running test on sauceLabs - -1. Connect to sauce labs https://saucelabs.com/docs/connect -2. `grunt test --sauce-local=true --sauce-username=**** --sauce-access-key=***` - -other browsers can be used with `--browsers` flag, available browsers: sl_opera,sl_iphone,sl_safari,sl_ie10,sl_chrome,sl_firefox - -## Origin -Flow.js was inspired by and evolved from https://github.com/23/resumable.js. Library has been supplemented with tests and features, such as drag and drop for folders, upload speed, time remaining estimation, separate files pause, resume and more. +Fork of Flow.js to add meta to each file, & remove IIFE wrap. diff --git a/src/flow.js b/src/flow.js index aceab42..98650a6 100644 --- a/src/flow.js +++ b/src/flow.js @@ -1,1675 +1,1653 @@ /** * @license MIT */ -(function(window, document, undefined) {'use strict'; - if (!window || !document) { - console.warn('Flowjs needs window and document objects to work'); + +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|Function} [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.} [opts.permanentErrors] + * @param {Array.} [opts.successStatuses] + * @param {Function} [opts.initFileFn] + * @param {Function} [opts.readFileFn] + * @param {Function} [opts.generateUniqueIdentifier] + * @constructor + */ +export 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; } - // 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|Function} [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.} [opts.permanentErrors] - * @param {Array.} [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} + */ + var tmpDirTestInput = document.createElement('input'); + if ('webkitdirectory' in tmpDirTestInput + || 'mozdirectory' in tmpDirTestInput + || 'odirectory' in tmpDirTestInput + || 'msdirectory' in tmpDirTestInput + || 'directory' in tmpDirTestInput) { + this.supportDirectory = true; + } else { + this.supportDirectory = false; + } - /** - * Check if directory upload is supported - * @type {boolean} - */ - var tmpDirTestInput = document.createElement('input'); - if ('webkitdirectory' in tmpDirTestInput - || 'mozdirectory' in tmpDirTestInput - || 'odirectory' in tmpDirTestInput - || 'msdirectory' in tmpDirTestInput - || 'directory' in tmpDirTestInput) { - this.supportDirectory = true; - } else { - this.supportDirectory = false; - } + /** + * List of FlowFile objects + * @type {Array.} + */ + this.files = []; - /** - * List of FlowFile objects - * @type {Array.} - */ - 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, - changeRawDataBeforeSend: 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 - }; + /** + * 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, + changeRawDataBeforeSend: 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 = {}; + /** + * Current options + * @type {Object} + */ + this.opts = {}; - /** - * List of events: - * key stands for event name - * value array list of callbacks - * @type {} - */ - this.events = {}; + /** + * List of events: + * key stands for event name + * value array list of callbacks + * @type {} + */ + this.events = {}; - var $ = this; + 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); - } - }; + /** + * On drop event + * @function + * @param {MouseEvent} event + */ + this.onDrop = function (event, fileMeta) { + if ($.opts.onDropStopPropagation) { + event.stopPropagation(); + } + event.preventDefault(); + var dataTransfer = event.dataTransfer; + if (dataTransfer.items && dataTransfer.items[0] && + dataTransfer.items[0].webkitGetAsEntry) { + $.webkitReadDataTransfer(event, fileMeta); + } else { + $.addFiles(dataTransfer.files, event, fileMeta); + } + }; - /** - * Prevent default - * @function - * @param {MouseEvent} event - */ - this.preventEvent = function (event) { - event.preventDefault(); - }; + /** + * Prevent default + * @function + * @param {MouseEvent} event + */ + this.preventEvent = function (event) { + event.preventDefault(); + }; - /** - * Current options - * @type {Object} - */ - this.opts = Flow.extend({}, this.defaults, opts || {}); + /** + * 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) { +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 (!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]; + if (fn !== undefined) { + if (this.events.hasOwnProperty(event)) { + arrayRemove(this.events[event], fn); } } 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); + delete this.events[event]; } - function fileReadSuccess(file, fullPath) { - // relative path should not start with "/" - file.relativePath = fullPath.substring(1); - files.push(file); - decrement(); - } - function readError(fileError) { + } 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, fileMeta) { + var $ = this; + var queue = event.dataTransfer.items.length; + var files = []; + each(event.dataTransfer.items, function (item) { + var entry = item.webkitGetAsEntry(); + if (!entry) { decrement(); - throw fileError; - } - function decrement() { - if (--queue == 0) { - $.addFiles(files, event); - } + return; } - }, - - /** - * 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); + if (entry.isFile) { + // due to a bug in Chrome's File System API impl - #149735 + fileReadSuccess(item.getAsFile(), entry.fullPath); + } else { + readDirectory(entry.createReader()); } - // 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; + }); + 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) { + decrement(); + throw fileError; + } + function decrement() { + if (--queue == 0) { + $.addFiles(files, event, fileMeta); } + } + }, + + /** + * 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, ''); + }, - // Now, simply look for the next, best thing to upload + /** + * 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) { - each(file.chunks, function (chunk) { - if (chunk.status() === 'pending') { - chunk.send(); - found = true; - return false; - } - }); + if (!file.paused && file.chunks.length && + file.chunks[0].status() === 'pending') { + file.chunks[0].send(); + found = true; + return false; } - if (found) { + 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 true; + return found; } + } - // 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); + // 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; + } + }); } - return false; - }, - - - /** - * Assign a browse action to one or more DOM nodes. - * @function - * @param {Element|Array.} 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]; + if (found) { + return false; } + }); + if (found) { + return true; + } - 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.} domNodes - */ - assignDrop: function (domNodes) { - if (typeof domNodes.length === 'undefined') { - domNodes = [domNodes]; + // 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; } - 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); + }); + if (!outstanding && !preventEvents) { + // All chunks have been uploaded, complete + async(function () { + this.fire('complete'); }, 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; - } - } + } + return false; + }, + + + /** + * Assign a browse action to one or more DOM nodes. + * @function + * @param {Element|Array.} 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' }); - }); - // 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; + // 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); } - // 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 (!this.opts.singleFile && !singleFile) { + input.setAttribute('multiple', 'multiple'); } - if (!started) { - async(function () { - this.fire('complete'); - }, this); + if (isDirectory) { + input.setAttribute('webkitdirectory', 'webkitdirectory'); } - }, - - /** - * Resume uploading. - * @function - */ - resume: function () { - each(this.files, function (file) { - if (!file.isComplete()) { - file.resume(); - } + 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); + }, - /** - * 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(); + /** + * Assign one or more DOM nodes as a drop target. + * @function + * @param {Element|Array.} 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; } - }, - - /** - * 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); - } + }); + 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); - if (this.fire('filesAdded', files, event)) { - each(files, function (file) { - if (this.opts.singleFile && this.files.length > 0) { - this.removeFile(this.files[0]); + } + }, + + /** + * 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, fileMeta) { + this.addFiles([file], event, fileMeta); + }, + + /** + * 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, fileMeta) { + 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, fileMeta); + if (this.fire('fileAdded', f, event)) { + files.push(f); } - 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); + }, 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; + } + }, + + /** + * 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, fileMeta = {}) { + + /** + * 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 = (uniqueIdentifier === undefined ? flowObj.generateUniqueIdentifier(file) : uniqueIdentifier); + + /** + * Size of Each Chunk + * @type {number} + */ + this.chunkSize = 0; + + /** + * List of chunks + * @type {Array.} + */ + 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; + + /** + * Meta data for this file + * @type {object} + */ + this.fileMeta = fileMeta; + + /** + * 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; } - }); - 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; + 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; } - }); - if (sizeDelta && !averageSpeed) { - return Number.POSITIVE_INFINITY; + 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") { + var ret = this.flowObj.opts.initFileFn(this); + if (ret && 'then' in ret) { + ret.then(this._bootstrap.bind(this)); + return; } - if (!sizeDelta && !averageSpeed) { - return 0; + } + this._bootstrap(); + }, + + _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; + this.chunkSize = evalOpts(this.flowObj.opts.chunkSize, this); + var chunks = Math.max( + round(this.size / this.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 Math.floor(sizeDelta / averageSpeed); + }); + 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) { + /** - * 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; - - /** - * 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); - - /** - * Size of Each Chunk - * @type {number} - */ - this.chunkSize = 0; - - /** - * List of chunks - * @type {Array.} - */ - this.chunks = []; - - /** - * Indicated if file is paused - * @type {boolean} - */ - this.paused = false; + * Reference to parent flow object + * @type {Flow} + */ + this.flowObj = flowObj; - /** - * Indicated if file has encountered an error - * @type {boolean} - */ - this.error = false; + /** + * Reference to parent FlowFile object + * @type {FlowFile} + */ + this.fileObj = fileObj; - /** - * Average upload speed - * @type {number} - */ - this.averageSpeed = 0; + /** + * File offset + * @type {number} + */ + this.offset = offset; - /** - * Current upload speed - * @type {number} - */ - this.currentSpeed = 0; + /** + * Indicates if chunk existence was checked on the server + * @type {boolean} + */ + this.tested = false; - /** - * 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; + /** + * Number of retries performed + * @type {number} + */ + this.retries = 0; - this.bootstrap(); - } + /** + * Pending retry + * @type {boolean} + */ + this.pendingRetry = false; - 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") { - var ret = this.flowObj.opts.initFileFn(this); - if (ret && 'then' in ret) { - ret.then(this._bootstrap.bind(this)); - return; - } - } - this._bootstrap(); - }, - - _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; - this.chunkSize = evalOpts(this.flowObj.opts.chunkSize, this); - var chunks = Math.max( - round(this.size / this.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(); - } - }; + /** + * Preprocess state + * @type {number} 0 = unprocessed, 1 = processing, 2 = finished + */ + this.preprocessState = 0; /** - * Default read function using the webAPI - * - * @function webAPIFileRead(fileObj, startByte, endByte, fileType, chunk) - * + * Read state + * @type {number} 0 = not read, 1 = reading, 2 = finished */ - function webAPIFileRead(fileObj, startByte, endByte, fileType, chunk) { - var function_name = 'slice'; + this.readState = 0; - if (fileObj.file.slice) - function_name = 'slice'; - else if (fileObj.file.mozSlice) - function_name = 'mozSlice'; - else if (fileObj.file.webkitSlice) - function_name = 'webkitSlice'; + /** + * Used to store the bytes read + * @type {Blob|string} + */ + this.bytes = undefined; - chunk.readFinished(fileObj.file[function_name](startByte, endByte, fileType)); - } + /** + * Bytes transferred from total request size + * @type {number} + */ + this.loaded = 0; + /** + * Total request size + * @type {number} + */ + this.total = 0; /** - * 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; + * Size of a chunk + * @type {number} + */ + this.chunkSize = this.fileObj.chunkSize; - /** - * 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; - - /** - * Used to store the bytes read - * @type {Blob|string} - */ - this.bytes = undefined; - - /** - * Bytes transferred from total request size - * @type {number} - */ - this.loaded = 0; + /** + * Chunk start byte in a file + * @type {number} + */ + this.startByte = this.offset * this.chunkSize; - /** - * Total request size - * @type {number} - */ - this.total = 0; + /** + * A specific filename for this chunk which otherwise default to the main name + * @type {string} + */ + this.filename = null; - /** - * Size of a chunk - * @type {number} - */ - this.chunkSize = this.fileObj.chunkSize; - - /** - * Chunk start byte in a file - * @type {number} - */ - this.startByte = this.offset * this.chunkSize; - - /** - * A specific filename for this chunk which otherwise default to the main name - * @type {string} - */ - this.filename = null; - - /** - * 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; + /** + * 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(); + /** + * Chunk end byte in a file + * @type {number} + */ + this.endByte = this.computeEndByte(); - /** - * XMLHttpRequest - * @type {XMLHttpRequest} - */ - this.xhr = null; + /** + * XMLHttpRequest + * @type {XMLHttpRequest} + */ + this.xhr = null; - var $ = this; + 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); - }; + /** + * 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(); - } - }; + /** + * 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 if (!$.fileObj.paused) { - $.event('retry', $.message()); - $.pendingRetry = true; - $.abort(); - $.retries++; - var retryInterval = $.flowObj.opts.chunkRetryInterval; - if (retryInterval !== null) { - setTimeout(function () { - $.send(); - }, retryInterval); - } else { + /** + * 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 if (!$.fileObj.paused) { + $.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.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 }; - } + }, - FlowChunk.prototype = { - /** - * Get params for a request - * @function - */ - getParams: function () { - return { - flowChunkNumber: this.offset + 1, - flowChunkSize: this.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 (params.length == 0) { - return target; - } + /** + * Get target option with query params + * @function + * @param params + * @returns {string} + */ + getTarget: function (target, params) { + if (params.length == 0) { + return target; + } - 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) { + 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.readState = 1; - read(this.fileObj, this.startByte, this.endByte, this.fileObj.file.type, this); + this.preprocessState = 1; + preprocess(this); return; case 1: return; } - if (this.flowObj.opts.testChunks && !this.tested) { - this.test(); + } + switch (this.readState) { + case 0: + this.readState = 1; + read(this.fileObj, this.startByte, this.endByte, this.fileObj.file.type, this); 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); - var changeRawDataBeforeSend = this.flowObj.opts.changeRawDataBeforeSend; - if (typeof changeRawDataBeforeSend === 'function') { - data = changeRawDataBeforeSend(this, data); - } - 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.filename || this.fileObj.file.name); - } - } - - this.xhr.open(method, target, true); - this.xhr.withCredentials = this.flowObj.opts.withCredentials; + case 1: + return; + } + if (this.flowObj.opts.testChunks && !this.tested) { + this.test(); + return; + } - // Add data from header options - each(evalOpts(this.flowObj.opts.headers, this.fileObj, this, isTest), function (v, k) { - this.xhr.setRequestHeader(k, v); - }, this); + this.loaded = 0; + this.total = 0; + this.pendingRetry = false; - return data; + // 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); + var changeRawDataBeforeSend = this.flowObj.opts.changeRawDataBeforeSend; + if (typeof changeRawDataBeforeSend === 'function') { + data = changeRawDataBeforeSend(this, data); } - }; + this.xhr.send(data); + }, /** - * Remove value from array - * @param array - * @param value + * Abort current xhr request + * @function */ - function arrayRemove(array, value) { - var index = array.indexOf(value); - if (index > -1) { - array.splice(index, 1); + abort: function () { + // Abort and reset + var xhr = this.xhr; + this.xhr = null; + if (xhr) { + xhr.abort(); } - } + }, /** - * If option is a function, evaluate it with given params - * @param {*} data - * @param {...} args arguments of a callback - * @returns {*} + * Retrieve current chunk upload status + * @function + * @returns {string} 'pending', 'uploading', 'success', 'error' */ - 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)); + 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'; + } } - return data; - } - Flow.evalOpts = evalOpts; + }, /** - * Execute function asynchronously - * @param fn - * @param context + * Get response from xhr request + * @function + * @returns {String} */ - function async(fn, context) { - setTimeout(fn.bind(context), 0); - } + message: function () { + return this.xhr ? this.xhr.responseText : ''; + }, /** - * Extends the destination object `dst` by copying all of the properties from - * the `src` object(s) to `dst`. You can specify multiple `src` objects. + * Get upload progress * @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; + * @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; + } + }, /** - * Iterate each element of an object + * Count total size uploaded * @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. + * @returns {number} */ - function each(obj, callback, context) { - if (!obj) { - return ; + 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; } - 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 ; - } - } + 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.fileObj.fileMeta, 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 { - for (key in obj) { - if (obj.hasOwnProperty(key) && callback.call(context, obj[key], key) === false) { - return ; - } + // 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.filename || 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; } - Flow.each = each; +}; - /** - * FlowFile constructor - * @type {FlowFile} - */ - Flow.FlowFile = FlowFile; +/** + * Remove value from array + * @param array + * @param value + */ +function arrayRemove(array, value) { + var index = array.indexOf(value); + if (index > -1) { + array.splice(index, 1); + } +} - /** - * FlowFile constructor - * @type {FlowChunk} - */ - Flow.FlowChunk = FlowChunk; +/** + * 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; - /** - * Library version - * @type {string} - */ - Flow.version = '<%= version %>'; +/** + * 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; - 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; +/** + * 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 { - // 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; } ); + for (key in obj) { + if (obj.hasOwnProperty(key) && callback.call(context, obj[key], key) === false) { + return; + } } } -})(typeof window !== 'undefined' && window, typeof document !== 'undefined' && document); +} +Flow.each = each; + +/** + * FlowFile constructor + * @type {FlowFile} + */ +Flow.FlowFile = FlowFile; + +/** + * FlowFile constructor + * @type {FlowChunk} + */ +Flow.FlowChunk = FlowChunk; + +/** + * Library version + * @type {string} + */ +Flow.version = '<%= version %>';