/** * /* * MIT Licensed * https://www.twentythree.com * https://github.com/23/resumable.js * Steffen Fagerström Christensen, steffen@twentythree.com * * @format */ (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: 2 * 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', dragOverClass: 'dragover', throttleProgressCallbacks: 0.5, query: {}, headers: {}, preprocess: null, preprocessFile: 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, 401, 403, 404, 409, 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]; } } }; $.indexOf = function (array, obj) { if (array.indexOf) { return array.indexOf(obj); } for (var i = 0; i < array.length; i++) { if (array[i] === obj) { return i; } } return -1; }; // 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 < arguments.length; i++) args.push(arguments[i]); // Find event listeners, and support pseudo-event `catchAll` var event = args[0].toLowerCase(); for (var i = 0; i <= $.events.length; i += 2) { if ($.events[i] == event) $.events[i + 1].apply($, args.slice(1)); if ($.events[i] == 'catchall') $.events[i + 1].apply(null, args); } if (event == 'fileerror') $.fire('error', args[2], args[1]); if (event == 'fileprogress') $.fire('progress'); }; // INTERNAL HELPER METHODS (handy, but ultimately not part of uploading) var $h = { stopEvent: function (e) { e.stopPropagation(); e.preventDefault(); }, each: function (o, callback) { if (typeof o.length !== 'undefined') { for (var i = 0; i < o.length; i++) { // Array or FileList if (callback(o[i]) === false) return; } } else { for (i in o) { // Object if (callback(i, o[i]) === false) return; } } }, generateUniqueIdentifier: function (file, event) { var custom = $.getOpt('generateUniqueIdentifier'); if (typeof custom === 'function') { return custom(file, event); } var relativePath = file.webkitRelativePath || file.relativePath || file.fileName || file.name; // Some confusion in different versions of Firefox var size = file.size; return size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/gim, ''); }, contains: function (array, test) { var result = false; $h.each(array, function (value) { if (value == test) { result = true; return false; } return true; }); return result; }, formatSize: function (size) { if (size < 1024) { return size + ' bytes'; } else if (size < 1024 * 1024) { return (size / 1024.0).toFixed(0) + ' KB'; } else if (size < 1024 * 1024 * 1024) { return (size / 1024.0 / 1024.0).toFixed(1) + ' MB'; } else { return (size / 1024.0 / 1024.0 / 1024.0).toFixed(1) + ' GB'; } }, getTarget: function (request, params) { var target = $.getOpt('target'); if (request === 'test' && $.getOpt('testTarget')) { target = $.getOpt('testTarget') === '/' ? $.getOpt('target') : $.getOpt('testTarget'); } if (typeof target === 'function') { return target(params); } var separator = target.indexOf('?') < 0 ? '?' : '&'; var joinedParams = params.join('&'); if (joinedParams) target = target + separator + joinedParams; return target; }, }; var onDrop = function (e) { e.currentTarget.classList.remove($.getOpt('dragOverClass')); $h.stopEvent(e); //handle dropped things as items if we can (this lets us deal with folders nicer in some cases) if (e.dataTransfer && e.dataTransfer.items) { loadFiles(e.dataTransfer.items, e); } //else handle them as files else if (e.dataTransfer && e.dataTransfer.files) { loadFiles(e.dataTransfer.files, e); } }; var onDragLeave = function (e) { e.currentTarget.classList.remove($.getOpt('dragOverClass')); }; var onDragOverEnter = function (e) { e.preventDefault(); var dt = e.dataTransfer; if ($.indexOf(dt.types, 'Files') >= 0) { // only for file drop e.stopPropagation(); dt.dropEffect = 'copy'; dt.effectAllowed = 'copy'; e.currentTarget.classList.add($.getOpt('dragOverClass')); } else { // not work on IE/Edge.... dt.dropEffect = 'none'; dt.effectAllowed = 'none'; } }; /** * processes a single upload item (file or directory) * @param {Object} item item to upload, may be file or directory entry * @param {string} path current file path * @param {File[]} items list of files to append new items to * @param {Function} cb callback invoked when item is processed */ function processItem(item, path, items, cb) { var entry; if (item.isFile) { // file provided return item.file(function (file) { file.relativePath = path + file.name; items.push(file); cb(); }); } else if (item.isDirectory) { // item is already a directory entry, just assign entry = item; } else if (item instanceof File) { items.push(item); } if ('function' === typeof item.webkitGetAsEntry) { // get entry from file object entry = item.webkitGetAsEntry(); } if (entry && entry.isDirectory) { // directory provided, process it return processDirectory(entry, path + entry.name + '/', items, cb); } if ('function' === typeof item.getAsFile) { // item represents a File object, convert it item = item.getAsFile(); if (item instanceof File) { item.relativePath = path + item.name; items.push(item); } } cb(); // indicate processing is done } /** * cps-style list iteration. * invokes all functions in list and waits for their callback to be * triggered. * @param {Function[]} items list of functions expecting callback parameter * @param {Function} cb callback to trigger after the last callback has been invoked */ function processCallbacks(items, cb) { if (!items || items.length === 0) { // empty or no list, invoke callback return cb(); } // invoke current function, pass the next part as continuation items[0](function () { processCallbacks(items.slice(1), cb); }); } /** * recursively traverse directory and collect files to upload * @param {Object} directory directory to process * @param {string} path current path * @param {File[]} items target list of items * @param {Function} cb callback invoked after traversing directory */ function processDirectory(directory, path, items, cb) { var dirReader = directory.createReader(); var allEntries = []; function readEntries() { dirReader.readEntries(function (entries) { if (entries.length) { allEntries = allEntries.concat(entries); return readEntries(); } // process all conversion callbacks, finally invoke own one processCallbacks( allEntries.map(function (entry) { // bind all properties except for callback return processItem.bind(null, entry, path, items); }), cb, ); }); } readEntries(); } /** * process items to extract files to be uploaded * @param {File[]} items items to process * @param {Event} event event that led to upload */ function loadFiles(items, event) { if (!items.length) { return; // nothing to do } $.fire('beforeAdd'); var files = []; processCallbacks( Array.prototype.map.call(items, function (item) { // bind all properties except for callback var entry = item; if ('function' === typeof item.webkitGetAsEntry) { entry = item.webkitGetAsEntry(); } return processItem.bind(null, entry, '', files); }), function () { if (files.length) { // at least one file found appendFilesFromFileList(files, event); } }, ); } var appendFilesFromFileList = function (fileList, event) { // check for uploading too many files var errorCount = 0; var o = $.getOpt([ 'maxFiles', 'minFileSize', 'maxFileSize', 'maxFilesErrorCallback', 'minFileSizeErrorCallback', 'maxFileSizeErrorCallback', 'fileType', 'fileTypeErrorCallback', ]); if (typeof o.maxFiles !== 'undefined' && o.maxFiles < fileList.length + $.files.length) { // if single-file upload, file is already added, and trying to add 1 new file, simply replace the already-added file if (o.maxFiles === 1 && $.files.length === 1 && fileList.length === 1) { $.removeFile($.files[0]); } else { o.maxFilesErrorCallback(fileList, errorCount++); return false; } } var files = [], filesSkipped = [], remaining = fileList.length; var decreaseReamining = function () { if (!--remaining) { // all files processed, trigger event if (!files.length && !filesSkipped.length) { // no succeeded files, just skip return; } window.setTimeout(function () { $.fire('filesAdded', files, filesSkipped); }, 0); } }; $h.each(fileList, function (file) { var fileName = file.name; var fileType = file.type; // e.g video/mp4 if (o.fileType.length > 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).toLowerCase() === 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 true; } } if (typeof o.minFileSize !== 'undefined' && file.size < o.minFileSize) { o.minFileSizeErrorCallback(file, errorCount++); return true; } if (typeof o.maxFileSize !== 'undefined' && file.size > o.maxFileSize) { o.maxFileSizeErrorCallback(file, errorCount++); return true; } 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 = ''; $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished 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', $, message); // 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; offset < maxOffset; offset++) { (function (offset) { $.chunks.push(new ResumableChunk($.resumableObj, $, offset, chunkEvent)); $.resumableObj.fire('chunkingProgress', $, offset / maxOffset); })(offset); } window.setTimeout(function () { $.resumableObj.fire('chunkingComplete', $); }, 0); }; $.progress = function () { if (_error) return 1; // Sum up progress across everything var ret = 0; var error = false; $h.each($.chunks, function (c) { if (c.status() == 'error') error = true; ret += c.progress(true); // get chunk progress relative to entire file }); ret = error ? 1 : ret > 0.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; if ($.preprocessState === 1) { return 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; }; $.preprocessFinished = function () { $.preprocessState = 2; $.upload(); }; $.upload = function () { var found = false; if ($.isPaused() === false) { var preprocess = $.getOpt('preprocessFile'); if (typeof preprocess === 'function') { switch ($.preprocessState) { case 0: $.preprocessState = 1; preprocess($); return true; case 1: return true; case 2: break; } } $h.each($.chunks, function (chunk) { if (chunk.status() == 'pending' && chunk.preprocessState !== 1) { chunk.send(); found = true; return false; } }); } return found; }; $.markChunksCompleted = function (chunkNumber) { if (!$.chunks || $.chunks.length <= chunkNumber) { return; } for (var num = 0; num < chunkNumber; num++) { $.chunks[num].markComplete = true; } }; // 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 $.markComplete = false; // 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 ($.markComplete) { return 'success'; } 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 400, 404, 409, 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) && !$.markComplete) factor *= 0.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) { found = file.upload(); 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', onDragOverEnter, false); domNode.addEventListener('dragenter', onDragOverEnter, false); domNode.addEventListener('dragleave', onDragLeave, false); domNode.addEventListener('drop', onDrop, false); }); }; $.unAssignDrop = function (domNodes) { if (typeof domNodes.length == 'undefined') domNodes = [domNodes]; $h.each(domNodes, function (domNode) { domNode.removeEventListener('dragover', onDragOverEnter); domNode.removeEventListener('dragenter', onDragOverEnter); domNode.removeEventListener('dragleave', onDragLeave); 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') { // left here for backwards compatibility module.exports = Resumable; module.exports.Resumable = 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; } })();