前言:
大文件传输一直是技术上的一大难点。文件过大时,一些性提交所有的内容进内存是不现实的。大文件带来问题还有是否支持断点传输和多文件同时传输。
本文以resumableJs为例,介绍了如何在ASP.NET中实现大文件传输。同时本文利用了Html5的新特性:支持拖拽。
本文的主要技术点在于:如何接收resumableJs的传送内容(官网不太清楚)和如何合并文件,难度并不高。如果要改为MVC中的Controller处理文件传输,方法也大同小异。
注:原博客中,此文章在原站点个人代码备份所用,注释不多,如有不懂,请在评论中给出。
效果:
ASPX File:
<html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>Resumable.js Test</title> </head> <body> <form id="form1" runat="server"> <div id="container" style="300px;height:200px;background-color:lightgray"> </div> </form> <span id="info">welcome</span> <script src="scripts/resumable.js" type="text/javascript"></script> <script type="text/javascript"> var showInfo = function (msg) { document.getElementById("info").innerHTML = msg; } showInfo("Test begin"); var r = new Resumable({ target: 'FileHandler.ashx', }); r.assignBrowse(document.getElementById('container')); r.assignDrop(document.getElementById('container')); if (!r.support) showInfo("not support"); r.on('fileAdded', function (file, event) { r.upload(); }); r.on('filesAdded', function (array) { for (var i = 0; i < array.length; i++) { var html = document.getElementById("info").innerHTML; html += "<br>"+array[i].name; } }); r.on('uploadStart', function () { showInfo('start'); }); r.on('complete', function () { r.files.pop(); //if want to upload one file multiple times, you should remove it from r.files after completing. //pop后,才可再次重新拖拽上传此文件。此机制可避免一次上传多个文件时重复添加,但拖拽上传时不用检测。 }); r.on('progress', function (e) { showInfo(r.progress()); }); </script> </body> </html>
FileHandler
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Web; namespace UploadTest { /// <summary> /// Summary description for FileHandler /// </summary> public class FileHandler : IHttpHandler { string _tempFolder; object _lock = new object(); public void ProcessRequest(HttpContext context) { _tempFolder = context.Server.MapPath("~/temp"); var method = context.Request.HttpMethod; if (method.Equals("GET")) { HandleGet(context); } if (method.Equals("POST")) { HandlePost(context); } } private void HandlePost(HttpContext context) { var queryString = context.Request.Form; if (queryString.Count == 0) return; try { // Read parameters var uploadToken = queryString.Get("upload_Token"); int resumableChunkNumber = int.Parse(queryString.Get("resumableChunkNumber")); var resumableTotalChunks = int.Parse(queryString.Get("resumableTotalChunks")); var resumableTotalSize = long.Parse(queryString.Get("resumableTotalSize")); var resumableFilename = queryString.Get("resumableFilename"); // Save File if (context.Request.Files.Count == 0) { context.Response.StatusCode = (int)System.Net.HttpStatusCode.InternalServerError; } else { var filePath = string.Format("{0}/{1}/{1}.part{2}", _tempFolder, resumableFilename, resumableChunkNumber.ToString("0000")); var directory = Path.GetDirectoryName(filePath); if (File.Exists(directory)) { File.Delete(directory); } if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } if (!System.IO.File.Exists(filePath)) { context.Request.Files[0].SaveAs(filePath); } if (IsCompleted(directory,resumableTotalChunks,resumableTotalSize)) { MergeFiles(directory); } } } catch (Exception exception) { throw exception; } } private void HandleGet(HttpContext context) { var queryString = context.Request.QueryString; if (queryString.Count == 0) return; try { // Read parameters var uploadToken = queryString.Get("upload_Token"); int resumableChunkNumber = int.Parse(queryString.Get("resumableChunkNumber")); var resumableFilename = queryString.Get("resumableFilename"); var resumableChunkSize = long.Parse(queryString.Get("resumableChunkSize")); var filePath = string.Format("{0}/{1}/{1}.part{2}", _tempFolder, resumableFilename, resumableChunkNumber.ToString("0000")); // Check for existance and chunksize if (System.IO.File.Exists(filePath) && new FileInfo(filePath).Length == resumableChunkSize) { context.Response.Status = "200 OK"; context.Response.StatusCode = 200; } else { context.Response.Status = "404 Not Found"; context.Response.StatusCode = 404; } } catch (Exception exception) { throw exception; } } private bool IsCompleted(string directory,int numChunks, long totalSize ) { var physicalFolder = Path.Combine(_tempFolder, directory); var files = Directory.GetFiles(physicalFolder); //numbers if (files.Length != numChunks) return false; //files all exisit var fileName = Path.GetFileName(directory); for (int i = 1; i <= numChunks; i++) { var filePath = string.Format("{0}/{1}.part{2}", directory, fileName, i.ToString("0000")); if (!File.Exists(filePath)) { return false; } } //size long tmpSize = 0; foreach (var file in files) { tmpSize += new FileInfo(file).Length; } return totalSize==tmpSize; } private void MergeFiles(string directoryPath) { lock (_lock) { if (Directory.Exists(directoryPath)) { var fileName = Path.GetFileName(directoryPath); var folder = Path.GetDirectoryName(directoryPath); var tempPath = Path.Combine(directoryPath + ".tmp"); var files = Directory.GetFiles(directoryPath); files = files.OrderBy(f => f).ToArray(); FileStream wholeStream = new FileStream(tempPath, FileMode.Append, FileAccess.Write); for(int i=0;i<files.Length;i++) { FileStream parcialStream = new FileStream(files[i], FileMode.Open); BinaryReader parcialReader = new BinaryReader(parcialStream); byte[] buffer = new byte[parcialStream.Length]; buffer = parcialReader.ReadBytes((int)parcialStream.Length); BinaryWriter parcialWriter = new BinaryWriter(wholeStream); parcialWriter.Write(buffer); parcialStream.Close(); } wholeStream.Close(); Directory.Delete(directoryPath,true); File.Move(tempPath, directoryPath); } } } public bool IsReusable { get { return false; } } } }
附录:
1 技术难点
a. 文件过大。修改webconfig无用。
b. 断点续传。
c. 多文件上传。
2 resumable.js
API: http://www.resumablejs.com/
工作流程:
拖文件至DIV -> 开始上传,uploadStart -> 反复触发progress事件 -> compete
主要参数:
Get:
resumableChunkNumber=1&
resumableChunkSize=1048576&
resumableCurrentChunkSize=1048576&
resumableTotalSize=27778318&
resumableType=&
resumableIdentifier=27778318-Samples7z&
resumableFilename=Samples.7z&
resumableRelativePath=Samples.7z&
resumableTotalChunks=26
Post:
—————————–111061030216033
Content-Disposition: form-data; name=”resumableChunkNumber”
140
—————————–111061030216033
Content-Disposition: form-data; name=”resumableChunkSize”
1048576
—————————–111061030216033
Content-Disposition: form-data; name=”resumableCurrentChunkSize”
1048576
—————————–111061030216033
Content-Disposition: form-data; name=”resumableTotalSize”
171309601
—————————–111061030216033
Content-Disposition: form-data; name=”resumableType”
—————————–111061030216033
Content-Disposition: form-data; name=”resumableIdentifier”
171309601-sample7z
—————————–111061030216033
Content-Disposition: form-data; name=”resumableFilename”
sample.7z
—————————–111061030216033
Content-Disposition: form-data; name=”resumableRelativePath”
sample.7z
—————————–111061030216033
Content-Disposition: form-data; name=”resumableTotalChunks”
163
—————————–111061030216033
Content-Disposition: form-data; name=”file”; filename=”blob”
Content-Type: application/octet-stream
XXXCONTENT
—————————–309022088923579–