[开源应用]利用HTTPHandler+resumableJs+HTML5实现拖拽上传[大]文件

时间:2022-02-12 10:34:50

前言:

大文件传输一直是技术上的一大难点。文件过大时,一些性提交所有的内容进内存是不现实的。大文件带来问题还有是否支持断点传输和多文件同时传输。

本文以resumableJs为例,介绍了如何在ASP.NET中实现大文件传输。同时本文利用了Html5的新特性:支持拖拽。

本文的主要技术点在于:如何接收resumableJs的传送内容(官网不太清楚)和如何合并文件,难度并不高。如果要改为MVC中的Controller处理文件传输,方法也大同小异。

注:原博客中,此文章在原站点个人代码备份所用,注释不多,如有不懂,请在评论中给出。

效果

aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPIAAAA3CAIAAAC5L4D0AAAEsklEQVR4nO2YS3IaMRCGxYJd9vgK5AhQxcZLcgDnArioyspeOoeYolxlb5xjQEjFFxlt4jwc4gd2bMePwTZZMEgtqTVDCCSD6v9Ki5lWq9Vq/tFoEAkAwSGOAAgOcXJ6fjw4+Xo8+Pz1+9GX46Mv3z6hoa14E8OLy9Oz4Y+Ts8GP0++D0+PBCRraqjdxdX1z+fNqePHzfHh5Nrw4O0dDW/kmbm/vbn7dXt/8urq+SdsVGtpqN/GQJPcPD3f3D3d398tshzvVlzsfFxfw49tq9fVBvNScC9IOd6ovq7TtHKre+OD11J5VjSy3+N0GF1nbN97F2l8ebCz0d1xOE4+Pj6PR42g0SkajJFle+7BVWdvqLy5gf7tSebUXLzXnYrR4v+lZabz3qlLZ7ucVpL+1prvi/Sb9IfrbleZ+TH6jytaHJBklidxrrjX35ORiahwl/W19XeAmnp6en/4F79+8ePHm/WLjre/KxQUsLN7KWSWQu+uso10pubs+vSeXVgjeSe6ur0bNxfM89Nrlcrvn3MpOo9zo9DqNckqjI/khvXaZ8bF66BTPz5PwalCn7QzNTWAyigSeKQ06FxOEZGUGoR10LbxddhrOglUHZ2cdGc+eUyhlmZRL2iOmwdIufdlrz5bJ/0eM56G7WSptdp1bGdVLpZLq6m7qazrEsXNOk2D1SI49fiXaOyZjMhIwRmSnwc9lBzGSpDcyqtMSqYg+e2at6/XJstg1c5lY4y27mjQtd7pCd2X1SJoXvskLx5yybgnR6jq3MqoJUSNLl1Fteq+GEJsePTF0W7RHRjU1C71Wg8wwyi0jASs6n0bWXFYQ65as0qqQr3L52As1a6RduHKohRpTyqgmhGh10wvSabpOu/XS/yjx/8riZW2s3ZCsx8e1jPVPldpdNTiK4kO5Cfg8lSVrLrPPzUGHnSzAFYLPPjPeB8N5po0ppx1Ey8yArOC6AuIvl7B8Fi5rt0x5sibR1AYhRKtLPD2DWFnnJaA9+TQy53JlzTB1IKsxIvrss8HuAnaeXI+eMN3xmUi+4NOt2ngb8VMVggLIWlmsLnr7B7v1vLJeyG7tgT8y+e1ZuGt0l5AfwXNS4oMbW7V9qCwk88tar97UJa0KVwb/oZZ5WLxna/6NO1MC2nXGszWJ6nmeZ6vYPGdt/lilCunpyoT74MmMoE/Vgcuayid9pSpZa11RXdIyOHbuqJG+NpXyDMXanUZe+Qk4M+feeGRtP0d69/V9W+Z9c7q4T5Ia4O4u3qO1ipA1ho2gtmojmSAPIWN6Wmt1zQNGRA/Ihj97KrX/uCBm6x1t9Eb+T8ZZEshMw+zR62ODGCdl523DhOftWQcIOsJy8XRZ0aiXUzR/EcbkwOJ4F3erHv+FrDlmPNstjyUlUOwXLnCBrHODFPx9Cxggaw7znzts1CvHQmUNQDGArEGAQNYgQCBrECCQNQgQyBoECGQNAgSyBgECWYMAgaxBgEDWIEAgaxAgkDUIEMgaBAhkDQIEsgYBAlmDAIGsQYBA1iBAIGsQIJA1CBDIGgQIZA0CBLIGAQJZgwCBrEGAQNYgQCBrECCQNQgQyBoECGQNAgSyBgECWYMAgaxBgPwGsDR6iJxOIcwAAAAASUVORK5CYII=" alt="" />

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="width: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–