一、功能原理
断点续传,顾名思义就是将文件分割成一段段的过程,然后一段一段的传。
以前文件无法分割,但随着HTML5新特性的引入,类似普通字符串、数组的分割,我们可以可以使用slice方法来分割文件。
所以断点续传的最基本实现也就是:前端通过FileList对象获取到相应的文件,按照指定的分割方式将大文件分段,然后一段一段地传给后端,后端再按顺序一段段将文件进行拼接。
同时,将传送的进度记录记录到浏览器的缓存中。每次传送数据都更新浏览器的缓存
二、实现过程
1、前端代码
<table id="tbResult" class="table table-normal"> <thead> <tr> <td>序号</td> <td>地图名称</td> <td>地图版本</td> <td>地图类型</td> <td>类型名称</td> <td>创建时间</td> <td style="250px;">操作</td> </tr> </thead> <tbody> <tr index="0"> </tr> </tbody> </table>
2、计算文件的大小
// 计算文件大小 var size = file.size > 1024 file.size / 1024 > 1024 ? file.size / (1024 * 1024) > 1024 ? (file.size / (1024 * 1024 * 1024)).toFixed(2) + 'GB' : (file.size / (1024 * 1024)).toFixed(2) + 'MB' : (file.size / 1024).toFixed(2) + 'KB' : (file.size).toFixed(2) + 'B';
3、选择文件后显示文件的信息,在模版中替换一下数据
var fileList = ""; var uploadVal = '开始上传'; var files = document.getElementById('myFile').files; fileCount = files.length; if (files) { for (var i = 0, j = files.length; i < j; ++i) { var file = this.files[i]; // 计算文件大小 var size = file.size > 1024 ? file.size / 1024 > 1024 ? file.size / (1024 * 1024) > 1024 ? (file.size / (1024 * 1024 * 1024)).toFixed(2) + 'GB' : (file.size / (1024 * 1024)).toFixed(2) + 'MB' : (file.size / 1024).toFixed(2) + 'KB' : (file.size).toFixed(2) + 'B'; // 初始通过本地记录,判断该文件是否曾经上传过 var percent = window.localStorage.getItem(file.name + '_p'); if (percent && percent !== '100.0') { uploadVal = "继续上传" } fileList += "<tr><td>" + file.name + "</td><td>" + file.type + "</td><td>" + size + "</td><td class='upload-progress'>" + percent + "</td><td><div class='upload-item-btn' data-name='" + file.name + "' data-size='" + file.size + "' data-state='default' style='display:none'>" + uploadVal + "</div></td></tr>"; } } $("#fileList").append(fileList);
4、不过,在显示文件信息的时候,可能这个文件之前之前已经上传过了,为了断点续传,需要判断并在界面上做出提示通过查询本地看是否有相应的数据(这里的做法是当本地记录的是已经上传100%时,就直接是重新上传而不是继续上传了)
// 初始通过本地记录,判断该文件是否曾经上传过 var percent = window.localStorage.getItem(file.name + '_p'); if (percent && percent !== '100.0') { uploadVal = "继续上传" }
5、显示文件信息列表
6、点击开始上传,可以上传相应的文件
var $this = $(this); var fileName = $this.attr('data-name'); var totalSize = $this.attr('data-size'); var eachSize = 1024 * 1024; var chunks = Math.ceil(totalSize / eachSize); var $progress = $this.closest('tr').find('.upload-progress')
7、接下来是分段过程
// 上传之前查询是否以及上传过分片 var chunk = window.localStorage.getItem(fileName + '_chunk') || 0; chunk = parseInt(chunk, 10); // 判断是否为末分片 var isLastChunk = (chunk == (chunks - 1) ? 1 : 0); // 如果第一次上传就为末分片,即文件已经上传完成,则重新覆盖上传 if (times === 'first' && isLastChunk === 1 && totalSize > eachSize) { window.localStorage.setItem(fileName + '_chunk', 0); chunk = 0; isLastChunk = 0; } // 设置分片的开始结尾 var blobFrom = chunk * eachSize, // 分段开始 blobTo = (chunk + 1) * eachSize > totalSize ? totalSize : (chunk + 1) * eachSize, // 分段结尾 percent = (100 * blobTo / totalSize).toFixed(1), // 已上传的百分比 timeout = 5000, // 超时时间 fd = new FormData($('#myForm')[0]); fd.append('json', JSON.stringify(record)); // 文件名 fd.append('theFile', findTheFile(fileName).slice(blobFrom, blobTo)); // 分好段的文件(实际上传递的就是这个文件) fd.append('fileName', fileName); // 文件名 //fd.append('totalSize', totalSize); // 文件总大小 fd.append('isLastChunk', isLastChunk); // 是否为末段 //fd.append('isFirstUpload', times === 'first' ? 1 : 0); // 是否是第一段(第一次上传) fd.append('chunks', chunks); // 总片段 fd.append('chunk', chunk); // 当前片段
8、AJAX上传
$.ajax({ url: serviceBaseUrl + URL.ADD_MapManage, type: 'POST', data: fd, async: true, //cache: false, contentType: false, processData: false }).then(function (res) { // 已经上传完毕 window.localStorage.setItem(fileName + '_p', percent); if (chunk === (chunks - 1)) { $progress.text('上传完毕'); if (!$('#upload-list').find('.upload-item-btn:not(:disabled)').length) { $('#upload-all-btn').val('已经上传').prop('disabled', true).css('cursor', 'not-allowed'); } uploadCount++; if (uploadCount == fileCount) { swal({ title: "操作成功!", type: "success", text: "2秒后自动关闭。", timer: 2000, showConfirmButton: true }); closeEdit(); } } else { // 记录已经上传的分片 window.localStorage.setItem(fileName + '_chunk', ++chunk); $progress.text(percent + '%'); startUpload(); } })
9、完成的js逻辑如下
//附件选择 $('body').on('change', '#myFile', function (e) { var fileList = ""; var uploadVal = '开始上传'; var files = document.getElementById('myFile').files; fileCount = files.length; if (files) { for (var i = 0, j = files.length; i < j; ++i) { var file = this.files[i]; // 计算文件大小 var size = file.size > 1024 ? file.size / 1024 > 1024 ? file.size / (1024 * 1024) > 1024 ? (file.size / (1024 * 1024 * 1024)).toFixed(2) + 'GB' : (file.size / (1024 * 1024)).toFixed(2) + 'MB' : (file.size / 1024).toFixed(2) + 'KB' : (file.size).toFixed(2) + 'B'; // 初始通过本地记录,判断该文件是否曾经上传过 var percent = window.localStorage.getItem(file.name + '_p'); if (percent && percent !== '100.0') { uploadVal = "继续上传" } fileList += "<tr><td>" + file.name + "</td><td>" + file.type + "</td><td>" + size + "</td><td class='upload-progress'>" + percent + "</td><td><div class='upload-item-btn' data-name='" + file.name + "' data-size='" + file.size + "' data-state='default' style='display:none'>" + uploadVal + "</div></td></tr>"; } } $("#fileList").append(fileList); })
//附件全部上传
$('body').on('click', '#btnSaveAll', function (e) {
// 未选择文件
if (!$('#myFile').val()) {
//$('#myFile').focus();
var fd = new FormData($('#myForm')[0]);
fd.append('json', JSON.stringify(record)); // 文件名
$.ajax({
url: serviceBaseUrl + URL.ADD_MapManage,
type: 'POST',
data: fd,
//async: false,
//cache: false,
contentType: false,
processData: false
}).then(function (res) {
swal({
title: "操作成功!",
type: "success",
text: "2秒后自动关闭。",
timer: 2000,
showConfirmButton: true
});
closeEdit();
})
}
// 模拟点击其他可上传的文件
else {
$('#upload-list .upload-item-btn').each(function () {
$(this).click();
uploadCount = 0;
});
}
})
//文件单个上传功能
$('body').on('click', '.upload-item-btn', function () {
if (inputValidator !== undefined) {
inputValidator.checkValidity();
}
if ($(".validContainer input.invalid").length == 0) {
var $this = $(this);
var fileName = $this.attr('data-name');
var totalSize = $this.attr('data-size');//文件的总大小
var eachSize = 1024 * 1024;//每次上传1M的数据
var chunks = Math.ceil(totalSize / eachSize);//一共多少片段
var $progress = $this.closest('tr').find('.upload-progress')
//var fileCount=document.getElementById('myFile').files;
// 第一次点击上传
startUpload('first');
// 上传操作 times: 第几次
function startUpload(times) {
// 上传之前查询是否以及上传过分片
var chunk = window.localStorage.getItem(fileName + '_chunk') || 0;
chunk = parseInt(chunk, 10);
// 判断是否为末分片
var isLastChunk = (chunk == (chunks - 1) ? 1 : 0);
// 如果第一次上传就为末分片,即文件已经上传完成,则重新覆盖上传
if (times === 'first' && isLastChunk === 1 && totalSize > eachSize) {
window.localStorage.setItem(fileName + '_chunk', 0);
chunk = 0;
isLastChunk = 0;
}
// 设置分片的开始结尾
var blobFrom = chunk * eachSize, // 分段开始
blobTo = (chunk + 1) * eachSize > totalSize ? totalSize : (chunk + 1) * eachSize, // 分段结尾
percent = (100 * blobTo / totalSize).toFixed(1), // 已上传的百分比
timeout = 5000, // 超时时间
fd = new FormData($('#myForm')[0]);
fd.append('json', JSON.stringify(record)); // 文件名
fd.append('theFile', findTheFile(fileName).slice(blobFrom, blobTo)); // 分好段的文件(实际上传递的就是这个文件)
fd.append('fileName', fileName); // 文件名
//fd.append('totalSize', totalSize); // 文件总大小
fd.append('isLastChunk', isLastChunk); // 是否为末段
//fd.append('isFirstUpload', times === 'first' ? 1 : 0); // 是否是第一段(第一次上传)
fd.append('chunks', chunks); // 总片段
fd.append('chunk', chunk); // 当前片段
//$progress.text(percent + '%');
$.ajax({
url: serviceBaseUrl + URL.ADD_MapManage,
type: 'POST',
data: fd,
async: true,
//cache: false,
contentType: false,
processData: false
}).then(function (res) {
// 已经上传完毕
window.localStorage.setItem(fileName + '_p', percent);
if (chunk === (chunks - 1)) {
$progress.text('上传完毕');
if (!$('#upload-list').find('.upload-item-btn:not(:disabled)').length) {
$('#upload-all-btn').val('已经上传').prop('disabled', true).css('cursor', 'not-allowed');
}
uploadCount++;
if (uploadCount == fileCount) {
swal({
title: "操作成功!",
type: "success",
text: "2秒后自动关闭。",
timer: 2000,
showConfirmButton: true
});
closeEdit();
}
} else {
// 记录已经上传的分片
window.localStorage.setItem(fileName + '_chunk', ++chunk);
$progress.text(percent + '%');
startUpload();
}
})
}
}
})
三、后端实现
try { HttpPostedFile file = fileCollection[0]; //二进制数组 byte[] fileBytes = null; fileBytes = new byte[file.ContentLength]; //创建Stream对象,并指向上传文件 Stream fileStream = file.InputStream; //从当前流中读取字节,读入字节数组中 fileStream.Read(fileBytes, 0, file.ContentLength); //全路径(路劲+文件名) string timePath = DateTime.Now.ToString("yyyy") + DateTime.Now.ToString("MM"); string fullPath = path + "Files\Drones\" + timePath + "\" + fileName + "_" + chunk; //保存到磁盘 var fullAllPath = Path.GetDirectoryName(fullPath); //如果没有此文件夹,则新建 if (!Directory.Exists(fullAllPath)) { Directory.CreateDirectory(fullAllPath); } //创建文件,返回一个 FileStream,它提供对 path 中指定的文件的读/写访问。 using (FileStream stream = File.Create(fullPath)) { //将字节数组写入流 stream.Write(fileBytes, 0, fileBytes.Length); stream.Close(); } //最后的片段需要将文件进行合并 if (isLastChunk == "1") { List<string> list = new List<string>(); for (int i = 0; i < Convert.ToInt32(chunks); i++) { //获取所有片段文件的位置 list.Add(path + "Files\Drones\" + timePath + "\" + fileName + "_" + i); } int coutSize = 0; //文件合并 using (FileStream fileNew = new FileStream(path + "Files\Drones\" + timePath + "\" + fileName, FileMode.Create, FileAccess.Write)) { int count = -1; for (int i = 0; i < list.Count; i++) { using (FileStream readStream = new FileStream(list[i], FileMode.Open, FileAccess.Read)) { byte[] buffer = new byte[readStream.Length]; coutSize += buffer.Length; while ((count = readStream.Read(buffer, 0, buffer.Length)) > 0) { fileNew.Write(buffer, 0, count); } } } } //每次航线新增完毕都需要做查询是否有相同ID的数据,如果有就不做添加 var entity = JsonConvert.DeserializeObject<T_DRO_MAPMANAGEEntity>(josn); int retCount = mDAL.GetCount($" ID='{entity.ID}'").Result; if (retCount == 0) { //entity.ID = Guid.NewGuid().ToString(); entity.CHUANGJIAN_SJ = DateTime.Now; entity.STA = "A"; var result = mDAL.AddData(entity, addId: true); } else { entity.STA = "U"; var result = mDAL.UpdateData(entity); } //添加附件实体 T_DRO_MAPFILESEntity mapfile = new T_DRO_MAPFILESEntity(); mapfile.ID = Guid.NewGuid().ToString(); mapfile.FILEPATH = "Files\Drones\" + timePath + "\" + fileName; mapfile.FILESIZE = coutSize.ToString(); mapfile.CREATETIME = DateTime.Now; mapfile.PK_MAP_ID = entity.ID; mapfile.SUFFIX = System.IO.Path.GetExtension(fileName); mapfile.FILENAME = fileName; //mapfile.State = "已完成"; var ret = fDAL.AddData(mapfile, addId: true); //删除片段文件 for (int i = 0; i < list.Count; i++) { //获取所有片段文件的位置 if (File.Exists(list[i])) { File.Delete(list[i]); } } //fDAL return new SerializeJson<int>(Enum.ResultType.succeed, entity.ID, 1).ToString(); } return ""; } catch (Exception ex) { return new SerializeJson<int>(Enum.ResultType.failed, ex.Message, -1).ToString(); }
四、最后的结果如图所