From 1f524f660448bfe4277c2ded2b788956911bd659 Mon Sep 17 00:00:00 2001 From: Nathan Bridgewater Date: Thu, 29 Sep 2011 23:26:36 -0500 Subject: [PATCH] setting up chunk upload for file progress. work in progrs.. broken --- UploaderSL-v2/SLHttpUploader/ChunkInfo.cs | 15 + UploaderSL-v2/SLHttpUploader/ChunkSequence.cs | 13 + .../SLHttpUploader/FilePostBehavior.cs | 24 ++ .../SLHttpUploader/HttpChunkUtility.cs | 304 ++++++++++++++++++ UploaderSL-v2/SLHttpUploader/HttpUtility.cs | 116 +++---- UploaderSL-v2/SLHttpUploader/MainPage.xaml.cs | 29 +- .../SLHttpUploader/PostRequestInfo.cs | 32 ++ .../SLHttpUploader/ProgressReportEventArgs.cs | 36 +++ .../SLHttpUploader/SLHttpUploader.csproj | 7 + UploaderSL-v2/SLHttpUploader/Settings.cs | 3 +- .../UploadCompletedEventArgs.cs | 24 ++ .../Sample.Web/Controllers/ChunkInfo.cs | 15 + .../Sample.Web/Controllers/ChunkSequence.cs | 13 + .../Sample.Web/Controllers/HomeController.cs | 170 ++++++---- UploaderSL-v2/Sample.Web/Sample.Web.csproj | 3 + .../Sample.Web/Scripts/jquery.uploader.js | 233 +++++++------- UploaderSL-v2/Sample.Web/TextUtility.cs | 208 ++++++++++++ .../Sample.Web/Views/Home/Index.cshtml | 109 +++---- 18 files changed, 1032 insertions(+), 322 deletions(-) create mode 100644 UploaderSL-v2/SLHttpUploader/ChunkInfo.cs create mode 100644 UploaderSL-v2/SLHttpUploader/ChunkSequence.cs create mode 100644 UploaderSL-v2/SLHttpUploader/FilePostBehavior.cs create mode 100644 UploaderSL-v2/SLHttpUploader/HttpChunkUtility.cs create mode 100644 UploaderSL-v2/SLHttpUploader/PostRequestInfo.cs create mode 100644 UploaderSL-v2/SLHttpUploader/ProgressReportEventArgs.cs create mode 100644 UploaderSL-v2/SLHttpUploader/UploadCompletedEventArgs.cs create mode 100644 UploaderSL-v2/Sample.Web/Controllers/ChunkInfo.cs create mode 100644 UploaderSL-v2/Sample.Web/Controllers/ChunkSequence.cs create mode 100644 UploaderSL-v2/Sample.Web/TextUtility.cs diff --git a/UploaderSL-v2/SLHttpUploader/ChunkInfo.cs b/UploaderSL-v2/SLHttpUploader/ChunkInfo.cs new file mode 100644 index 0000000..2dc12aa --- /dev/null +++ b/UploaderSL-v2/SLHttpUploader/ChunkInfo.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace SLHttpUploader +{ + public class ChunkInfo + { + public Guid FileId { get; set; } + public ChunkSequence Sequence { get; set; } + public byte[] Chunk { get; set; } + public string Hash { get; set; } + } +} \ No newline at end of file diff --git a/UploaderSL-v2/SLHttpUploader/ChunkSequence.cs b/UploaderSL-v2/SLHttpUploader/ChunkSequence.cs new file mode 100644 index 0000000..3b85b7b --- /dev/null +++ b/UploaderSL-v2/SLHttpUploader/ChunkSequence.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace SLHttpUploader +{ + public enum ChunkSequence + { + Body, + End + } +} diff --git a/UploaderSL-v2/SLHttpUploader/FilePostBehavior.cs b/UploaderSL-v2/SLHttpUploader/FilePostBehavior.cs new file mode 100644 index 0000000..3e3daac --- /dev/null +++ b/UploaderSL-v2/SLHttpUploader/FilePostBehavior.cs @@ -0,0 +1,24 @@ +using System; +using System.Net; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Ink; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.Windows.Shapes; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Runtime.Serialization.Json; + +namespace SLHttpUploader +{ + public enum FilePostBehavior + { + AllAtOnce, + OneAtATime + } +} diff --git a/UploaderSL-v2/SLHttpUploader/HttpChunkUtility.cs b/UploaderSL-v2/SLHttpUploader/HttpChunkUtility.cs new file mode 100644 index 0000000..474171c --- /dev/null +++ b/UploaderSL-v2/SLHttpUploader/HttpChunkUtility.cs @@ -0,0 +1,304 @@ +using System; +using System.Net; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Ink; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.Windows.Shapes; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Runtime.Serialization.Json; + +namespace SLHttpUploader +{ + public class HttpChunkUtility + { + System.Windows.Threading.Dispatcher dispatcher; + IEnumerable files; + FilePostBehavior behavior; + Dictionary formData; + string url; + IEnumerator enumerator; + bool done; + int uploadCount; + int lastFileSequenceProgressReport; + int lastFileContentProgressReport; + int chunkSize = 32768; + long bytesWritten = 0; + + private event Action uploadCompleted; + public event Action UploadCompleted + { + add { uploadCompleted += value; } + remove { uploadCompleted -= value; } + } + + private event Action fileContentProgressReport; + public event Action FileContentProgressReport + { + add { fileContentProgressReport += value; } + remove { fileContentProgressReport -= value; } + } + + private event Action fileSequenceProgressReport; + public event Action FileSequenceProgressReport + { + add { fileSequenceProgressReport += value; } + remove { fileSequenceProgressReport -= value; } + } + + public HttpChunkUtility() + { + done = false; + lastFileSequenceProgressReport = 0; + lastFileContentProgressReport = 0; + uploadCount = 0; + } + + public void PostFileContents(string url, IEnumerable files, FilePostBehavior behavior, Dictionary formData, System.Windows.Threading.Dispatcher dispatcher, int? chunkSize) + { + throw new NotImplementedException("chunk upload is broken"); + + this.chunkSize = chunkSize ?? 32768; + this.dispatcher = dispatcher; + this.url = url; + this.behavior = behavior; + this.formData = formData; + this.files = files; + this.enumerator = files.GetEnumerator(); + + bytesWritten = 0; + StartUpload(); + } + + void StartUpload() + { + try + { + //next chunk + OnFileSequenceProgressReport(0, 1); + //write to POST request + HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(url); + req.Method = "POST"; + var boundary = "---------------" + DateTime.Now.Ticks.ToString("x"); + req.ContentType = string.Format("multipart/form-data; boundary={0}", boundary); + var info = new PostRequestInfo() { FormData = formData, Request = req, Boundary = boundary, Dispatcher = dispatcher, Files=new List { enumerator.Current } }; + req.BeginGetRequestStream(GetRequestStream, info); + } + catch (Exception ex) + { + OnUploadCompleted(false, ex); + } + } + + void FinishUpload(PostRequestInfo info) + { + //request thread + this.dispatcher.BeginInvoke(() => + { + uploadCount++; + OnFileSequenceProgressReport(uploadCount, files.Count()); + StartUpload(); + }); + } + //just write the contents of the stream to the output buffer. + void WriteFileContentRange(Stream destination, Stream source, string boundary, string filename, long fileSize, long contentStart) + { + var enc = new System.Text.UTF8Encoding(false, false); + string contentTemplate = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"Chunk\"; filename=\"{1}\"\r\nContent-Type: {2}\r\n\r\n", + boundary, filename, DetermineMimeTypeFromExtension(filename)); + + byte[] temp = enc.GetBytes(contentTemplate); + destination.Write(temp, 0, temp.Length); + + byte[] buffer = new byte[this.chunkSize]; + int read = 1; + source.Seek(contentStart, SeekOrigin.Begin); + + + //write bytes from stream to output. + read = source.Read(buffer, 0, buffer.Length); + if (read > 0) + { + //test.Write(buffer, 0, read); + destination.Write(buffer, 0, read); + bytesWritten += read; + } + + temp = enc.GetBytes("\r\n"); + destination.Write(temp, 0, temp.Length); + } + + void UploadChunk(PostRequestInfo info) + { + } + void FinishUploadChunk(PostRequestInfo info) + { + } + + private void GetRequestStream(IAsyncResult result) + { + var post = (PostRequestInfo)result.AsyncState; + var enc = new System.Text.UTF8Encoding(false, false); + + try + { + using (var stream = post.Request.EndGetRequestStream(result)) + { + var file = enumerator.Current; + using (var reader = file.OpenRead()) + { + WriteFileContentRange(stream, reader, post.Boundary, file.Name, file.Length, bytesWritten + 1 ); + } + + StringBuilder builder = new StringBuilder(); + //write form contents + if (post.FormData != null) + { + string template = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}"; + + foreach (var field in post.FormData) + builder.AppendFormat(template, post.Boundary, field.Key, field.Value); + } + + builder.AppendFormat("\r\n--{0}--\r\n", post.Boundary); + byte[] temp = enc.GetBytes(builder.ToString()); + stream.Write(temp, 0, temp.Length); + } + + //get the response. + post.Request.BeginGetResponse(GetResponse, post); + + } + catch (Exception ex) + { + this.dispatcher.BeginInvoke(() => OnUploadCompleted(false, ex)); + } + } + private void GetResponse(IAsyncResult result) + { + try + { + var post = (PostRequestInfo)result.AsyncState; + HttpWebResponse response = (HttpWebResponse)post.Request.EndGetResponse(result); + + string responseText = null; + var enc = new UTF8Encoding(false, false); + using (var stream = response.GetResponseStream()) + { + using (var reader = new StreamReader(stream, enc)) + responseText = reader.ReadToEnd(); + } + + JsonResponse json = null; + if (!string.IsNullOrEmpty(responseText)) + { + using (MemoryStream ms = new MemoryStream(enc.GetBytes(responseText))) + { + var js = new DataContractJsonSerializer(typeof(JsonResponse)); + json = js.ReadObject(ms) as JsonResponse; + } + } + + if (json != null && json.Success) + { + throw new NotImplementedException("fix this"); + //if last chunk, then finish + //else continue sending chunks. + FinishUpload(post); + } + else + this.dispatcher.BeginInvoke(() => OnUploadCompleted(false, new Exception("Upload failed. - " + json.Message))); + } + catch (Exception ex) + { + this.dispatcher.BeginInvoke(() => OnUploadCompleted(false, ex)); + } + } + + protected void OnUploadCompleted(bool success, Exception error) + { + //all callers are in the UI thread. + if (uploadCompleted != null) + uploadCompleted(new UploadCompletedEventArgs() { Error = error, Success = success }); + } + protected void OnFileSequenceProgressReport(long current, long total) + { + //consider skipping a number of calls for performance. We don't ned to report b ack to the UI every chunk uploaded. + //The caller is in the request thread. + Action action = (c, t) => + { + var arg = new ProgressReportEventArgs() { Current = c, Total = t }; + var perc = arg.Percentage; + if (perc == 0 || perc != lastFileSequenceProgressReport) + { + lastFileSequenceProgressReport = perc; + if (fileSequenceProgressReport != null) + fileSequenceProgressReport(arg); + } + }; + + this.dispatcher.BeginInvoke(action, current, total); + } + + protected void OnFileContentProgressReport(long current, long total) + { + //consider skipping a number of calls for performance. We don't ned to report b ack to the UI every chunk uploaded. + //The caller is in the request thread. + Action action = (c, t) => + { + var arg = new ProgressReportEventArgs() { Current = c, Total = t }; + var perc = arg.Percentage; + if (perc == 0 || perc != lastFileContentProgressReport) + { + lastFileContentProgressReport = perc; + if (fileContentProgressReport != null) + fileContentProgressReport(arg); + } + }; + + this.dispatcher.BeginInvoke(action, current, total); + } + static string DetermineMimeTypeFromExtension(string filename) + { + var ext = System.IO.Path.GetExtension(filename).ToLower(); + var match = MIME_TYPES.Where(o => o.Value.Contains(ext)).Select(o => o.Key).FirstOrDefault(); + + if (match != null) + return match; + return "application/octet-stream"; //default; + } + public static readonly Dictionary> MIME_TYPES = new Dictionary>() + { + {"text/plain", new List() {".txt"}}, + {"text/html", new List() {".html", ".htm"}}, + {"text/xml", new List() {".xml"}}, + {"text/richtext", new List() {".rtf"}}, + {"audio/x-aiff", new List() {".aiff"}}, + {"audio/basic", new List() {".basic"}}, + {"audio/mid", new List() {".mid"}}, + {"audio/wav", new List() {".wav"}}, + {"image/gif", new List() {".gif"}}, + {"image/jpeg", new List() {".jpg",".jpeg"}}, + {"image/png", new List() {".png"}}, + {"image/x-png", new List() {".png"}}, + {"image/tiff", new List() {".tif", "tiff"}}, + {"image/bmp", new List() {".bmp"}}, + {"image/x-emf", new List() {".emf"}}, + {"image/x-wmf", new List() {".wmf"}}, + {"video/avi", new List() {".avi"}}, + {"video/mpeg", new List() {".mpeg", ".mpg"}}, + {"application/postscript", new List() {".ps"}}, + {"application/pdf", new List() {".pdf"}}, + {"application/xml", new List() {".xml"}}, + {"application/rss+xml", new List() {".rss"}}, + {"application/x-zip-compressed", new List() {".zip"}}, + {"application/x-gzip-compressed", new List() {".gz"}} + }; + } +} diff --git a/UploaderSL-v2/SLHttpUploader/HttpUtility.cs b/UploaderSL-v2/SLHttpUploader/HttpUtility.cs index a25088e..9bfebac 100644 --- a/UploaderSL-v2/SLHttpUploader/HttpUtility.cs +++ b/UploaderSL-v2/SLHttpUploader/HttpUtility.cs @@ -26,27 +26,35 @@ public class HttpUtility IEnumerator enumerator; bool done; int uploadCount; - int lastProgressReport; + int lastFileSequenceProgressReport; + int lastFileContentProgressReport; - private event UploadedCompletedEventHandler uploadCompleted; - public event UploadedCompletedEventHandler UploadCompleted + private event Action uploadCompleted; + public event Action UploadCompleted { add { uploadCompleted += value; } remove { uploadCompleted -= value; } } - private event ProgressReportedEventHandler progressReport; - public event ProgressReportedEventHandler ProgressReport + private event Action fileContentProgressReport; + public event Action FileContentProgressReport { - add { progressReport += value; } - remove { progressReport -= value; } + add { fileContentProgressReport += value; } + remove { fileContentProgressReport -= value; } } + private event Action fileSequenceProgressReport; + public event Action FileSequenceProgressReport + { + add { fileSequenceProgressReport += value; } + remove { fileSequenceProgressReport -= value; } + } public HttpUtility() { done = false; - lastProgressReport = 0; + lastFileSequenceProgressReport = 0; + lastFileContentProgressReport = 0; uploadCount = 0; } @@ -58,7 +66,7 @@ public void PostFileContents(string url, IEnumerable files, FilePostBe this.formData = formData; this.files = files; this.enumerator = files.GetEnumerator(); - OnProgressReport(0, 1); + OnFileSequenceProgressReport(0, 1); this.StartUpload(); } @@ -68,6 +76,7 @@ void StartUpload() { //write to POST request HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(url); + req.AllowReadStreamBuffering = false; req.Method = "POST"; var boundary = "---------------" + DateTime.Now.Ticks.ToString("x"); req.ContentType = string.Format("multipart/form-data; boundary={0}", boundary); @@ -104,13 +113,13 @@ void FinishUpload(PostRequestInfo info) if (behavior == FilePostBehavior.OneAtATime && !done) { uploadCount++; - OnProgressReport(uploadCount, files.Count()); + OnFileSequenceProgressReport(uploadCount, files.Count()); StartUpload(); } else { //should be totally done. Fire an event to inform the front end that the upload is complete. - OnProgressReport(1, 1); + OnFileSequenceProgressReport(1, 1); OnUploadCompleted(true, null); } }); @@ -124,9 +133,10 @@ void WriteFileContent(Stream destination, Stream source, string boundary, string byte[] temp = enc.GetBytes(contentTemplate); destination.Write(temp, 0, temp.Length); - byte[] buffer = new byte[4096]; + byte[] buffer = new byte[1024*32]; int read = 1; long totalWritten = 0; + OnFileContentProgressReport(totalWritten, fileSize); //write bytes from stream to output. while (read > 0) @@ -136,7 +146,11 @@ void WriteFileContent(Stream destination, Stream source, string boundary, string { //test.Write(buffer, 0, read); destination.Write(buffer, 0, read); + destination.Flush(); //blocks thread until sent. totalWritten += read; + + //report progress. + OnFileContentProgressReport(totalWritten, fileSize); } } @@ -144,7 +158,7 @@ void WriteFileContent(Stream destination, Stream source, string boundary, string destination.Write(temp, 0, temp.Length); } - public void GetRequestStream(IAsyncResult result) + private void GetRequestStream(IAsyncResult result) { var post = (PostRequestInfo)result.AsyncState; var enc = new System.Text.UTF8Encoding(false, false); @@ -186,7 +200,7 @@ public void GetRequestStream(IAsyncResult result) this.dispatcher.BeginInvoke(() => OnUploadCompleted(false, ex)); } } - public void GetResponse(IAsyncResult result) + private void GetResponse(IAsyncResult result) { try { @@ -226,9 +240,9 @@ protected void OnUploadCompleted(bool success, Exception error) { //all callers are in the UI thread. if (uploadCompleted != null) - uploadCompleted(this, new UploadCompletedEventArgs() { Error = error, Success = success }); + uploadCompleted(new UploadCompletedEventArgs() { Error = error, Success = success }); } - protected void OnProgressReport(long current, long total) + protected void OnFileSequenceProgressReport(long current, long total) { //consider skipping a number of calls for performance. We don't ned to report b ack to the UI every chunk uploaded. //The caller is in the request thread. @@ -236,17 +250,35 @@ protected void OnProgressReport(long current, long total) { var arg = new ProgressReportEventArgs() { Current = c, Total = t }; var perc = arg.Percentage; - if (perc == 0 || perc != lastProgressReport) + if (perc == 0 || perc != lastFileSequenceProgressReport) { - lastProgressReport = perc; - if (progressReport != null) - progressReport(this, arg); + lastFileSequenceProgressReport = perc; + if (fileSequenceProgressReport != null) + fileSequenceProgressReport(arg); } }; this.dispatcher.BeginInvoke(action, current, total); } + protected void OnFileContentProgressReport(long current, long total) + { + //consider skipping a number of calls for performance. We don't ned to report b ack to the UI every chunk uploaded. + //The caller is in the request thread. + Action action = (c, t) => + { + var arg = new ProgressReportEventArgs() { Current = c, Total = t }; + var perc = arg.Percentage; + if (perc == 0 || perc != lastFileContentProgressReport) + { + lastFileContentProgressReport = perc; + if (fileContentProgressReport != null) + fileContentProgressReport(arg); + } + }; + + this.dispatcher.BeginInvoke(action, current, total); + } static string DetermineMimeTypeFromExtension(string filename) { var ext = System.IO.Path.GetExtension(filename).ToLower(); @@ -284,46 +316,4 @@ static string DetermineMimeTypeFromExtension(string filename) {"application/x-gzip-compressed", new List() {".gz"}} }; } - - public class ProgressReportEventArgs : EventArgs - { - public long Current { get; set; } - public long Total { get; set; } - - public int Percentage - { - get - { - if (Total == 0) - return 0; - - double perc = (double)Current / (double)Total; - return (int)(perc * 100); - } - } - } - public class UploadCompletedEventArgs : EventArgs - { - public bool Success { get; set; } - public Exception Error { get; set; } - } - - public delegate void UploadedCompletedEventHandler(object sender, UploadCompletedEventArgs e); - public delegate void ProgressReportedEventHandler(object sender, ProgressReportEventArgs e); - - public enum FilePostBehavior - { - AllAtOnce, - OneAtATime - } - - public class PostRequestInfo - { - public IEnumerable Files { get; set; } - public Dictionary FormData { get; set; } - public FilePostBehavior Behavior { get; set; } - public WebRequest Request { get; set; } - public string Boundary { get; set; } - public System.Windows.Threading.Dispatcher Dispatcher { get; set; } - } -} +} \ No newline at end of file diff --git a/UploaderSL-v2/SLHttpUploader/MainPage.xaml.cs b/UploaderSL-v2/SLHttpUploader/MainPage.xaml.cs index e0f4ad1..345f6c6 100644 --- a/UploaderSL-v2/SLHttpUploader/MainPage.xaml.cs +++ b/UploaderSL-v2/SLHttpUploader/MainPage.xaml.cs @@ -17,6 +17,8 @@ namespace SLHttpUploader public partial class MainPage : UserControl { Settings currentSettings; + int sequenceProgressCalls = 0; + int contentProgressCalls = 0; public MainPage() { @@ -46,8 +48,9 @@ private void uxSelectFiles_Click(object sender, RoutedEventArgs e) if (total <= currentSettings.MaxUploadSize) { HttpUtility utility = new HttpUtility(); - utility.UploadCompleted += new UploadedCompletedEventHandler(utility_UploadCompleted); - utility.ProgressReport += new ProgressReportedEventHandler(utility_ProgressReport); + utility.UploadCompleted += new Action(utility_UploadCompleted); + utility.FileSequenceProgressReport += new Action(utility_FileSequenceProgressReport); + utility.FileContentProgressReport += new Action(utility_FileContentProgressReport); callScriptStartup(); utility.PostFileContents(currentSettings.PostUrl, @@ -56,27 +59,32 @@ private void uxSelectFiles_Click(object sender, RoutedEventArgs e) this.Dispatcher); } else - utility_UploadCompleted(null, new UploadCompletedEventArgs() { Success = false, Error = new Exception("File size too large.") }); + utility_UploadCompleted(new UploadCompletedEventArgs() { Success = false, Error = new Exception("File size too large.") }); } } - int progressCalls = 0; + + void utility_FileContentProgressReport(ProgressReportEventArgs e) + { + contentProgressCalls++; + HtmlPage.Window.Invoke(currentSettings.ScriptContentProgressHandler, e.Percentage); + } void callScriptStartup() { HtmlPage.Window.Invoke(currentSettings.ScriptStartupHandler); } - void utility_ProgressReport(object sender, ProgressReportEventArgs e) + void utility_FileSequenceProgressReport(ProgressReportEventArgs e) { - progressCalls++; - HtmlPage.Window.Invoke(currentSettings.ScriptProgressHandler, e.Percentage); + sequenceProgressCalls++; + HtmlPage.Window.Invoke(currentSettings.ScriptSequenceProgressHandler, e.Percentage); } - void utility_UploadCompleted(object sender, UploadCompletedEventArgs e) + void utility_UploadCompleted(UploadCompletedEventArgs e) { HtmlPage.Window.Invoke(currentSettings.ScriptCompletedHandler, e.Success, e.Error != null ? e.Error.Message : string.Empty); } [ScriptableMember] - public void Setup(string url, int maxUploadSize, bool uploadFilesIndividually, string progressHandler, string completeHandler, string startupHandler, string customData, string buttonText) + public void Setup(string url, int maxUploadSize, bool uploadFilesIndividually, string sequenceProgressHandler, string contentProgressHandler, string completeHandler, string startupHandler, string customData, string buttonText) { if (!string.IsNullOrEmpty(buttonText)) this.uxSelectFiles.Content = buttonText; @@ -88,7 +96,8 @@ public void Setup(string url, int maxUploadSize, bool uploadFilesIndividually, s currentSettings.PostUrl = Utility.BaseUrl + url.TrimStart('/'); currentSettings.ScriptCompletedHandler = completeHandler; - currentSettings.ScriptProgressHandler = progressHandler; + currentSettings.ScriptSequenceProgressHandler = sequenceProgressHandler; + currentSettings.ScriptContentProgressHandler = contentProgressHandler; currentSettings.ScriptStartupHandler = startupHandler; //parse customData diff --git a/UploaderSL-v2/SLHttpUploader/PostRequestInfo.cs b/UploaderSL-v2/SLHttpUploader/PostRequestInfo.cs new file mode 100644 index 0000000..286f5ad --- /dev/null +++ b/UploaderSL-v2/SLHttpUploader/PostRequestInfo.cs @@ -0,0 +1,32 @@ +using System; +using System.Net; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Ink; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.Windows.Shapes; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Runtime.Serialization.Json; + +namespace SLHttpUploader +{ + public class PostRequestInfo + { + public IEnumerable Files { get; set; } + public Dictionary FormData { get; set; } + public FilePostBehavior Behavior { get; set; } + public WebRequest Request { get; set; } + public string Boundary { get; set; } + public System.Windows.Threading.Dispatcher Dispatcher { get; set; } + + //by chunk + public long ChunkProgress { get; set; } + public byte[] FileChunk { get; set; } + } +} diff --git a/UploaderSL-v2/SLHttpUploader/ProgressReportEventArgs.cs b/UploaderSL-v2/SLHttpUploader/ProgressReportEventArgs.cs new file mode 100644 index 0000000..eb6a378 --- /dev/null +++ b/UploaderSL-v2/SLHttpUploader/ProgressReportEventArgs.cs @@ -0,0 +1,36 @@ +using System; +using System.Net; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Ink; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.Windows.Shapes; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Runtime.Serialization.Json; + +namespace SLHttpUploader +{ + public class ProgressReportEventArgs : EventArgs + { + public long Current { get; set; } + public long Total { get; set; } + + public int Percentage + { + get + { + if (Total == 0) + return 0; + + double perc = (double)Current / (double)Total; + return (int)(perc * 100); + } + } + } +} diff --git a/UploaderSL-v2/SLHttpUploader/SLHttpUploader.csproj b/UploaderSL-v2/SLHttpUploader/SLHttpUploader.csproj index 214e0dc..859f946 100644 --- a/UploaderSL-v2/SLHttpUploader/SLHttpUploader.csproj +++ b/UploaderSL-v2/SLHttpUploader/SLHttpUploader.csproj @@ -78,13 +78,20 @@ App.xaml + + + + MainPage.xaml + + + diff --git a/UploaderSL-v2/SLHttpUploader/Settings.cs b/UploaderSL-v2/SLHttpUploader/Settings.cs index 1232886..ca6cd29 100644 --- a/UploaderSL-v2/SLHttpUploader/Settings.cs +++ b/UploaderSL-v2/SLHttpUploader/Settings.cs @@ -17,7 +17,8 @@ public class Settings public string PostUrl { get; set; } public bool UploadFilesIndividually { get; set; } public int MaxUploadSize { get; set; } - public string ScriptProgressHandler { get; set; } + public string ScriptSequenceProgressHandler { get; set; } + public string ScriptContentProgressHandler { get; set; } public string ScriptCompletedHandler { get; set; } public string ScriptStartupHandler { get; set; } public Dictionary CustomData { get; set; } diff --git a/UploaderSL-v2/SLHttpUploader/UploadCompletedEventArgs.cs b/UploaderSL-v2/SLHttpUploader/UploadCompletedEventArgs.cs new file mode 100644 index 0000000..9ff9c42 --- /dev/null +++ b/UploaderSL-v2/SLHttpUploader/UploadCompletedEventArgs.cs @@ -0,0 +1,24 @@ +using System; +using System.Net; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Ink; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.Windows.Shapes; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Runtime.Serialization.Json; + +namespace SLHttpUploader +{ + public class UploadCompletedEventArgs : EventArgs + { + public bool Success { get; set; } + public Exception Error { get; set; } + } +} diff --git a/UploaderSL-v2/Sample.Web/Controllers/ChunkInfo.cs b/UploaderSL-v2/Sample.Web/Controllers/ChunkInfo.cs new file mode 100644 index 0000000..4ab9de9 --- /dev/null +++ b/UploaderSL-v2/Sample.Web/Controllers/ChunkInfo.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Sample.Web.Controllers +{ + public class ChunkInfo + { + public string Filename { get; set; } + public Guid FileId { get; set; } + public ChunkSequence Sequence { get; set; } + public string Hash { get; set; } + } +} \ No newline at end of file diff --git a/UploaderSL-v2/Sample.Web/Controllers/ChunkSequence.cs b/UploaderSL-v2/Sample.Web/Controllers/ChunkSequence.cs new file mode 100644 index 0000000..6105832 --- /dev/null +++ b/UploaderSL-v2/Sample.Web/Controllers/ChunkSequence.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Sample.Web.Controllers +{ + public enum ChunkSequence + { + Body, + End + } +} diff --git a/UploaderSL-v2/Sample.Web/Controllers/HomeController.cs b/UploaderSL-v2/Sample.Web/Controllers/HomeController.cs index d0d8305..8621057 100644 --- a/UploaderSL-v2/Sample.Web/Controllers/HomeController.cs +++ b/UploaderSL-v2/Sample.Web/Controllers/HomeController.cs @@ -1,66 +1,108 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using System.Web.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Mvc; using System.IO; -namespace Sample.Web.Controllers -{ - public class HomeController : Controller - { - public ActionResult Index() - { - return View(); - } - - [HttpPost] - public JsonResult Upload() //optionally, custom data can be plugged in here - { - bool success = false; - - //handle these uploads the same as you would a normal File post. - try - { - for (int ix = 0; ix < Request.Files.Count; ix++) - { - var file = Request.Files[ix]; - - //TODO: file type check; don't accept executables or scripts - var filename = Path.GetFileName(file.FileName); - - //TODO: pick a destination folder. - //TODO: make sure the destination is a read-only & (non-executable) path to anonymous visitors & app pool identity - var destination = Server.MapPath("~/App_Data/uploads/"); - - - if (!Directory.Exists(destination)) - Directory.CreateDirectory(destination); - - var destinationFile = Path.Combine(destination, filename); - - //write the data. - using (var fs = System.IO.File.Open(destinationFile, FileMode.Create, FileAccess.Write)) - { - int read = 4096; - byte[] buffer = new byte[read]; - while (read > 0) - { - read = file.InputStream.Read(buffer, 0, buffer.Length); - if (read > 0) - fs.Write(buffer, 0, read); - } - } - } - - success = true; - } - catch (Exception ex) - { - //TODO: logging or failure response. - } - - return Json(new { Success=success }); - } - } -} +namespace Sample.Web.Controllers +{ + public class HomeController : Controller + { + // + // GET: /Home/ + + public ActionResult Index() + { + return View(); + } + + public ActionResult Upload() + { + try + { + if (Request.Files != null && Request.Files.Count > 0) + { + for (int ix = 0; ix < Request.Files.Count; ix++) + { + var file = Request.Files[ix]; + var path = Server.MapPath(string.Format("~/App_Data/{0}", file.FileName)); + var dir = Path.GetDirectoryName(path); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + using (var fs = System.IO.File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None)) + { + int read = 1; + byte[] buffer = new byte[4096]; + while (read > 0) + { + read = file.InputStream.Read(buffer, 0, buffer.Length); + if (read > 0) + fs.Write(buffer, 0, read); + } + } + } + } + } + catch (Exception ex) + { + return Json(new { Success=false, Message = ex.Message }); + } + return Json(new { Success = true }); + } + + [HttpPost] + public JsonResult UploadChunked(ChunkInfo post) + { + try + { + if (Request.Files.Count > 0) + { + var file = Server.MapPath("~/App_Data/" + post.FileId.ToString()); + AppendFileContent(file); + if (post.Sequence == ChunkSequence.End) + { + //validate full file hash. + string hash = null; + using (var fs = System.IO.File.OpenRead(file)) + hash = TextUtility.GetHashSHA256(fs); + if (hash == post.Hash) + { + //success... send file to storage. + System.IO.File.Move(file, Server.MapPath("~/App_Data/" + post.Filename)); + return Json(new { Success = true }); + } + else + { + //fail...resend. + return Json(new { Success = false, Message = "Hash fail" }); + } + } + } + } + catch (Exception ex) + { + return Json(new { Success = false, Message = ex.Message }); + } + return Json(new { Success = true }); + } + + private void AppendFileContent(string file) + { + var postedData = Request.Files[0]; + byte[] buffer = new byte[32*1024]; + int read = 1; + + using (var fs = System.IO.File.Open(file, FileMode.Append, FileAccess.Write, FileShare.None)) + { + while (read > 0) + { + read = postedData.InputStream.Read(buffer, 0, buffer.Length); + if (read > 0) + fs.Write(buffer, 0, read); + } + } + } + } +} diff --git a/UploaderSL-v2/Sample.Web/Sample.Web.csproj b/UploaderSL-v2/Sample.Web/Sample.Web.csproj index de48cf2..c2d00e6 100644 --- a/UploaderSL-v2/Sample.Web/Sample.Web.csproj +++ b/UploaderSL-v2/Sample.Web/Sample.Web.csproj @@ -59,11 +59,14 @@ + + Global.asax + diff --git a/UploaderSL-v2/Sample.Web/Scripts/jquery.uploader.js b/UploaderSL-v2/Sample.Web/Scripts/jquery.uploader.js index 5ac84f9..f8b4e53 100644 --- a/UploaderSL-v2/Sample.Web/Scripts/jquery.uploader.js +++ b/UploaderSL-v2/Sample.Web/Scripts/jquery.uploader.js @@ -1,112 +1,121 @@ -(function ($) { - var methods = { - init: function (options) { - - return this.each(function () { - if (options) { - $.extend($(this).uploader.settings, options); - } - }); - }, - progress: function (percent) { - this.uploader.settings.progress(percent); - return this; - }, - complete: function (success, message) { - this.uploader.settings.complete(success, message); - return this; - }, - starting: function () { - this.uploader.settings.starting(); - return this; - } - }; - - - $.fn.uploader = function (method) { - - if (methods[method]) { - return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); - } else if (typeof method === 'object' || !method) { - return methods.init.apply(this, arguments); - } else { - $.error('Method ' + method + ' does not exist on jQuery.uploader'); - } - - }; - - $.fn.uploader.settings = { - 'complete': function (success, message) { }, - 'progress': function (percent) { }, - 'starting': function () { }, - 'url': 'http://localhost/Test/Upload', - 'maxSize': 30720, - 'uploadInividually': true, - 'callbackCompleted': 'uploadComplete', - 'callbackProgress': 'uploadProgress', - 'callbackStarting': 'uploadStarting', - 'customData': null, - 'buttonText': 'Select Files' - }; -})(jQuery); - - -//default callbacks for this silverlight control -function uploadProgress(percent) { - $('#SilverlightControl').uploader('progress', percent); -} -function uploadComplete(success, message) { - $('#SilverlightControl').uploader('complete', success, message); -} -function uploadStarting() { - $('#SilverlightControl').uploader('starting'); -} - -function uploaderOnLoad() { - var settings = $('#SilverlightControl').uploader.settings; - var raw = document.getElementById("SilverlightControl"); - raw.content.page.Setup(settings.url, settings.maxSize, settings.uploadInividually, settings.callbackProgress, settings.callbackCompleted, settings.callbackStarting, serializeCustomData(settings.customData), settings.buttonText); -} - -function serializeCustomData(data) { - var s = ''; - var ix = 0; - for (property in data) { - if (ix > 0) - s += ';'; - s += property + ':' + data[property].toString(); - ix++; - } - return s; -} - -function onSilverlightError(sender, args) { - - var appSource = ""; - if (sender != null && sender != 0) { - appSource = sender.getHost().Source; - } - var errorType = args.ErrorType; - var iErrorCode = args.ErrorCode; - - var errMsg = "Unhandled Error in Silverlight 2 Application " + appSource + "\n"; - - errMsg += "Code: " + iErrorCode + " \n"; - errMsg += "Category: " + errorType + " \n"; - errMsg += "Message: " + args.ErrorMessage + " \n"; - - if (errorType == "ParserError") { - errMsg += "File: " + args.xamlFile + " \n"; - errMsg += "Line: " + args.lineNumber + " \n"; - errMsg += "Position: " + args.charPosition + " \n"; - } - else if (errorType == "RuntimeError") { - if (args.lineNumber != 0) { - errMsg += "Line: " + args.lineNumber + " \n"; - errMsg += "Position: " + args.charPosition + " \n"; - } - errMsg += "MethodName: " + args.methodName + " \n"; - } - - throw new Error(errMsg); -} +(function ($) { + var methods = { + init: function (options) { + + return this.each(function () { + if (options) { + $.extend($(this).uploader.settings, options); + } + }); + }, + sequenceProgress: function (percent) { + this.uploader.settings.sequenceProgress(percent); + return this; + }, + contentProgress: function (percent) { + this.uploader.settings.contentProgress(percent); + return this; + }, + complete: function (success, message) { + this.uploader.settings.complete(success, message); + return this; + }, + starting: function () { + this.uploader.settings.starting(); + return this; + } + }; + + + $.fn.uploader = function (method) { + + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || !method) { + return methods.init.apply(this, arguments); + } else { + $.error('Method ' + method + ' does not exist on jQuery.uploader'); + } + + }; + + $.fn.uploader.settings = { + 'complete': function (success, message) { }, + 'sequenceProgress': function (percent) { }, + 'contentProgress': function (percent) { }, + 'starting': function () { }, + 'url': 'http://localhost/Test/Upload', + 'maxSize': 30720, + 'uploadInividually': true, + 'callbackCompleted': 'uploadComplete', + 'callbackSequenceProgress': 'uploadSequenceProgress', + 'callbackContentProgress': 'uploadContentProgress', + 'callbackStarting': 'uploadStarting', + 'customData': null, + 'buttonText': 'Select Files' + }; +})(jQuery); + + +//default callbacks for this silverlight control +function uploadSequenceProgress(percent) { + $('#SilverlightControl').uploader('sequenceProgress', percent); +} +function uploadContentProgress(percent) { + $('#SilverlightControl').uploader('contentProgress', percent); +} +function uploadComplete(success, message) { + $('#SilverlightControl').uploader('complete', success, message); +} +function uploadStarting() { + $('#SilverlightControl').uploader('starting'); +} + +function uploaderOnLoad() { + var settings = $('#SilverlightControl').uploader.settings; + var raw = document.getElementById("SilverlightControl"); + raw.content.page.Setup(settings.url, settings.maxSize, settings.uploadInividually, settings.callbackSequenceProgress, settings.callbackContentProgress, settings.callbackCompleted, settings.callbackStarting, serializeCustomData(settings.customData), settings.buttonText); +} + +function serializeCustomData(data) { + var s = ''; + var ix = 0; + for (property in data) { + if (ix > 0) + s += ';'; + s += property + ':' + data[property].toString(); + ix++; + } + return s; +} + +function onSilverlightError(sender, args) { + + var appSource = ""; + if (sender != null && sender != 0) { + appSource = sender.getHost().Source; + } + var errorType = args.ErrorType; + var iErrorCode = args.ErrorCode; + + var errMsg = "Unhandled Error in Silverlight 2 Application " + appSource + "\n"; + + errMsg += "Code: " + iErrorCode + " \n"; + errMsg += "Category: " + errorType + " \n"; + errMsg += "Message: " + args.ErrorMessage + " \n"; + + if (errorType == "ParserError") { + errMsg += "File: " + args.xamlFile + " \n"; + errMsg += "Line: " + args.lineNumber + " \n"; + errMsg += "Position: " + args.charPosition + " \n"; + } + else if (errorType == "RuntimeError") { + if (args.lineNumber != 0) { + errMsg += "Line: " + args.lineNumber + " \n"; + errMsg += "Position: " + args.charPosition + " \n"; + } + errMsg += "MethodName: " + args.methodName + " \n"; + } + + throw new Error(errMsg); +} diff --git a/UploaderSL-v2/Sample.Web/TextUtility.cs b/UploaderSL-v2/Sample.Web/TextUtility.cs new file mode 100644 index 0000000..191a3a2 --- /dev/null +++ b/UploaderSL-v2/Sample.Web/TextUtility.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml.Serialization; +using System.Xml; +using System.IO; +using System.Security.Cryptography; + +namespace Sample.Web +{ + public static class TextUtility + { + public static string DecodeUTF8Binary(byte[] data) + { + if (data != null) + { + var enc = new UTF8Encoding(false, false); + return enc.GetString(data); + } + return null; + } + + public static byte[] EncodeUTF8Binary(string text) + { + if (string.IsNullOrEmpty(text)) + return null; + + var enc = new UTF8Encoding(false, false); + return enc.GetBytes(text); + } + + public static void SerializeXml(Stream output, object data) + { + XmlSerializer xs = new XmlSerializer(data.GetType()); + XmlWriterSettings settings = new XmlWriterSettings(); + settings.Encoding = new UTF8Encoding(false, false); + XmlWriter writer = XmlTextWriter.Create(output, settings); + xs.Serialize(writer, data); + writer.Flush(); + writer.Close(); + } + public static T DeserializeXml(string xml) where T : class, new() + { + object result = null; + + XmlSerializer xs = new XmlSerializer(typeof(T)); + using (MemoryStream ms = new MemoryStream(new UTF8Encoding(false, false).GetBytes(xml))) + { + result = xs.Deserialize(ms); + } + + return result as T; + } + + public static string GetHashSHA256(byte[] data) + { + System.Security.Cryptography.SHA256Managed sha = new System.Security.Cryptography.SHA256Managed(); + byte[] result = sha.ComputeHash(data); + return Convert.ToBase64String(result); + } + public static string GetHashSHA256(Stream data) + { + System.Security.Cryptography.SHA256Managed sha = new System.Security.Cryptography.SHA256Managed(); + byte[] result = sha.ComputeHash(data); + return Convert.ToBase64String(result); + } + public static string GetRandomString(int length) + { + //returns random string of length specified with characters including: 0-9, a-z, A-Z + char[] ca = new char[length]; + byte[] random = new Byte[length]; + //RNGCryptoServiceProvider is an implementation of a random number generator. + + RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider(); + rng.GetBytes(random); // The array is now filled with cryptographically strong random bytes. + + for (int i = 0; i < length; i++) + { + bool found = false; + int rand = (int)random[i]; + while (!found) + { + if (((rand >= 48) && (rand <= 57)) || ((rand >= 65) && (rand <= 90)) || ((rand >= 97) && (rand <= 122))) + { + found = true; + } + else + { + //get a new random int. + byte[] single = new byte[1]; + rng.GetBytes(single); + rand = single[0]; + } + } + char ci = (char)rand; + ca[i] = ci; + } + string s = new string(ca); + return s; + } + /// + /// Gets the top element of an XML string. + /// + /// + /// The XML string to extract the top element from. + /// + /// + /// The name of the first regular XML element. + /// + public static string GetTopElement(byte[] Xml) + { + using (MemoryStream ms = new MemoryStream(Xml)) + { + return GetTopElement(ms); + } + } + + /// + /// Gets the top element of an XML string. + /// + /// + /// The XML string to extract the top element from. + /// + /// + /// The name of the first regular XML element. + /// + public static string GetTopElement(string Xml) + { + return GetTopElement(System.Text.Encoding.UTF8.GetBytes(Xml)); + } + + /// + /// Gets the top element of an XML string. + /// + /// + /// The XML string to extract the top element from. + /// + /// + /// The name of the first regular XML element. + /// + public static string GetTopElement(Stream Xml) + { + //we know that network streams can have issues so we may want to add + //code to see if someone passed in a network stream + //For now we are going to run unit tests and see if any issues come back + + //set the begin postion so we can set the stream back when we are done. + long beginPos = Xml.Position; + Xml.Position = 0; + XmlTextReader XReader = new XmlTextReader(Xml); + XReader.WhitespaceHandling = WhitespaceHandling.None; + XReader.Read(); + XReader.Read(); + string RetVal = XReader.Name; + //Do not close the stream, we will still need it for additional + //operations. + //XReader.Close(); + //reposition the stream to where it started + Xml.Position = beginPos; + return RetVal; + } + public enum SerializationOptions + { + Default = 0, + ExcludeNamespaces = 1, + OmitDeclaration = 2 + } + + public static string GetFriendlyTimeSpanName(TimeSpan ts) + { + if (ts.TotalDays >= 3650) + return "never"; + else if (ts.TotalDays >= 730) + return ((int)(ts.TotalDays / 365)).ToString() + " years ago"; + else if (ts.TotalDays > 365) + return "a year ago"; + else if ((int)ts.TotalDays >= 60) + return ((int)(ts.TotalDays / 30)).ToString() + " months ago"; + else if ((int)ts.TotalDays > 30) + return "a month ago"; + else if ((int)ts.TotalDays > 1) + return ((int)ts.TotalDays).ToString() + " days ago"; + else if ((int)ts.TotalDays > 0) + return "yesterday"; + else if ((int)ts.TotalHours > 1) + return ((int)ts.TotalHours).ToString() + " hours ago"; + else if ((int)ts.TotalHours > 0) + return "an hour ago"; + else if ((int)ts.TotalMinutes > 1) + return ((int)ts.TotalMinutes).ToString() + " minutes ago"; + else if ((int)ts.TotalMinutes > 0) + return "a minute ago"; + + return "just now"; + } + + public static string GetFriendlyByteSize(int size) + { + if (size >= 1024) + return (size / 1024.0d).ToString("N0") + " kB"; + else if (size >= 1048576.0d) + return (size / 1048576.0d).ToString("N1") + " MB"; + + return size.ToString("N0") + " bytes"; + } + } +} diff --git a/UploaderSL-v2/Sample.Web/Views/Home/Index.cshtml b/UploaderSL-v2/Sample.Web/Views/Home/Index.cshtml index cfe9d05..9ec2d29 100644 --- a/UploaderSL-v2/Sample.Web/Views/Home/Index.cshtml +++ b/UploaderSL-v2/Sample.Web/Views/Home/Index.cshtml @@ -3,58 +3,36 @@ } @section Head { - + - - + google.load("jqueryui", "1.8"); + + + }

Index

-

Upload new file

- +
+
+ + + + + + + + + Get Microsoft Silverlight + + + +
+
+
+