大文件的上傳是我一直以來想學習的一個技術點,今天在項目閑暇之時,終於有機會自己嘗試了一把,本文僅僅是個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的改進~
