JQ+asp.net实现文件上传的断点续传功能
一、功能原理
断点续传,顾名思义就是将文件分割成一段段的过程,然后一段一段的传。
以前文件无法分割,但随着HTML5新特性的引入,类似普通字符串、数组的分割,我们可以可以使用slice方法来分割文件。
所以断点续传的最基本实现也就是:前端通过FileList对象获取到相应的文件,按照指定的分割方式将大文件分段,然后一段一段地传给后端,后端再按顺序一段段将文件进行拼接。
同时,将传送的进度记录记录到浏览器的缓存中。每次传送数据都更新浏览器的缓存
二、实现过程
1、前端代码
序号 | 地图名称 | 地图版本 | 地图类型 | 类型名称 | 创建时间 | "width:250px;">操作 |
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 += ""; } } $("#fileList").append(fileList); " + file.name + " " + file.type + " " + size + " " + percent + "
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 += "//附件全部上传 $('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; }); } })"; } } $("#fileList").append(fileList); }) " + file.name + " " + file.type + " " + size + " " + percent + "
//文件单个上传功能
$('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(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(); }
四、最后的结果如图所