【原創】MVC +WebUploader 實現分片上傳大文件

大文件的上傳是我一直以來想學習的一個技術點,今天在項目閒暇之時,終於有機會本身嘗試了一把,本文僅僅是個Demo,各類錯誤處理都麼有,僅限於你們來學習思路。javascript

參考博文:http://www.cnblogs.com/Leo_wl/p/4990116.htmlcss

http://www.linuxidc.com/Linux/2014-09/106816.htmhtml

1、開始

  • 做爲一個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     });

2、前端準備上傳分片

給開始上傳按鈕綁定上一個Click事件,來調用WebUploader的upload事件,若是你開啓了自動上傳,能夠省略這一步。這樣子,點了按鈕就會開始上傳工做,WebUploader就會自動把文件分片分好,而後上傳到服務器端。前端

1 $("#ctlBtn").click(function () {
2         uploader.upload();
3   });

 

3、後端接收上傳文件

開始以前,先說一下基本的思路:java

在特定的上傳目錄下面,先根據前端傳過來的GUID建立一個臨時的目錄,而後接受分塊,把全部的分塊分別保存起來,上傳完成以後合併這些分塊。linux

好了,開始上代碼:git

 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裏面取到,除了我取到的這些值,還有最後修改日期,總共多少分塊、文件總大小等,卡個斷點一看便知。github

4、上傳完畢,合併文件,刪除分片

因爲WebUploader是多線程的上傳,因此不必定文件塊會按照順序來到服務器,因此我本來打算在Upload中進行合併文件的想法泡湯了~固然,或許能夠去判斷當前文件夾中的文件數目等於分塊數目這種方式來處理,可是總感受不靠譜,萬一當時只是把那個文件建立出來了,內容還沒寫進去怎麼辦?你們有更好的思路,歡迎探討~web

我目前的作法,是經過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 }

 

5、總體代碼送上

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>
index.cshtml

後端:

 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 }
HomeController.cs

若是有什麼不足或您有更好的想法,歡迎評論探討~

錯誤修正:

感謝網友@豬豬→小熊 反饋的文件合併後打開錯誤的問題,通過查看我在文件合併時所用的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的改進~

相關文章
相關標籤/搜索