Created
April 16, 2012 23:41
-
-
Save pauldbau/2402377 to your computer and use it in GitHub Desktop.
Quick and dirty async file upload using a copy of ServiceStack AsyncServiceClient as a base. Hardcoded to expect a JSON response. This could be used as a reference for integrating into ServiceStack properly, just don't have the time to do so personally
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.IO; | |
using System.Net; | |
using System.Text; | |
using System.Threading; | |
using ServiceStack.Common.Web; | |
using ServiceStack.Logging; | |
using ServiceStack.ServiceHost; | |
using ServiceStack.Text; | |
using ServiceStack.ServiceClient.Web; | |
namespace ServiceStack.ServiceClient.Web | |
{ | |
/// <summary> | |
/// This is a stripped down copy of the standard ServiceStack AsyncServiceClient, | |
/// modified to upload files as a multipart form via HTTP POST | |
/// </summary> | |
public class AsyncFileUploadClient | |
{ | |
private static readonly ILog Log = LogManager.GetLogger(typeof(AsyncFileUploadClient)); | |
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60); | |
public static Action<HttpWebRequest> HttpWebRequestFilter { get; set; } | |
const int BufferSize = 4096; | |
public ICredentials Credentials { get; set; } | |
public bool StoreCookies { get; set; } | |
public CookieContainer CookieContainer { get; set; } | |
public string BaseUri { get; set; } | |
internal class RequestState<TResponse> : IDisposable | |
{ | |
public RequestState() | |
{ | |
BufferRead = new byte[BufferSize]; | |
TextData = new StringBuilder(); | |
BytesData = new MemoryStream(BufferSize); | |
WebRequest = null; | |
ResponseStream = null; | |
} | |
public string HttpMethod; | |
public string Url; | |
public StringBuilder TextData; | |
public MemoryStream BytesData; | |
public byte[] BufferRead; | |
public object Request; | |
public HttpWebRequest WebRequest; | |
public HttpWebResponse WebResponse; | |
public Stream ResponseStream; | |
public int Completed; | |
public int RequestCount; | |
public Timer Timer; | |
public string FileName; // Added to hold file name | |
public string MimeType; // Added to hold mime type | |
public string Boundary; // Added to hold boundary string | |
public Action<TResponse> OnSuccess; | |
public Action<TResponse, Exception> OnError; | |
#if SILVERLIGHT && !WINDOWS_PHONE && !MONODROID | |
public bool HandleCallbackOnUIThread { get; set; } | |
#endif | |
public void HandleSuccess(TResponse response) | |
{ | |
if (this.OnSuccess == null) | |
return; | |
#if SILVERLIGHT && !WINDOWS_PHONE && !MONODROID | |
if (this.HandleCallbackOnUIThread) | |
System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() => this.OnSuccess(response)); | |
else | |
this.OnSuccess(response); | |
#else | |
this.OnSuccess(response); | |
#endif | |
} | |
public void HandleError(TResponse response, Exception ex) | |
{ | |
if (this.OnError == null) | |
return; | |
#if SILVERLIGHT && !WINDOWS_PHONE && !MONODROID | |
if (this.HandleCallbackOnUIThread) | |
System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() => this.OnError(response, ex)); | |
else | |
this.OnError(response, ex); | |
#else | |
OnError(response, ex); | |
#endif | |
} | |
public void StartTimer(TimeSpan timeOut) | |
{ | |
this.Timer = new Timer(this.TimedOut, this, (int)timeOut.TotalMilliseconds, System.Threading.Timeout.Infinite); | |
} | |
public void TimedOut(object state) | |
{ | |
if (Interlocked.Increment(ref Completed) == 1) | |
{ | |
if (this.WebRequest != null) | |
{ | |
this.WebRequest.Abort(); | |
} | |
} | |
this.Timer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); | |
this.Timer.Dispose(); | |
this.Dispose(); | |
} | |
public void Dispose() | |
{ | |
if (this.BytesData == null) return; | |
this.BytesData.Dispose(); | |
this.BytesData = null; | |
} | |
} | |
public string UserName { get; set; } | |
public string Password { get; set; } | |
public void SetCredentials(string userName, string password) | |
{ | |
this.UserName = userName; | |
this.Password = password; | |
} | |
public TimeSpan? Timeout { get; set; } | |
public string ContentType { get; set; } | |
public StreamSerializerDelegate StreamSerializer { get; set; } | |
public StreamDeserializerDelegate StreamDeserializer { get; set; } | |
#if SILVERLIGHT && !WINDOWS_PHONE && !MONODROID | |
public bool HandleCallbackOnUIThread { get; set; } | |
public bool UseBrowserHttpHandling { get; set; } | |
public bool ShareCookiesWithBrowser { get; set; } | |
#endif | |
/// <summary> | |
/// Writes out a multipart form request, including file upload | |
/// NOTE: This method closes the fileStream to avoid callback nonsense | |
/// </summary> | |
private void SerializeToMultiPartForm(Stream fileStream, string fileName, string mimeType, string boundary, Stream postStream) | |
{ | |
try | |
{ | |
var boundarybytes = System.Text.Encoding.UTF8.GetBytes("\r\n--" + boundary + "\r\n"); | |
var headerTemplate = "\r\n--" + boundary + | |
"\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{0}\"\r\nContent-Type: {1}\r\n\r\n"; | |
var header = string.Format(headerTemplate, fileName, mimeType); | |
var headerbytes = System.Text.Encoding.UTF8.GetBytes(header); | |
// ContentLength is not available in WP7 | |
//httpReq.ContentLength = fileStream.Length + headerbytes.Length + boundarybytes.Length; | |
postStream.Write(headerbytes, 0, headerbytes.Length); | |
byte[] buffer = new byte[4096]; | |
int byteCount; | |
while ((byteCount = fileStream.Read(buffer, 0, 4096)) > 0) | |
{ | |
postStream.Write(buffer, 0, byteCount); | |
} | |
postStream.Write(boundarybytes, 0, boundarybytes.Length); | |
} | |
finally { fileStream.Close(); } | |
} | |
public void PostFileAsync<TResponse>(string absoluteUrl, Stream fileStream, string fileName, string mimeType, | |
Action<TResponse> onSuccess, Action<TResponse, Exception> onError) | |
{ | |
// Expect Json response | |
this.StreamDeserializer = JsonSerializer.DeserializeFromStream; | |
this.ContentType = "application/json"; | |
var requestUri = absoluteUrl; | |
#if SILVERLIGHT && !WINDOWS_PHONE && !MONODROID | |
var creator = this.UseBrowserHttpHandling | |
? System.Net.Browser.WebRequestCreator.BrowserHttp | |
: System.Net.Browser.WebRequestCreator.ClientHttp; | |
var webRequest = (HttpWebRequest)creator.Create(new Uri(requestUri)); | |
if (StoreCookies && !UseBrowserHttpHandling) | |
{ | |
if (ShareCookiesWithBrowser) | |
{ | |
if (CookieContainer == null) | |
CookieContainer = new CookieContainer(); | |
CookieContainer.SetCookies(new Uri(BaseUri), System.Windows.Browser.HtmlPage.Document.Cookies); | |
} | |
webRequest.CookieContainer = CookieContainer; | |
} | |
#else | |
var webRequest = (HttpWebRequest)WebRequest.Create(requestUri); | |
if (StoreCookies) | |
{ | |
webRequest.CookieContainer = CookieContainer; | |
} | |
#endif | |
var requestState = new RequestState<TResponse> | |
{ | |
HttpMethod = HttpMethod.Post, | |
Url = requestUri, | |
WebRequest = webRequest, | |
Request = fileStream, | |
FileName = fileName, | |
MimeType = mimeType, | |
Boundary = "----------------------------" + DateTime.Now.Ticks.ToString("x"), | |
OnSuccess = onSuccess, | |
OnError = onError, | |
#if SILVERLIGHT && !WINDOWS_PHONE && !MONODROID | |
HandleCallbackOnUIThread = HandleCallbackOnUIThread, | |
#endif | |
}; | |
webRequest.ContentType = "multipart/form-data; boundary=" + requestState.Boundary; | |
requestState.StartTimer(this.Timeout.GetValueOrDefault(DefaultTimeout)); | |
SendWebRequestAsync(HttpMethod.Post, fileStream, requestState, webRequest); | |
} | |
private void SendWebRequestAsync<TResponse>(string httpMethod, object request, | |
RequestState<TResponse> requestState, HttpWebRequest webRequest) | |
{ | |
var httpGetOrDelete = (httpMethod == "GET" || httpMethod == "DELETE"); | |
webRequest.Accept = string.Format("{0}, */*", ContentType); | |
#if !SILVERLIGHT || WINDOWS_PHONE || MONODROID | |
webRequest.Method = httpMethod; | |
#else | |
//Methods others than GET and POST are only supported by Client request creator, see | |
//http://msdn.microsoft.com/en-us/library/cc838250(v=vs.95).aspx | |
if (this.UseBrowserHttpHandling && httpMethod != "GET" && httpMethod != "POST") | |
{ | |
webRequest.Method = "POST"; | |
webRequest.Headers[HttpHeaders.XHttpMethodOverride] = httpMethod; | |
} | |
else | |
{ | |
webRequest.Method = httpMethod; | |
} | |
#endif | |
if (this.Credentials != null) | |
{ | |
webRequest.Credentials = this.Credentials; | |
} | |
if (HttpWebRequestFilter != null) | |
{ | |
HttpWebRequestFilter(webRequest); | |
} | |
if (!httpGetOrDelete && request != null) | |
{ | |
// ContentType is set to multipart earlier in PostFileAsync, so don't overwrite | |
if (webRequest.ContentType == null) webRequest.ContentType = ContentType; | |
webRequest.BeginGetRequestStream(RequestCallback<TResponse>, requestState); | |
} | |
else | |
{ | |
requestState.WebRequest.BeginGetResponse(ResponseCallback<TResponse>, requestState); | |
} | |
} | |
private void RequestCallback<T>(IAsyncResult asyncResult) | |
{ | |
var requestState = (RequestState<T>)asyncResult.AsyncState; | |
try | |
{ | |
var req = requestState.WebRequest; | |
var postStream = req.EndGetRequestStream(asyncResult); | |
//StreamSerializer(null, requestState.Request, postStream); | |
SerializeToMultiPartForm(requestState.Request as Stream, requestState.FileName, requestState.MimeType, requestState.Boundary, postStream); | |
postStream.Close(); | |
requestState.WebRequest.BeginGetResponse(ResponseCallback<T>, requestState); | |
} | |
catch (Exception ex) | |
{ | |
HandleResponseError(ex, requestState); | |
} | |
} | |
private void ResponseCallback<T>(IAsyncResult asyncResult) | |
{ | |
var requestState = (RequestState<T>)asyncResult.AsyncState; | |
try | |
{ | |
var webRequest = requestState.WebRequest; | |
requestState.WebResponse = (HttpWebResponse)webRequest.EndGetResponse(asyncResult); | |
// Read the response into a Stream object. | |
var responseStream = requestState.WebResponse.GetResponseStream(); | |
requestState.ResponseStream = responseStream; | |
responseStream.BeginRead(requestState.BufferRead, 0, BufferSize, ReadCallBack<T>, requestState); | |
return; | |
} | |
catch (Exception ex) | |
{ | |
var firstCall = Interlocked.Increment(ref requestState.RequestCount) == 1; | |
if (firstCall && WebRequestUtils.ShouldAuthenticate(ex, this.UserName, this.Password)) | |
{ | |
try | |
{ | |
requestState.WebRequest = (HttpWebRequest)WebRequest.Create(requestState.Url); | |
requestState.WebRequest.AddBasicAuth(this.UserName, this.Password); | |
SendWebRequestAsync( | |
requestState.HttpMethod, requestState.Request, | |
requestState, requestState.WebRequest); | |
} | |
catch (Exception /*subEx*/) | |
{ | |
HandleResponseError(ex, requestState); | |
} | |
return; | |
} | |
HandleResponseError(ex, requestState); | |
} | |
} | |
private void ReadCallBack<T>(IAsyncResult asyncResult) | |
{ | |
var requestState = (RequestState<T>)asyncResult.AsyncState; | |
try | |
{ | |
var responseStream = requestState.ResponseStream; | |
int read = responseStream.EndRead(asyncResult); | |
if (read > 0) | |
{ | |
requestState.BytesData.Write(requestState.BufferRead, 0, read); | |
responseStream.BeginRead( | |
requestState.BufferRead, 0, BufferSize, ReadCallBack<T>, requestState); | |
return; | |
} | |
Interlocked.Increment(ref requestState.Completed); | |
var response = default(T); | |
try | |
{ | |
requestState.BytesData.Position = 0; | |
using (var reader = requestState.BytesData) | |
{ | |
response = (T)this.StreamDeserializer(typeof(T), reader); | |
} | |
#if SILVERLIGHT && !WINDOWS_PHONE && !MONODROID | |
if (this.StoreCookies && this.ShareCookiesWithBrowser && !this.UseBrowserHttpHandling) | |
{ | |
// browser cookies must be set on the ui thread | |
System.Windows.Deployment.Current.Dispatcher.BeginInvoke( | |
() => | |
{ | |
var cookieHeader = this.CookieContainer.GetCookieHeader(new Uri(BaseUri)); | |
System.Windows.Browser.HtmlPage.Document.Cookies = cookieHeader; | |
}); | |
} | |
#endif | |
requestState.HandleSuccess(response); | |
} | |
catch (Exception ex) | |
{ | |
Log.Debug(string.Format("Error Reading Response Error: {0}", ex.Message), ex); | |
requestState.HandleError(default(T), ex); | |
} | |
finally | |
{ | |
responseStream.Close(); | |
} | |
} | |
catch (Exception ex) | |
{ | |
HandleResponseError(ex, requestState); | |
} | |
} | |
private void HandleResponseError<TResponse>(Exception exception, RequestState<TResponse> requestState) | |
{ | |
var webEx = exception as WebException; | |
if (webEx != null | |
#if !SILVERLIGHT || WINDOWS_PHONE || MONODROID | |
&& webEx.Status == WebExceptionStatus.ProtocolError | |
#endif | |
) | |
{ | |
var errorResponse = ((HttpWebResponse)webEx.Response); | |
Log.Error(webEx); | |
Log.DebugFormat("Status Code : {0}", errorResponse.StatusCode); | |
Log.DebugFormat("Status Description : {0}", errorResponse.StatusDescription); | |
var serviceEx = new WebServiceException(errorResponse.StatusDescription) | |
{ | |
StatusCode = (int)errorResponse.StatusCode, | |
}; | |
try | |
{ | |
using (var stream = errorResponse.GetResponseStream()) | |
{ | |
//Uncomment to Debug exceptions: | |
//var strResponse = new StreamReader(stream).ReadToEnd(); | |
//Console.WriteLine("Response: " + strResponse); | |
//stream.Position = 0; | |
serviceEx.ResponseDto = this.StreamDeserializer(typeof(TResponse), stream); | |
requestState.HandleError((TResponse)serviceEx.ResponseDto, serviceEx); | |
} | |
} | |
catch (Exception innerEx) | |
{ | |
// Oh, well, we tried | |
Log.Debug(string.Format("WebException Reading Response Error: {0}", innerEx.Message), innerEx); | |
requestState.HandleError(default(TResponse), new WebServiceException(errorResponse.StatusDescription, innerEx) | |
{ | |
StatusCode = (int)errorResponse.StatusCode, | |
}); | |
} | |
return; | |
} | |
var authEx = exception as AuthenticationException; | |
if (authEx != null) | |
{ | |
var customEx = WebRequestUtils.CreateCustomException(requestState.Url, authEx); | |
Log.Debug(string.Format("AuthenticationException: {0}", customEx.Message), customEx); | |
requestState.HandleError(default(TResponse), authEx); | |
} | |
Log.Debug(string.Format("Exception Reading Response Error: {0}", exception.Message), exception); | |
requestState.HandleError(default(TResponse), exception); | |
} | |
public void Dispose() { } | |
} | |
#if MONOTOUCH | |
/// <summary> | |
/// Copied from ServiceStack due to Monotouch version missing the class. Needed for enabling the above to compile | |
/// </summary> | |
public class AuthenticationException : Exception | |
{ | |
public AuthenticationException() | |
{ | |
} | |
public AuthenticationException(string message) | |
: base(message) | |
{ | |
} | |
public AuthenticationException(string message, Exception innerException) | |
: base(message, innerException) | |
{ | |
} | |
} | |
#endif | |
/// <summary> | |
/// Copied from ServiceStack due to class being internal. Needed for enabling the above to compile | |
/// </summary> | |
internal static class WebRequestUtils | |
{ | |
internal static AuthenticationException CreateCustomException(string uri, AuthenticationException ex) | |
{ | |
if (uri.StartsWith("https")) | |
{ | |
return new AuthenticationException( | |
string.Format("Invalid remote SSL certificate, overide with: \nServicePointManager.ServerCertificateValidationCallback += ((sender, certificate, chain, sslPolicyErrors) => isValidPolicy);"), | |
ex); | |
} | |
return null; | |
} | |
internal static bool ShouldAuthenticate(Exception ex, string userName, string password) | |
{ | |
var webEx = ex as WebException; | |
return (webEx != null | |
&& webEx.Response != null | |
&& ((HttpWebResponse)webEx.Response).StatusCode == HttpStatusCode.Unauthorized | |
&& !string.IsNullOrEmpty(userName) | |
&& !string.IsNullOrEmpty(password)); | |
} | |
internal static void AddBasicAuth(this WebRequest client, string userName, string password) | |
{ | |
client.Headers[HttpHeaders.Authorization] | |
= "basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(userName + ":" + password)); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment