最近做一个csv文件上传,网上找了几个,兼容性不好,年代也比较久远,就去github上看看找找,发现了一个resumable.js,支持分片上传,多文件上传,完全满足需求~项目地址:https://github.com/23/resumable.js。
简单说下应用,顺便保存下代码,万一哪天再用了,忘了还得重新找.....这才是关键。
后台代码,两个文件,一个model,一个webapi,基于C#的。
model代码:
namespace Resumable.Models
{
public class ResumableConfiguration
{
/// <summary>
/// Gets or sets number of expected chunks in this upload.
/// </summary>
public int Chunks { get; set; }
/// <summary>
/// Gets or sets unique identifier for current upload.
/// </summary>
public string Identifier { get; set; }
/// <summary>
/// Gets or sets file name.
/// </summary>
public string FileName { get; set; }
public ResumableConfiguration()
{
}
/// <summary>
/// Creates an object with file upload configuration.
/// </summary>
/// <param name="identifier">Upload unique identifier.</param>
/// <param name="filename">File name.</param>
/// <param name="chunks">Number of file chunks.</param>
/// <returns>File upload configuration.</returns>
public static ResumableConfiguration Create(string identifier, string filename, int chunks)
{
return new ResumableConfiguration { Identifier = identifier, FileName = filename, Chunks = chunks };
}
}
}
API代码:
using Resumable.Models;
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
namespace Resumable.Controllers
{
[RoutePrefix("api/File")]
public class FileUploadController : ApiController
{
private string root = System.Web.Hosting.HostingEnvironment.MapPath("~/upload");
[Route("Upload"), HttpOptions]
public object UploadFileOptions()
{
return Request.CreateResponse(HttpStatusCode.OK);
}
[Route("Upload"), HttpGet]
public object Upload(int resumableChunkNumber, string resumableIdentifier)
{
return ChunkIsHere(resumableChunkNumber, resumableIdentifier) ? Request.CreateResponse(HttpStatusCode.OK) : Request.CreateResponse(HttpStatusCode.NoContent);
}
[Route("Upload"), HttpPost]
public async Task<object> Upload()
{
// Check if the request contains multipart/form-data.
if (!Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
if (!Directory.Exists(root)) Directory.CreateDirectory(root);
var provider = new MultipartFormDataStreamProvider(root);
if (await readPart(provider))
{
// Success
return Request.CreateResponse(HttpStatusCode.OK);
}
else
{
// Fail
var message = DeleteInvalidChunkData(provider) ? "Cannot read multi part file data." : "Cannot delete temporary file chunk data.";
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, new System.Exception(message));
}
}
private static bool DeleteInvalidChunkData(MultipartFormDataStreamProvider provider)
{
try
{
var localFileName = provider.FileData[0].LocalFileName;
if (File.Exists(localFileName))
{
File.Delete(localFileName);
}
return true;
}
catch {
return false;
}
}
private async Task<bool> readPart(MultipartFormDataStreamProvider provider)
{
try
{
await Request.Content.ReadAsMultipartAsync(provider);
ResumableConfiguration configuration = GetUploadConfiguration(provider);
int chunkNumber = GetChunkNumber(provider);
// Rename generated file
MultipartFileData chunk = provider.FileData[0]; // Only one file in multipart message
RenameChunk(chunk, chunkNumber, configuration.Identifier);
// Assemble chunks into single file if they're all here
TryAssembleFile(configuration);
return true;
}
catch {
return false;
}
}
#region Get configuration
[NonAction]
private ResumableConfiguration GetUploadConfiguration(MultipartFormDataStreamProvider provider)
{
return ResumableConfiguration.Create(identifier: GetId(provider), filename: GetFileName(provider), chunks: GetTotalChunks(provider));
}
[NonAction]
private string GetFileName(MultipartFormDataStreamProvider provider)
{
var filename = provider.FormData["resumableFilename"];
return !String.IsNullOrEmpty(filename) ? filename : provider.FileData[0].Headers.ContentDisposition.FileName.Trim('\"');
}
[NonAction]
private string GetId(MultipartFormDataStreamProvider provider)
{
var id = provider.FormData["resumableIdentifier"];
return !String.IsNullOrEmpty(id) ? id : Guid.NewGuid().ToString();
}
[NonAction]
private int GetTotalChunks(MultipartFormDataStreamProvider provider)
{
var total = provider.FormData["resumableTotalChunks"];
return !String.IsNullOrEmpty(total) ? Convert.ToInt32(total) : 1;
}
[NonAction]
private int GetChunkNumber(MultipartFormDataStreamProvider provider)
{
var chunk = provider.FormData["resumableChunkNumber"];
return !String.IsNullOrEmpty(chunk) ? Convert.ToInt32(chunk) : 1;
}
#endregion
#region Chunk methods
[NonAction]
private string GetChunkFileName(int chunkNumber, string identifier)
{
return Path.Combine(root, string.Format("{0}_{1}", identifier, chunkNumber.ToString()));
}
[NonAction]
private void RenameChunk(MultipartFileData chunk, int chunkNumber, string identifier)
{
string generatedFileName = chunk.LocalFileName;
string chunkFileName = GetChunkFileName(chunkNumber, identifier);
if (File.Exists(chunkFileName)) File.Delete(chunkFileName);
File.Move(generatedFileName, chunkFileName);
}
[NonAction]
private string GetFilePath(ResumableConfiguration configuration)
{
return Path.Combine(root, configuration.Identifier);
}
[NonAction]
private bool ChunkIsHere(int chunkNumber, string identifier)
{
string fileName = GetChunkFileName(chunkNumber, identifier);
return File.Exists(fileName);
}
[NonAction]
private bool AllChunksAreHere(ResumableConfiguration configuration)
{
for (int chunkNumber = 1; chunkNumber <= configuration.Chunks; chunkNumber++)
if (!ChunkIsHere(chunkNumber, configuration.Identifier)) return false;
return true;
}
[NonAction]
private void TryAssembleFile(ResumableConfiguration configuration)
{
if (AllChunksAreHere(configuration))
{
// Create a single file
var path = ConsolidateFile(configuration);
// Rename consolidated with original name of upload
RenameFile(path, Path.Combine(root, configuration.FileName));
// Delete chunk files
DeleteChunks(configuration);
}
}
[NonAction]
private void DeleteChunks(ResumableConfiguration configuration)
{
for (int chunkNumber = 1; chunkNumber <= configuration.Chunks; chunkNumber++)
{
var chunkFileName = GetChunkFileName(chunkNumber, configuration.Identifier);
File.Delete(chunkFileName);
}
}
[NonAction]
private string ConsolidateFile(ResumableConfiguration configuration)
{
var path = GetFilePath(configuration);
using (var destStream = File.Create(path, 15000))
{
for (int chunkNumber = 1; chunkNumber <= configuration.Chunks; chunkNumber++)
{
var chunkFileName = GetChunkFileName(chunkNumber, configuration.Identifier);
using (var sourceStream = File.OpenRead(chunkFileName))
{
sourceStream.CopyTo(destStream);
}
}
destStream.Close();
}
return path;
}
#endregion
[NonAction]
private string RenameFile(string sourceName, string targetName)
{
targetName = Path.GetFileName(targetName); // Strip to filename if directory is specified (avoid cross-directory attack)
string realFileName = Path.Combine(root, targetName);
if (File.Exists(realFileName)) File.Delete(realFileName);
File.Move(sourceName, realFileName);
return targetName;
}
}
}
github上给的demo里边的代码~完全可以拿来用。然而并没有webapp的代码,我照着java的抄,发生了悲剧。不过稍微改改就好。
贴出来我的代码
<div id="frame">
<link href="~/Content/resumablestyle.css" rel="stylesheet" />
<script src="~/Scripts/jquery-1.10.2.min.js"></script>
<script src="~/Scripts/resumable.js"></script>
<br />
<div class="resumable-drop" οndragenter="jQuery(this).addClass('resumable-dragover');" οndragend="jQuery(this).removeClass('resumable-dragover');" οndrοp="jQuery(this).removeClass('resumable-dragover');">
将文件拖拽到此处上传 <a class="resumable-browse"><u>点击选择要上传的文件</u></a>
</div>
<div class="resumable-progress">
<table>
<tr>
<td width="100%"><div class="progress-container"><div class="progress-bar"></div></div></td>
<td class="progress-text" nowrap="nowrap"></td>
<td class="progress-pause" nowrap="nowrap">
<a href="#" οnclick="r.upload(); return(false);" class="progress-resume-link"><img src="~/Img/resume.png" title="Resume upload" /></a>
<a href="#" οnclick="r.pause(); return(false);" class="progress-pause-link"><img src="~/Img/pause.png" title="Pause upload" /></a>
</td>
</tr>
</table>
</div>
<ul class="resumable-list"></ul>
<script>
var r = new Resumable({
target: '@Url.Content("~/api/File/Upload")',
chunkSize: 1 * 1024 * 1024,
simultaneousUploads: 3,
//testChunks: false,
throttleProgressCallbacks: 1,
fileType: ["csv"]
//method: "octet"
});
// Resumable.js isn't supported, fall back on a different method
if (!r.support) {
$('.resumable-error').show();
} else {
// Show a place for dropping/selecting files
$('.resumable-drop').show();
r.assignDrop($('.resumable-drop')[0]);
r.assignBrowse($('.resumable-browse')[0]);
// Handle file add event
r.on('fileAdded', function (file) {
// Show progress pabr
$('.resumable-progress, .resumable-list').show();
// Show pause, hide resume
$('.resumable-progress .progress-resume-link').hide();
$('.resumable-progress .progress-pause-link').show();
// Add the file to the list
$('.resumable-list').append('<li class="resumable-file-' + file.uniqueIdentifier + '">Uploading <span class="resumable-file-name"></span> <span class="resumable-file-progress"></span>');
$('.resumable-file-' + file.uniqueIdentifier + ' .resumable-file-name').html(file.fileName);
// Actually start the upload
r.upload();
});
r.on('pause', function () {
// Show resume, hide pause
$('.resumable-progress .progress-resume-link').show();
$('.resumable-progress .progress-pause-link').hide();
});
r.on('complete', function () {
// Hide pause/resume when the upload has completed
$('.resumable-progress .progress-resume-link, .resumable-progress .progress-pause-link').hide();
});
r.on('fileSuccess', function (file, message) {
// Reflect that the file upload has completed
$('.resumable-file-' + file.uniqueIdentifier + ' .resumable-file-progress').html('(completed)');
});
r.on('fileError', function (file, message) {
// Reflect that the file upload has resulted in error
$('.resumable-file-' + file.uniqueIdentifier + ' .resumable-file-progress').html('(file could not be uploaded: ' + message + ')');
});
r.on('fileProgress', function (file) {
// Handle progress for both the file and the overall upload
$('.resumable-file-' + file.uniqueIdentifier + ' .resumable-file-progress').html(Math.floor(file.progress() * 100) + '%');
$('.progress-bar').css({ width: Math.floor(r.progress() * 100) + '%' });
});
}
</script>
</div>
css样式,图标文件,都是用的demo里的。直接用就好。css中主要的就是:
/* Reset */
#frame {margin:0 auto; width:800px; text-align:left;}
/* Uploader: Drag & Drop */
/*.resumable-error {display:none; font-size:14px; font-style:italic;}
.resumable-drop {padding:15px; font-size:13px; text-align:center; color:#666; font-weight:bold;background-color:#eee; border:2px dashed #aaa; border-radius:10px; margin-top:40px; z-index:9999; display:none;}
.resumable-dragover {padding:30px; color:#555; background-color:#ddd; border:1px solid #999;}*/
.resumable-error {display:none; font-size:14px; font-style:italic;}
.resumable-drop { padding:15px;font-size:13px; text-align:center; color:#666; font-weight:bold;background-color:#eee; border:2px dashed #aaa; border-radius:10px; z-index:9999; display:none;}
.resumable-dragover {padding:30px; color:#555; background-color:#ddd; border:1px solid #999;}
/* Uploader: Progress bar */
.resumable-progress {margin:30px 0 30px 0; width:100%; display:none;}
.progress-container {height:7px; background:#9CBD94; position:relative; }
.progress-bar {position:absolute; top:0; left:0; bottom:0; background:#45913A; width:0;}
.progress-text {font-size:11px; line-height:9px; padding-left:10px;}
.progress-pause {padding:0 0 0 7px;}
.progress-resume-link {display:none;}
.is-paused .progress-resume-link {display:inline;}
.is-paused .progress-pause-link {display:none;}
.is-complete .progress-pause {display:none;}
/* Uploader: List of items being uploaded */
.resumable-list {overflow:auto; margin-right:-20px; display:none;}
.uploader-item {width:148px; height:90px; background-color:#666; position:relative; border:2px solid black; float:left; margin:0 6px 6px 0;}
.uploader-item-thumbnail {width:100%; height:100%; position:absolute; top:0; left:0;}
.uploader-item img.uploader-item-thumbnail {opacity:0;}
.uploader-item-creating-thumbnail {padding:0 5px; font-size:9px; color:white;}
.uploader-item-title {position:absolute; font-size:9px; line-height:11px; padding:3px 50px 3px 5px; bottom:0; left:0; right:0; color:white; background-color:rgba(0,0,0,0.6); min-height:27px;}
.uploader-item-status {position:absolute; bottom:3px; right:3px;}
/* Uploader: Hover & Active status */
.uploader-item:hover, .is-active .uploader-item {border-color:#4a873c; cursor:pointer; }
.uploader-item:hover .uploader-item-title, .is-active .uploader-item .uploader-item-title {background-color:rgba(74,135,60,0.8);}
/* Uploader: Error status */
.is-error .uploader-item:hover, .is-active.is-error .uploader-item {border-color:#900;}
.is-error .uploader-item:hover .uploader-item-title, .is-active.is-error .uploader-item .uploader-item-title {background-color:rgba(153,0,0,0.6);}
.is-error .uploader-item-creating-thumbnail {display:none;}
基本就这么多,简单粗暴.....效果还是很好的,能实现大文件分片上传,多文件上传处理,自带进度条提示,省了好多事,说下参数含义:target:后台API,chunkSize:分片大小,fileType:文件类型。