標籤:
最近做一個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)){// Successreturn Request.CreateResponse(HttpStatusCode.OK);}else{// Failvar 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 fileMultipartFileData chunk = provider.FileData[0]; // Only one file in multipart messageRenameChunk(chunk, chunkNumber, configuration.Identifier);// Assemble chunks into single file if they‘re all hereTryAssembleFile(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 filevar path = ConsolidateFile(configuration);// Rename consolidated with original name of uploadRenameFile(path, Path.Combine(root, configuration.FileName));// Delete chunk filesDeleteChunks(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的抄,發生了悲劇。不過稍微改改就好。
貼出來My Code
<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" ondragenter="jQuery(this).addClass(‘resumable-dragover‘);" ondragend="jQuery(this).removeClass(‘resumable-dragover‘);" ondrop="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="#" onclick="r.upload(); return(false);" class="progress-resume-link"><img src="~/Img/resume.png" title="Resume upload" /></a> <a href="#" onclick="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:檔案類型。
Html5 js上傳外掛程式resumable.js 的使用