Skip to content

Instantly share code, notes, and snippets.

@dbeattie71
Forked from pauldbau/AsyncFileUploadClient.cs
Created April 17, 2012 01:00
Show Gist options
  • Save dbeattie71/2402664 to your computer and use it in GitHub Desktop.
Save dbeattie71/2402664 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
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