JQ+asp.net实现文件上传的断点续传功能


一、功能原理

断点续传,顾名思义就是将文件分割成一段段的过程,然后一段一段的传。

以前文件无法分割,但随着HTML5新特性的引入,类似普通字符串、数组的分割,我们可以可以使用slice方法来分割文件。

所以断点续传的最基本实现也就是:前端通过FileList对象获取到相应的文件,按照指定的分割方式将大文件分段,然后一段一段地传给后端,后端再按顺序一段段将文件进行拼接。

同时,将传送的进度记录记录到浏览器的缓存中。每次传送数据都更新浏览器的缓存

二、实现过程

  1、前端代码

           "tbResult"class="table table-normal">
                "0">
                    
序号 地图名称 地图版本 地图类型 类型名称 创建时间 "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 += "" + file.name + "" + file.type + "" + size + "" + percent + "";
                }
            }
            $("#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 += "" + file.name + "" + file.type + "" + size + "" + percent + "";
                }
            }
            $("#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(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();
            }

四、最后的结果如图所