【原创】MVC +WebUploader 实现分片上传大文件
大文件的上传是我一直以来想学习的一个技术点,今天在项目闲暇之时,终于有机会自己尝试了一把,本文仅仅是个Demo,各种错误处理都么有,仅限于大家来学习思路。
参考博文:http://www.cnblogs.com/Leo_wl/p/4990116.html
http://www.linuxidc.com/Linux/2014-09/106816.htm
一、开始
- 作为一个Demo,肯定是得先新建项目啦~笔者在这里使用的是VS 2012,所以只能新建MVC 4的项目
- 项目新建好之后,从官网下载WebUploader的包 http://fex.baidu.com/webuploader/download.html
- 在Index.cshtml中引入Jquery、webuploader.css、webuploader.js
- 照着官网的Getting Started 里面的例子,初始化WebUploader,这里不再详细描述
- 初始化的时候,有几个参数需要特别处理,看我的初始化参数
1 var GUID = WebUploader.Base.guid();//一个GUID 2 var uploader = WebUploader.create({ 3 swf: '/Scripts/Plugins/webuploader-0.1.5/Uploader.swf', 4 server: '@Url.Action("Upload")', 5 pick: '#picker', 6 resize: false, 7 chunked: true,//开始分片上传 8 chunkSize: 2048000,//每一片的大小 9 formData: { 10 guid: GUID //自定义参数,待会儿解释 11 } 12 });
二、前端准备上传分片
给开始上传按钮绑定上一个Click事件,来调用WebUploader的upload事件,如果你开启了自动上传,可以省略这一步。这样子,点了按钮就会开始上传工作,WebUploader就会自动把文件分片分好,然后上传到服务器端。
1 $("#ctlBtn").click(function () { 2 uploader.upload(); 3 });
三、后端接收上传文件
开始之前,先说一下基本的思路:
在特定的上传目录下面,先根据前端传过来的GUID创建一个临时的目录,然后接受分块,把所有的分块分别保存起来,上传完成之后合并这些分块。
好了,开始上代码:
1 [HttpPost] 2 public ActionResult Upload() 3 { 4 string fileName = Request["name"]; 5 int index = Convert.ToInt32(Request["chunk"]);//当前分块序号 6 var guid = Request["guid"];//前端传来的GUID号 7 var dir = Server.MapPath("~/Upload");//文件上传目录 8 dir = Path.Combine(dir, guid);//临时保存分块的目录 9 if (!System.IO.Directory.Exists(dir)) 10 System.IO.Directory.CreateDirectory(dir); 11 string filePath = Path.Combine(dir, index.ToString());//分块文件名为索引名,更严谨一些可以加上是否存在的判断,防止多线程时并发冲突 12 var data = Request.Files["file"];//表单中取得分块文件 13 data.SaveAs(filePath);//保存 14 return Json(new { erron = 0 });//Demo,随便返回了个值,请勿参考 15 }
需要注明的是,分块的序号、文件名等,均可以在WebUploader上传过来的Request里面取到,除了我取到的这些值,还有最后修改日期,总共多少分块、文件总大小等,卡个断点一看便知。
四、上传完毕,合并文件,删除分片
由于WebUploader是多线程的上传,所以不一定文件块会按照顺序来到服务器,所以我原本打算在Upload中进行合并文件的想法泡汤了~当然,或许可以去判断当前文件夹中的文件数目等于分块数目这种方式来处理,但是总感觉不靠谱,万一当时只是把那个文件创建出来了,内容还没写进去怎么办?大家有更好的思路,欢迎探讨~
我目前的做法,是通过WebUploader的uploadSuccess来手动出发合并文件,Js代码如下:
1 uploader.on('uploadSuccess', function (file,response) { 2 $.post('@Url.Action("Merge")', { guid: GUID, fileName: file.name }, function (data) { 3 $list.text('已上传'); 4 }); 5 6 });
当前端判断说所有分片上传成功的时候,去调用后端接口,告诉他GUID和文件名称(有文件的格式就好了,不然没法正确存储),后端合并代码如下:
1 public ActionResult Merge() 2 { 3 var guid = Request["guid"];//GUID 4 var uploadDir = Server.MapPath("~/Upload");//Upload 文件夹 5 var dir = Path.Combine(uploadDir, guid);//临时文件夹 6 var fileName = Request["fileName"];//文件名 7 var files = System.IO.Directory.GetFiles(dir);//获得下面的所有文件 8 var finalPath = Path.Combine(uploadDir, fileName);//最终的文件名(demo中保存的是它上传时候的文件名,实际操作肯定不能这样) 9 var fs = new FileStream(finalPath, FileMode.Create); 10 foreach (var part in files.OrderBy(x => x.Length).ThenBy(x => x))//排一下序,保证从0-N Write 11 { 12 var bytes = System.IO.File.ReadAllBytes(part); 13 fs.Write(bytes, 0, bytes.Length); 14 bytes = null; 15 System.IO.File.Delete(part);//删除分块 16 } 17 fs.Close(); 18 System.IO.Directory.Delete(dir);//删除文件夹 19 return Json(new { error = 0 });//随便返回个值,实际中根据需要返回 20 }
五、整体代码送上
2016年6月12日更新:加入暂停功能,加入进度条。
前端:
1 @{ 2 ViewBag.Title = "Home Page"; 3 } 4 5 <h2>Index</h2> 6 <div id="uploader" class="wu-example"> 7 <!--用来存放文件信息--> 8 <div class="filename"></div> 9 <div class="state"></div> 10 <div class="progress"> 11 <div class="progress-bar progress-bar-info progress-bar-striped active" role="progressbar" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100" style="width: 0%"> 12 <span class="sr-only">40% Complete (success)</span> 13 </div> 14 </div> 15 <div class="btns"> 16 <div id="picker">选择文件</div> 17 <button id="ctlBtn" class="btn btn-default">开始上传</button> 18 <button id="pause" class="btn btn-danger">暂停上传</button> 19 </div> 20 </div> 21 22 <script type="text/javascript"> 23 $(function () { 24 var GUID = WebUploader.Base.guid();//一个GUID 25 var uploader = WebUploader.create({ 26 swf: '/Scripts/Plugins/webuploader-0.1.5/Uploader.swf', 27 server: '@Url.Action("Upload")', 28 pick: '#picker', 29 resize: false, 30 chunked: true,//开始分片上传 31 chunkSize: 2048000,//每一片的大小 32 formData: { 33 guid: GUID //自定义参数,待会儿解释 34 } 35 }); 36 uploader.on('fileQueued', function (file) { 37 $("#uploader .filename").html("文件名:" + file.name); 38 $("#uploader .state").html('等待上传'); 39 }); 40 uploader.on('uploadSuccess', function (file, response) { 41 $.post('@Url.Action("Merge")', { guid: GUID, fileName: file.name }, function (data) { 42 $list.text('已上传'); 43 }); 44 }); 45 uploader.on('uploadProgress', function (file, percentage) { 46 $("#uploader .progress-bar").width(percentage * 100 + '%'); 47 console.log(percentage); 48 }); 49 uploader.on('uploadSuccess', function () { 50 $("#uploader .progress-bar").removeClass('progress-bar-striped').removeClass('active').removeClass('progress-bar-info').addClass('progress-bar-success'); 51 $("#uploader .state").html("上传成功..."); 52 53 }); 54 uploader.on('uploadError', function () { 55 $("#uploader .progress-bar").removeClass('progress-bar-striped').removeClass('active').removeClass('progress-bar-info').addClass('progress-bar-danger'); 56 $("#uploader .state").html("上传失败..."); 57 }); 58 59 $("#ctlBtn").click(function () { 60 uploader.upload(); 61 $("#ctlBtn").text("上传"); 62 $('#ctlBtn').attr('disabled', 'disabled'); 63 $("#uploader .progress-bar").addClass('progress-bar-striped').addClass('active'); 64 $("#uploader .state").html("上传中..."); 65 }); 66 $('#pause').click(function () { 67 uploader.stop(true); 68 $('#ctlBtn').removeAttr('disabled'); 69 $("#ctlBtn").text("继续上传"); 70 $("#uploader .state").html("暂停中..."); 71 $("#uploader .progress-bar").removeClass('progress-bar-striped').removeClass('active'); 72 }); 73 }); 74 75 </script> 76 <link href="~/Scripts/Plugins/webuploader-0.1.5/webuploader.css" rel="stylesheet" /> 77 <script src="~/Scripts/Plugins/webuploader-0.1.5/webuploader.nolog.js"></script>
后端:
1 using System; 2 using System.Collections.Generic; 3 using System.IO; 4 using System.Linq; 5 using System.Web; 6 using System.Web.Mvc; 7 8 namespace BigFileUpload.Controllers 9 { 10 public class HomeController : Controller 11 { 12 // 13 // GET: /Home/ 14 15 public ActionResult Index() 16 { 17 return View(); 18 } 19 [HttpPost] 20 public ActionResult Upload() 21 { 22 string fileName = Request["name"]; 23 int index = Convert.ToInt32(Request["chunk"]);//当前分块序号 24 var guid = Request["guid"];//前端传来的GUID号 25 var dir = Server.MapPath("~/Upload");//文件上传目录 26 dir = Path.Combine(dir, guid);//临时保存分块的目录 27 if (!System.IO.Directory.Exists(dir)) 28 System.IO.Directory.CreateDirectory(dir); 29 string filePath = Path.Combine(dir, index.ToString());//分块文件名为索引名,更严谨一些可以加上是否存在的判断,防止多线程时并发冲突 30 var data = Request.Files["file"];//表单中取得分块文件 31 if (data != null)//为null可能是暂停的那一瞬间 32 { 33 data.SaveAs(filePath);//报错 34 } 35 return Json(new { erron = 0 });//Demo,随便返回了个值,请勿参考 36 } 37 public ActionResult Merge() 38 { 39 var guid = Request["guid"];//GUID 40 var uploadDir = Server.MapPath("~/Upload");//Upload 文件夹 41 var dir = Path.Combine(uploadDir, guid);//临时文件夹 42 var fileName = Request["fileName"];//文件名 43 var files = System.IO.Directory.GetFiles(dir);//获得下面的所有文件 44 var finalPath = Path.Combine(uploadDir, fileName);//最终的文件名(demo中保存的是它上传时候的文件名,实际操作肯定不能这样) 45 var fs = new FileStream(finalPath, FileMode.Create); 46 foreach (var part in files.OrderBy(x => x.Length).ThenBy(x => x))//排一下序,保证从0-N Write 47 { 48 var bytes = System.IO.File.ReadAllBytes(part); 49 fs.Write(bytes, 0, bytes.Length); 50 bytes = null; 51 System.IO.File.Delete(part);//删除分块 52 } 53 fs.Close(); 54 System.IO.Directory.Delete(dir);//删除文件夹 55 return Json(new { error = 0 });//随便返回个值,实际中根据需要返回 56 } 57 } 58 }
如果有什么不足或您有更好的想法,欢迎评论探讨~
错误修正:
感谢网友@豬豬→小熊 反馈的文件合并后打开错误的问题,经过查看我在文件合并时所用的files.OrderBy(x=>x)不可行,因为在当文件从小到大时,字符串的排序并不会按照数字的排序去排,举个例子:从0到1000,字符串排序的结果出来会是:0,1,10,100,1000,101,10001,因为他是从第一位开始比较的,2的第一位比11的第一位1大,所以11会排在2前面,所以应该将此排序改为:files.OrderBy(x => x.Length).ThenBy(x => x),这段代码的含义是:长度小的排在前面,如果长度一样,则按字符串从小到大排列,这样子就能保证文件在合并时的排序正确,并保证最终合并完成能够打开。
Github:https://github.com/ODotNet/BigFileUploader
欢迎Pull你对此Demo的改进~