diff --git a/README.md b/README.md index 0e906634..7cfb1a6a 100644 --- a/README.md +++ b/README.md @@ -65,13 +65,13 @@ Most of the magic for Flow.js happens in the user's browser, but files still nee 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). +* `chunkNumber`: The index of the chunk in the current upload. First chunk is `1` (no base-0 counting here). +* `totalChunks`: The total number of chunks. +* `chunkSize`: The general chunk size. Using this value and `totalSize` you can calculate the total number of chunks. Please note that the size of the data received in the HTTP might be lower than `chunkSize` of this for the last chunk for a file. +* `totalSize`: The total file size. +* `requestId`: A unique identifier for the file contained in the request. +* `filename`: The original file name (since a bug in Firefox results in the file name not being transmitted in chunk multipart posts). +* `relativePath`: 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. @@ -195,6 +195,7 @@ added. * `.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. +* `.readErrors(files, folders, event)` This event fires before fileAdded or filesAdded events only if errors occur while reading files or folders. First argument `files` is collection of files read errors, second argument `folders` is collection of folder read errors. * `.uploadStart()` Upload has been started on the Flow object. * `.complete()` Uploading completed. * `.progress()` Uploading progress. diff --git a/dist/flow.js b/dist/flow.js index e61f0a75..b4ace2aa 100644 --- a/dist/flow.js +++ b/dist/flow.js @@ -230,55 +230,109 @@ */ 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 ; + getEntries(event.dataTransfer.items).then(function (result) { + getFiles(result.files).then(function (entries) { + var files = []; + var errors = []; + each(entries, function (entry) { + if (entry.error) { + errors.push(entry); + } else { + files.push(entry); + } + }); + if (result.errors.length || errors.length) { + $.fire('readErrors', errors, result.errors, event); + } + if (files.length) { + $.addFiles(files, event); + } + }); + }); + function getEntries(items) { + var files = []; + var errors = []; + var promises = []; + + function readEntry(entry, promises) { + if (entry.isFile) { + files.push(entry); + } else if (entry.isDirectory) { + promises.push(readDirectory(entry)); + } } - 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(entry) { + var reader = entry.createReader(); + return new Promise(function (resolve, reject) { + var promises = []; + readEntries(entry, reader, promises, resolve); + }); } - }); - 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()); - } + + function readEntries(entry, reader, promises, resolve) { + reader.readEntries(function (entries) { + if (entries.length) { + var promises2 = []; + each(entries, function (entry2) { + readEntry(entry2, promises2); + }); + promises.push(Promise.all(promises2)); + readEntries(entry, reader, promises, resolve); + return; + } + resolve(Promise.all(promises)); + }, function (error) { + errors.push({ + path: entry.fullPath, + error: error }); - readDirectory(reader); - } else { - decrement(); + resolve(promises); + }); + } + + each(items, function (item) { + var entry = item.webkitGetAsEntry(); + if (!entry) { + return; } - }, readError); + if (entry.isFile) { + // due to a bug in Chrome's File System API impl - #149735 + files.push(getFile(item.getAsFile(), entry.fullPath)); + return; + } + readEntry(entry, promises); + }); + + return new Promise(function (resolve, reject) { + return Promise.all(promises).then(function () { + resolve({ files: files, errors: errors }); + }); + }); } - function fileReadSuccess(file, fullPath) { + function getFiles(entries) { + return Promise.all(entries.map(function (entry) { + return new Promise(function (resolve, reject) { + if (entry.file) { + var fullPath = entry.fullPath; + entry.file(function (file) { + resolve(getFile(file, fullPath)); + }, function (file) { + resolve({ + path: entry.fullPath, + error: file + }); + }); + } else { + resolve(entry); + } + }); + })); + } + function getFile(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); - } + return file; } }, @@ -588,8 +642,12 @@ * @param {Event} [event] event is optional */ addFiles: function (fileList, event) { + var $ = this; var files = []; - each(fileList, function (file) { + var errors = []; + var promises = []; + + function addFile(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); @@ -600,16 +658,54 @@ } } } - }, 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); } + + /** + * Chrome's FileSystem API has a bug that files from dropped folders or files from input dialog's selected folder, + * with read errors (has absolute paths which exceed 260 chars) will have zero file size. + */ + function validateFile(file) { + // files with size greater than zero can upload + if (file.size > 0) { + addFile.bind($)(file); + return; + } + + // try to read from from zero size file, + // if error occurs than file cannot be uploaded + promises.push(new Promise(function (resolve, reject) { + var reader = new FileReader(); + reader.onloadend = function () { + if (reader.error) { + errors.push({ + path: file.webkitRelativePath || file.name, + error: reader.error + }); + } else { + addFile.bind($)(file); + } + resolve(); + }.bind($); + reader.readAsArrayBuffer(file); + })); + } + + each(fileList, validateFile); + + Promise.all(promises).then(function () { + if (errors.length) { + this.fire('readErrors', errors, [], event); + } + 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); + } + }.bind(this)); }, @@ -716,12 +812,6 @@ */ this.flowObj = flowObj; - /** - * Used to store the bytes read - * @type {Blob|string} - */ - this.bytes = null; - /** * Reference to file * @type {File} @@ -937,9 +1027,16 @@ */ bootstrap: function () { if (typeof this.flowObj.opts.initFileFn === "function") { - this.flowObj.opts.initFileFn(this); + 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 @@ -1144,6 +1241,11 @@ */ this.readState = 0; + /** + * Used to store the bytes read + * @type {Blob|string} + */ + this.bytes = undefined; /** * Bytes transferred from total request size @@ -1280,14 +1382,14 @@ */ 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 + chunkNumber: this.offset + 1, + chunkSize: this.chunkSize, + currentChunkSize: this.endByte - this.startByte, + totalSize: this.fileObj.size, + requestId: this.fileObj.uniqueIdentifier, + filename: this.fileObj.name, + relativePath: this.fileObj.relativePath, + totalChunks: this.fileObj.chunks.length }; }, diff --git a/dist/flow.min.js b/dist/flow.min.js index ef301778..1db27a62 100644 --- a/dist/flow.min.js +++ b/dist/flow.min.js @@ -1,2 +1,2 @@ /*! @flowjs/flow.js 2.14.1 */ -!function(a,b,c){"use strict";function d(b){if(this.support=!("undefined"==typeof File||"undefined"==typeof Blob||"undefined"==typeof FileList||!Blob.prototype.slice&&!Blob.prototype.webkitSlice&&!Blob.prototype.mozSlice),this.support){this.supportDirectory=/Chrome/.test(a.navigator.userAgent)||/Firefox/.test(a.navigator.userAgent)||/Edge/.test(a.navigator.userAgent),this.files=[],this.defaults={chunkSize:1048576,forceChunkSize:!1,simultaneousUploads:3,singleFile:!1,fileParameterName:"file",progressCallbacksInterval:500,speedSmoothingFactor:.1,query:{},headers:{},withCredentials:!1,preprocess:null,changeRawDataBeforeSend:null,method:"multipart",testMethod:"GET",uploadMethod:"POST",prioritizeFirstAndLastChunk:!1,allowDuplicateUploads:!1,target:"/",testChunks:!0,generateUniqueIdentifier:null,maxChunkRetries:0,chunkRetryInterval:null,permanentErrors:[404,413,415,500,501],successStatuses:[200,201,202],onDropStopPropagation:!1,initFileFn:null,readFileFn:f},this.opts={},this.events={};var c=this;this.onDrop=function(a){c.opts.onDropStopPropagation&&a.stopPropagation(),a.preventDefault();var b=a.dataTransfer;b.items&&b.items[0]&&b.items[0].webkitGetAsEntry?c.webkitReadDataTransfer(a):c.addFiles(b.files,a)},this.preventEvent=function(a){a.preventDefault()},this.opts=d.extend({},this.defaults,b||{})}}function e(a,b,d){this.flowObj=a,this.bytes=null,this.file=b,this.name=b.fileName||b.name,this.size=b.size,this.relativePath=b.relativePath||b.webkitRelativePath||this.name,this.uniqueIdentifier=d===c?a.generateUniqueIdentifier(b):d,this.chunkSize=0,this.chunks=[],this.paused=!1,this.error=!1,this.averageSpeed=0,this.currentSpeed=0,this._lastProgressCallback=Date.now(),this._prevUploadedSize=0,this._prevProgress=0,this.bootstrap()}function f(a,b,c,d,e){var f="slice";a.file.slice?f="slice":a.file.mozSlice?f="mozSlice":a.file.webkitSlice&&(f="webkitSlice"),e.readFinished(a.file[f](b,c,d))}function g(a,b,c){this.flowObj=a,this.fileObj=b,this.offset=c,this.tested=!1,this.retries=0,this.pendingRetry=!1,this.preprocessState=0,this.readState=0,this.loaded=0,this.total=0,this.chunkSize=this.fileObj.chunkSize,this.startByte=this.offset*this.chunkSize,this.filename=null,this.computeEndByte=function(){var a=Math.min(this.fileObj.size,(this.offset+1)*this.chunkSize);return this.fileObj.size-a-1&&a.splice(c,1)}function i(a,b){return"function"==typeof a&&(b=Array.prototype.slice.call(arguments),a=a.apply(null,b.slice(1))),a}function j(a,b){setTimeout(a.bind(b),0)}function k(a,b){return l(arguments,function(b){b!==a&&l(b,function(b,c){a[c]=b})}),a}function l(a,b,c){if(a){var d;if("undefined"!=typeof a.length){for(d=0;d1&&"pending"===a.chunks[a.chunks.length-1].status()?(a.chunks[a.chunks.length-1].send(),b=!0,!1):void 0}),b))return b;if(l(this.files,function(a){if(a.paused||l(a.chunks,function(a){if("pending"===a.status())return a.send(),b=!0,!1}),b)return!1}),b)return!0;var c=!1;return l(this.files,function(a){if(!a.isComplete())return c=!0,!1}),c||a||j(function(){this.fire("complete")},this),!1},assignBrowse:function(a,c,d,e){a instanceof Element&&(a=[a]),l(a,function(a){var f;"INPUT"===a.tagName&&"file"===a.type?f=a:(f=b.createElement("input"),f.setAttribute("type","file"),k(f.style,{visibility:"hidden",position:"absolute",width:"1px",height:"1px"}),a.appendChild(f),a.addEventListener("click",function(){f.click()},!1)),this.opts.singleFile||d||f.setAttribute("multiple","multiple"),c&&f.setAttribute("webkitdirectory","webkitdirectory"),l(e,function(a,b){f.setAttribute(b,a)});var g=this;f.addEventListener("change",function(a){a.target.value&&(g.addFiles(a.target.files,a),a.target.value="")},!1)},this)},assignDrop:function(a){"undefined"==typeof a.length&&(a=[a]),l(a,function(a){a.addEventListener("dragover",this.preventEvent,!1),a.addEventListener("dragenter",this.preventEvent,!1),a.addEventListener("drop",this.onDrop,!1)},this)},unAssignDrop:function(a){"undefined"==typeof a.length&&(a=[a]),l(a,function(a){a.removeEventListener("dragover",this.preventEvent),a.removeEventListener("dragenter",this.preventEvent),a.removeEventListener("drop",this.onDrop)},this)},isUploading:function(){var a=!1;return l(this.files,function(b){if(b.isUploading())return a=!0,!1}),a},_shouldUploadNext:function(){var a=0,b=!0,c=this.opts.simultaneousUploads;return l(this.files,function(d){l(d.chunks,function(d){if("uploading"===d.status()&&(a++,a>=c))return b=!1,!1})}),b&&a},upload:function(){var a=this._shouldUploadNext();if(a!==!1){this.fire("uploadStart");for(var b=!1,c=1;c<=this.opts.simultaneousUploads-a;c++)b=this.uploadNextChunk(!0)||b;b||j(function(){this.fire("complete")},this)}},resume:function(){l(this.files,function(a){a.isComplete()||a.resume()})},pause:function(){l(this.files,function(a){a.pause()})},cancel:function(){for(var a=this.files.length-1;a>=0;a--)this.files[a].cancel()},progress:function(){var a=0,b=0;return l(this.files,function(c){a+=c.progress()*c.size,b+=c.size}),b>0?a/b:0},addFile:function(a,b){this.addFiles([a],b)},addFiles:function(a,b){var c=[];l(a,function(a){if((!m||m&&a.size>0)&&(a.size%4096!==0||"."!==a.name&&"."!==a.fileName)){var d=this.generateUniqueIdentifier(a);if(this.opts.allowDuplicateUploads||!this.getFromUniqueIdentifier(d)){var f=new e(this,a,d);this.fire("fileAdded",f,b)&&c.push(f)}}},this),this.fire("filesAdded",c,b)&&(l(c,function(a){this.opts.singleFile&&this.files.length>0&&this.removeFile(this.files[0]),this.files.push(a)},this),this.fire("filesSubmitted",c,b))},removeFile:function(a){for(var b=this.files.length-1;b>=0;b--)this.files[b]===a&&(this.files.splice(b,1),a.abort(),this.fire("fileRemoved",a))},getFromUniqueIdentifier:function(a){var b=!1;return l(this.files,function(c){c.uniqueIdentifier===a&&(b=c)}),b},getSize:function(){var a=0;return l(this.files,function(b){a+=b.size}),a},sizeUploaded:function(){var a=0;return l(this.files,function(b){a+=b.sizeUploaded()}),a},timeRemaining:function(){var a=0,b=0;return l(this.files,function(c){c.paused||c.error||(a+=c.size-c.sizeUploaded(),b+=c.averageSpeed)}),a&&!b?Number.POSITIVE_INFINITY:a||b?Math.floor(a/b):0}},e.prototype={measureSpeed:function(){var a=Date.now()-this._lastProgressCallback;if(a){var b=this.flowObj.opts.speedSmoothingFactor,c=this.sizeUploaded();this.currentSpeed=Math.max((c-this._prevUploadedSize)/a*1e3,0),this.averageSpeed=b*this.currentSpeed+(1-b)*this.averageSpeed,this._prevUploadedSize=c}},chunkEvent:function(a,b,c){switch(b){case"progress":if(Date.now()-this._lastProgressCallback.9999?1:b),this._prevProgress},isUploading:function(){var a=!1;return l(this.chunks,function(b){if("uploading"===b.status())return a=!0,!1}),a},isComplete:function(){var a=!1;return l(this.chunks,function(b){var c=b.status();if("pending"===c||"uploading"===c||"reading"===c||1===b.preprocessState||1===b.readState)return a=!0,!1}),!a},sizeUploaded:function(){var a=0;return l(this.chunks,function(b){a+=b.sizeUploaded()}),a},timeRemaining:function(){if(this.paused||this.error)return 0;var a=this.size-this.sizeUploaded();return a&&!this.averageSpeed?Number.POSITIVE_INFINITY:a||this.averageSpeed?Math.floor(a/this.averageSpeed):0},getType:function(){return this.file.type&&this.file.type.split("/")[1]},getExtension:function(){return this.name.substr((~-this.name.lastIndexOf(".")>>>0)+2).toLowerCase()}},g.prototype={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}},getTarget:function(a,b){return 0==b.length?a:(a+=a.indexOf("?")<0?"?":"&",a+b.join("&"))},test:function(){this.xhr=new XMLHttpRequest,this.xhr.addEventListener("load",this.testHandler,!1),this.xhr.addEventListener("error",this.testHandler,!1);var a=i(this.flowObj.opts.testMethod,this.fileObj,this),b=this.prepareXhrRequest(a,!0);this.xhr.send(b)},preprocessFinished:function(){this.endByte=this.computeEndByte(),this.preprocessState=2,this.send()},readFinished:function(a){this.readState=2,this.bytes=a,this.send()},send:function(){var a=this.flowObj.opts.preprocess,b=this.flowObj.opts.readFileFn;if("function"==typeof a)switch(this.preprocessState){case 0:return this.preprocessState=1,void a(this);case 1:return}switch(this.readState){case 0:return this.readState=1,void b(this.fileObj,this.startByte,this.endByte,this.fileObj.file.type,this);case 1:return}if(this.flowObj.opts.testChunks&&!this.tested)return void this.test();this.loaded=0,this.total=0,this.pendingRetry=!1,this.xhr=new XMLHttpRequest,this.xhr.upload.addEventListener("progress",this.progressHandler,!1),this.xhr.addEventListener("load",this.doneHandler,!1),this.xhr.addEventListener("error",this.doneHandler,!1);var c=i(this.flowObj.opts.uploadMethod,this.fileObj,this),d=this.prepareXhrRequest(c,!1,this.flowObj.opts.method,this.bytes),e=this.flowObj.opts.changeRawDataBeforeSend;"function"==typeof e&&(d=e(this,d)),this.xhr.send(d)},abort:function(){var a=this.xhr;this.xhr=null,a&&a.abort()},status:function(a){return 1===this.readState?"reading":this.pendingRetry||1===this.preprocessState?"uploading":this.xhr?this.xhr.readyState<4?"uploading":this.flowObj.opts.successStatuses.indexOf(this.xhr.status)>-1?"success":this.flowObj.opts.permanentErrors.indexOf(this.xhr.status)>-1||!a&&this.retries>=this.flowObj.opts.maxChunkRetries?"error":(this.abort(),"pending"):"pending"},message:function(){return this.xhr?this.xhr.responseText:""},progress:function(){if(this.pendingRetry)return 0;var a=this.status();return"success"===a||"error"===a?1:"pending"===a?0:this.total>0?this.loaded/this.total:0},sizeUploaded:function(){var a=this.endByte-this.startByte;return"success"!==this.status()&&(a=this.progress()*a),a},prepareXhrRequest:function(a,b,c,d){var e=i(this.flowObj.opts.query,this.fileObj,this,b);e=k(e||{},this.getParams());var f=i(this.flowObj.opts.target,this.fileObj,this,b),g=null;if("GET"===a||"octet"===c){var h=[];l(e,function(a,b){h.push([encodeURIComponent(b),encodeURIComponent(a)].join("="))}),f=this.getTarget(f,h),g=d||null}else g=new FormData,l(e,function(a,b){g.append(b,a)}),"undefined"!=typeof d&&g.append(this.flowObj.opts.fileParameterName,d,this.filename||this.fileObj.file.name);return this.xhr.open(a,f,!0),this.xhr.withCredentials=this.flowObj.opts.withCredentials,l(i(this.flowObj.opts.headers,this.fileObj,this,b),function(a,b){this.xhr.setRequestHeader(b,a)},this),g}},d.evalOpts=i,d.extend=k,d.each=l,d.FlowFile=e,d.FlowChunk=g,d.version="2.14.1","object"==typeof module&&module&&"object"==typeof module.exports?module.exports=d:(a.Flow=d,"function"==typeof define&&define.amd&&define("flow",[],function(){return d}))}("undefined"!=typeof window&&window,"undefined"!=typeof document&&document); \ No newline at end of file +!function(a,b,c){"use strict";function d(b){if(this.support=!("undefined"==typeof File||"undefined"==typeof Blob||"undefined"==typeof FileList||!Blob.prototype.slice&&!Blob.prototype.webkitSlice&&!Blob.prototype.mozSlice),this.support){this.supportDirectory=/Chrome/.test(a.navigator.userAgent)||/Firefox/.test(a.navigator.userAgent)||/Edge/.test(a.navigator.userAgent),this.files=[],this.defaults={chunkSize:1048576,forceChunkSize:!1,simultaneousUploads:3,singleFile:!1,fileParameterName:"file",progressCallbacksInterval:500,speedSmoothingFactor:.1,query:{},headers:{},withCredentials:!1,preprocess:null,changeRawDataBeforeSend:null,method:"multipart",testMethod:"GET",uploadMethod:"POST",prioritizeFirstAndLastChunk:!1,allowDuplicateUploads:!1,target:"/",testChunks:!0,generateUniqueIdentifier:null,maxChunkRetries:0,chunkRetryInterval:null,permanentErrors:[404,413,415,500,501],successStatuses:[200,201,202],onDropStopPropagation:!1,initFileFn:null,readFileFn:f},this.opts={},this.events={};var c=this;this.onDrop=function(a){c.opts.onDropStopPropagation&&a.stopPropagation(),a.preventDefault();var b=a.dataTransfer;b.items&&b.items[0]&&b.items[0].webkitGetAsEntry?c.webkitReadDataTransfer(a):c.addFiles(b.files,a)},this.preventEvent=function(a){a.preventDefault()},this.opts=d.extend({},this.defaults,b||{})}}function e(a,b,d){this.flowObj=a,this.file=b,this.name=b.fileName||b.name,this.size=b.size,this.relativePath=b.relativePath||b.webkitRelativePath||this.name,this.uniqueIdentifier=d===c?a.generateUniqueIdentifier(b):d,this.chunkSize=0,this.chunks=[],this.paused=!1,this.error=!1,this.averageSpeed=0,this.currentSpeed=0,this._lastProgressCallback=Date.now(),this._prevUploadedSize=0,this._prevProgress=0,this.bootstrap()}function f(a,b,c,d,e){var f="slice";a.file.slice?f="slice":a.file.mozSlice?f="mozSlice":a.file.webkitSlice&&(f="webkitSlice"),e.readFinished(a.file[f](b,c,d))}function g(a,b,d){this.flowObj=a,this.fileObj=b,this.offset=d,this.tested=!1,this.retries=0,this.pendingRetry=!1,this.preprocessState=0,this.readState=0,this.bytes=c,this.loaded=0,this.total=0,this.chunkSize=this.fileObj.chunkSize,this.startByte=this.offset*this.chunkSize,this.filename=null,this.computeEndByte=function(){var a=Math.min(this.fileObj.size,(this.offset+1)*this.chunkSize);return this.fileObj.size-a-1&&a.splice(c,1)}function i(a,b){return"function"==typeof a&&(b=Array.prototype.slice.call(arguments),a=a.apply(null,b.slice(1))),a}function j(a,b){setTimeout(a.bind(b),0)}function k(a,b){return l(arguments,function(b){b!==a&&l(b,function(b,c){a[c]=b})}),a}function l(a,b,c){if(a){var d;if("undefined"!=typeof a.length){for(d=0;d1&&"pending"===a.chunks[a.chunks.length-1].status()?(a.chunks[a.chunks.length-1].send(),b=!0,!1):void 0}),b))return b;if(l(this.files,function(a){if(a.paused||l(a.chunks,function(a){if("pending"===a.status())return a.send(),b=!0,!1}),b)return!1}),b)return!0;var c=!1;return l(this.files,function(a){if(!a.isComplete())return c=!0,!1}),c||a||j(function(){this.fire("complete")},this),!1},assignBrowse:function(a,c,d,e){a instanceof Element&&(a=[a]),l(a,function(a){var f;"INPUT"===a.tagName&&"file"===a.type?f=a:(f=b.createElement("input"),f.setAttribute("type","file"),k(f.style,{visibility:"hidden",position:"absolute",width:"1px",height:"1px"}),a.appendChild(f),a.addEventListener("click",function(){f.click()},!1)),this.opts.singleFile||d||f.setAttribute("multiple","multiple"),c&&f.setAttribute("webkitdirectory","webkitdirectory"),l(e,function(a,b){f.setAttribute(b,a)});var g=this;f.addEventListener("change",function(a){a.target.value&&(g.addFiles(a.target.files,a),a.target.value="")},!1)},this)},assignDrop:function(a){"undefined"==typeof a.length&&(a=[a]),l(a,function(a){a.addEventListener("dragover",this.preventEvent,!1),a.addEventListener("dragenter",this.preventEvent,!1),a.addEventListener("drop",this.onDrop,!1)},this)},unAssignDrop:function(a){"undefined"==typeof a.length&&(a=[a]),l(a,function(a){a.removeEventListener("dragover",this.preventEvent),a.removeEventListener("dragenter",this.preventEvent),a.removeEventListener("drop",this.onDrop)},this)},isUploading:function(){var a=!1;return l(this.files,function(b){if(b.isUploading())return a=!0,!1}),a},_shouldUploadNext:function(){var a=0,b=!0,c=this.opts.simultaneousUploads;return l(this.files,function(d){l(d.chunks,function(d){if("uploading"===d.status()&&(a++,a>=c))return b=!1,!1})}),b&&a},upload:function(){var a=this._shouldUploadNext();if(a!==!1){this.fire("uploadStart");for(var b=!1,c=1;c<=this.opts.simultaneousUploads-a;c++)b=this.uploadNextChunk(!0)||b;b||j(function(){this.fire("complete")},this)}},resume:function(){l(this.files,function(a){a.isComplete()||a.resume()})},pause:function(){l(this.files,function(a){a.pause()})},cancel:function(){for(var a=this.files.length-1;a>=0;a--)this.files[a].cancel()},progress:function(){var a=0,b=0;return l(this.files,function(c){a+=c.progress()*c.size,b+=c.size}),b>0?a/b:0},addFile:function(a,b){this.addFiles([a],b)},addFiles:function(a,b){function c(a){if((!m||m&&a.size>0)&&(a.size%4096!==0||"."!==a.name&&"."!==a.fileName)){var c=this.generateUniqueIdentifier(a);if(this.opts.allowDuplicateUploads||!this.getFromUniqueIdentifier(c)){var d=new e(this,a,c);this.fire("fileAdded",d,b)&&g.push(d)}}}function d(a){return a.size>0?void c.bind(f)(a):void i.push(new Promise(function(b,d){var e=new FileReader;e.onloadend=function(){e.error?h.push({path:a.webkitRelativePath||a.name,error:e.error}):c.bind(f)(a),b()}.bind(f),e.readAsArrayBuffer(a)}))}var f=this,g=[],h=[],i=[];l(a,d),Promise.all(i).then(function(){h.length&&this.fire("readErrors",h,[],b),this.fire("filesAdded",g,b)&&(l(g,function(a){this.opts.singleFile&&this.files.length>0&&this.removeFile(this.files[0]),this.files.push(a)},this),this.fire("filesSubmitted",g,b))}.bind(this))},removeFile:function(a){for(var b=this.files.length-1;b>=0;b--)this.files[b]===a&&(this.files.splice(b,1),a.abort(),this.fire("fileRemoved",a))},getFromUniqueIdentifier:function(a){var b=!1;return l(this.files,function(c){c.uniqueIdentifier===a&&(b=c)}),b},getSize:function(){var a=0;return l(this.files,function(b){a+=b.size}),a},sizeUploaded:function(){var a=0;return l(this.files,function(b){a+=b.sizeUploaded()}),a},timeRemaining:function(){var a=0,b=0;return l(this.files,function(c){c.paused||c.error||(a+=c.size-c.sizeUploaded(),b+=c.averageSpeed)}),a&&!b?Number.POSITIVE_INFINITY:a||b?Math.floor(a/b):0}},e.prototype={measureSpeed:function(){var a=Date.now()-this._lastProgressCallback;if(a){var b=this.flowObj.opts.speedSmoothingFactor,c=this.sizeUploaded();this.currentSpeed=Math.max((c-this._prevUploadedSize)/a*1e3,0),this.averageSpeed=b*this.currentSpeed+(1-b)*this.averageSpeed,this._prevUploadedSize=c}},chunkEvent:function(a,b,c){switch(b){case"progress":if(Date.now()-this._lastProgressCallback.9999?1:b),this._prevProgress},isUploading:function(){var a=!1;return l(this.chunks,function(b){if("uploading"===b.status())return a=!0,!1}),a},isComplete:function(){var a=!1;return l(this.chunks,function(b){var c=b.status();if("pending"===c||"uploading"===c||"reading"===c||1===b.preprocessState||1===b.readState)return a=!0,!1}),!a},sizeUploaded:function(){var a=0;return l(this.chunks,function(b){a+=b.sizeUploaded()}),a},timeRemaining:function(){if(this.paused||this.error)return 0;var a=this.size-this.sizeUploaded();return a&&!this.averageSpeed?Number.POSITIVE_INFINITY:a||this.averageSpeed?Math.floor(a/this.averageSpeed):0},getType:function(){return this.file.type&&this.file.type.split("/")[1]},getExtension:function(){return this.name.substr((~-this.name.lastIndexOf(".")>>>0)+2).toLowerCase()}},g.prototype={getParams:function(){return{chunkNumber:this.offset+1,chunkSize:this.chunkSize,currentChunkSize:this.endByte-this.startByte,totalSize:this.fileObj.size,requestId:this.fileObj.uniqueIdentifier,filename:this.fileObj.name,relativePath:this.fileObj.relativePath,totalChunks:this.fileObj.chunks.length}},getTarget:function(a,b){return 0==b.length?a:(a+=a.indexOf("?")<0?"?":"&",a+b.join("&"))},test:function(){this.xhr=new XMLHttpRequest,this.xhr.addEventListener("load",this.testHandler,!1),this.xhr.addEventListener("error",this.testHandler,!1);var a=i(this.flowObj.opts.testMethod,this.fileObj,this),b=this.prepareXhrRequest(a,!0);this.xhr.send(b)},preprocessFinished:function(){this.endByte=this.computeEndByte(),this.preprocessState=2,this.send()},readFinished:function(a){this.readState=2,this.bytes=a,this.send()},send:function(){var a=this.flowObj.opts.preprocess,b=this.flowObj.opts.readFileFn;if("function"==typeof a)switch(this.preprocessState){case 0:return this.preprocessState=1,void a(this);case 1:return}switch(this.readState){case 0:return this.readState=1,void b(this.fileObj,this.startByte,this.endByte,this.fileObj.file.type,this);case 1:return}if(this.flowObj.opts.testChunks&&!this.tested)return void this.test();this.loaded=0,this.total=0,this.pendingRetry=!1,this.xhr=new XMLHttpRequest,this.xhr.upload.addEventListener("progress",this.progressHandler,!1),this.xhr.addEventListener("load",this.doneHandler,!1),this.xhr.addEventListener("error",this.doneHandler,!1);var c=i(this.flowObj.opts.uploadMethod,this.fileObj,this),d=this.prepareXhrRequest(c,!1,this.flowObj.opts.method,this.bytes),e=this.flowObj.opts.changeRawDataBeforeSend;"function"==typeof e&&(d=e(this,d)),this.xhr.send(d)},abort:function(){var a=this.xhr;this.xhr=null,a&&a.abort()},status:function(a){return 1===this.readState?"reading":this.pendingRetry||1===this.preprocessState?"uploading":this.xhr?this.xhr.readyState<4?"uploading":this.flowObj.opts.successStatuses.indexOf(this.xhr.status)>-1?"success":this.flowObj.opts.permanentErrors.indexOf(this.xhr.status)>-1||!a&&this.retries>=this.flowObj.opts.maxChunkRetries?"error":(this.abort(),"pending"):"pending"},message:function(){return this.xhr?this.xhr.responseText:""},progress:function(){if(this.pendingRetry)return 0;var a=this.status();return"success"===a||"error"===a?1:"pending"===a?0:this.total>0?this.loaded/this.total:0},sizeUploaded:function(){var a=this.endByte-this.startByte;return"success"!==this.status()&&(a=this.progress()*a),a},prepareXhrRequest:function(a,b,c,d){var e=i(this.flowObj.opts.query,this.fileObj,this,b);e=k(e||{},this.getParams());var f=i(this.flowObj.opts.target,this.fileObj,this,b),g=null;if("GET"===a||"octet"===c){var h=[];l(e,function(a,b){h.push([encodeURIComponent(b),encodeURIComponent(a)].join("="))}),f=this.getTarget(f,h),g=d||null}else g=new FormData,l(e,function(a,b){g.append(b,a)}),"undefined"!=typeof d&&g.append(this.flowObj.opts.fileParameterName,d,this.filename||this.fileObj.file.name);return this.xhr.open(a,f,!0),this.xhr.withCredentials=this.flowObj.opts.withCredentials,l(i(this.flowObj.opts.headers,this.fileObj,this,b),function(a,b){this.xhr.setRequestHeader(b,a)},this),g}},d.evalOpts=i,d.extend=k,d.each=l,d.FlowFile=e,d.FlowChunk=g,d.version="2.14.1","object"==typeof module&&module&&"object"==typeof module.exports?module.exports=d:(a.Flow=d,"function"==typeof define&&define.amd&&define("flow",[],function(){return d}))}("undefined"!=typeof window&&window,"undefined"!=typeof document&&document); \ No newline at end of file diff --git a/samples/Backend on Go.md b/samples/Backend on Go.md index 2e135286..9dc6e0ad 100644 --- a/samples/Backend on Go.md +++ b/samples/Backend on Go.md @@ -74,7 +74,7 @@ func (fn streamHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func continueUpload(w http.ResponseWriter, r *http.Request) { - chunkDirPath := "./incomplete/" + r.FormValue("flowFilename") + "/" + r.FormValue("flowChunkNumber") + chunkDirPath := "./incomplete/" + r.FormValue("filename") + "/" + r.FormValue("chunkNumber") if _, err := os.Stat(chunkDirPath); err != nil { w.WriteHeader(204) return @@ -84,7 +84,7 @@ func continueUpload(w http.ResponseWriter, r *http.Request) { func chunkedReader(w http.ResponseWriter, r *http.Request) error { r.ParseMultipartForm(25) - chunkDirPath := "./incomplete/" + r.FormValue("flowFilename") + chunkDirPath := "./incomplete/" + r.FormValue("filename") err := os.MkdirAll(chunkDirPath, 02750) if err != nil { return err @@ -97,7 +97,7 @@ func chunkedReader(w http.ResponseWriter, r *http.Request) error { } defer src.Close() - dst, err := os.Create(chunkDirPath + "/" + r.FormValue("flowChunkNumber")) + dst, err := os.Create(chunkDirPath + "/" + r.FormValue("chunkNumber")) if err != nil { return err } diff --git a/samples/Backend on Haskell.md b/samples/Backend on Haskell.md index 062cf132..4fdd035e 100644 --- a/samples/Backend on Haskell.md +++ b/samples/Backend on Haskell.md @@ -76,15 +76,15 @@ uploadStart = action POST (pathJSON >/> pathId withAuth $ chunkForm :: DeformActionM f (Upload, Int64, Word64) chunkForm = do csrfForm - up <- "flowIdentifier" .:> (lift . (maybeAction <=< lookupUpload) =<< deform) + up <- "requestId" .:> (lift . (maybeAction <=< lookupUpload) =<< deform) let z = uploadSize up - "flowFilename" .:> (deformGuard "Filename mismatch." . (uploadFilename up ==) =<< deform) - "flowTotalSize" .:> (deformGuard "File size mismatch." . (z ==) =<< fileSizeForm) - c <- "flowChunkSize" .:> (deformCheck "Chunk size too small." (256 <=) =<< deform) - n <- "flowTotalChunks" .:> (deformCheck "Chunk count mismatch." ((1 >=) . abs . (pred z `div` c -)) =<< deform) - i <- "flowChunkNumber" .:> (deformCheck "Chunk number out of range." (\i -> 0 <= i && i < n) =<< pred <$> deform) + "filename" .:> (deformGuard "Filename mismatch." . (uploadFilename up ==) =<< deform) + "totalSize" .:> (deformGuard "File size mismatch." . (z ==) =<< fileSizeForm) + c <- "chunkSize" .:> (deformCheck "Chunk size too small." (256 <=) =<< deform) + n <- "totalChunks" .:> (deformCheck "Chunk count mismatch." ((1 >=) . abs . (pred z `div` c -)) =<< deform) + i <- "chunkNumber" .:> (deformCheck "Chunk number out of range." (\i -> 0 <= i && i < n) =<< pred <$> deform) let o = c * i - l <- "flowCurrentChunkSize" .:> (deformCheck "Current chunk size out of range." (\l -> (c == l || i == pred n) && o + l <= z) =<< deform) + l <- "currentChunkSize" .:> (deformCheck "Current chunk size out of range." (\l -> (c == l || i == pred n) && o + l <= z) =<< deform) return (up, o, fromIntegral l) uploadChunk :: ActionRoute () diff --git a/samples/Node.js/flow-node.js b/samples/Node.js/flow-node.js index 883397dd..ad49ac26 100644 --- a/samples/Node.js/flow-node.js +++ b/samples/Node.js/flow-node.js @@ -64,11 +64,11 @@ module.exports = flow = function(temporaryFolder) { //'found', filename, original_filename, identifier //'not_found', null, null, null $.get = function(req, callback) { - var chunkNumber = req.param('flowChunkNumber', 0); - var chunkSize = req.param('flowChunkSize', 0); - var totalSize = req.param('flowTotalSize', 0); - var identifier = req.param('flowIdentifier', ""); - var filename = req.param('flowFilename', ""); + var chunkNumber = req.param('chunkNumber', 0); + var chunkSize = req.param('chunkSize', 0); + var totalSize = req.param('totalSize', 0); + var identifier = req.param('requestId', ""); + var filename = req.param('filename', ""); if (validateRequest(chunkNumber, chunkSize, totalSize, identifier, filename) == 'valid') { var chunkFilename = getChunkFilename(chunkNumber, identifier); @@ -93,11 +93,11 @@ module.exports = flow = function(temporaryFolder) { var fields = req.body; var files = req.files; - var chunkNumber = fields['flowChunkNumber']; - var chunkSize = fields['flowChunkSize']; - var totalSize = fields['flowTotalSize']; - var identifier = cleanIdentifier(fields['flowIdentifier']); - var filename = fields['flowFilename']; + var chunkNumber = fields['chunkNumber']; + var chunkSize = fields['chunkSize']; + var totalSize = fields['totalSize']; + var identifier = cleanIdentifier(fields['requestId']); + var filename = fields['filename']; if (!files[$.fileParameterName] || !files[$.fileParameterName].size) { callback('invalid_flow_request', null, null, null); diff --git a/samples/Ruby backend in Sinatra.md b/samples/Ruby backend in Sinatra.md index 9b102a5a..be033344 100644 --- a/samples/Ruby backend in Sinatra.md +++ b/samples/Ruby backend in Sinatra.md @@ -89,19 +89,19 @@ private ## # Determine if this is the last chunk based on the chunk number. def last_chunk? - params[:flowChunkNumber].to_i == params[:flowTotalChunks].to_i + params[:chunkNumber].to_i == params[:totalChunks].to_i end ## # ./tmp/flow/abc-123/upload.txt.part1 def chunk_file_path - File.join(chunk_file_directory, "#{params[:flowFilename]}.part#{params[:flowChunkNumber]}") + File.join(chunk_file_directory, "#{params[:filename]}.part#{params[:chunkNumber]}") end ## # ./tmp/flow/abc-123 def chunk_file_directory - File.join "tmp", "flow", params[:flowIdentifier] + File.join "tmp", "flow", params[:requestId] end ## @@ -123,7 +123,7 @@ private ## # /final/resting/place/upload.txt def final_file_path - File.join final_file_directory, params[:flowFilename] + File.join final_file_directory, params[:filename] end ## diff --git a/src/flow.js b/src/flow.js index 1b016c0e..2d533cd5 100644 --- a/src/flow.js +++ b/src/flow.js @@ -230,55 +230,109 @@ */ 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 ; + getEntries(event.dataTransfer.items).then(function (result) { + getFiles(result.files).then(function (entries) { + var files = []; + var errors = []; + each(entries, function (entry) { + if (entry.error) { + errors.push(entry); + } else { + files.push(entry); + } + }); + if (result.errors.length || errors.length) { + $.fire('readErrors', errors, result.errors, event); + } + if (files.length) { + $.addFiles(files, event); + } + }); + }); + function getEntries(items) { + var files = []; + var errors = []; + var promises = []; + + function readEntry(entry, promises) { + if (entry.isFile) { + files.push(entry); + } else if (entry.isDirectory) { + promises.push(readDirectory(entry)); + } } - 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(entry) { + var reader = entry.createReader(); + return new Promise(function (resolve, reject) { + var promises = []; + readEntries(entry, reader, promises, resolve); + }); } - }); - 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()); - } + + function readEntries(entry, reader, promises, resolve) { + reader.readEntries(function (entries) { + if (entries.length) { + var promises2 = []; + each(entries, function (entry2) { + readEntry(entry2, promises2); + }); + promises.push(Promise.all(promises2)); + readEntries(entry, reader, promises, resolve); + return; + } + resolve(Promise.all(promises)); + }, function (error) { + errors.push({ + path: entry.fullPath, + error: error }); - readDirectory(reader); - } else { - decrement(); + resolve(promises); + }); + } + + each(items, function (item) { + var entry = item.webkitGetAsEntry(); + if (!entry) { + return; + } + if (entry.isFile) { + // due to a bug in Chrome's File System API impl - #149735 + files.push(getFile(item.getAsFile(), entry.fullPath)); + return; } - }, readError); + readEntry(entry, promises); + }); + + return new Promise(function (resolve, reject) { + return Promise.all(promises).then(function () { + resolve({ files: files, errors: errors }); + }); + }); + } + function getFiles(entries) { + return Promise.all(entries.map(function (entry) { + return new Promise(function (resolve, reject) { + if (entry.file) { + var fullPath = entry.fullPath; + entry.file(function (file) { + resolve(getFile(file, fullPath)); + }, function (file) { + resolve({ + path: entry.fullPath, + error: file + }); + }); + } else { + resolve(entry); + } + }); + })); } - function fileReadSuccess(file, fullPath) { + function getFile(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); - } + return file; } }, @@ -588,8 +642,12 @@ * @param {Event} [event] event is optional */ addFiles: function (fileList, event) { + var $ = this; var files = []; - each(fileList, function (file) { + var errors = []; + var promises = []; + + function addFile(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); @@ -600,16 +658,54 @@ } } } - }, 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); } + + /** + * Chrome's FileSystem API has a bug that files from dropped folders or files from input dialog's selected folder, + * with read errors (has absolute paths which exceed 260 chars) will have zero file size. + */ + function validateFile(file) { + // files with size greater than zero can upload + if (file.size > 0) { + addFile.bind($)(file); + return; + } + + // try to read from from zero size file, + // if error occurs than file cannot be uploaded + promises.push(new Promise(function (resolve, reject) { + var reader = new FileReader(); + reader.onloadend = function () { + if (reader.error) { + errors.push({ + path: file.webkitRelativePath || file.name, + error: reader.error + }); + } else { + addFile.bind($)(file); + } + resolve(); + }.bind($); + reader.readAsArrayBuffer(file); + })); + } + + each(fileList, validateFile); + + Promise.all(promises).then(function () { + if (errors.length) { + this.fire('readErrors', errors, [], event); + } + 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); + } + }.bind(this)); }, @@ -1286,14 +1382,14 @@ */ 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 + chunkNumber: this.offset + 1, + chunkSize: this.chunkSize, + currentChunkSize: this.endByte - this.startByte, + totalSize: this.fileObj.size, + requestId: this.fileObj.uniqueIdentifier, + filename: this.fileObj.name, + relativePath: this.fileObj.relativePath, + totalChunks: this.fileObj.chunks.length }; },