Skip to content

Commit 5fc3988

Browse files
committed
feat(Core): add progress reporting for HTTP uploads
1 parent 93687f1 commit 5fc3988

11 files changed

Lines changed: 151 additions & 311 deletions

File tree

SnapX.Avalonia/Views/HomePageView.axaml.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
using SnapX.Core;
1111
using SnapX.Core.Upload;
1212
using SnapX.Core.Utils;
13-
using SnapX.Core.Utils.Miscellaneous;
13+
using HttpClientFactory = SnapX.Core.Utils.Miscellaneous.HttpClientFactory;
1414

1515
namespace SnapX.Avalonia;
1616

SnapX.CommonUI/Changelog.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using SnapX.CommonUI.Types;
55
using SnapX.Core;
66
using SnapX.Core.Utils.Miscellaneous;
7+
using HttpClientFactory = SnapX.Core.Utils.Miscellaneous.HttpClientFactory;
78

89
namespace SnapX.CommonUI;
910

SnapX.Core/GlobalUsings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
global using HttpClientFactory = SnapX.Core.Utils.Miscellaneous.HttpClientFactory;

SnapX.Core/SnapX.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
using SnapX.Core.Upload;
1616
using SnapX.Core.Utils;
1717
using SnapX.Core.Utils.Extensions;
18-
using SnapX.Core.Utils.Miscellaneous;
1918
#if WINDOWS
2019
using SnapX.Core.Utils.Native;
2120
#endif

SnapX.Core/Upload/BaseUploaders/Uploader.cs

Lines changed: 93 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44

55
using System.Collections.Specialized;
66
using System.Net;
7+
using System.Net.Http.Handlers;
78
using System.Text;
89
using SnapX.Core.Upload.OAuth;
910
using SnapX.Core.Upload.Utils;
1011
using SnapX.Core.Utils;
1112
using SnapX.Core.Utils.Extensions;
12-
using SnapX.Core.Utils.Miscellaneous;
1313
using Math = System.Math;
1414

