webuploader-异步切片上传(暂不支持断点续传)及 下载方法!C#/.NET
十年河东,十年河西,莫欺少年穷
学无止境,精益求精
进入正题:
关于webuploader,参考网址:https://fex.baidu.com/webuploader/:
本篇博客范例下载地址:https://download.csdn.net/download/wolongbb/11958864
WebUploader是由Baidu WebFE(FEX)团队开发的一个简单的以HTML5为主,FLASH为辅的现代文件上传组件。在现代的浏览器里面能充分发挥HTML5的优势,同时又不摒弃主流IE浏览器,沿用原来的FLASH运行时,兼容IE6+,iOS 6+, android 4+。两套运行时,同样的调用方式,可供用户任意选用。采用大文件分片并发上传,极大的提高了文件上传效率。
作为一个上传插件,我们首先要做的是下载资源包,方便项目引用。
关于下载资源包,参考网址上有下载链接,可自行下载!
下面构造项目:
1、引入相关文件:
<meta name="viewport" content="width=device-width" /> <title>大文件上传测试</title> <script src="~/Scripts/jquery-1.8.2.min.js"></script> <link href="/Content/webuploader-0.1.5/webuploader.css" rel="stylesheet" /> <script src="/Content/webuploader-0.1.5/webuploader.js"></script>
2、参数说明:
3、前端HTML如下:
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>大文件上传测试</title> <script src="~/Scripts/jquery-1.8.2.min.js"></script> <link href="/Content/webuploader-0.1.5/webuploader.css" rel="stylesheet" /> <script src="/Content/webuploader-0.1.5/webuploader.js"></script> <script type="text/javascript"> $(function () { var GUID = WebUploader.Base.guid();//一个GUID var uploader = WebUploader.create({ // {Boolean} [可选] [默认值:false] 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 auto: true, swf: '/Content/webuploader-0.1.5/Uploader.swf', server: '@Url.Action("Upload")', pick: '#picker', resize: false, chunked: true,//开始分片上传 chunkSize: 2048000,//每一片的大小 formData: { guid: GUID //自定义参数,待会儿解释 } }); uploader.on('fileQueued', function (file) { $("#uploader .filename").html("文件名:" + file.name); $("#uploader .state").html('等待上传'); }); uploader.on('uploadSuccess', function (file, response) { $.post('@Url.Action("Merge")', { guid: GUID, fileName: file.name }, function (data) { //alert("上传成功!") }); }); uploader.on('uploadProgress', function (file, percentage) { $("#uploader .progress-bar").width(percentage * 100 + '%'); $(".sr-only").html('上传进度:'+(percentage * 100).toFixed(2) + '%') }); uploader.on('uploadSuccess', function () { $("#uploader .progress-bar").removeClass('progress-bar-striped').removeClass('active').removeClass('progress-bar-info').addClass('progress-bar-success'); $("#uploader .state").html("上传成功..."); }); uploader.on('uploadError', function () { $("#uploader .progress-bar").removeClass('progress-bar-striped').removeClass('active').removeClass('progress-bar-info').addClass('progress-bar-danger'); $("#uploader .state").html("上传失败..."); }); $("#ctlBtn").click(function () { uploader.upload(); $("#ctlBtn").text("上传"); $('#ctlBtn').attr('disabled', 'disabled'); $("#uploader .progress-bar").addClass('progress-bar-striped').addClass('active'); $("#uploader .state").html("上传中..."); }); $('#pause').click(function () { uploader.stop(true); $('#ctlBtn').removeAttr('disabled'); $("#ctlBtn").text("继续上传"); $("#uploader .state").html("暂停中..."); $("#uploader .progress-bar").removeClass('progress-bar-striped').removeClass('active'); }); }); function downLoadFile() { window.open("/Home/Download") } </script> </head> <body> <div id="uploader" class="wu-example"> <!--用来存放文件信息--> <div class="filename"></div> <div class="state"></div> <div class="progress"> <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%"> <span class="sr-only"></span> </div> </div> <div class="btns"> <div id="picker">选择文件</div> </div> <div class="btns" style="margin-top:25px;" onclick="downLoadFile()"> <div id="picker" class="webuploader-container"><div class="webuploader-pick">下载文件</div><div id="rt_rt_1deem09edokba40iet6gmkem2" style="position: absolute; top: 0px; left: 0px; width: 94px; height: 41px; overflow: hidden; bottom: auto; right: auto;"><input type="file" name="file" class="webuploader-element-invisible" multiple="multiple"><label style="opacity: 0; width: 100%; height: 100%; display: block; cursor: pointer; background: rgb(255, 255, 255);"></label></div></div> </div> </div> </body> </html>
4、后端代码如下:
namespace WebUploader.Controllers { public class HomeController : Controller { // // GET: /Home/ public ActionResult Index() { return View(); } /// <summary> /// 分切片上传-异步 /// </summary> /// <returns></returns> [HttpPost] public ActionResult Upload() { string fileName = Request["name"]; int index = Convert.ToInt32(Request["chunk"]);//当前分块序号 var guid = Request["guid"];//前端传来的GUID号 var dir = Server.MapPath("~/Upload");//文件上传目录 dir = Path.Combine(dir, guid);//临时保存分块的目录 if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir); string filePath = Path.Combine(dir, index.ToString());//分块文件名为索引名,更严谨一些可以加上是否存在的判断,防止多线程时并发冲突 var data = Request.Files["file"];//表单中取得分块文件 if (data != null)//为null可能是暂停的那一瞬间 { data.SaveAs(filePath);//报错 } return Json(new { erron = 0 });//Demo,随便返回了个值,请勿参考 } /// <summary> /// 合并切片 /// </summary> /// <returns></returns> public ActionResult Merge() { var guid = Request["guid"];//GUID var uploadDir = Server.MapPath("~/Upload");//Upload 文件夹 var dir = Path.Combine(uploadDir, guid);//临时文件夹 var fileName = Request["fileName"];//文件名 var files = System.IO.Directory.GetFiles(dir);//获得下面的所有文件 var finalPath = Path.Combine(uploadDir, fileName);//最终的文件名(demo中保存的是它上传时候的文件名,实际操作肯定不能这样) var fs = new FileStream(finalPath, FileMode.Create); foreach (var part in files.OrderBy(x => x.Length).ThenBy(x => x))//排一下序,保证从0-N Write { var bytes = System.IO.File.ReadAllBytes(part); fs.Write(bytes, 0, bytes.Length); bytes = null; System.IO.File.Delete(part);//删除分块 } fs.Close(); System.IO.Directory.Delete(dir);//删除文件夹 return Json(new { error = 0 });//随便返回个值,实际中根据需要返回 } #region 文件下载处理 /// <summary> /// 下载文件,支持大文件、续传、速度限制。支持续传的响应头Accept-Ranges、ETag,请求头Range 。 /// Accept-Ranges:响应头,向客户端指明,此进程支持可恢复下载.实现后台智能传输服务(BITS),值为:bytes; /// ETag:响应头,用于对客户端的初始(200)响应,以及来自客户端的恢复请求, /// 必须为每个文件提供一个唯一的ETag值(可由文件名和文件最后被修改的日期组成),这使客户端软件能够验证它们已经下载的字节块是否仍然是最新的。 /// Range:续传的起始位置,即已经下载到客户端的字节数,值如:bytes=1474560- 。 /// 另外:UrlEncode编码后会把文件名中的空格转换中+(+转换为%2b),但是浏览器是不能理解加号为空格的,所以在浏览器下载得到的文件,空格就变成了加号; /// 解决办法:UrlEncode 之后, 将 "+" 替换成 "%20",因为浏览器将%20转换为空格 /// </summary> /// <param name="httpContext">当前请求的HttpContext</param> /// <param name="filePath">下载文件的物理路径,含路径、文件名</param> /// <param name="speed">下载速度:每秒允许下载的字节数</param> /// <returns>true下载成功,false下载失败</returns> public static bool DownloadFile(HttpContext httpContext, string filePath, long speed) { bool ret = true; try { #region 验证:HttpMethod,请求的文件是否存在 switch (httpContext.Request.HttpMethod.ToUpper()) { //目前只支持GET和HEAD方法 case "GET": case "HEAD": break; default: httpContext.Response.StatusCode = 501; return false; } if (!System.IO.File.Exists(filePath)) { httpContext.Response.StatusCode = 404; return false; } #endregion #region 定义局部变量 long startBytes = 0; int packSize = 1024 * 10; //分块读取,每块10K bytes string fileName = Path.GetFileName(filePath); FileStream myFile = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); BinaryReader br = new BinaryReader(myFile); long fileLength = myFile.Length; int sleep = (int)Math.Ceiling(1000.0 * packSize / speed);//毫秒数:读取下一数据块的时间间隔 string lastUpdateTiemStr = System.IO.File.GetLastWriteTimeUtc(filePath).ToString("r"); string eTag = HttpUtility.UrlEncode(fileName, Encoding.UTF8) + lastUpdateTiemStr;//便于恢复下载时提取请求头; #endregion //--验证:文件是否太大,是否是续传,且在上次被请求的日期之后是否被修改过-------------- try { //-------添加重要响应头、解析请求头、相关验证------------------- #region -------向客户端发送数据块------------------- br.BaseStream.Seek(startBytes, SeekOrigin.Begin); int maxCount = (int)Math.Ceiling((fileLength - startBytes + 0.0) / packSize);//分块下载,剩余部分可分成的块数 for (int i = 0; i < maxCount && httpContext.Response.IsClientConnected; i++) {//客户端中断连接,则暂停 httpContext.Response.BinaryWrite(br.ReadBytes(packSize)); httpContext.Response.Flush(); if (sleep > 1) Thread.Sleep(sleep); } #endregion } catch { ret = false; } finally { br.Close(); myFile.Close(); } } catch { ret = false; } return ret; } #endregion public ActionResult Download() { string filePath = Server.MapPath("~/Upload/TortoiseSVN-1.8.11.26392-x64.zip"); string fileName = "TortoiseSVN-1.8.11.26392-x64.zip"; return new FileResult(filePath, fileName); } } /// <summary> /// 该类继承了ActionResult,通过重写ExecuteResult方法,进行文件的下载 /// </summary> public class FileResult : ActionResult { private readonly string _filePath;//文件路径 private readonly string _fileName;//文件名称 public FileResult(string filePath, string fileName) { _filePath = filePath; _fileName = fileName; } public override void ExecuteResult(ControllerContext context) { string fileName = _fileName; HttpResponseBase response = context.HttpContext.Response; if (System.IO.File.Exists(_filePath)) { FileStream fs = null; byte[] fileBuffer = new byte[1024];//每次读取1024字节大小的数据 try { using (fs = System.IO.File.OpenRead(_filePath)) { long totalLength = fs.Length; response.ContentType = "application/octet-stream"; response.AddHeader("Content-Disposition", "attachment; filename=" + HttpUtility.UrlEncode(fileName)); while (totalLength > 0 && response.IsClientConnected)//持续传输文件 { int length = fs.Read(fileBuffer, 0, fileBuffer.Length);//每次读取1024个字节长度的内容 fs.Flush(); response.OutputStream.Write(fileBuffer, 0, length);//写入到响应的输出流 response.Flush();//刷新响应 totalLength = totalLength - length; } response.Close();//文件传输完毕,关闭相应流 } } catch (Exception ex) { response.Write(ex.Message); } finally { if (fs != null) fs.Close();//最后记得关闭文件流 } } } } }
5、说明如下
上传时,会将大附件切成切片并存在一个临时文件夹中,待所有切片上传成功后,会调用Merge()方法合成文件!随后删除切片文件夹!
上述代码中包含一个下载附件的代码!
时间有限,没时间写博客,太忙了!
关于下载的方法,优化如下:
场景:如果遇到打包压缩后再下载,则下载完成后有可能需要删除压缩后的文件,现优化如下:
关于压缩文件可参考:C# 压缩文件
2019年7月17日 20点26分
增加是否可以删除下载的文件,如下:
public class downLoadFileResult : ActionResult { private readonly string _filePath;//文件路径 private readonly string _fileName;//文件名称 private readonly bool _isDeleted;//下载完成后,是否可删除 /// <summary> /// 构造删除 /// </summary> /// <param name="filePath">路径</param> /// <param name="fileName">文件名</param> /// <param name="isDeleted">下载完成后,是否可删除</param> public downLoadFileResult(string filePath, string fileName,bool isDeleted) { _filePath = filePath; _fileName = fileName; _isDeleted = isDeleted; } public override void ExecuteResult(ControllerContext context) { string fileName = _fileName; HttpResponseBase response = context.HttpContext.Response; if (System.IO.File.Exists(_filePath)) { FileStream fs = null; byte[] fileBuffer = new byte[1024];//每次读取1024字节大小的数据 try { using (fs = System.IO.File.OpenRead(_filePath)) { long totalLength = fs.Length; response.ContentType = "application/octet-stream"; response.AddHeader("Content-Disposition", "attachment; filename=" + HttpUtility.UrlEncode(fileName)); while (totalLength > 0 && response.IsClientConnected)//持续传输文件 { int length = fs.Read(fileBuffer, 0, fileBuffer.Length);//每次读取1024个字节长度的内容 fs.Flush(); response.OutputStream.Write(fileBuffer, 0, length);//写入到响应的输出流 response.Flush();//刷新响应 totalLength = totalLength - length; } response.Close();//文件传输完毕,关闭相应流 } } catch (Exception ex) { response.Write(ex.Message); } finally { if (fs != null) { fs.Close();//最后记得关闭文件流 if (_isDeleted) { System.IO.File.Delete(_filePath); } } } } } }
如果附件比较大,例如超过一个GB,则需要分块下载
/// <summary> /// Response分块下载,输出硬盘文件,提供下载 支持大文件、续传、速度限制、资源占用小 /// </summary> /// <param name="fileName">客户端保存的文件名</param> /// <param name="filePath">客户端保存的文件路径(包括文件名)</param> /// <returns></returns> /// <summary> /// 使用OutputStream.Write分块下载文件 /// </summary> /// <param name="filePath"></param> public void PostDownFile(string path, string name) { string fileName = name; LogHelper.WriteTextLog("读取文件", path, DateTime.Now); path = EncodeHelper.DefaultDecrypt(path); LogHelper.WriteTextLog("读取文件成功", path, DateTime.Now); var ef = CfDocument.GetByCondition("Url='" + path + "'").FirstOrDefault(); //防止带有#号文件名 if (ef != null && !string.IsNullOrEmpty(ef.Documentname)) { fileName = ef.Documentname; } var filePath = HttpContext.Current.Server.MapPath("~" + path); if (!File.Exists(filePath)) { return; } FileInfo info = new FileInfo(filePath); //指定块大小 long chunkSize = 4096; //建立一个4K的缓冲区 byte[] buffer = new byte[chunkSize]; //剩余的字节数 long dataToRead = 0; FileStream stream = null; try { //打开文件 stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); dataToRead = stream.Length; //添加Http头 HttpContext.Current.Response.Charset = "UTF-8"; HttpContext.Current.Response.ContentEncoding = System.Text.Encoding.GetEncoding("UTF-8"); HttpContext.Current.Response.ContentType = "application/octet-stream"; HttpContext.Current.Response.AddHeader("Content-Disposition", "attachment; filename=" + HttpContext.Current.Server.UrlEncode(fileName)); while (dataToRead > 0) { if (HttpContext.Current.Response.IsClientConnected) { int length = stream.Read(buffer, 0, Convert.ToInt32(chunkSize)); HttpContext.Current.Response.OutputStream.Write(buffer, 0, length); HttpContext.Current.Response.Flush(); HttpContext.Current.Response.Clear(); dataToRead -= length; } else { //防止client失去连接 dataToRead = -1; } } } catch (Exception ex) { HttpContext.Current.Response.Write("Error:" + ex.Message); } finally { if (stream != null) { stream.Close(); } HttpContext.Current.Response.Close(); } }
只需将上述方法写在webApi中
@陈卧龙的博客