From 95eba3220c5a587de7fa65e5ac6b2e2d85545727 Mon Sep 17 00:00:00 2001 From: dolymood Date: Tue, 7 Apr 2015 14:07:35 +0800 Subject: [PATCH 1/9] add only show folder --- samples/Node.js/public/folder.html | 240 +++++++++++ src/flow.js | 662 ++++++++++++++++++++++++++++- 2 files changed, 889 insertions(+), 13 deletions(-) create mode 100644 samples/Node.js/public/folder.html diff --git a/samples/Node.js/public/folder.html b/samples/Node.js/public/folder.html new file mode 100644 index 00000000..9ab553c1 --- /dev/null +++ b/samples/Node.js/public/folder.html @@ -0,0 +1,240 @@ + + + + Flow.js - Multiple simultaneous, stable and resumable uploads via the HTML5 File API + + + + +
+ +

Flow.js

+

It's a JavaScript library providing multiple simultaneous, stable and resumable uploads via the HTML5 File API.

+ +

The library is designed to introduce fault-tolerance into the upload of large files through HTTP. This is done by splitting each files into small chunks; 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 and resume uploads without loosing state.

+ +

Flow.js relies on the HTML5 File API and the ability to chunks files into smaller pieces. Currently, this means that support is limited to Firefox 4+ and Chrome 11+.

+ +
+ +

Demo

