You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1281 lines
42 KiB
1281 lines
42 KiB
/**
|
|
* /*
|
|
* 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;
|
|
}
|
|
})();
|
|
|