From 22c749688792d6e3e17bf08233bda69ee16d258b Mon Sep 17 00:00:00 2001 From: Wen Li Date: Wed, 6 Dec 2017 16:17:26 +0000 Subject: [PATCH] Add support of uploading large file via Resumable.js --- resumable.js | 1101 +++++++++++++++++++++++++++++++++++++++++++ tinyfilemanager.php | 191 +++++++- 2 files changed, 1291 insertions(+), 1 deletion(-) create mode 100644 resumable.js diff --git a/resumable.js b/resumable.js new file mode 100644 index 0000000..c7fab93 --- /dev/null +++ b/resumable.js @@ -0,0 +1,1101 @@ +/* +* MIT Licensed +* http://www.23developer.com/opensource +* http://github.com/23/resumable.js +* Steffen Tiedemann Christensen, steffen@23company.com +*/ + +(function(){ +"use strict"; + + var Resumable = function(opts){ + if ( !(this instanceof Resumable) ) { + return new Resumable(opts); + } + this.version = 1.0; + // SUPPORTED BY BROWSER? + // Check if these features are support by the browser: + // - File object type + // - Blob object type + // - FileList object type + // - slicing files + this.support = ( + (typeof(File)!=='undefined') + && + (typeof(Blob)!=='undefined') + && + (typeof(FileList)!=='undefined') + && + (!!Blob.prototype.webkitSlice||!!Blob.prototype.mozSlice||!!Blob.prototype.slice||false) + ); + if(!this.support) return(false); + + + // PROPERTIES + var $ = this; + $.files = []; + $.defaults = { + chunkSize:1*1024*1024, + forceChunkSize:false, + simultaneousUploads:3, + fileParameterName:'file', + chunkNumberParameterName: 'resumableChunkNumber', + chunkSizeParameterName: 'resumableChunkSize', + currentChunkSizeParameterName: 'resumableCurrentChunkSize', + totalSizeParameterName: 'resumableTotalSize', + typeParameterName: 'resumableType', + identifierParameterName: 'resumableIdentifier', + fileNameParameterName: 'resumableFilename', + relativePathParameterName: 'resumableRelativePath', + totalChunksParameterName: 'resumableTotalChunks', + throttleProgressCallbacks: 0.5, + query:{}, + headers:{}, + preprocess:null, + method:'multipart', + uploadMethod: 'POST', + testMethod: 'GET', + prioritizeFirstAndLastChunk:false, + target:'/', + testTarget: null, + parameterNamespace:'', + testChunks:true, + generateUniqueIdentifier:null, + getTarget:null, + maxChunkRetries:100, + chunkRetryInterval:undefined, + permanentErrors:[400, 404, 415, 500, 501], + maxFiles:undefined, + withCredentials:false, + xhrTimeout:0, + clearInput:true, + chunkFormat:'blob', + setChunkTypeFromFile:false, + maxFilesErrorCallback:function (files, errorCount) { + var maxFiles = $.getOpt('maxFiles'); + alert('Please upload no more than ' + maxFiles + ' file' + (maxFiles === 1 ? '' : 's') + ' at a time.'); + }, + minFileSize:1, + minFileSizeErrorCallback:function(file, errorCount) { + alert(file.fileName||file.name +' is too small, please upload files larger than ' + $h.formatSize($.getOpt('minFileSize')) + '.'); + }, + maxFileSize:undefined, + maxFileSizeErrorCallback:function(file, errorCount) { + alert(file.fileName||file.name +' is too large, please upload files less than ' + $h.formatSize($.getOpt('maxFileSize')) + '.'); + }, + fileType: [], + fileTypeErrorCallback: function(file, errorCount) { + alert(file.fileName||file.name +' has type not allowed, please upload files of type ' + $.getOpt('fileType') + '.'); + } + }; + $.opts = opts||{}; + $.getOpt = function(o) { + var $opt = this; + // Get multiple option if passed an array + if(o instanceof Array) { + var options = {}; + $h.each(o, function(option){ + options[option] = $opt.getOpt(option); + }); + return options; + } + // Otherwise, just return a simple option + if ($opt instanceof ResumableChunk) { + if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; } + else { $opt = $opt.fileObj; } + } + if ($opt instanceof ResumableFile) { + if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; } + else { $opt = $opt.resumableObj; } + } + if ($opt instanceof Resumable) { + if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; } + else { return $opt.defaults[o]; } + } + }; + + // EVENTS + // catchAll(event, ...) + // fileSuccess(file), fileProgress(file), fileAdded(file, event), filesAdded(files, filesSkipped), fileRetry(file), + // fileError(file, message), complete(), progress(), error(message, file), pause() + $.events = []; + $.on = function(event,callback){ + $.events.push(event.toLowerCase(), callback); + }; + $.fire = function(){ + // `arguments` is an object, not array, in FF, so: + var args = []; + for (var i=0; i 0){ + var fileTypeFound = false; + for(var index in o.fileType){ + // For good behaviour we do some inital sanitizing. Remove spaces and lowercase all + o.fileType[index] = o.fileType[index].replace(/\s/g, '').toLowerCase(); + + // Allowing for both [extension, .extension, mime/type, mime/*] + var extension = ((o.fileType[index].match(/^[^.][^/]+$/)) ? '.' : '') + o.fileType[index]; + + if ((fileName.substr(-1 * extension.length) === extension) || + //If MIME type, check for wildcard or if extension matches the files tiletype + (extension.indexOf('/') !== -1 && ( + (extension.indexOf('*') !== -1 && fileType.substr(0, extension.indexOf('*')) === extension.substr(0, extension.indexOf('*'))) || + fileType === extension + )) + ){ + fileTypeFound = true; + break; + } + } + if (!fileTypeFound) { + o.fileTypeErrorCallback(file, errorCount++); + return false; + } + } + + if (typeof(o.minFileSize)!=='undefined' && file.sizeo.maxFileSize) { + o.maxFileSizeErrorCallback(file, errorCount++); + return false; + } + + function addFile(uniqueIdentifier){ + if (!$.getFromUniqueIdentifier(uniqueIdentifier)) {(function(){ + file.uniqueIdentifier = uniqueIdentifier; + var f = new ResumableFile($, file, uniqueIdentifier); + $.files.push(f); + files.push(f); + f.container = (typeof event != 'undefined' ? event.srcElement : null); + window.setTimeout(function(){ + $.fire('fileAdded', f, event) + },0); + })()} else { + filesSkipped.push(file); + }; + decreaseReamining(); + } + // directories have size == 0 + var uniqueIdentifier = $h.generateUniqueIdentifier(file, event); + if(uniqueIdentifier && typeof uniqueIdentifier.then === 'function'){ + // Promise or Promise-like object provided as unique identifier + uniqueIdentifier + .then( + function(uniqueIdentifier){ + // unique identifier generation succeeded + addFile(uniqueIdentifier); + }, + function(){ + // unique identifier generation failed + // skip further processing, only decrease file count + decreaseReamining(); + } + ); + }else{ + // non-Promise provided as unique identifier, process synchronously + addFile(uniqueIdentifier); + } + }); + }; + + // INTERNAL OBJECT TYPES + function ResumableFile(resumableObj, file, uniqueIdentifier){ + var $ = this; + $.opts = {}; + $.getOpt = resumableObj.getOpt; + $._prevProgress = 0; + $.resumableObj = resumableObj; + $.file = file; + $.fileName = file.fileName||file.name; // Some confusion in different versions of Firefox + $.size = file.size; + $.relativePath = file.relativePath || file.webkitRelativePath || $.fileName; + $.uniqueIdentifier = uniqueIdentifier; + $._pause = false; + $.container = ''; + var _error = uniqueIdentifier !== undefined; + + // Callback when something happens within the chunk + var chunkEvent = function(event, message){ + // event can be 'progress', 'success', 'error' or 'retry' + switch(event){ + case 'progress': + $.resumableObj.fire('fileProgress', $, message); + break; + case 'error': + $.abort(); + _error = true; + $.chunks = []; + $.resumableObj.fire('fileError', $, message); + break; + case 'success': + if(_error) return; + $.resumableObj.fire('fileProgress', $); // it's at least progress + if($.isComplete()) { + $.resumableObj.fire('fileSuccess', $, message); + } + break; + case 'retry': + $.resumableObj.fire('fileRetry', $); + break; + } + }; + + // Main code to set up a file object with chunks, + // packaged to be able to handle retries if needed. + $.chunks = []; + $.abort = function(){ + // Stop current uploads + var abortCount = 0; + $h.each($.chunks, function(c){ + if(c.status()=='uploading') { + c.abort(); + abortCount++; + } + }); + if(abortCount>0) $.resumableObj.fire('fileProgress', $); + }; + $.cancel = function(){ + // Reset this file to be void + var _chunks = $.chunks; + $.chunks = []; + // Stop current uploads + $h.each(_chunks, function(c){ + if(c.status()=='uploading') { + c.abort(); + $.resumableObj.uploadNextChunk(); + } + }); + $.resumableObj.removeFile($); + $.resumableObj.fire('fileProgress', $); + }; + $.retry = function(){ + $.bootstrap(); + var firedRetry = false; + $.resumableObj.on('chunkingComplete', function(){ + if(!firedRetry) $.resumableObj.upload(); + firedRetry = true; + }); + }; + $.bootstrap = function(){ + $.abort(); + _error = false; + // Rebuild stack of chunks from file + $.chunks = []; + $._prevProgress = 0; + var round = $.getOpt('forceChunkSize') ? Math.ceil : Math.floor; + var maxOffset = Math.max(round($.file.size/$.getOpt('chunkSize')),1); + for (var offset=0; offset0.99999 ? 1 : ret)); + ret = Math.max($._prevProgress, ret); // We don't want to lose percentages when an upload is paused + $._prevProgress = ret; + return(ret); + }; + $.isUploading = function(){ + var uploading = false; + $h.each($.chunks, function(chunk){ + if(chunk.status()=='uploading') { + uploading = true; + return(false); + } + }); + return(uploading); + }; + $.isComplete = function(){ + var outstanding = false; + $h.each($.chunks, function(chunk){ + var status = chunk.status(); + if(status=='pending' || status=='uploading' || chunk.preprocessState === 1) { + outstanding = true; + return(false); + } + }); + return(!outstanding); + }; + $.pause = function(pause){ + if(typeof(pause)==='undefined'){ + $._pause = ($._pause ? false : true); + }else{ + $._pause = pause; + } + }; + $.isPaused = function() { + return $._pause; + }; + + + // Bootstrap and return + $.resumableObj.fire('chunkingStart', $); + $.bootstrap(); + return(this); + } + + + function ResumableChunk(resumableObj, fileObj, offset, callback){ + var $ = this; + $.opts = {}; + $.getOpt = resumableObj.getOpt; + $.resumableObj = resumableObj; + $.fileObj = fileObj; + $.fileObjSize = fileObj.size; + $.fileObjType = fileObj.file.type; + $.offset = offset; + $.callback = callback; + $.lastProgressCallback = (new Date); + $.tested = false; + $.retries = 0; + $.pendingRetry = false; + $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished + + // Computed properties + var chunkSize = $.getOpt('chunkSize'); + $.loaded = 0; + $.startByte = $.offset*chunkSize; + $.endByte = Math.min($.fileObjSize, ($.offset+1)*chunkSize); + if ($.fileObjSize-$.endByte < chunkSize && !$.getOpt('forceChunkSize')) { + // The last chunk will be bigger than the chunk size, but less than 2*chunkSize + $.endByte = $.fileObjSize; + } + $.xhr = null; + + // test() makes a GET request without any data to see if the chunk has already been uploaded in a previous session + $.test = function(){ + // Set up request and listen for event + $.xhr = new XMLHttpRequest(); + + var testHandler = function(e){ + $.tested = true; + var status = $.status(); + if(status=='success') { + $.callback(status, $.message()); + $.resumableObj.uploadNextChunk(); + } else { + $.send(); + } + }; + $.xhr.addEventListener('load', testHandler, false); + $.xhr.addEventListener('error', testHandler, false); + $.xhr.addEventListener('timeout', testHandler, false); + + // Add data from the query options + var params = []; + var parameterNamespace = $.getOpt('parameterNamespace'); + var customQuery = $.getOpt('query'); + if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $); + $h.each(customQuery, function(k,v){ + params.push([encodeURIComponent(parameterNamespace+k), encodeURIComponent(v)].join('=')); + }); + // Add extra data to identify chunk + params = params.concat( + [ + // define key/value pairs for additional parameters + ['chunkNumberParameterName', $.offset + 1], + ['chunkSizeParameterName', $.getOpt('chunkSize')], + ['currentChunkSizeParameterName', $.endByte - $.startByte], + ['totalSizeParameterName', $.fileObjSize], + ['typeParameterName', $.fileObjType], + ['identifierParameterName', $.fileObj.uniqueIdentifier], + ['fileNameParameterName', $.fileObj.fileName], + ['relativePathParameterName', $.fileObj.relativePath], + ['totalChunksParameterName', $.fileObj.chunks.length] + ].filter(function(pair){ + // include items that resolve to truthy values + // i.e. exclude false, null, undefined and empty strings + return $.getOpt(pair[0]); + }) + .map(function(pair){ + // map each key/value pair to its final form + return [ + parameterNamespace + $.getOpt(pair[0]), + encodeURIComponent(pair[1]) + ].join('='); + }) + ); + // Append the relevant chunk and send it + $.xhr.open($.getOpt('testMethod'), $h.getTarget('test', params)); + $.xhr.timeout = $.getOpt('xhrTimeout'); + $.xhr.withCredentials = $.getOpt('withCredentials'); + // Add data from header options + var customHeaders = $.getOpt('headers'); + if(typeof customHeaders === 'function') { + customHeaders = customHeaders($.fileObj, $); + } + $h.each(customHeaders, function(k,v) { + $.xhr.setRequestHeader(k, v); + }); + $.xhr.send(null); + }; + + $.preprocessFinished = function(){ + $.preprocessState = 2; + $.send(); + }; + + // send() uploads the actual data in a POST call + $.send = function(){ + var preprocess = $.getOpt('preprocess'); + if(typeof preprocess === 'function') { + switch($.preprocessState) { + case 0: $.preprocessState = 1; preprocess($); return; + case 1: return; + case 2: break; + } + } + if($.getOpt('testChunks') && !$.tested) { + $.test(); + return; + } + + // Set up request and listen for event + $.xhr = new XMLHttpRequest(); + + // Progress + $.xhr.upload.addEventListener('progress', function(e){ + if( (new Date) - $.lastProgressCallback > $.getOpt('throttleProgressCallbacks') * 1000 ) { + $.callback('progress'); + $.lastProgressCallback = (new Date); + } + $.loaded=e.loaded||0; + }, false); + $.loaded = 0; + $.pendingRetry = false; + $.callback('progress'); + + // Done (either done, failed or retry) + var doneHandler = function(e){ + var status = $.status(); + if(status=='success'||status=='error') { + $.callback(status, $.message()); + $.resumableObj.uploadNextChunk(); + } else { + $.callback('retry', $.message()); + $.abort(); + $.retries++; + var retryInterval = $.getOpt('chunkRetryInterval'); + if(retryInterval !== undefined) { + $.pendingRetry = true; + setTimeout($.send, retryInterval); + } else { + $.send(); + } + } + }; + $.xhr.addEventListener('load', doneHandler, false); + $.xhr.addEventListener('error', doneHandler, false); + $.xhr.addEventListener('timeout', doneHandler, false); + + // Set up the basic query data from Resumable + var query = [ + ['chunkNumberParameterName', $.offset + 1], + ['chunkSizeParameterName', $.getOpt('chunkSize')], + ['currentChunkSizeParameterName', $.endByte - $.startByte], + ['totalSizeParameterName', $.fileObjSize], + ['typeParameterName', $.fileObjType], + ['identifierParameterName', $.fileObj.uniqueIdentifier], + ['fileNameParameterName', $.fileObj.fileName], + ['relativePathParameterName', $.fileObj.relativePath], + ['totalChunksParameterName', $.fileObj.chunks.length], + ].filter(function(pair){ + // include items that resolve to truthy values + // i.e. exclude false, null, undefined and empty strings + return $.getOpt(pair[0]); + }) + .reduce(function(query, pair){ + // assign query key/value + query[$.getOpt(pair[0])] = pair[1]; + return query; + }, {}); + // Mix in custom data + var customQuery = $.getOpt('query'); + if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $); + $h.each(customQuery, function(k,v){ + query[k] = v; + }); + + var func = ($.fileObj.file.slice ? 'slice' : ($.fileObj.file.mozSlice ? 'mozSlice' : ($.fileObj.file.webkitSlice ? 'webkitSlice' : 'slice'))); + var bytes = $.fileObj.file[func]($.startByte, $.endByte, $.getOpt('setChunkTypeFromFile') ? $.fileObj.file.type : ""); + var data = null; + var params = []; + + var parameterNamespace = $.getOpt('parameterNamespace'); + if ($.getOpt('method') === 'octet') { + // Add data from the query options + data = bytes; + $h.each(query, function (k, v) { + params.push([encodeURIComponent(parameterNamespace + k), encodeURIComponent(v)].join('=')); + }); + } else { + // Add data from the query options + data = new FormData(); + $h.each(query, function (k, v) { + data.append(parameterNamespace + k, v); + params.push([encodeURIComponent(parameterNamespace + k), encodeURIComponent(v)].join('=')); + }); + if ($.getOpt('chunkFormat') == 'blob') { + data.append(parameterNamespace + $.getOpt('fileParameterName'), bytes, $.fileObj.fileName); + } + else if ($.getOpt('chunkFormat') == 'base64') { + var fr = new FileReader(); + fr.onload = function (e) { + data.append(parameterNamespace + $.getOpt('fileParameterName'), fr.result); + $.xhr.send(data); + } + fr.readAsDataURL(bytes); + } + } + + var target = $h.getTarget('upload', params); + var method = $.getOpt('uploadMethod'); + + $.xhr.open(method, target); + if ($.getOpt('method') === 'octet') { + $.xhr.setRequestHeader('Content-Type', 'application/octet-stream'); + } + $.xhr.timeout = $.getOpt('xhrTimeout'); + $.xhr.withCredentials = $.getOpt('withCredentials'); + // Add data from header options + var customHeaders = $.getOpt('headers'); + if(typeof customHeaders === 'function') { + customHeaders = customHeaders($.fileObj, $); + } + + $h.each(customHeaders, function(k,v) { + $.xhr.setRequestHeader(k, v); + }); + + if ($.getOpt('chunkFormat') == 'blob') { + $.xhr.send(data); + } + }; + $.abort = function(){ + // Abort and reset + if($.xhr) $.xhr.abort(); + $.xhr = null; + }; + $.status = function(){ + // Returns: 'pending', 'uploading', 'success', 'error' + if($.pendingRetry) { + // 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(!$.xhr) { + return('pending'); + } else if($.xhr.readyState<4) { + // Status is really 'OPENED', 'HEADERS_RECEIVED' or 'LOADING' - meaning that stuff is happening + return('uploading'); + } else { + if($.xhr.status == 200 || $.xhr.status == 201) { + // HTTP 200, 201 (created) + return('success'); + } else if($h.contains($.getOpt('permanentErrors'), $.xhr.status) || $.retries >= $.getOpt('maxChunkRetries')) { + // HTTP 415/500/501, permanent error + return('error'); + } else { + // this should never happen, but we'll reset and queue a retry + // a likely case for this would be 503 service unavailable + $.abort(); + return('pending'); + } + } + }; + $.message = function(){ + return($.xhr ? $.xhr.responseText : ''); + }; + $.progress = function(relative){ + if(typeof(relative)==='undefined') relative = false; + var factor = (relative ? ($.endByte-$.startByte)/$.fileObjSize : 1); + if($.pendingRetry) return(0); + if(!$.xhr || !$.xhr.status) factor*=.95; + var s = $.status(); + switch(s){ + case 'success': + case 'error': + return(1*factor); + case 'pending': + return(0*factor); + default: + return($.loaded/($.endByte-$.startByte)*factor); + } + }; + return(this); + } + + // QUEUE + $.uploadNextChunk = function(){ + var found = false; + + // 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. + if ($.getOpt('prioritizeFirstAndLastChunk')) { + $h.each($.files, function(file){ + if(file.chunks.length && file.chunks[0].status()=='pending' && file.chunks[0].preprocessState === 0) { + file.chunks[0].send(); + found = true; + return(false); + } + if(file.chunks.length>1 && file.chunks[file.chunks.length-1].status()=='pending' && file.chunks[file.chunks.length-1].preprocessState === 0) { + file.chunks[file.chunks.length-1].send(); + found = true; + return(false); + } + }); + if(found) return(true); + } + + // Now, simply look for the next, best thing to upload + $h.each($.files, function(file){ + if(file.isPaused()===false){ + $h.each(file.chunks, function(chunk){ + if(chunk.status()=='pending' && chunk.preprocessState === 0) { + chunk.send(); + found = true; + return(false); + } + }); + } + if(found) return(false); + }); + if(found) return(true); + + // The are no more outstanding chunks to upload, check is everything is done + var outstanding = false; + $h.each($.files, function(file){ + if(!file.isComplete()) { + outstanding = true; + return(false); + } + }); + if(!outstanding) { + // All chunks have been uploaded, complete + $.fire('complete'); + } + return(false); + }; + + + // PUBLIC METHODS FOR RESUMABLE.JS + $.assignBrowse = function(domNodes, isDirectory){ + if(typeof(domNodes.length)=='undefined') domNodes = [domNodes]; + $h.each(domNodes, function(domNode) { + var input; + if(domNode.tagName==='INPUT' && domNode.type==='file'){ + input = domNode; + } else { + input = document.createElement('input'); + input.setAttribute('type', 'file'); + input.style.display = 'none'; + domNode.addEventListener('click', function(){ + input.style.opacity = 0; + input.style.display='block'; + input.focus(); + input.click(); + input.style.display='none'; + }, false); + domNode.appendChild(input); + } + var maxFiles = $.getOpt('maxFiles'); + if (typeof(maxFiles)==='undefined'||maxFiles!=1){ + input.setAttribute('multiple', 'multiple'); + } else { + input.removeAttribute('multiple'); + } + if(isDirectory){ + input.setAttribute('webkitdirectory', 'webkitdirectory'); + } else { + input.removeAttribute('webkitdirectory'); + } + var fileTypes = $.getOpt('fileType'); + if (typeof (fileTypes) !== 'undefined' && fileTypes.length >= 1) { + input.setAttribute('accept', fileTypes.map(function (e) { + e = e.replace(/\s/g, '').toLowerCase(); + if(e.match(/^[^.][^/]+$/)){ + e = '.' + e; + } + return e; + }).join(',')); + } + else { + input.removeAttribute('accept'); + } + // When new files are added, simply append them to the overall list + input.addEventListener('change', function(e){ + appendFilesFromFileList(e.target.files,e); + var clearInput = $.getOpt('clearInput'); + if (clearInput) { + e.target.value = ''; + } + }, false); + }); + }; + $.assignDrop = function(domNodes){ + if(typeof(domNodes.length)=='undefined') domNodes = [domNodes]; + + $h.each(domNodes, function(domNode) { + domNode.addEventListener('dragover', preventDefault, false); + domNode.addEventListener('dragenter', preventDefault, false); + domNode.addEventListener('drop', onDrop, false); + }); + }; + $.unAssignDrop = function(domNodes) { + if (typeof(domNodes.length) == 'undefined') domNodes = [domNodes]; + + $h.each(domNodes, function(domNode) { + domNode.removeEventListener('dragover', preventDefault); + domNode.removeEventListener('dragenter', preventDefault); + domNode.removeEventListener('drop', onDrop); + }); + }; + $.isUploading = function(){ + var uploading = false; + $h.each($.files, function(file){ + if (file.isUploading()) { + uploading = true; + return(false); + } + }); + return(uploading); + }; + $.upload = function(){ + // Make sure we don't start too many uploads at once + if($.isUploading()) return; + // Kick off the queue + $.fire('uploadStart'); + for (var num=1; num<=$.getOpt('simultaneousUploads'); num++) { + $.uploadNextChunk(); + } + }; + $.pause = function(){ + // Resume all chunks currently being uploaded + $h.each($.files, function(file){ + file.abort(); + }); + $.fire('pause'); + }; + $.cancel = function(){ + $.fire('beforeCancel'); + for(var i = $.files.length - 1; i >= 0; i--) { + $.files[i].cancel(); + } + $.fire('cancel'); + }; + $.progress = function(){ + var totalDone = 0; + var totalSize = 0; + // Resume all chunks currently being uploaded + $h.each($.files, function(file){ + totalDone += file.progress()*file.size; + totalSize += file.size; + }); + return(totalSize>0 ? totalDone/totalSize : 0); + }; + $.addFile = function(file, event){ + appendFilesFromFileList([file], event); + }; + $.addFiles = function(files, event){ + appendFilesFromFileList(files, event); + }; + $.removeFile = function(file){ + for(var i = $.files.length - 1; i >= 0; i--) { + if($.files[i] === file) { + $.files.splice(i, 1); + } + } + }; + $.getFromUniqueIdentifier = function(uniqueIdentifier){ + var ret = false; + $h.each($.files, function(f){ + if(f.uniqueIdentifier==uniqueIdentifier) ret = f; + }); + return(ret); + }; + $.getSize = function(){ + var totalSize = 0; + $h.each($.files, function(file){ + totalSize += file.size; + }); + return(totalSize); + }; + $.handleDropEvent = function (e) { + onDrop(e); + }; + $.handleChangeEvent = function (e) { + appendFilesFromFileList(e.target.files, e); + e.target.value = ''; + }; + $.updateQuery = function(query){ + $.opts.query = query; + }; + + return(this); + }; + + + // Node.js-style export for Node and Component + if (typeof module != 'undefined') { + module.exports = Resumable; + } else if (typeof define === "function" && define.amd) { + // AMD/requirejs: Define the module + define(function(){ + return Resumable; + }); + } else { + // Browser: Expose to window + window.Resumable = Resumable; + } + +})(); diff --git a/tinyfilemanager.php b/tinyfilemanager.php index 2cc257f..3c287f2 100644 --- a/tinyfilemanager.php +++ b/tinyfilemanager.php @@ -473,6 +473,72 @@ if (isset($_POST['upl']) && !FM_READONLY) { fm_redirect(FM_SELF_URL . '?p=' . urlencode(FM_PATH)); } +// Resumable Upload, adapted from https://github.com/23/resumable.js/blob/master/samples/Backend%20on%20PHP.md +//check if request is GET and the requested chunk exists or not. this makes testChunks work +if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['rupl']) && !FM_READONLY) { + if(!(isset($_GET['resumableIdentifier']) && trim($_GET['resumableIdentifier'])!='')){ + $_GET['resumableIdentifier']=''; + } + $temp_dir = FM_ROOT_PATH . '/temp/'.$_GET['resumableIdentifier']; + if(!(isset($_GET['resumableFilename']) && trim($_GET['resumableFilename'])!='')){ + $_GET['resumableFilename']=''; + } + if(!(isset($_GET['resumableChunkNumber']) && trim($_GET['resumableChunkNumber'])!='')){ + $_GET['resumableChunkNumber']=''; + } + $chunk_file = $temp_dir.'/'.$_GET['resumableFilename'].'.part'.$_GET['resumableChunkNumber']; + if (file_exists($chunk_file)) { + header("HTTP/1.0 200 Ok"); + } else { + header("HTTP/1.0 404 Not Found"); + } + exit; +} + +// Resumable Upload, adapted from https://github.com/23/resumable.js/blob/master/samples/Backend%20on%20PHP.md +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['rupl']) && !FM_READONLY) { + $path = FM_ROOT_PATH; + if (FM_PATH != '') { + $path .= '/' . FM_PATH; + } + + $errors = 0; + $uploads = 0; + $total = count($_FILES['upload']['name']); + $allowed = (FM_EXTENSION) ? explode(',', FM_EXTENSION) : false; + + // loop through files and move the chunks to a temporarily created directory + if (!empty($_FILES)) foreach ($_FILES as $file) { + + // check the error status + if ($file['error'] != 0) { + _log('error '.$file['error'].' in file '.$_POST['resumableFilename']); + continue; + } + + // init the destination file (format .part<#chunk> + // the file is stored in a temporary directory + if(isset($_POST['resumableIdentifier']) && trim($_POST['resumableIdentifier'])!=''){ + $temp_dir = FM_ROOT_PATH.'/temp/'.$_POST['resumableIdentifier']; + } + $dest_file = $temp_dir .'/'.$_POST['resumableFilename'].'.part'.$_POST['resumableChunkNumber']; + + // create the temporary directory + if (!is_dir($temp_dir)) { + mkdir($temp_dir, 0777, true); + } + + // move the temporary file + if (!move_uploaded_file($file['tmp_name'], $dest_file)) { + _log('Error saving (move_uploaded_file) chunk '.$_POST['resumableChunkNumber'].' for file '.$_POST['resumableFilename']); + } else { + // check if all the parts present, and create the final destination file + createFileFromChunks($temp_dir, $path, $_POST['resumableFilename'] ,$_POST['resumableChunkSize'], $_POST['resumableTotalSize'], $_POST['resumableTotalChunks']); + } + } + exit; +} + // Mass deleting if (isset($_POST['group'], $_POST['delete']) && !FM_READONLY) { $path = FM_ROOT_PATH; @@ -709,6 +775,65 @@ if (isset($_GET['upload']) && !FM_READONLY) { exit; } +if (isset($_GET['rupload']) && !FM_READONLY) { + fm_show_header(); // HEADER + fm_show_nav_path(FM_PATH); // current path + ?> +
+ Drop files here to upload or select from your computer + + +
+
+ + + + + + + + = $totalSize) { + // create the final destination file + if (($fp = fopen($dest_dir . '/' . $fileName, 'w')) !== false) { + for ($i=1; $i<=$total_files; $i++) { + fwrite($fp, file_get_contents($temp_dir . '/' . $fileName.'.part'.$i)); + _log('writing chunk '.$i); + } + fclose($fp); + } else { + _log('cannot create the destination file'); + return false; + } + + // rename the temporary directory (to avoid access from other + // concurrent chunks uploads) and than delete it + if (rename($temp_dir, $temp_dir.'_UNUSED')) { + fm_rdelete($temp_dir.'_UNUSED'); + } else { + fm_rdelete($temp_dir); + } + } + +} + + /** * Delete file or folder (recursively) * @param string $path @@ -1953,7 +2142,7 @@ function fm_show_nav_path($path)
- +