+ + + +
+ Your browser, unfortunately, is not supported by Flow.js. The library requires support for the HTML5 File API along with file slicing. +
+ +
+ Drop files here to upload or select folder or select from your computer or select images +
+ +
+ + + + + + +
+ + + +
+
+ + + + + +
+ + + + + diff --git a/src/flow.js b/src/flow.js index 6f3ddee6..241f3cee 100644 --- a/src/flow.js +++ b/src/flow.js @@ -62,6 +62,17 @@ */ this.files = []; + /** + * all parsedFiles hashMap + * @type {HashMap} + */ + // this._parsedFilesHashMap = new HashMap(); + /** + * List of FlowFile|FlowFolder objects + * @type {Array.} + */ + this.parsedFiles = []; + /** * Default options for flow.js * @type {Object} @@ -89,7 +100,33 @@ chunkRetryInterval: null, permanentErrors: [404, 415, 500, 501], successStatuses: [200, 201, 202], - onDropStopPropagation: false + onDropStopPropagation: false, + getFolderTarget: function(paths, flowFolderObj, callback) { + /** + * paths: ['a/', 'a/b/'] + * should be async + * and the parameter must be like this: + { + 'a/': 'entryId or some other things', + 'a/b/': 'entryId or some other things' + } + */ + callback({}) + }, + parseTarget: function(target, fileObj) { + /** + * fileObj.flowFolderObj will be an object if fileObj.file is in a folder. + * and the fileObj.flowFolderObj.pathsInfo will be: + pathsInfo => { + 'a/': 'entryId1', + 'a/b/': 'entryId2', + 'a/b/c/': 'entryId3', + 'a/c/': 'entryId4' + } + * so the real target may be like this: `target + pathsInfo[fileObj.path]` + */ + return target; + } }; /** @@ -502,7 +539,7 @@ * @function */ resume: function () { - each(this.files, function (file) { + each(this.parsedFiles, function (file) { file.resume(); }); }, @@ -512,18 +549,18 @@ * @function */ pause: function () { - each(this.files, function (file) { + each(this.parsedFiles, function (file) { file.pause(); }); }, /** - * Cancel upload of all FlowFile objects and remove them from the list. + * Cancel upload of all FlowFile|FlowFolder objects and remove them from the list. * @function */ cancel: function () { - for (var i = this.files.length - 1; i >= 0; i--) { - this.files[i].cancel(); + for (var i = this.parsedFiles.length - 1; i >= 0; i--) { + this.parsedFiles[i].cancel(); } }, @@ -575,32 +612,78 @@ } } }, this); - if (this.fire('filesAdded', files, event)) { + var hashMap = new HashMap(); + parseFlowFiles(hashMap, files); + var newParsedFiles = this.getParsedFiles(hashMap); + + if (this.fire('filesAdded', files, newParsedFiles, event)) { each(files, function (file) { if (this.opts.singleFile && this.files.length > 0) { this.removeFile(this.files[0]); } this.files.push(file); }, this); + newParsedFiles.push.apply(this.parsedFiles, newParsedFiles); } - this.fire('filesSubmitted', files, event); + this.fire('filesSubmitted', files, newParsedFiles, event); }, + /** + * get parsedFiles list. + * @function + * @param {FlowFile} file + * @return {Array} parsedFiles + */ + getParsedFiles: function(hashMap) { + // var parsedFilesHashMap = this._parsedFilesHashMap; + var parsedFiles = []; + var f; + each(hashMap.items, function(k) { + f = hashMap.getItem(k); + if (f.constructor == HashMap) { + // is a folder + f = new FlowFolder(this, f, k); + } + // parsedFilesHashMap.setItem(k, f); + parsedFiles.push(f); + }, this); + return parsedFiles; + }, /** * Cancel upload of a specific FlowFile object from the list. * @function * @param {FlowFile} file */ - removeFile: function (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(); + if (file.flowFolderObj) { + // remove file from flowFolderObj.files + file.flowFolderObj.removeFile(file); + } else { + file.abort(); + } } } }, + /** + * Cancel upload of a specific FlowFolder object from the parsedFiles list. + * @function + * @param {FlowFolder} file + */ + removeParsedFile: function(file) { + for (var i = this.parsedFiles.length - 1, k; i >= 0; i--) { + k = this.parsedFiles[i]; + if (k === file) { + this.parsedFiles.splice(i, 1); + } + } + // this._parsedFilesHashMap.removeItem(file); + }, + /** * Look up a FlowFile object by its unique identifier. * @function @@ -669,7 +752,494 @@ }; + /** + * parse relativePath to paths + * @function + * @return {Array} parsed paths + * @example + * 'a/b/c/dd.jpg' => ['a/', 'a/b/', 'a/b/c/'] + */ + function parsePaths(path) { + var ret = []; + var paths = path.split('/'); + var len = paths.length; + var i = 1; + paths.splice(len - 1, 1); + len--; + if (paths.length) { + while (i <= len) { + ret.push(paths.slice(0, i++).join('/') + '/'); + } + } + return ret; + } + + /** + * update the result hashMap by checking the flowFiles + * @function + * @param {HashMap} ret + * @param {Array} flowFiles + */ + function parseFlowFiles(ret, flowFiles) { + if (!flowFiles.length) return; + each(flowFiles, function(flowFile) { + var ppaths = parsePaths(flowFile.relativePath); + if (ppaths.length) { + // in a folder + each(ppaths, function(path, i) { + var item = ret.getItem(path); + if (!item) { + item = new HashMap(); + ret.setItem(path, item); + } + if (ppaths[i + 1]) { + // a folder + // do nothing + } else { + // a file + item.setItem(flowFile.relativePath, flowFile); + } + }); + flowFile.path = ppaths[ppaths.length - 1]; + } else { + // a file + ret.setItem(flowFile.relativePath, flowFile); + } + }); + for (var i = 0, len = ret.items.length, k, v, ppaths; i < len; i++) { + k = ret.items[i]; + v = ret.getItem(k); + if (v.constructor == HashMap) { + // folder + ppaths = parsePaths(k); + if (ppaths[0] && ppaths[0] !== k) { + // add sub folder to root folder + // so the ret hashMap will be a chain + ret.getItem(ppaths[0]).setItem(k, v); + if (ret.delItem(k)) { + i--; + len--; + } + } + } + } + } + + /** + * HashMap class + * @name HashMap + * @constructor + */ + function HashMap() { + this.items = []; + this.itemsObj = {}; + } + + HashMap.prototype = { + constructor: HashMap, + + setItem: function(item, obj) { + if (!this.itemsObj[item] && this.itemsObj[item] !== obj) { + this.itemsObj[item] = obj; + this.items.push(item); + } + return this; + }, + + getItem: function(k) { + return this.itemsObj[k]; + }, + + removeItem: function(v) { + var ret = false; + each(this.items, function(k1, i) { + if (this.itemsObj[k1] === v) { + delete this.itemsObj[k1]; + this.items.splice(i , 1); + ret = true; + } + }, this); + return ret; + }, + + delItem: function(k) { + var ret = false; + each(this.items, function(k1, i) { + if (k1 === k) { + delete this.itemsObj[k]; + this.items.splice(i , 1); + ret = true; + } + }, this); + return ret; + } + + }; + + // get all flowFiles in hashmap + function getFlowFilesByHM(hashmap, flowFolderObj, ret) { + each(hashmap.items, function(k) { + var v = hashmap.getItem(k); + if (v.constructor == HashMap) { + getFlowFilesByHM(v, flowFolderObj, ret); + } else { + // in a folder + flowFolderObj && (v.flowFolderObj = flowFolderObj); + ret.push(v); + } + }); + } + + // get all paths in items(hash.items) without no repeat + function getFolderPaths(items, rObj) { + var ret = []; + if (!rObj) rObj = {}; + each(items, function(k) { + if (!rObj[k]) { + var paths = parsePaths(k); + each(paths, function(path) { + if (!rObj[path]) { + rObj[path] = 1; + ret.push(path); + } + }); + rObj[k] = 1; + } + }); + return ret; + } + + + + /** + * FlowFolder class + * @name FlowFolder + * @param {Flow} flowObj + * @param {hashMap} hashMap + * @param {pathname} pathname + * @constructor + */ + function FlowFolder(flowObj, hashMap, pathname) { + + /** + * Reference to parent Flow instance + * @type {Flow} + */ + this.flowObj = flowObj; + + this.name = pathname.substr(0, pathname.length - 1); + + this.isFolder = true; + + /** + * Average upload speed + * @type {number} + */ + this.averageSpeed = 0; + + /** + * Current upload speed + * @type {number} + */ + this.currentSpeed = 0; + + this.pathsInfo = {}; + + var allPaths = getFolderPaths(hashMap.items); + + allPaths.sort(); + + var $ = this; + var inited = false; + + this.flowObj.opts.getFolderTarget(allPaths, this, function(data) { + if (!inited) { + inited = true; + init(); + } + + $.setPathsInfo(data); + + var parseTarget = $.flowObj.opts.parseTarget; + $.files.forEach(function(flowFile) { + flowFile.target = parseTarget(flowFile.flowObj.opts.target, flowFile); + // upload now + flowFile.resume(); + }); + + }); + + if (!inited) { + inited = true; + init(); + } + + function init() { + /** + * all files in the folder + * @type {Array} + */ + $.files = []; + getFlowFilesByHM(hashMap, $, $.files); + + // pause for now + // because we will fix the flowFile's target + $.pause(true); + } + + } + + FlowFolder.prototype = { + + /** + * Returns a boolean indicating whether or not the instance is currently + * uploading anything. + * @function + * @returns {boolean} + */ + isUploading: function () { + if (!this.isPaused() && !this.isComplete()) { + return true; + } + return false; + }, + + /** + * Indicates if the files has finished + * @function + * @returns {boolean} + */ + isComplete: function () { + var isComplete = true; + each(this.files, function (file) { + if (!file.isComplete()) { + isComplete = false; + return false; + } + }); + return isComplete; + }, + + /** + * Whether one of the files has errored + * @function + * @returns {boolean} + */ + hasError: function() { + var nerr = false; + each(this.files, function (file) { + if (!file.error) { + nerr = true; + return false; + } + }); + return !nerr; + }, + + /** + * Cancel upload of a specific FlowFile object from the files 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(); + } + } + if (this.files.length <= 0) { + // now remove the current FlowFolder Object + this.flowObj.removeParsedFile(this); + } + }, + + /** + * Indicates if one of the files has paused + * @function + * @returns {boolean} + */ + isPaused: function() { + var paused = false; + each(this.files, function (file) { + if (file.paused) { + paused = true; + return false; + } + }); + return paused; + }, + + /** + * Indicates if one of the files has started + * @function + * @returns {boolean} + */ + isStarted: function() { + var started = false; + each(this.files, function (file) { + if (file.started) { + started = true; + return false; + } + }); + return started; + }, + + /** + * Retry aborted file upload + * @function + */ + retry: function (single) { + if (single) { + single.bootstrap() + } else { + each(this.files, function (file) { + file.bootstrap() + }) + } + this.flowObj.upload(); + }, + + /** + * Set pathsInfo + * @function + * @param {Object} data + */ + setPathsInfo: function(data) { + var pathsInfo = this.pathsInfo; + each(data, function(v, key) { + pathsInfo[key] = v; + }); + }, + + /** + * Resume uploading. + * @function + */ + resume: function () { + each(this.files, function (file) { + file.resume(); + }); + }, + + /** + * Pause uploading. + * @function + * @param {Boolean|Undefined} isAbort + */ + pause: function (isAbort) { + var funcName = isAbort ? 'abort' : 'pause'; + each(this.files, function (file) { + file[funcName](); + }); + }, + + /** + * Cancel upload of the files and remove them from the files list. + * @function + */ + cancel: function () { + for (var i = this.files.length - 1; i >= 0; i--) { + this.files[i].cancel(true); + } + this.flowObj.removeParsedFile(this); + }, + + /** + * 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 : + this.isComplete() ? 1 : 0;; + }, + + /** + * 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; + }, + + /** + * Update speed parameters + * @function + */ + measureSpeed: function() { + var averageSpeeds = 0; + var currentSpeeds = 0; + var num = 0; + each(this.files, function (file) { + if (!file.paused && !file.error) { + num += 1; + averageSpeeds += file.averageSpeed || 0; + currentSpeeds += file.currentSpeed || 0; + } + }); + if (num) { + this.averageSpeed = averageSpeeds / num; + this.currentSpeed = currentSpeeds / num; + } else { + this.averageSpeed = 0; + this.currentSpeed = 0; + } + }, + + /** + * 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); + } + + }; @@ -688,6 +1258,12 @@ */ this.flowObj = flowObj; + /** + * Reference to parent FlowFolder instance + * @type {FlowFolder} + */ + this.flowFolderObj = null; + /** * Reference to file * @type {File} @@ -724,6 +1300,12 @@ */ this.chunks = []; + /** + * Indicated if file is started + * @type {boolean} + */ + this.started = false; + /** * Indicated if file is paused * @type {boolean} @@ -769,10 +1351,22 @@ */ this._prevProgress = 0; + this.target = this.flowObj.opts.parseTarget(this.flowObj.opts.target, this); + this.bootstrap(); } FlowFile.prototype = { + + /** + * Whether the file has error + * @function + * @returns {boolean} + */ + hasError: function() { + return this.error; + }, + /** * Update speed parameters * @link http://stackoverflow.com/questions/2779600/how-to-estimate-download-time-remaining-accurately @@ -789,6 +1383,9 @@ this.currentSpeed = Math.max((uploaded - this._prevUploadedSize) / timeSpan * 1000, 0); this.averageSpeed = smoothingFactor * this.currentSpeed + (1 - smoothingFactor) * this.averageSpeed; this._prevUploadedSize = uploaded; + if (this.flowFolderObj) { + this.flowFolderObj.measureSpeed(); + } }, /** @@ -837,6 +1434,15 @@ } }, + /** + * Indicates if the flowFile has paused + * @function + * @returns {boolean} + */ + isPaused: function() { + return this.paused; + }, + /** * Pause file upload * @function @@ -852,7 +1458,9 @@ */ resume: function() { this.paused = false; - this.flowObj.upload(); + if (this.started) { + this.flowObj.upload(); + } }, /** @@ -878,8 +1486,11 @@ * Cancel current upload and remove from a list * @function */ - cancel: function () { + cancel: function (iFolder) { this.flowObj.removeFile(this); + if (!iFolder) { + this.flowObj.removeParsedFile(this); + } }, /** @@ -952,6 +1563,15 @@ return uploading; }, + /** + * Indicates if the flowFile has started + * @function + * @returns {boolean} + */ + isStarted: function() { + return this.started; + }, + /** * Indicates if file is has finished uploading and received a response * @function @@ -1002,6 +1622,15 @@ return Math.floor(delta / this.averageSpeed); }, + /** + * Returns the file's size in bytes. + * @function + * @returns {number} + */ + getSize: function () { + return this.size; + }, + /** * Get file type * @function @@ -1264,6 +1893,7 @@ */ send: function () { var preprocess = this.flowObj.opts.preprocess; + this.fileObj.started = true; if (typeof preprocess === 'function') { switch (this.preprocessState) { case 0: @@ -1332,7 +1962,7 @@ } 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. + // 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) { @@ -1525,6 +2155,12 @@ */ Flow.FlowFile = FlowFile; + /** + * FlowFolder constructor + * @type {FlowFolder} + */ + Flow.FlowFolder = FlowFolder; + /** * FlowFile constructor * @type {FlowChunk} From a03983e33ed6b2c487baa03ea048d5c102f63903 Mon Sep 17 00:00:00 2001 From: dolymood Date: Tue, 7 Apr 2015 18:10:12 +0800 Subject: [PATCH 2/9] fix trigger uploadstart repeatedly --- README.md | 7 +- samples/Node.js/public/folder.html | 49 ++++++++----- src/flow.js | 113 ++++++++++++++++++----------- 3 files changed, 106 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 9bc2fdb3..fd40ed5a 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ parameter must be adjusted together with `progressCallbacksInterval` parameter. * `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]`) - +* `getFolderPathsInfo` function to get all files paths's info. It will be passed a paths array, a FlowFolder object and a callback function.And the callback function should be passed an Object(`{"a/": entryId or some other things, "a/c/": entryId or some other things}`) as parameter. #### Properties @@ -139,6 +139,7 @@ parameter must be adjusted together with `progressCallbacksInterval` parameter. * `.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). +* `.parsedFiles` An array of `FlowFile` or `FlowFolder` file objects added by the user (see full docs for this object type below). #### Methods @@ -181,8 +182,8 @@ 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)` Can be used to start upload of currently added files. +* `.filesAdded(array/*FlowFile*/, array/*FlowFolder*/, event)` Same as fileAdded, but used for multiple file validation. +* `.filesSubmitted(array/*FlowFile*/, array/*FlowFolder*/, event)` Can be used to start upload of currently added files. * `.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. diff --git a/samples/Node.js/public/folder.html b/samples/Node.js/public/folder.html index 9ab553c1..ee835e10 100644 --- a/samples/Node.js/public/folder.html +++ b/samples/Node.js/public/folder.html @@ -49,25 +49,38 @@