1515
namespace SnapX.Core.Upload.BaseUploaders;
@@ -112,18 +112,63 @@ protected bool SendRequestDownload(HttpMethod method, string? url, Stream downlo
112112
{
113113
if (method == null) method = HttpMethod.Post;
114114

115-
var boundary = RequestHelpers.CreateBoundary();
116-
var contentType = headers.Get("Content-Type") ?? RequestHelpers.ContentTypeMultipartFormData + "; boundary=" + boundary;
117115

118-
// Create multipart/form-data body
119-
var data = RequestHelpers.MakeInputContent(boundary, args);
116+
var multipartContent = GetMultipartFormDataContent(args, null, null, null, null);
120117

121-
using var stream = new MemoryStream(data);
118+
var requestMessage = new HttpRequestMessage(method, url)
119+
{
120+
Content = multipartContent
121+
};
122+
123+
if (headers != null)
124+
{
125+
foreach (var key in headers.AllKeys)
126+
{
127+
requestMessage.Headers.TryAddWithoutValidation(key, headers[key]);
128+
}
129+
}
122130

123-
using var response = GetResponse(method, url, stream, contentType, args, headers, cookies);
131+
if (cookies != null)
132+
{
133+
var cookieHeader = string.Join("; ", cookies.Select(c => $"{c.Name}={c.Value}"));
134+
requestMessage.Headers.Add("Cookie", cookieHeader);
135+
}
136+
var client = HttpClientFactory.Get();
137+
var response = client.SendAsync(requestMessage).GetAwaiter().GetResult();
138+
response.EnsureSuccessStatusCode();
124139
return ProcessWebResponseText(response);
125140
}
126141

142+
protected MultipartFormDataContent GetMultipartFormDataContent(Dictionary<string, string?>? args, Stream? data, string? fileName, string? fileFormName, string? relatedData = null)
143+
{
144+
var multipartContent = new MultipartFormDataContent();
145+
146+
if (args != null)
147+
{
148+
foreach (var arg in args)
149+
{
150+
multipartContent.Add(new StringContent(arg.Value), arg.Key);
151+
}
152+
}
153+
154+
if (relatedData != null)
155+
{
156+
multipartContent.Add(
157+
new StringContent(relatedData, Encoding.UTF8, "application/json"),
158+
"file",
159+
fileName
160+
);
161+
}
162+
else if (data is not null)
163+
{
164+
var fileContent = new StreamContent(data);
165+
var mimeType = MimeTypes.GetMimeType(fileName);
166+
167+
fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(mimeType);
168+
multipartContent.Add(fileContent, fileFormName, fileName);
169+
}
170+
return multipartContent;
171+
}
127172
protected UploadResult SendRequestFile(string? url, Stream data, string? fileName, string fileFormName,
128173
Dictionary<string, string?> args = null, NameValueCollection headers = null, CookieCollection cookies = null,
129174
HttpMethod? method = null, string contentType = null, string relatedData = null)
@@ -133,36 +178,12 @@ protected UploadResult SendRequestFile(string? url, Stream data, string? fileNam
133178
var result = new UploadResult();
134179
IsUploading = true;
135180
StopUploadRequested = false;
181+
EventHandler<HttpProgressEventArgs>? handler = null;
136182

137183
try
138184
{
139185
var client = HttpClientFactory.Get();
140-
var multipartContent = new MultipartFormDataContent();
141-
142-
if (args != null)
143-
{
144-
foreach (var arg in args)
145-
{
146-
multipartContent.Add(new StringContent(arg.Value), arg.Key);
147-
}
148-
}
149-
150-
if (relatedData != null)
151-
{
152-
multipartContent.Add(
153-
new StringContent(relatedData, Encoding.UTF8, "application/json"),
154-
"file",
155-
fileName
156-
);
157-
}
158-
else
159-
{
160-
var fileContent = new StreamContent(data);
161-
var mimeType = MimeTypes.GetMimeType(fileName);
162-
163-
fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(mimeType);
164-
multipartContent.Add(fileContent, fileFormName, fileName);
165-
}
186+
var multipartContent = GetMultipartFormDataContent(args, data, fileName, fileFormName, relatedData);
166187

167188
var requestMessage = new HttpRequestMessage(method, url)
168189
{
@@ -176,13 +197,27 @@ protected UploadResult SendRequestFile(string? url, Stream data, string? fileNam
176197
requestMessage.Headers.TryAddWithoutValidation(key, headers[key]);
177198
}
178199
}
179-
200+
requestMessage.Headers.TransferEncodingChunked = true;
201+
requestMessage.Content.Headers.ContentLength = null;
180202
if (cookies != null)
181203
{
182204
var cookieHeader = string.Join("; ", cookies.Select(c => $"{c.Name}={c.Value}"));
183205
requestMessage.Headers.Add("Cookie", cookieHeader);
184206
}
185207

208+
var ph = HttpClientFactory._ph!;
209+
var requestLength = data.Length;
210+
var progress = new ProgressManager(requestLength);
211+
212+
handler = (_, args) =>
213+
{
214+
progress.Length = new[] { requestLength, args.TotalBytes ?? 0, args.BytesTransferred }.Max();
215+
216+
if (AllowReportProgress && progress.UpdateAbsoluteProgress(args.BytesTransferred))
217+
OnProgressChanged(progress);
218+
};
219+
220+
ph.HttpSendProgress += handler;
186221
var response = client.SendAsync(requestMessage).GetAwaiter().GetResult();
187222

188223
if (response.IsSuccessStatusCode)
@@ -214,6 +249,7 @@ protected UploadResult SendRequestFile(string? url, Stream data, string? fileNam
214249
finally
215250
{
216251
IsUploading = false;
252+
HttpClientFactory._ph!.HttpSendProgress -= handler; // prevent dangling event ref
217253
}
218254

219255
return result;
@@ -353,16 +389,34 @@ public override int Read(byte[] buffer, int offset, int count)
353389
{
354390
contentLength = data.Length;
355391
}
356-
var request = RequestHelpers.CreateHttpRequest(method, url, headers, cookies, contentType, contentLength).ConfigureAwait(false).GetAwaiter().GetResult();
392+
393+
StreamContent? requestContent = null;
357394

358395
if (data != null && (method == HttpMethod.Post || method == HttpMethod.Put))
359396
{
360-
using var requestStream = request.Content?.ReadAsStreamAsync().GetAwaiter().GetResult();
361-
if (requestStream is not null) data.CopyTo(requestStream);
397+
requestContent = new StreamContent(data);
398+
}
399+
var requestMessage = new HttpRequestMessage(method, url)
400+
{
401+
Content = requestContent
402+
};
403+
404+
if (headers != null)
405+
{
406+
foreach (var key in headers.AllKeys)
407+
{
408+
requestMessage.Headers.TryAddWithoutValidation(key, headers[key]);
409+
}
410+
}
411+
412+
if (cookies != null)
413+
{
414+
var cookieHeader = string.Join("; ", cookies.Select(c => $"{c.Name}={c.Value}"));
415+
requestMessage.Headers.Add("Cookie", cookieHeader);
362416
}
363417

364418
var client = HttpClientFactory.Get();
365-
var response = client.SendAsync(request).ConfigureAwait(false).GetAwaiter().GetResult();
419+
var response = client.SendAsync(requestMessage).ConfigureAwait(false).GetAwaiter().GetResult();
366420
if (!response.IsSuccessStatusCode && !allowNon2xxResponses)
367421
{
368422
response.EnsureSuccessStatusCode();
@@ -505,13 +559,6 @@ protected bool TransferData(Stream dataStream, Stream requestStream, long dataPo
505559
return responseText;
506560
}
507561

508-
private HttpRequestMessage CreateHttpRequest(HttpMethod method, string? url, NameValueCollection headers = null, CookieCollection cookies = null,
509-
string contentType = null, long contentLength = 0)
510-
{
511-
LastResponseInfo = null;
512-
513-
return RequestHelpers.CreateHttpRequest(method, url, headers, cookies, contentType, contentLength).GetAwaiter().GetResult();
514-
}
515562
private Dictionary<string, string> ConvertHeadersToDictionary(HttpResponseMessage response)
516563
{
517564
var headersDictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
@@ -556,7 +603,7 @@ private ResponseInfo ProcessWebResponse(HttpResponseMessage response)
556603
ResponseText = response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
557604
};
558605

559-
DebugHelper.WriteLine($"ResponseInfo: StatusCode={responseInfo.StatusCode} StatusDescription={responseInfo.StatusDescription} ResponseText={responseInfo.ResponseText} ResponseURL={responseInfo.ResponseURL}");
606+
// DebugHelper.WriteLine($"ResponseInfo: StatusCode={responseInfo.StatusCode} StatusDescription={responseInfo.StatusDescription} ResponseText={responseInfo.ResponseText} ResponseURL={responseInfo.ResponseURL}");
560607

561608
LastResponseInfo = responseInfo;
562609
return responseInfo;

SnapX.Core/Upload/Img/GooglePhotos.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
using SnapX.Core.Upload.OAuth;
99
using SnapX.Core.Upload.Utils;
1010
using SnapX.Core.Utils.Extensions;
11-
using SnapX.Core.Utils.Miscellaneous;
1211

1312
namespace SnapX.Core.Upload.Img;
1413

SnapX.Core/Upload/Utils/ProgressManager.cs

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace SnapX.Core.Upload.Utils;
1010
public class ProgressManager
1111
{
1212
public long Position { get; private set; }
13-
public long Length { get; private set; }
13+
public long Length { get; set; }
1414

1515
public double Percentage => (double)Position / Length * 100;
1616

@@ -53,20 +53,63 @@ public bool UpdateProgress(long bytesRead)
5353
if (Position >= Length)
5454
{
5555
startTimer.Stop();
56+
Speed = Length / Math.Max(startTimer.Elapsed.TotalSeconds, 1);
5657
return true;
5758
}
5859

59-
if (smoothTimer.ElapsedMilliseconds <= smoothTime)
60+
if (smoothTimer.ElapsedMilliseconds < smoothTime)
6061
return false;
6162

62-
averageSpeed.Enqueue(speedTest / smoothTimer.Elapsed.TotalSeconds);
63-
Speed = averageSpeed.Average();
63+
double intervalSeconds = smoothTimer.Elapsed.TotalSeconds;
64+
if (intervalSeconds > 0)
65+
{
66+
double currentSpeed = speedTest / intervalSeconds;
67+
averageSpeed.Enqueue(currentSpeed);
68+
while (averageSpeed.Count > 5)
69+
averageSpeed.Dequeue();
70+
Speed = averageSpeed.Average();
71+
}
6472

6573
speedTest = 0;
66-
smoothTimer.Reset();
67-
smoothTimer.Start();
74+
smoothTimer.Restart();
6875

6976
return true;
7077
}
78+
79+
public bool UpdateAbsoluteProgress(long totalBytesTransferred)
80+
{
81+
long bytesDelta = totalBytesTransferred - Position;
82+
if (bytesDelta < 0)
83+
bytesDelta = 0;
84+
85+
Position = totalBytesTransferred;
86+
speedTest += bytesDelta;
87+
88+
if (Position >= Length)
89+
{
90+
startTimer.Stop();
91+
Speed = Length / Math.Max(startTimer.Elapsed.TotalSeconds, 1);
92+
return true;
93+
}
94+
95+
if (smoothTimer.ElapsedMilliseconds < smoothTime)
96+
return false;
97+
98+
double intervalSeconds = smoothTimer.Elapsed.TotalSeconds;
99+
if (intervalSeconds > 0)
100+
{
101+
double currentSpeed = speedTest / intervalSeconds;
102+
averageSpeed.Enqueue(currentSpeed);
103+
while (averageSpeed.Count > 5)
104+
averageSpeed.Dequeue();
105+
Speed = averageSpeed.Average();
106+
}
107+
108+
speedTest = 0;
109+
smoothTimer.Restart();
110+
111+
return true;
112+
}
113+
71114
}
72115

0 commit comments

Comments
 (0)