upload.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>PHP+Resumablejs实现分块上传 断点续传</title>
</head>
<div>
<a href="#" rel="external nofollow" id="browseButton">选择文件</a>
<div>
<div>
<input id="btnCancel" type="button" onClick='r.pause()' value="取消所有上传"
style="margin-left: 2px; height: 22px; font-size: 8pt;"/>
<br/>
</div>
<script src="resumable.js"></script>
<script>
let r = new Resumable({
target: 'upload.php',
chunkSize: 2 * 1024 * 1024,
simultaneousUploads: 4,
testChunks: true,
throttleProgressCallbacks: 1,
});
r.assignBrowse(document.getElementById('browseButton'));
r.on('fileSuccess', function (file) {
// console.debug(file);
});
r.on('fileProgress', function (file) {
// console.debug(file);
});
r.on('fileAdded', function (file, event) {
r.upload();
console.debug(file, event);
});
r.on('fileRetry', function (file) {
//console.debug(file);
});
r.on('fileError', function (file, message) {
//console.debug(file, message);
});
r.on('uploadStart', function () {
//console.debug();
});
r.on('complete', function () {
//console.debug();
});
r.on('progress', function () {
// console.debug();
});
r.on('error', function (message, file) {
//console.debug(message, file);
});
r.on('pause', function (file, message) {
//console.debug();
});
r.on('cancel', function () {
//console.debug();
});
</script>
</div>
</div>
</html>
upload.php
<?php
/**
*
* 这是Resumable.js客户端脚本的服务器端部分的实现,它将文件分几块上传到服务器。
*
* 该脚本以标准的方式接收文件,就像使用标准的HTML表格(多部分)上传文件一样
*
* 这个PHP脚本将一个文件的所有块存储在一个临时目录(`temp`)中,扩展名为`_part<#ChunkN>`。
* 一旦所有的部分被上传,一个最终的目标文件正在从所有存储的部分中创建(逐个追加)。
*
*/
################################################################################
/**
*
* 数组中的参数
*
* resumableChunkNumber 当前分片序号 => 59
* resumableChunkSize 规定分片大小 => 2097152 字节
* resumableCurrentChunkSize 当前分片大小 => 2097152 字节
* resumableTotalSize 文件的总大小 => 192848785 字节
* resumableType 上传文件类型 => video/mp4
* resumableIdentifier 分片的标识号 => 192848785-13mp4
* resumableFilename 上传的文件名 => 千机伞13种全形态.mp4
* resumableRelativePath 文件相对路径 => 千机伞13种全形态.mp4
* resumableTotalChunks 文件总分片数 => 91
*
*/
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$temp_dir = 'temp/' . $_GET['resumableIdentifier'];
$chunk_file = $temp_dir . '/' . $_GET['resumableFilename'] . '.part' . $_GET['resumableChunkNumber'];
// file_exists()检查文件或目录是否存在
if (file_exists($chunk_file)) {
header("HTTP/1.0 200 Ok");
} else {
header("HTTP/1.0 204 Not Found");
}
}
// 循环浏览文件,并将大块文件移到一个临时创建的目录中。
if (!empty($_FILES)) {
foreach ($_FILES as $file) {
// 检查错误状态
if ($file['error'] != 0) {
_log('在文件 ' . $_POST['resumableFilename'] . ' 中出现错误: ' . $file['error']);
continue;
}
// 启动目标文件(格式为<filename.ext>.part<#chunk>)。
// 该文件被存储在一个临时目录中
$temp_dir = 'temp/' . $_POST['resumableIdentifier'];
$dest_file = $temp_dir . '/' . $_POST['resumableFilename'] . '.part' . $_POST['resumableChunkNumber'];
// 创建临时目录
if (!is_dir($temp_dir)) {
mkdir($temp_dir, 0777, true);
}
// 移动临时文件
if (!move_uploaded_file($file['tmp_name'], $dest_file)) {
_log('文件 ' . $_POST['resumableFilename'] . ' 在保存(移动上传文件)第' . $_POST['resumableChunkNumber'] . '分片时出错');
} else {
// 检查所有部分是否存在,并创建最终目标文1
json_decode(createFileFromChunks($temp_dir, $_POST['resumableFilename'],
$_POST['resumableChunkSize'], $_POST['resumableTotalSize']), true);
if (file_exists('temp/' . $_POST['resumableFilename']) && $_POST['resumableChunkNumber'] == $_POST['resumableTotalChunks']) {
$data['code'] = 1;
$data['msg'] = '上传完成';
exit(json_encode($data));
} else {
if (empty($_POST['resumableChunkNumber'])) {
$data['code'] = -1;
$data['msg'] = '上传失败';
exit(json_encode($data));
} else {
$data['code'] = 2;
$data['msg'] = '分片' . $_POST['resumableChunkNumber'] . '上传完成';
exit(json_encode($data));
}
}
}
}
}
/**
*
* 记录操作到文件(upload_log.txt)和 stdout
* @param string $str - 记录的字符串1
*/
function _log($str)
{
// 输出到日志
$log_str = date('Y-m-d') . ": {$str}\r\n";
// echo $log_str;
// 记录到文件
if (($fp = fopen('temp/upload_log.txt', 'a+')) !== false) {
fputs($fp, $log_str);
fclose($fp);
}
}
/**
* 检查所有分片是否存在,并合并所有分片
*
* @param string $dir - 保存文件所有部分的临时目录
* @param string $fileName - 原始文件名
* @param string $chunkSize - 每个分片的大小(以字节为单位)
* @param string $totalSize - 原始文件的大小(以字节为单位)
*/
function createFileFromChunks($temp_dir, $fileName, $chunkSize, $totalSize)
{
// 计算这个文件的所有部分
$total_files = 0;
foreach (scandir($temp_dir) as $file) {
if (stripos($file, $fileName) !== false) {
$total_files++;
}
}
// 检查所有的部分是否都存在
// 最后一个部分的大小在 chunkSize 和 2*$chunkSize 之间
if ($total_files * $chunkSize >= ($totalSize - $chunkSize + 1)) {
// 创建最终目标文件
if (($fp = fopen('temp/' . $fileName, 'w')) !== false) {
for ($i = 1; $i <= $total_files; $i++) {
fwrite($fp, file_get_contents($temp_dir . '/' . $fileName . '.part' . $i));
_log('写入(合并)分片 ' . $i);
}
fclose($fp);
} else {
_log('无法创建目标文件');
return false;
}
// 重命名临时目录(以避免其他同时上传的块的访问),然后删除它
if (rename($temp_dir, $temp_dir . '_UNUSED')) {
rrmdir($temp_dir . '_UNUSED');
} else {
rrmdir($temp_dir);
}
}
}
/**
*
* 循环删除一个目录
*
* @param string $dir - 目录路径
* @link http://php.net/manual/en/function.rmdir.php
*/
function rrmdir($dir)
{
if (is_dir($dir)) {
$objects = scandir($dir);
foreach ($objects as $object) {
if ($object != "." && $object != "..") {
if (filetype($dir . "/" . $object) == "dir") {
rrmdir($dir . "/" . $object);
} else {
unlink($dir . "/" . $object);
}
}
}
reset($objects);
rmdir($dir);
}
}
Resumable.js
/*
* 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',
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 < 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.fileName || file.name; // Some confusion in different versions of Firefox
var size = file.size;
return (size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, ''));
},
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('&');
return target + separator + joinedParams;
}
};
var onDrop = function (event) {
$h.stopEvent(event);
//handle dropped things as items if we can (this lets us deal with folders nicer in some cases)
if (event.dataTransfer && event.dataTransfer.items) {
loadFiles(event.dataTransfer.items, event);
}
//else handle them as files
else if (event.dataTransfer && event.dataTransfer.files) {
loadFiles(event.dataTransfer.files, event);
}
};
var preventDefault = function (e) {
e.preventDefault();
};
/**
* 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();
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();
dirReader.readEntries(function (entries) {
if (!entries.length) {
// empty directory, skip
return cb();
}
// process all conversion callbacks, finally invoke own one
processCallbacks(
entries.map(function (entry) {
// bind all properties except for callback
return processItem.bind(null, entry, path, items);
}),
cb
);
});
}
/**
* 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
return processItem.bind(null, item, "", 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;
if (o.fileType.length > 0) {
var fileTypeFound = false;
for (var index in o.fileType) {
var extension = '.' + o.fileType[index];
if (fileName.toLowerCase().indexOf(extension.toLowerCase(), fileName.length - extension.length) !== -1) {
fileTypeFound = true;
break;
}
}
if (!fileTypeFound) {
o.fileTypeErrorCallback(file, errorCount++);
return false;
}
}
if (typeof (o.minFileSize) !== 'undefined' && file.size < o.minFileSize) {
o.minFileSizeErrorCallback(file, errorCount++);
return false;
}
if (typeof (o.maxFileSize) !== 'undefined' && file.size > o.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', $);
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; offset < maxOffset; offset++) {
(function (offset) {
window.setTimeout(function () {
$.chunks.push(new ResumableChunk($.resumableObj, $, offset, chunkEvent));
$.resumableObj.fire('chunkingProgress', $, offset / maxOffset);
}, 0);
})(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;
$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);
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');
}
// 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);
};
$.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;
}
})();