Demo

(function () { var showed = false; var r = new Flow({ - target: '/upload', + target: function(fileObj) { + if (fileObj.folderObj) { + // in a folder + // the target may be : '/upload/' + fileObj.folderObj.pathsInfo[fileObj.path] + !showed && console.log('pathsInfo', fileObj.folderObj.pathsInfo); + showed = true; + } + return '/upload'; + }, + query: function(fileObj) { + if (fileObj.folderObj) { + // in a folder + // we may be need to add the `parentId` + // pathsInfo: see getPathsInfo + return { + 'parentId': fileObj.folderObj.pathsInfo[fileObj.path] + }; + } + return {}; + }, chunkSize: 1024*1024, testChunks: false, - getFolderTarget: function(paths, flowFolderObj, callback) { + getFolderPathsInfo: function(paths, folderObj, callback) { console.log('allPaths', paths); setTimeout(function() { + console.log('got'); var ret = {}; paths.forEach(function(path) { ret[path] = ~~(Math.random() * 100); }); callback(ret) - }, 100); - }, - parseTarget: function(target, fileObj) { - if (fileObj.flowFolderObj) { - !showed && console.log('pathsInfo', fileObj.flowFolderObj.pathsInfo); - showed = true; - } - return target; + }, 500); } }); // Flow.js isn't supported, fall back on a different method @@ -137,9 +150,9 @@

Demo

$('.flow-progress .progress-resume-link, .flow-progress .progress-pause-link').hide(); }); r.on('fileSuccess', function(file,message){ - if (file.flowFolderObj) { - if (file.flowFolderObj.isComplete()) { - file = file.flowFolderObj; + if (file.folderObj) { + if (file.folderObj.isComplete()) { + file = file.folderObj; } else { return; } @@ -151,9 +164,9 @@

Demo

$self.find('.flow-file-download').attr('href', '/download/' + file.__id__).show(); }); r.on('fileError', function(file, message){ - if (file.flowFolderObj) { - if (file.flowFolderObj.hasError()) { - file = file.flowFolderObj; + if (file.folderObj) { + if (file.folderObj.hasError()) { + file = file.folderObj; } else { return; } @@ -162,8 +175,8 @@

Demo

$('.flow-file-'+file.__id__+' .flow-file-progress').html('(file could not be uploaded: '+message+')'); }); r.on('fileProgress', function(file){ - if (file.flowFolderObj) { - file = file.flowFolderObj; + if (file.folderObj) { + file = file.folderObj; } // Handle progress for both the file and the overall upload $('.flow-file-'+file.__id__+' .flow-file-progress') diff --git a/src/flow.js b/src/flow.js index 241f3cee..5099f698 100644 --- a/src/flow.js +++ b/src/flow.js @@ -101,31 +101,16 @@ permanentErrors: [404, 415, 500, 501], successStatuses: [200, 201, 202], onDropStopPropagation: false, - getFolderTarget: function(paths, flowFolderObj, callback) { + getFolderPathsInfo: function(paths, folderObj, callback) { /** * paths: ['a/', 'a/b/'] - * should be async - * and the parameter must be like this: + * the callback's parameter will be like this: { - 'a/': 'entryId or some other things', - 'a/b/': 'entryId or some other things' + 'a/': entryId or some other things, + 'a/b/': entryId or some other things } */ callback({}) - }, - parseTarget: function(target, fileObj) { - /** - * fileObj.flowFolderObj will be an object if fileObj.file is in a folder. - * and the fileObj.flowFolderObj.pathsInfo will be: - pathsInfo => { - 'a/': 'entryId1', - 'a/b/': 'entryId2', - 'a/b/c/': 'entryId3', - 'a/c/': 'entryId4' - } - * so the real target may be like this: `target + pathsInfo[fileObj.path]` - */ - return target; } }; @@ -229,8 +214,25 @@ * returned false. Otherwise it returns true. */ fire: function (event, args) { + var args = Array.prototype.slice.call(arguments); + args.unshift(true); + return this._fire.apply(this, args); + }, + + /** + * Fire an event + * @function + * @param {Boolean} catchall or not + * @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. + * @private + */ + _fire: function (catchall, event, args) { // `arguments` is an object, not array, in FF, so: args = Array.prototype.slice.call(arguments); + args.shift(); event = event.toLowerCase(); var preventDefault = false; if (this.events.hasOwnProperty(event)) { @@ -238,7 +240,7 @@ preventDefault = callback.apply(this, args.slice(1)) === false || preventDefault; }, this); } - if (event != 'catchall') { + if (catchall && event != 'catchall') { args.unshift('catchAll'); preventDefault = this.fire.apply(this, args) === false || preventDefault; } @@ -659,9 +661,9 @@ for (var i = this.files.length - 1; i >= 0; i--) { if (this.files[i] === file) { this.files.splice(i, 1); - if (file.flowFolderObj) { - // remove file from flowFolderObj.files - file.flowFolderObj.removeFile(file); + if (file.folderObj) { + // remove file from folderObj.files + file.folderObj.removeFile(file); } else { file.abort(); } @@ -878,14 +880,14 @@ }; // get all flowFiles in hashmap - function getFlowFilesByHM(hashmap, flowFolderObj, ret) { + function getFlowFilesByHM(hashmap, folderObj, ret) { each(hashmap.items, function(k) { var v = hashmap.getItem(k); if (v.constructor == HashMap) { - getFlowFilesByHM(v, flowFolderObj, ret); + getFlowFilesByHM(v, folderObj, ret); } else { // in a folder - flowFolderObj && (v.flowFolderObj = flowFolderObj); + folderObj && (v.folderObj = folderObj); ret.push(v); } }); @@ -953,7 +955,10 @@ var $ = this; var inited = false; - this.flowObj.opts.getFolderTarget(allPaths, this, function(data) { + this.waiting = true; + + // getFolderPathsInfo + this.flowObj.opts.getFolderPathsInfo(allPaths, this, function(data) { if (!inited) { inited = true; init(); @@ -961,12 +966,12 @@ $.setPathsInfo(data); - var parseTarget = $.flowObj.opts.parseTarget; - $.files.forEach(function(flowFile) { - flowFile.target = parseTarget(flowFile.flowObj.opts.target, flowFile); - // upload now - flowFile.resume(); - }); + $.waiting = false; + + // upload now + $.resume(); + + $.flowObj._fire(false, '_gotPathsInfo', $); }); @@ -984,7 +989,7 @@ getFlowFilesByHM(hashMap, $, $.files); // pause for now - // because we will fix the flowFile's target + // because getFolderPathsInfo may be async $.pause(true); } @@ -1262,7 +1267,7 @@ * Reference to parent FlowFolder instance * @type {FlowFolder} */ - this.flowFolderObj = null; + this.folderObj = null; /** * Reference to file @@ -1288,6 +1293,12 @@ */ this.relativePath = file.relativePath || file.webkitRelativePath || this.name; + /** + * File path, it will be rewrited if the file is in a folder. + * @type {string} + */ + this.path = ''; + /** * File unique identifier * @type {string} @@ -1351,8 +1362,6 @@ */ this._prevProgress = 0; - this.target = this.flowObj.opts.parseTarget(this.flowObj.opts.target, this); - this.bootstrap(); } @@ -1383,8 +1392,8 @@ this.currentSpeed = Math.max((uploaded - this._prevUploadedSize) / timeSpan * 1000, 0); this.averageSpeed = smoothingFactor * this.currentSpeed + (1 - smoothingFactor) * this.averageSpeed; this._prevUploadedSize = uploaded; - if (this.flowFolderObj) { - this.flowFolderObj.measureSpeed(); + if (this.folderObj) { + this.folderObj.measureSpeed(); } }, @@ -1459,6 +1468,9 @@ resume: function() { this.paused = false; if (this.started) { + if (this.folderObj && this.folderObj.waiting) { + return; + } this.flowObj.upload(); } }, @@ -1470,6 +1482,7 @@ abort: function (reset) { this.currentSpeed = 0; this.averageSpeed = 0; + this.started = false; var chunks = this.chunks; if (reset) { this.chunks = []; @@ -1870,7 +1883,7 @@ */ test: function () { // Set up request and listen for event - this.xhr = new XMLHttpRequest(); + if (!this.xhr) 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); @@ -1892,8 +1905,24 @@ * @function */ send: function () { - var preprocess = this.flowObj.opts.preprocess; this.fileObj.started = true; + var $ = this; + var folderObj = this.fileObj.folderObj; + if (folderObj && folderObj.waiting) { + this.xhr = new XMLHttpRequest(); + this.flowObj.on('_gotPathsInfo', function(_folderObj) { + if (_folderObj === folderObj) { + $._doSend(); + } + }); + return; + } + + this._doSend(); + }, + + _doSend: function () { + var preprocess = this.flowObj.opts.preprocess; if (typeof preprocess === 'function') { switch (this.preprocessState) { case 0: @@ -1920,7 +1949,7 @@ var bytes = this.fileObj.file[func](this.startByte, this.endByte, this.fileObj.file.type); // Set up request and listen for event - this.xhr = new XMLHttpRequest(); + if (!this.xhr) 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); From 6279280a7acc7beb550212f1757c3120de28bed6 Mon Sep 17 00:00:00 2001 From: dolymood Date: Tue, 7 Apr 2015 18:51:41 +0800 Subject: [PATCH 3/9] update README and some comments --- README.md | 37 +++++++++++++++++++++++++++++++++++-- src/flow.js | 27 ++++++++++++++------------- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index fd40ed5a..c518d4a8 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ parameter must be adjusted together with `progressCallbacksInterval` parameter. * `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]`) -* `getFolderPathsInfo` function to get all files paths's info. It will be passed a paths array, a FlowFolder object and a callback function.And the callback function should be passed an Object(`{"a/": entryId or some other things, "a/c/": entryId or some other things}`) as parameter. +* `getFolderPathsInfo` function to get all files paths info. It will be passed a paths(string) array, a FlowFolder object and a callback function.And the callback function should be passed an Object(`{"a/": entryId or some other things, "a/c/": entryId or some other things}`) as parameter. #### Properties @@ -198,6 +198,7 @@ FlowFile constructor can be accessed in `Flow.FlowFile`. #### Properties * `.flowObj` A back-reference to the parent `Flow` object. +* `.folderObj` A back-reference to the parent `FlowFolder` object if the file is in a file. * `.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) @@ -208,21 +209,53 @@ FlowFile constructor can be accessed in `Flow.FlowFile`. * `.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. +* `.started` Indicated if file has started uploading. #### 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. +* `.progress()` Returns a float between 0 and 1 indicating the current upload progress of the file. * `.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. +* `.isStarted()` Returns a boolean indicating whether file has started uploading. * `.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. +* `.isPaused()` Returns a boolean indicating whether file is paused. * `.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. +* `.getSize()` Returns file size. +* `.hasError()` Returns a boolean indicating whether file has an error. + +### FlowFolder +FlowFolder constructor can be accessed in `Flow.FlowFolder`. +#### Properties + +* `.flowObj` A back-reference to the parent `Flow` object. +* `.files` An array of FlowFile file objects in the folder. +* `.name` The name of the folder. +* `.pathsInfo` The paths info of the folder. +* `.averageSpeed` Average upload speed, bytes per second. +* `.currentSpeed` Current upload speed, bytes per second. + +#### Methods + +* `.progress(relative)` Returns a float between 0 and 1 indicating the current upload progress of the folder files. +* `.pause()` Pause uploading the folder files. +* `.resume()` Resume uploading the folder files. +* `.cancel()` Abort uploading the folder files and delete them from the list of files to upload. +* `.retry()` Retry uploading the folder files. +* `.isStarted()` Returns a boolean indicating whether folder files has started uploading. +* `.isUploading()` Returns a boolean indicating whether folder files is uploading. +* `.isComplete()` Returns a boolean indicating whether the folder files has completed uploading and received a server response. +* `.isPaused()` Returns a boolean indicating whether file is paused. +* `.sizeUploaded()` Returns size uploaded in bytes. +* `.timeRemaining()` Returns remaining time to finish upload folder files file in seconds. Accuracy is based on average speed. If speed is zero, time remaining will be equal to positive infinity `Number.POSITIVE_INFINITY` +* `.getSize()` Returns folder files size. +* `.hasError()` Returns a boolean indicating whether file has an error. ## Contribution diff --git a/src/flow.js b/src/flow.js index 5099f698..5fbc85c1 100644 --- a/src/flow.js +++ b/src/flow.js @@ -957,6 +957,12 @@ this.waiting = true; + /** + * all files in the folder + * @type {Array} + */ + this.files = []; + // getFolderPathsInfo this.flowObj.opts.getFolderPathsInfo(allPaths, this, function(data) { if (!inited) { @@ -972,7 +978,7 @@ $.resume(); $.flowObj._fire(false, '_gotPathsInfo', $); - + }); if (!inited) { @@ -981,13 +987,7 @@ } function init() { - /** - * all files in the folder - * @type {Array} - */ - $.files = []; getFlowFilesByHM(hashMap, $, $.files); - // pause for now // because getFolderPathsInfo may be async $.pause(true); @@ -1027,7 +1027,7 @@ }, /** - * Whether one of the files has errored + * Whether one of the files has an error * @function * @returns {boolean} */ @@ -1061,7 +1061,7 @@ }, /** - * Indicates if one of the files has paused + * Indicates if one of the files is paused * @function * @returns {boolean} */ @@ -1077,7 +1077,7 @@ }, /** - * Indicates if one of the files has started + * Indicates if one of the files is started * @function * @returns {boolean} */ @@ -1199,6 +1199,7 @@ /** * Update speed parameters * @function + * @private */ measureSpeed: function() { var averageSpeeds = 0; @@ -1368,7 +1369,7 @@ FlowFile.prototype = { /** - * Whether the file has error + * Whether the file has an error * @function * @returns {boolean} */ @@ -1444,7 +1445,7 @@ }, /** - * Indicates if the flowFile has paused + * Indicates if the flowFile is paused * @function * @returns {boolean} */ @@ -1577,7 +1578,7 @@ }, /** - * Indicates if the flowFile has started + * Indicates if the flowFile is started * @function * @returns {boolean} */ From d95cebd11e9db2dfb82defa9b33f8c26ec98b7aa Mon Sep 17 00:00:00 2001 From: dolymood Date: Tue, 7 Apr 2015 19:43:42 +0800 Subject: [PATCH 4/9] remove parsedFilesHashMap --- src/flow.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/flow.js b/src/flow.js index 5fbc85c1..2c92c3c7 100644 --- a/src/flow.js +++ b/src/flow.js @@ -62,11 +62,6 @@ */ this.files = []; - /** - * all parsedFiles hashMap - * @type {HashMap} - */ - // this._parsedFilesHashMap = new HashMap(); /** * List of FlowFile|FlowFolder objects * @type {Array.} @@ -637,7 +632,6 @@ * @return {Array} parsedFiles */ getParsedFiles: function(hashMap) { - // var parsedFilesHashMap = this._parsedFilesHashMap; var parsedFiles = []; var f; each(hashMap.items, function(k) { @@ -646,7 +640,6 @@ // is a folder f = new FlowFolder(this, f, k); } - // parsedFilesHashMap.setItem(k, f); parsedFiles.push(f); }, this); return parsedFiles; @@ -683,7 +676,6 @@ this.parsedFiles.splice(i, 1); } } - // this._parsedFilesHashMap.removeItem(file); }, /** From 0e43ee631de30a67521fe0ab5a42634b5bc1f976 Mon Sep 17 00:00:00 2001 From: dolymood Date: Wed, 8 Apr 2015 12:15:12 +0800 Subject: [PATCH 5/9] exports HashMap --- src/flow.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/flow.js b/src/flow.js index 2c92c3c7..2ad94188 100644 --- a/src/flow.js +++ b/src/flow.js @@ -755,6 +755,8 @@ */ function parsePaths(path) { var ret = []; + if (!path) return ret; + var paths = path.split('/'); var len = paths.length; var i = 1; @@ -2177,6 +2179,11 @@ */ Flow.FlowFile = FlowFile; + /** + * HashMap constructor + * @type {HashMap} + */ + Flow.HashMap = HashMap; /** * FlowFolder constructor * @type {FlowFolder} From a854939d00e396880b3044b2037ed54493f10d0b Mon Sep 17 00:00:00 2001 From: dolymood Date: Wed, 8 Apr 2015 12:16:29 +0800 Subject: [PATCH 6/9] tests --- test/fileAddSpec.js | 12 +++++++++--- test/setupSpec.js | 5 +++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/test/fileAddSpec.js b/test/fileAddSpec.js index 222fce1a..e4108614 100644 --- a/test/fileAddSpec.js +++ b/test/fileAddSpec.js @@ -24,15 +24,19 @@ describe('fileAdd event', function() { it('should call filesAdded event', function() { var count = 0; - flow.on('filesAdded', function (files) { + var count2 = 0; + flow.on('filesAdded', function (files, parsedFiles) { count = files.length; + count2 = parsedFiles.length; }); flow.addFiles([ new Blob(['file part']), new Blob(['file 2 part']) ]); expect(count).toBe(2); + expect(count2).toBe(2); expect(flow.files.length).toBe(2); + expect(flow.parsedFiles.length).toBe(2); }); it('should validate fileAdded', function() { @@ -41,6 +45,7 @@ describe('fileAdd event', function() { }); flow.addFile(new Blob(['file part'])); expect(flow.files.length).toBe(0); + expect(flow.parsedFiles.length).toBe(0); }); it('should validate filesAdded', function() { @@ -49,6 +54,7 @@ describe('fileAdd event', function() { }); flow.addFile(new Blob(['file part'])); expect(flow.files.length).toBe(0); + expect(flow.parsedFiles.length).toBe(0); }); it('should validate fileAdded and filesAdded', function() { @@ -56,8 +62,8 @@ describe('fileAdd event', function() { return false; }); var valid = false; - flow.on('filesAdded', function (files) { - valid = files.length === 0; + flow.on('filesAdded', function (files, parsedFiles) { + valid = files.length === 0 && parsedFiles.length === 0; }); flow.addFile(new Blob(['file part'])); expect(valid).toBeTruthy(); diff --git a/test/setupSpec.js b/test/setupSpec.js index 91413e30..6f7943c2 100644 --- a/test/setupSpec.js +++ b/test/setupSpec.js @@ -21,6 +21,11 @@ describe('setup', function() { expect(flow.files.length).toBe(0); }); + it('parsedFiles should be empty', function() { + expect(flow.parsedFiles).toBeDefined(); + expect(flow.parsedFiles.length).toBe(0); + }); + it('events should be empty', function() { expect(flow.events).toBeDefined(); expect(Object.keys(flow.events).length).toBe(0); From de8137c225b19151cd512396bbc7f9956c83b3a6 Mon Sep 17 00:00:00 2001 From: dolymood Date: Wed, 8 Apr 2015 13:11:30 +0800 Subject: [PATCH 7/9] fix removeFile bug --- src/flow.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/flow.js b/src/flow.js index 2ad94188..59b4e8b9 100644 --- a/src/flow.js +++ b/src/flow.js @@ -659,21 +659,30 @@ file.folderObj.removeFile(file); } else { file.abort(); + this.removeParsedFile(file, true); } } } }, /** - * Cancel upload of a specific FlowFolder object from the parsedFiles list. + * Cancel upload of a specific FlowFolder|FlowFole object from the parsedFiles list. * @function - * @param {FlowFolder} file + * @param {FlowFolder|FlowFile} file + * @param {Boolean} fromRemoveFile */ - removeParsedFile: function(file) { + removeParsedFile: function(file, fromRemoveFile) { for (var i = this.parsedFiles.length - 1, k; i >= 0; i--) { k = this.parsedFiles[i]; if (k === file) { this.parsedFiles.splice(i, 1); + if (file.files) { + each(file.files, function(_file) { + this.removeFile(_file); + }, this); + } else { + !fromRemoveFile && this.removeFile(file); + } } } }, @@ -1045,9 +1054,10 @@ for (var i = this.files.length - 1; i >= 0; i--) { if (this.files[i] === file) { this.files.splice(i, 1); - file.abort(); + file.folderObj = null; } } + this.flowObj.removeFile(file); if (this.files.length <= 0) { // now remove the current FlowFolder Object this.flowObj.removeParsedFile(this); From 38d4b044a922ba7af845e6c3f2eb87c469757a20 Mon Sep 17 00:00:00 2001 From: dolymood Date: Wed, 8 Apr 2015 13:11:47 +0800 Subject: [PATCH 8/9] tests:Blob -> File --- test/fileAddSpec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fileAddSpec.js b/test/fileAddSpec.js index e4108614..ceec9e95 100644 --- a/test/fileAddSpec.js +++ b/test/fileAddSpec.js @@ -30,8 +30,8 @@ describe('fileAdd event', function() { count2 = parsedFiles.length; }); flow.addFiles([ - new Blob(['file part']), - new Blob(['file 2 part']) + new File(['file part'], 'file1'), + new File(['file 2 part'], 'file2') ]); expect(count).toBe(2); expect(count2).toBe(2); From 4ca385eee33639895bf39a77bdfc763e49a0bcf5 Mon Sep 17 00:00:00 2001 From: dolymood Date: Wed, 8 Apr 2015 13:45:15 +0800 Subject: [PATCH 9/9] remove started property --- README.md | 3 --- src/flow.js | 41 +++-------------------------------------- 2 files changed, 3 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index c518d4a8..9e256c82 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,6 @@ FlowFile constructor can be accessed in `Flow.FlowFile`. * `.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. -* `.started` Indicated if file has started uploading. #### Methods @@ -219,7 +218,6 @@ FlowFile constructor can be accessed in `Flow.FlowFile`. * `.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. -* `.isStarted()` Returns a boolean indicating whether file has started uploading. * `.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. * `.isPaused()` Returns a boolean indicating whether file is paused. @@ -248,7 +246,6 @@ FlowFolder constructor can be accessed in `Flow.FlowFolder`. * `.resume()` Resume uploading the folder files. * `.cancel()` Abort uploading the folder files and delete them from the list of files to upload. * `.retry()` Retry uploading the folder files. -* `.isStarted()` Returns a boolean indicating whether folder files has started uploading. * `.isUploading()` Returns a boolean indicating whether folder files is uploading. * `.isComplete()` Returns a boolean indicating whether the folder files has completed uploading and received a server response. * `.isPaused()` Returns a boolean indicating whether file is paused. diff --git a/src/flow.js b/src/flow.js index 59b4e8b9..d0dc93de 100644 --- a/src/flow.js +++ b/src/flow.js @@ -1080,22 +1080,6 @@ return paused; }, - /** - * Indicates if one of the files is started - * @function - * @returns {boolean} - */ - isStarted: function() { - var started = false; - each(this.files, function (file) { - if (file.started) { - started = true; - return false; - } - }); - return started; - }, - /** * Retry aborted file upload * @function @@ -1316,12 +1300,6 @@ */ this.chunks = []; - /** - * Indicated if file is started - * @type {boolean} - */ - this.started = false; - /** * Indicated if file is paused * @type {boolean} @@ -1472,12 +1450,10 @@ */ resume: function() { this.paused = false; - if (this.started) { - if (this.folderObj && this.folderObj.waiting) { - return; - } - this.flowObj.upload(); + if (this.folderObj && this.folderObj.waiting) { + return; } + this.flowObj.upload(); }, /** @@ -1487,7 +1463,6 @@ abort: function (reset) { this.currentSpeed = 0; this.averageSpeed = 0; - this.started = false; var chunks = this.chunks; if (reset) { this.chunks = []; @@ -1581,15 +1556,6 @@ return uploading; }, - /** - * Indicates if the flowFile is started - * @function - * @returns {boolean} - */ - isStarted: function() { - return this.started; - }, - /** * Indicates if file is has finished uploading and received a response * @function @@ -1910,7 +1876,6 @@ * @function */ send: function () { - this.fileObj.started = true; var $ = this; var folderObj = this.fileObj.folderObj; if (folderObj && folderObj.waiting) {