backlog! (also indefinitely abandoning this)
This commit is contained in:
parent
e135ad7159
commit
1ad0dab43c
10 changed files with 449 additions and 198 deletions
|
@ -706,7 +706,7 @@ namespace Maki
|
|||
{
|
||||
wr.Perform();
|
||||
|
||||
if (wr.Status != 200 || wr.Response.Length < 1)
|
||||
if (wr.Status != 200 || wr.ResponseString.Length < 1)
|
||||
throw new DiscordException("Failed to load message from API");
|
||||
|
||||
message = wr.ResponseJson<Message>();
|
||||
|
|
|
@ -58,7 +58,7 @@ namespace Maki
|
|||
|
||||
wr.Perform();
|
||||
|
||||
if (wr.Status != 200 || wr.Response.Length < 1)
|
||||
if (wr.Status != 200 || wr.ResponseString.Length < 1)
|
||||
// TODO: elaborate
|
||||
//throw new DiscordException("Failed to send message");
|
||||
return null;
|
||||
|
|
|
@ -60,7 +60,7 @@ namespace Maki
|
|||
});
|
||||
wr.Perform();
|
||||
|
||||
if (wr.Status != 200 || wr.Response.Length < 1)
|
||||
if (wr.Status != 200 || wr.ResponseString.Length < 1)
|
||||
// TODO: elaborate
|
||||
throw new DiscordException("Failed to edit role");
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ namespace Maki
|
|||
});
|
||||
wr.Perform();
|
||||
|
||||
if (wr.Status != 200 || wr.Response.Length < 1)
|
||||
if (wr.Status != 200 || wr.ResponseString.Length < 1)
|
||||
throw new DiscordException("Failed to create role");
|
||||
|
||||
roleStruct = wr.ResponseJson<Role>();
|
||||
|
|
|
@ -35,6 +35,15 @@
|
|||
<NoWarn>0649</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Threading.Tasks, Version=1.0.12.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Threading.Tasks.Extensions, Version=1.0.12.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Threading.Tasks.Extensions.Desktop, Version=1.0.168.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>$(SolutionDir)\packages\Newtonsoft.Json.10.0.3\lib\net40\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
|
@ -42,6 +51,7 @@
|
|||
<Reference Include="System.IO, Version=2.6.10.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Microsoft.Bcl.1.1.10\lib\net40\System.IO.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Net" />
|
||||
<Reference Include="System.Net.Http, Version=2.2.29.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Microsoft.Net.Http.2.2.29\lib\net40\System.Net.Http.dll</HintPath>
|
||||
</Reference>
|
||||
|
|
|
@ -5,8 +5,10 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Maki.Rest
|
||||
{
|
||||
|
@ -15,47 +17,95 @@ namespace Maki.Rest
|
|||
private const string USER_AGENT = @"DiscordBot (https://github.com/flashwave/maki, 1.0.0.0)";
|
||||
private const string GENERIC_CONTENT_TYPE = @"application/octet-stream";
|
||||
private const string JSON_CONTENT_TYPE = @"application/json";
|
||||
private const string FORM_CONTENT_TYPE = @"multipart/form-data";
|
||||
|
||||
private const long BUFFER_SIZE = 8192000;
|
||||
private const int MAX_RETRIES = 1;
|
||||
private const int TIMEOUT = 10000;
|
||||
|
||||
public int Timeout { get; set; } = TIMEOUT;
|
||||
|
||||
public event Action Started;
|
||||
public event Action Finished;
|
||||
public event Action<Exception> Failed;
|
||||
|
||||
public event Action<long, long> DownloadProgress;
|
||||
public event Action<long, long> UploadProgress;
|
||||
|
||||
public bool IsAborted { get; private set; }
|
||||
|
||||
public string Accept { get; set; }
|
||||
|
||||
private bool PrivateCompleted;
|
||||
public bool IsCompleted
|
||||
{
|
||||
get => PrivateCompleted;
|
||||
private set
|
||||
{
|
||||
PrivateCompleted = value;
|
||||
|
||||
if (!PrivateCompleted)
|
||||
return;
|
||||
|
||||
Started = null;
|
||||
Finished = null;
|
||||
DownloadProgress = null;
|
||||
UploadProgress = null;
|
||||
}
|
||||
}
|
||||
|
||||
private string PrivateUrl;
|
||||
private string Url
|
||||
{
|
||||
get => PrivateUrl;
|
||||
set
|
||||
{
|
||||
if (!value.StartsWith(@"http://") && !value.StartsWith(@"https://"))
|
||||
value = RestEndpoints.BASE_URL + RestEndpoints.BASE_PATH + value;
|
||||
|
||||
PrivateUrl = value;
|
||||
}
|
||||
}
|
||||
|
||||
private const int BUFFER_SIZE = 4096;
|
||||
private byte[] Buffer;
|
||||
|
||||
private static HttpClient HttpClient;
|
||||
|
||||
public readonly HttpMethod Method;
|
||||
public readonly string Url;
|
||||
|
||||
public string UserAgent { get; set; } = USER_AGENT;
|
||||
private bool HasBody => Method == HttpMethod.PUT || Method == HttpMethod.POST || Method == HttpMethod.PATCH;
|
||||
|
||||
public string ContentType { get; set; } = GENERIC_CONTENT_TYPE;
|
||||
public long ContentLength => HttpWebResponse.ContentLength < 1 ? BUFFER_SIZE : HttpWebResponse.ContentLength;
|
||||
|
||||
// TODO: make this not static
|
||||
[Obsolete]
|
||||
public long ContentLength => Response.Content.Headers.ContentLength ?? BytesDownloaded;
|
||||
|
||||
[Obsolete]
|
||||
internal static string Authorisation { get; set; }
|
||||
|
||||
private readonly Dictionary<string, string> Headers = new Dictionary<string, string>();
|
||||
private readonly Dictionary<string, string> Parameters = new Dictionary<string, string>();
|
||||
private readonly Dictionary<string, byte[]> Files = new Dictionary<string, byte[]>();
|
||||
|
||||
private readonly Dictionary<string, string> MimeTypes = new Dictionary<string, string>()
|
||||
{
|
||||
{ "png", "image/png" },
|
||||
{ "jpg", "image/jpeg" },
|
||||
{ "jpeg", "image/jpeg" },
|
||||
{ "gif", "image/gif" },
|
||||
};
|
||||
private long BytesUploaded = 0;
|
||||
private long BytesDownloaded = 0;
|
||||
|
||||
private HttpResponseMessage Response;
|
||||
|
||||
private MemoryStream RawRequestBody;
|
||||
|
||||
private byte[] RawRequestBody = new byte[0];
|
||||
private HttpWebRequest HttpWebRequest;
|
||||
private Stream RequestStream;
|
||||
private HttpWebResponse HttpWebResponse;
|
||||
private Stream ResponseStream;
|
||||
|
||||
private byte[] RawResponseValue;
|
||||
public byte[] RawResponse
|
||||
private CancellationTokenSource AbortToken;
|
||||
private CancellationTokenSource TimeoutToken;
|
||||
|
||||
public int Retries { get; private set; } = 0;
|
||||
|
||||
private byte[] PrivateResponseBytes;
|
||||
public byte[] ResponseBytes
|
||||
{
|
||||
get
|
||||
{
|
||||
if (RawResponseValue == null)
|
||||
if (PrivateResponseBytes == null)
|
||||
using (MemoryStream ms = new MemoryStream())
|
||||
{
|
||||
byte[] bytes = new byte[4096];
|
||||
|
@ -65,36 +115,39 @@ namespace Maki.Rest
|
|||
ms.Write(bytes, 0, read);
|
||||
|
||||
ms.Seek(0, SeekOrigin.Begin);
|
||||
RawResponseValue = new byte[ms.Length];
|
||||
ms.Read(RawResponseValue, 0, RawResponseValue.Length);
|
||||
PrivateResponseBytes = new byte[ms.Length];
|
||||
ms.Read(PrivateResponseBytes, 0, PrivateResponseBytes.Length);
|
||||
}
|
||||
|
||||
return RawResponseValue;
|
||||
return PrivateResponseBytes;
|
||||
}
|
||||
}
|
||||
|
||||
private string ResponseString = string.Empty;
|
||||
|
||||
public string Response
|
||||
private string PrivateResponseString = string.Empty;
|
||||
public string ResponseString
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(ResponseString))
|
||||
ResponseString = Encoding.UTF8.GetString(RawResponse);
|
||||
if (string.IsNullOrEmpty(PrivateResponseString))
|
||||
PrivateResponseString = Encoding.UTF8.GetString(ResponseBytes);
|
||||
|
||||
return ResponseString;
|
||||
return PrivateResponseString;
|
||||
}
|
||||
}
|
||||
|
||||
public T ResponseJson<T>() =>
|
||||
JsonConvert.DeserializeObject<T>(Response);
|
||||
JsonConvert.DeserializeObject<T>(ResponseString);
|
||||
|
||||
public short Status =>
|
||||
(short)HttpWebResponse?.StatusCode;
|
||||
[Obsolete]
|
||||
public short Status => (short)Response?.StatusCode;
|
||||
|
||||
static WebRequest()
|
||||
=> CreateHttpClientInstance();
|
||||
|
||||
public WebRequest(HttpMethod method, string url)
|
||||
{
|
||||
CreateHttpClientInstance();
|
||||
Method = method;
|
||||
Url = url;
|
||||
}
|
||||
|
||||
private static void CreateHttpClientInstance()
|
||||
|
@ -108,17 +161,25 @@ namespace Maki.Rest
|
|||
|
||||
HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT);
|
||||
HttpClient.DefaultRequestHeaders.ExpectContinue = true;
|
||||
HttpClient.Timeout = new TimeSpan(0, 0, 0, 0, Timeout.Infinite);
|
||||
HttpClient.Timeout = new TimeSpan(0, 0, 0, 0, System.Threading.Timeout.Infinite);
|
||||
}
|
||||
|
||||
public WebRequest(HttpMethod method, string url)
|
||||
public void AddRaw(Stream stream)
|
||||
{
|
||||
Method = method;
|
||||
Url = url;
|
||||
if (stream == null)
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
|
||||
RawRequestBody?.Dispose();
|
||||
RawRequestBody = new MemoryStream();
|
||||
|
||||
stream.CopyTo(RawRequestBody);
|
||||
}
|
||||
|
||||
public void AddRaw(byte[] bytes) =>
|
||||
RawRequestBody = bytes;
|
||||
public void AddRaw(byte[] bytes)
|
||||
{
|
||||
using (MemoryStream ms = new MemoryStream(bytes))
|
||||
AddRaw(ms);
|
||||
}
|
||||
|
||||
public void AddRaw(string str) =>
|
||||
AddRaw(Encoding.UTF8.GetBytes(str));
|
||||
|
@ -135,144 +196,293 @@ namespace Maki.Rest
|
|||
public void AddFile(string name, byte[] bytes) =>
|
||||
Files.Add(name, bytes);
|
||||
|
||||
public void Perform()
|
||||
public void AddHeader(string name, string value)
|
||||
{
|
||||
StringBuilder urlBuilder = new StringBuilder();
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
|
||||
if (!Url.StartsWith("http://") && !Url.StartsWith("https://"))
|
||||
{
|
||||
urlBuilder.Append(RestEndpoints.BASE_URL);
|
||||
urlBuilder.Append(RestEndpoints.BASE_PATH);
|
||||
}
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
|
||||
urlBuilder.Append(Url);
|
||||
|
||||
if (Method == HttpMethod.GET
|
||||
|| Method == HttpMethod.DELETE)
|
||||
if (Parameters.Count > 1)
|
||||
{
|
||||
if (!Url.Contains('?'))
|
||||
urlBuilder.Append(@"?");
|
||||
|
||||
foreach (KeyValuePair<string, string> param in Parameters)
|
||||
urlBuilder.Append($@"{param.Key}={param.Value}&");
|
||||
}
|
||||
|
||||
string url = urlBuilder.ToString().TrimEnd('&');
|
||||
|
||||
HttpWebRequest = System.Net.WebRequest.Create(url) as HttpWebRequest;
|
||||
HttpWebRequest.Method = Method.ToString();
|
||||
HttpWebRequest.UserAgent = UserAgent;
|
||||
HttpWebRequest.KeepAlive = true;
|
||||
HttpWebRequest.ReadWriteTimeout = Timeout.Infinite;
|
||||
HttpWebRequest.Timeout = Timeout.Infinite;
|
||||
|
||||
if (!string.IsNullOrEmpty(Authorisation) && url.StartsWith(RestEndpoints.BASE_URL + RestEndpoints.BASE_PATH))
|
||||
HttpWebRequest.Headers[HttpRequestHeader.Authorization] = Authorisation;
|
||||
|
||||
foreach (KeyValuePair<string, string> header in Headers)
|
||||
HttpWebRequest.Headers[header.Key] = header.Value;
|
||||
|
||||
if (Method == HttpMethod.POST
|
||||
|| Method == HttpMethod.PUT
|
||||
|| Method == HttpMethod.PATCH)
|
||||
{
|
||||
RequestStream = HttpWebRequest.GetRequestStream();
|
||||
|
||||
if (Parameters.Count + Files.Count < 1)
|
||||
RequestStream.Write(RawRequestBody, 0, RawRequestBody.Length);
|
||||
if (Headers.ContainsKey(name))
|
||||
Headers[name] = value;
|
||||
else
|
||||
Headers.Add(name, value);
|
||||
}
|
||||
|
||||
public void Abort()
|
||||
{
|
||||
string boundary = $@"-----------------------------{DateTime.Now.Ticks}";
|
||||
ContentType = $@"{FORM_CONTENT_TYPE}; boundary={boundary}";
|
||||
|
||||
if (Parameters.Count >= 1)
|
||||
{
|
||||
StringBuilder postBodyBuilder = new StringBuilder();
|
||||
byte[] postBody = new byte[0];
|
||||
|
||||
foreach (KeyValuePair<string, string> param in Parameters)
|
||||
{
|
||||
postBodyBuilder.AppendLine($@"--{boundary}");
|
||||
postBodyBuilder.AppendLine($@"Content-Disposition: form-data; name=""{param.Key}""");
|
||||
postBodyBuilder.AppendLine();
|
||||
postBodyBuilder.AppendLine(param.Value);
|
||||
}
|
||||
|
||||
postBody = Encoding.UTF8.GetBytes(postBodyBuilder.ToString());
|
||||
RequestStream.Write(postBody, 0, postBody.Length);
|
||||
}
|
||||
|
||||
if (Files.Count >= 1)
|
||||
{
|
||||
byte[] boundaryBytes = Encoding.UTF8.GetBytes($@"--{boundary}");
|
||||
byte[] newLineBytes = Encoding.UTF8.GetBytes("\r\n");
|
||||
|
||||
foreach (KeyValuePair<string, byte[]> file in Files)
|
||||
{
|
||||
string cType = GENERIC_CONTENT_TYPE;
|
||||
string fileExt = Path.GetExtension(file.Key).ToLower().TrimStart('.');
|
||||
|
||||
if (MimeTypes.ContainsKey(fileExt))
|
||||
cType = MimeTypes[fileExt];
|
||||
|
||||
byte[] cDisposBytes = Encoding.UTF8.GetBytes($@"Content-Disposition: form-data; name=""{file.Key}""; filename=""{file.Key}""");
|
||||
byte[] cTypeBytes = Encoding.UTF8.GetBytes($@"Content-Type: {cType}");
|
||||
|
||||
// Boundary + newline
|
||||
RequestStream.Write(boundaryBytes, 0, boundaryBytes.Length);
|
||||
RequestStream.Write(newLineBytes, 0, newLineBytes.Length);
|
||||
|
||||
// Disposition header + newline
|
||||
RequestStream.Write(cDisposBytes, 0, cDisposBytes.Length);
|
||||
RequestStream.Write(newLineBytes, 0, newLineBytes.Length);
|
||||
|
||||
// Type header + newline
|
||||
RequestStream.Write(cTypeBytes, 0, cTypeBytes.Length);
|
||||
RequestStream.Write(newLineBytes, 0, newLineBytes.Length);
|
||||
|
||||
// newline + contents + newline
|
||||
RequestStream.Write(newLineBytes, 0, newLineBytes.Length);
|
||||
RequestStream.Write(file.Value, 0, file.Value.Length);
|
||||
RequestStream.Write(newLineBytes, 0, newLineBytes.Length);
|
||||
}
|
||||
}
|
||||
|
||||
byte[] closingBound = Encoding.UTF8.GetBytes($@"--{boundary}--");
|
||||
RequestStream.Write(closingBound, 0, closingBound.Length);
|
||||
}
|
||||
}
|
||||
|
||||
HttpWebRequest.ContentType = ContentType;
|
||||
IsAborted = true;
|
||||
IsCompleted = true;
|
||||
|
||||
try
|
||||
{
|
||||
HttpWebResponse = HttpWebRequest.GetResponse() as HttpWebResponse;
|
||||
} catch (WebException ex)
|
||||
AbortToken?.Cancel();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
HttpWebResponse = ex.Response as HttpWebResponse;
|
||||
// just do nothign in this case
|
||||
}
|
||||
}
|
||||
|
||||
ResponseStream = HttpWebResponse.GetResponseStream();
|
||||
private System.Net.Http.HttpMethod FromInternalHttpMethod(HttpMethod method)
|
||||
{
|
||||
switch (method)
|
||||
{
|
||||
case HttpMethod.GET:
|
||||
return System.Net.Http.HttpMethod.Get;
|
||||
|
||||
case HttpMethod.DELETE:
|
||||
return System.Net.Http.HttpMethod.Delete;
|
||||
|
||||
case HttpMethod.POST:
|
||||
return System.Net.Http.HttpMethod.Post;
|
||||
|
||||
case HttpMethod.PATCH:
|
||||
return new System.Net.Http.HttpMethod(@"PATCH");
|
||||
|
||||
case HttpMethod.PUT:
|
||||
return System.Net.Http.HttpMethod.Put;
|
||||
}
|
||||
|
||||
#region IDisposable
|
||||
throw new InvalidOperationException($"Unsupported HTTP method {method}.");
|
||||
}
|
||||
|
||||
private bool IsDisposed = false;
|
||||
private void PrivatePerform()
|
||||
{
|
||||
using (AbortToken = new CancellationTokenSource())
|
||||
using (TimeoutToken = new CancellationTokenSource())
|
||||
using (CancellationTokenSource linkedToken = CancellationTokenSource.CreateLinkedTokenSource(AbortToken.Token, TimeoutToken.Token))
|
||||
{
|
||||
try
|
||||
{
|
||||
string requestUri = Url;
|
||||
HttpRequestMessage request = new HttpRequestMessage(FromInternalHttpMethod(Method), requestUri);
|
||||
|
||||
foreach (KeyValuePair<string, string> h in Headers)
|
||||
request.Headers.Add(h.Key, h.Value);
|
||||
|
||||
if (!string.IsNullOrEmpty(Accept))
|
||||
request.Headers.Accept.TryParseAdd(Accept);
|
||||
|
||||
if (HasBody)
|
||||
{
|
||||
Stream bodyContent;
|
||||
|
||||
if (RawRequestBody == null)
|
||||
{
|
||||
MultipartFormDataContent formData = new MultipartFormDataContent();
|
||||
|
||||
foreach (KeyValuePair<string, string> p in Parameters)
|
||||
formData.Add(new StringContent(p.Value), p.Key);
|
||||
|
||||
foreach (KeyValuePair<string, byte[]> f in Files)
|
||||
{
|
||||
ByteArrayContent bac = new ByteArrayContent(f.Value);
|
||||
bac.Headers.Add("Content-Type", GENERIC_CONTENT_TYPE);
|
||||
formData.Add(bac, f.Key, f.Key);
|
||||
}
|
||||
|
||||
bodyContent = formData.ReadAsStreamAsync().Result;
|
||||
} else
|
||||
{
|
||||
if (Parameters.Count > 0 || Files.Count > 0)
|
||||
throw new InvalidOperationException($"You cannot use {nameof(AddRaw)} at the same time as {nameof(AddParam)} or {nameof(AddFile)}");
|
||||
|
||||
bodyContent = new MemoryStream();
|
||||
RawRequestBody.Seek(0, SeekOrigin.Begin);
|
||||
RawRequestBody.CopyTo(bodyContent);
|
||||
bodyContent.Seek(0, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
request.Content = new StreamContent(RequestStream);
|
||||
|
||||
if (!string.IsNullOrEmpty(ContentType))
|
||||
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(ContentType);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Parameters.Count > 1)
|
||||
{
|
||||
StringBuilder urlBuilder = new StringBuilder();
|
||||
urlBuilder.Append(Url);
|
||||
|
||||
if (!Url.Contains('?'))
|
||||
urlBuilder.Append('?');
|
||||
|
||||
foreach (KeyValuePair<string, string> param in Parameters)
|
||||
{
|
||||
urlBuilder.Append(param.Key);
|
||||
urlBuilder.Append('=');
|
||||
urlBuilder.Append(param.Value);
|
||||
urlBuilder.Append('&');
|
||||
}
|
||||
|
||||
urlBuilder.Length -= 1;
|
||||
requestUri = urlBuilder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
ReportProgress();
|
||||
|
||||
using (request)
|
||||
Response = HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, linkedToken.Token).Result;
|
||||
|
||||
ResponseStream = new MemoryStream();
|
||||
|
||||
if (HasBody)
|
||||
{
|
||||
ReportProgress();
|
||||
UploadProgress?.Invoke(0, BytesUploaded);
|
||||
}
|
||||
|
||||
HandleResponse(linkedToken.Token);
|
||||
} catch (Exception) when (AbortToken.IsCancellationRequested)
|
||||
{
|
||||
Complete(new WebException(string.Format("Request to {0} was aborted by the user.", Url), WebExceptionStatus.RequestCanceled));
|
||||
} catch (Exception) when (TimeoutToken.IsCancellationRequested)
|
||||
{
|
||||
Complete(new WebException(string.Format("Request to {0} timed out after {1:N0} seconds idle (read {2:N0} bytes).", Url, TimeSinceLastAction / 1000, BytesDownloaded), WebExceptionStatus.Timeout));
|
||||
} catch (Exception ex)
|
||||
{
|
||||
if (IsCompleted)
|
||||
throw;
|
||||
|
||||
Complete(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PerformAsync()
|
||||
{
|
||||
if (IsCompleted)
|
||||
throw new InvalidOperationException($"{nameof(WebRequest)} has already been run, you can't reuse WebRequest objects.");
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Factory.StartNew(PrivatePerform, TaskCreationOptions.LongRunning);
|
||||
}
|
||||
catch (AggregateException ex)
|
||||
{
|
||||
if (ex.InnerExceptions.Count != 1)
|
||||
throw ex;
|
||||
|
||||
while (ex.InnerExceptions.Count == 1)
|
||||
{
|
||||
AggregateException innerEx = ex.InnerException as AggregateException;
|
||||
ex = innerEx ?? throw innerEx.InnerException;
|
||||
}
|
||||
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
public void Perform()
|
||||
=> PerformAsync().Wait();
|
||||
|
||||
private void HandleResponse(CancellationToken cancellationToken)
|
||||
{
|
||||
using (Stream responseStream = Response.Content.ReadAsStreamAsync().Result)
|
||||
{
|
||||
Started?.Invoke();
|
||||
|
||||
Buffer = new byte[BUFFER_SIZE];
|
||||
|
||||
while (true)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
int read = responseStream.Read(Buffer, 0, BUFFER_SIZE);
|
||||
|
||||
ReportProgress();
|
||||
|
||||
if (read > 0)
|
||||
{
|
||||
ResponseStream.Write(Buffer, 0, read);
|
||||
BytesDownloaded += read;
|
||||
DownloadProgress?.Invoke(BytesDownloaded, Response.Content.Headers.ContentLength ?? BytesDownloaded);
|
||||
} else
|
||||
{
|
||||
ResponseStream.Seek(0, SeekOrigin.Begin);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Complete(Exception exception = null)
|
||||
{
|
||||
if (IsAborted || IsCompleted)
|
||||
return;
|
||||
|
||||
bool allowRetry = true;
|
||||
|
||||
if (exception != null)
|
||||
{
|
||||
allowRetry = exception is WebException && (exception as WebException)?.Status == WebExceptionStatus.Timeout;
|
||||
} else if (!Response.IsSuccessStatusCode)
|
||||
{
|
||||
exception = new WebException($@"HTTP {Response.StatusCode}");
|
||||
|
||||
switch (Response.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.NotFound:
|
||||
case HttpStatusCode.MethodNotAllowed:
|
||||
case HttpStatusCode.Forbidden:
|
||||
case HttpStatusCode.Unauthorized:
|
||||
allowRetry = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (exception != null)
|
||||
if (allowRetry && Retries < MAX_RETRIES && BytesDownloaded < 1)
|
||||
{
|
||||
++Retries;
|
||||
PrivatePerform();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// process
|
||||
} catch (Exception ex)
|
||||
{
|
||||
exception = exception == null ? ex : new AggregateException(exception, ex);
|
||||
}
|
||||
|
||||
IsCompleted = true;
|
||||
|
||||
if (exception != null)
|
||||
{
|
||||
IsAborted = true;
|
||||
Failed?.Invoke(exception);
|
||||
throw exception;
|
||||
}
|
||||
|
||||
Finished?.Invoke();
|
||||
}
|
||||
|
||||
#region Timeout
|
||||
private long LastReportedAction = 0;
|
||||
private long TimeSinceLastAction => (DateTime.Now.Ticks - LastReportedAction) / TimeSpan.TicksPerMillisecond;
|
||||
|
||||
private void ReportProgress()
|
||||
{
|
||||
LastReportedAction = DateTime.Now.Ticks;
|
||||
TimeoutToken.CancelAfter(Timeout);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Disposal
|
||||
public bool IsDisposed { get; private set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects and releases all unmanaged objects
|
||||
/// </summary>
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return;
|
||||
|
||||
IsDisposed = true;
|
||||
RequestStream?.Dispose();
|
||||
HttpWebRequest?.Abort();
|
||||
ResponseStream?.Dispose();
|
||||
HttpWebResponse?.Close();
|
||||
// TODO: reimplement disposal
|
||||
|
||||
if (disposing)
|
||||
GC.SuppressFinalize(this);
|
||||
|
@ -283,7 +493,6 @@ namespace Maki.Rest
|
|||
|
||||
public void Dispose()
|
||||
=> Dispose(true);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,14 @@
|
|||
<assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-2.2.29.0" newVersion="2.2.29.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Runtime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-2.6.10.0" newVersion="2.6.10.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Threading.Tasks" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-2.6.10.0" newVersion="2.6.10.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Bcl" version="1.1.10" targetFramework="net40" />
|
||||
<package id="Microsoft.Bcl.Async" version="1.0.168" targetFramework="net40" />
|
||||
<package id="Microsoft.Bcl.Build" version="1.0.14" targetFramework="net40" />
|
||||
<package id="Microsoft.Net.Http" version="2.2.29" targetFramework="net40" />
|
||||
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net40" />
|
||||
|
|
|
@ -34,12 +34,6 @@
|
|||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Program.cs" />
|
||||
|
|
|
@ -1,13 +1,45 @@
|
|||
using Maki;
|
||||
using Maki.Rest;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
namespace MakiTest
|
||||
namespace Maki.Testing
|
||||
{
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
TestWebRequest(false, HttpMethod.GET, WEB_REQUEST_GET);
|
||||
TestWebRequest(true, HttpMethod.GET, WEB_REQUEST_GET);
|
||||
|
||||
//TestMainLibrary();
|
||||
|
||||
Console.WriteLine("Stopped, press any key to close the program...");
|
||||
Console.ReadKey();
|
||||
}
|
||||
|
||||
#region WebRequest tests
|
||||
private const string WEB_REQUEST_SECURE = @"https://";
|
||||
private const string WEB_REQUEST_INSECURE = @"http://";
|
||||
private const string WEB_REQUEST_BASE = @"httpbin.org";
|
||||
|
||||
private const string WEB_REQUEST_GET = WEB_REQUEST_BASE + @"/get";
|
||||
private const string WEB_REQUEST_FAIL = @"thisshouldneveresolve.flash.moe";
|
||||
|
||||
private static void TestWebRequest(bool secure, HttpMethod method, string url)
|
||||
{
|
||||
url = (secure ? WEB_REQUEST_SECURE : WEB_REQUEST_INSECURE) + url;
|
||||
|
||||
using (WebRequest wr = new WebRequest(method, url))
|
||||
{
|
||||
wr.Perform();
|
||||
|
||||
Console.WriteLine(wr.ResponseString);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
private static void TestMainLibrary()
|
||||
{
|
||||
string[] tokenInfo = File.ReadAllLines("token.txt");
|
||||
string token = tokenInfo[0] ?? string.Empty;
|
||||
|
@ -48,9 +80,6 @@ namespace MakiTest
|
|||
|
||||
mre.WaitOne();
|
||||
}
|
||||
|
||||
Console.WriteLine("Stopped, press any key to close the program...");
|
||||
Console.ReadKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Reference in a new issue