.NET Core 文件分块上传可暂停,进度条


原理:使用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没录下来,算了。

 

配合网络检测,还可以网络异常时自动重试

类别:DotNET   阅读(0)   评论(0)    发表时间:2019-08-20 21:20  星期二

评论区

发表评论

        姓名:
邮箱|网站:
        内容:

  (可按Ctrl+Enter提交)