原理:使用javascript的slice函数,将一个大文件分成小块。再把小块上传到服务器,由后端将这些小块按顺序组合成完整的文件,再将小块删除。
比如:
a. 一个文件为5.1M,那么按1M一块可以分成6块
chunk1 = file.slice(0M, 1M)
chunk2 = file.slice(1M, 2M)
...
...
chunk6 = file.slice(5M, 0.1M)
b. 再用FormData(不用FormData也可以,用FormData比较干净)构造一个from,把这些文件块装到form里。然后写个函数递归调用ajax把form POST到后端。POST成功chunkIndex+1。
c. 有了chunkIndex暂停、恢复就简单了,定义一个全局变量pause,点暂停时设为true,点恢复设为false。
注意:分块文件不宜设置太小,否则会比较慢。
slice函数IE的话要IE10+才支持。
Firefox 12及更早版本的使用mozSlice() 和Safari使用webkitSlice()。
1.HTML部分
<div>
<input type="file" id="file1" value="" />
<input type="button" id="btnUplaod" value="Upload" multiple="multiple" />
</div>
<div id="completedChunks"></div>
<div id="percent">0%</div>
<div id="progress" style="width:200px;height:10px;background:linear-gradient(45deg, #ff0084 0%, #e8c5d7 0%);"></div>
2.javascript 部分,要引入jQuery:
;(function ($) {
var pause = false;//是否暂停
var $file;
var $fileInput;//file input
var $completedChunks = $('#completedChunks');//上传完成块数
var $progress = $('#progress');//上传进度条
var $percent = $('#percent');//上传百分比
var MiB = 1024 * 1024;
var chunkSize = 8.56 * MiB;//xx MiB
var chunkIndex = 0;//上传到的块
var $btnUpload = $('#btnUplaod');
var totalSize;//文件总大小
var totalSizeH;//文件总大小M
var chunkCount;//分块数
var fileName;//文件名
$btnUpload.click(function () {
var val = $.trim($(this).val());
if (val === 'Upload') {
$fileInput = $('#file1');
$file = $fileInput[0].files[0];
if ($file === undefined) {
$completedChunks.html('please select a file !');
return false;
}
totalSize = $file.size;
chunkCount = Math.ceil(totalSize / chunkSize * 1.0);
totalSizeH = (totalSize / MiB).toFixed(2);
fileName = $file.name;
val = 'Pause';
pause = false;
chunkIndex = 0;
}
else if (val === 'Pause') {
val = 'Resume';
pause = true;
}
else if (val === 'Resume') {
val = 'Pause';
pause = false;
}
else {
val = '-';
}
$(this).val(val);
postChunk();
});
function postChunk() {
if (pause)
return false;
var isLastChunk = chunkIndex === chunkCount - 1;
var fromSize = chunkIndex * chunkSize;
var chunk = !isLastChunk ? $file.slice(fromSize, fromSize + chunkSize) : $file.slice(fromSize, totalSize);
var fd = new FormData();
fd.append('chunk', chunk);
fd.append('chunkIndex', chunkIndex);
fd.append('chunkCount', chunkCount);
fd.append('fileName', fileName);
$.ajax({
url: './SaveChunkFile',
type: 'POST',
data: fd,
cache: false,
contentType: false,
processData: false,
success: function (d) {
if (!d.success) {
$completedChunks.html(d.msg);
return false;
}
chunkIndex = d.nextIndex;
if (isLastChunk) {
$completedChunks.html('combining .. ');
$btnUpload.val('Upload').prop('disabled', true);
//合并文件
$.post('./CombineChunkFile', { fileName: fileName }, function (d) {
$completedChunks.html(d.msg);
$completedChunks.append('
destFile:' + d.destFile);
$btnUpload.val('Upload').prop('disabled', false);
$fileInput.val('');//清除文件
});
}
else {
postChunk();//递归上传文件块
//$completedChunks.html(chunkIndex + '/' + chunkCount );
$completedChunks.html((chunkIndex * chunkSize / MiB).toFixed(2) + 'M/' + totalSizeH + 'M');
}
var completed = chunkIndex / chunkCount * 100;
$percent.html(completed.toFixed(2) + '%').css('margin-left', parseInt(completed / 100 * $progress.width()) + 'px');
$progress.css('background', 'linear-gradient(to right, #ff0084 ' + completed + '%, #e8c5d7 ' + completed + '%)');
},
error: function (ex) {
$completedChunks.html('ex:' + ex.responseText);
}
});
}
})(jQuery);
3.后台代码部分
//用于保存的文件夹
static readonly string uploadFolder = "UploadFolder";
//目录分隔符,兼容不同系统
static readonly char dirSeparator = Path.DirectorySeparatorChar;
string GetTmpChunkDir(string fileName) => HttpContext.Session.TryGetValue(fileName, out byte[] bytes) ? Encoding.Default.GetString(bytes) : "";
//保存文件
public async Task SaveChunkFile(IFormFile chunk, string fileName, int chunkIndex, int chunkCount)
{
try
{
if (chunk.Length == 0)
{
return Json(new
{
success = false,
msg = "File Length 0",
});
}
if (chunkIndex == 0)
{
//第一次上传时,生成一个随机id,做为保存块的临时文件夹,记录到session
HttpContext.Session.Set(fileName, Encoding.Default.GetBytes(Guid.NewGuid().ToString("N")));
}
if (!Directory.Exists(uploadFolder))
Directory.CreateDirectory(uploadFolder);
var fullChunkDir = uploadFolder + dirSeparator + GetTmpChunkDir(fileName);
if (!Directory.Exists(fullChunkDir))
Directory.CreateDirectory(fullChunkDir);
var blob = chunk.FileName;
var newFileName = blob + chunkIndex + Path.GetExtension(fileName);
var filePath = fullChunkDir + Path.DirectorySeparatorChar + newFileName;
//保存文件块
using (var stream = new FileStream(filePath, FileMode.Create))
{
await chunk.CopyToAsync(stream);
}
//所有块上传完成
if (chunkIndex == chunkCount - 1)
{
//也可以在这合并,在这合并就不用ajax调用CombineChunkFile合并
//CombineChunkFile(fileName);
}
var obj = new
{
success = true,
date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
newFileName,
originalFileName = fileName,
size = chunk.Length,
nextIndex = chunkIndex+1,
};
return Json(obj);
}
catch (Exception ex)
{
return Json(new
{
success = false,
msg = ex.Message,
});
}
}
//合并文件
public async Task CombineChunkFile(string fileName)
{
try
{
return await Task.Run(() =>
{
var tmpDir = GetTmpChunkDir(fileName);
var fullChunkDir = uploadFolder + dirSeparator + tmpDir;
var beginTime = DateTime.Now;
var newFileName = tmpDir + Path.GetExtension(fileName);
var destFile = uploadFolder + dirSeparator + newFileName;
//获取临时文件夹内的所有文件块,排好序
var files = Directory.GetFiles(fullChunkDir).OrderBy(x => x.Length).ThenBy(x => x).ToList();
using (var destStream = System.IO.File.OpenWrite(destFile))
{
files.ForEach(chunk =>
{
using (var chunkStream = System.IO.File.OpenRead(chunk))
{
chunkStream.CopyTo(destStream);
}
System.IO.File.Delete(chunk);
});
Directory.Delete(fullChunkDir);
}
var totalTime = DateTime.Now.Subtract(beginTime).TotalSeconds;
return Json(new
{
success = true,
destFile= destFile.Replace('\\','/'),
msg = $"combine completed ! {totalTime} s",
});
});
}
catch (Exception ex)
{
return Json(new
{
success = false,
msg = ex.Message,
});
}
finally
{
HttpContext.Session.Remove(fileName);
}
}
效果:
后面还有合并完成的几秒,录了3次gif没录下来,算了。
配合网络检测,还可以网络异常时自动重试