LANCommander/LANCommander.SDK/Helpers/ApiRequestBuilder.cs
2026-07-07 01:50:25 -05:00

365 lines
No EOL
11 KiB
C#

using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using LANCommander.SDK.Abstractions;
using LANCommander.SDK.Exceptions;
using LANCommander.SDK.Extensions;
using LANCommander.SDK.Models;
using Semver;
using Action = System.Action;
namespace LANCommander.SDK.Helpers;
public class ApiRequestBuilder(
HttpClient httpClient,
ITokenProvider tokenProvider,
ISettingsProvider settingsProvider)
{
private bool _ignoreVersion { get; set; }
private object _body { get; set; }
private string _route { get; set; }
private HttpClient _httpClient { get; set; } = httpClient;
private HttpRequestMessage _request { get; set; } = new();
private CancellationToken _cancellationToken { get; set; } = CancellationToken.None;
private Action<DownloadProgressChangedEventArgs> _progressHandler { get; set; }
private Action _completeHandler { get; set; }
private Uri _baseAddress { get; set; } = settingsProvider.CurrentValue.Authentication.ServerAddress;
private async ValueTask<TResult> DeserializeResultAsync<TResult>(HttpResponseMessage response)
{
EnsureVersionCompatibility(response);
var body = await response.Content.ReadAsStringAsync(_cancellationToken);
try
{
return JsonSerializer.Deserialize<TResult>(body,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
catch
{
return default;
}
}
/// <summary>
/// The server advertises its API version on every response via the <c>X-API-Version</c>
/// header. When the server's major version differs from ours the payload schemas are
/// incompatible, which otherwise surfaces as confusing downstream errors (empty results,
/// null deserialization). Detect that here and throw a clear, actionable exception instead.
/// </summary>
private static void EnsureVersionCompatibility(HttpResponseMessage response)
{
if (!response.Headers.TryGetValues("X-API-Version", out var values))
return;
var serverRaw = values.FirstOrDefault();
if (String.IsNullOrWhiteSpace(serverRaw)
|| !SemVersion.TryParse(serverRaw, SemVersionStyles.Any, out var serverVersion))
return;
var clientVersion = VersionHelper.GetCurrentVersion();
if (serverVersion.Major != clientVersion.Major)
throw new ApiVersionMismatchException(clientVersion, serverVersion,
$"This launcher (v{clientVersion}) is not compatible with the server (v{serverVersion}). " +
"Update the launcher or server so their major versions match.");
}
public ApiRequestBuilder UseAuthenticationToken()
{
var token = tokenProvider.GetToken();
_request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);
return this;
}
public ApiRequestBuilder UseRoute(string route)
{
_request.RequestUri = _baseAddress.Join(route);
return this;
}
public ApiRequestBuilder UseCancellationToken(CancellationToken cancellationToken)
{
_cancellationToken = cancellationToken;
return this;
}
public ApiRequestBuilder UseVersioning()
{
_request.Headers.Add("X-API-Version", VersionHelper.GetCurrentVersion().ToString());
// _request.Interceptors = new List<Interceptor>() { new VersionInterceptor() };
return this;
}
public ApiRequestBuilder UseMethod(HttpMethod method)
{
_request.Method = method;
return this;
}
public ApiRequestBuilder UseBaseAddress(Uri baseAddress)
{
_baseAddress = baseAddress;
return this;
}
public ApiRequestBuilder SetTimeout(TimeSpan timeout)
{
_httpClient.Timeout = timeout;
return this;
}
public ApiRequestBuilder AddBody(object body)
{
_request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, MediaTypeNames.Application.Json);
return this;
}
public ApiRequestBuilder AddHeader(string key, string value)
{
_request.Headers.Add(key, value);
return this;
}
public ApiRequestBuilder OnProgress(Action<DownloadProgressChangedEventArgs> progressHandler)
{
_progressHandler = progressHandler;
return this;
}
public ApiRequestBuilder OnComplete(Action completeHandler)
{
_completeHandler = completeHandler;
return this;
}
public async Task<ApiResponseMessage<TResult>> SendAsync<TResult>() where TResult : class
{
var response = await _httpClient.SendAsync(_request, _cancellationToken);
var result = new ApiResponseMessage<TResult>
{
Response = response,
Data = await DeserializeResultAsync<TResult>(response)
};
return result;
}
public async Task<TResult> GetAsync<TResult>()
{
_request.Method = HttpMethod.Get;
var response = await _httpClient.SendAsync(_request, _cancellationToken);
response
.EnsureSuccessStatusCode();
return await DeserializeResultAsync<TResult>(response);
}
public async Task<TResult> PostAsync<TResult>()
{
_request.Method = HttpMethod.Post;
var response = await _httpClient.SendAsync(_request, _cancellationToken);
response
.EnsureSuccessStatusCode();
return await DeserializeResultAsync<TResult>(response);
}
public async Task PostAsync()
{
_request.Method = HttpMethod.Post;
var response = await _httpClient.SendAsync(_request, _cancellationToken);
response
.EnsureSuccessStatusCode();
}
public async Task<TResult> PutAsync<TResult>()
{
_request.Method = HttpMethod.Put;
var response = await _httpClient.SendAsync(_request, _cancellationToken);
response
.EnsureSuccessStatusCode();
return await DeserializeResultAsync<TResult>(response);
}
public async Task<TResult> DeleteAsync<TResult>()
{
_request.Method = HttpMethod.Delete;
var response = await _httpClient.SendAsync(_request, _cancellationToken);
response
.EnsureSuccessStatusCode();
return await DeserializeResultAsync<TResult>(response);
}
public async Task HeadAsync()
{
_request.Method = HttpMethod.Head;
var response = await _httpClient.SendAsync(_request, _cancellationToken);
response
.EnsureSuccessStatusCode();
}
public async Task<TResult> HeadAsync<TResult>()
{
_request.Method = HttpMethod.Head;
var response = await _httpClient.SendAsync(_request, _cancellationToken);
response
.EnsureSuccessStatusCode();
return await DeserializeResultAsync<TResult>(response);
}
public async Task<FileInfo> DownloadAsync(string destination)
{
_request.Method = HttpMethod.Get;
var response = await _httpClient.SendAsync(_request, _cancellationToken);
var folder = Path.GetDirectoryName(destination);
if (!Directory.Exists(folder))
Directory.CreateDirectory(folder);
await using (var fs = new FileStream(destination, FileMode.Create))
{
var responseStream = await response.Content.ReadAsTrackableStreamAsync(_cancellationToken);
responseStream.OnProgress += (position, length) =>
{
if (_progressHandler != null)
_progressHandler.Invoke(new DownloadProgressChangedEventArgs
{
BytesReceived = position,
TotalBytes = length,
});
};
await responseStream.CopyToAsync(fs, _cancellationToken);
if (_completeHandler != null)
_completeHandler();
}
return new FileInfo(destination);
}
public async Task<TrackableStream> StreamAsync()
{
_request.Method = HttpMethod.Get;
var response = await _httpClient.SendAsync(_request, HttpCompletionOption.ResponseHeadersRead, _cancellationToken);
return await response.Content.ReadAsTrackableStreamAsync(_cancellationToken);
}
public async Task<TResult> UploadAsync<TResult>(string fileName, byte[] data)
{
ArgumentException.ThrowIfNullOrWhiteSpace(fileName);
using (var form = new MultipartFormDataContent())
{
var dataContent = new ByteArrayContent(data);
form.Add(dataContent, "file", fileName);
_request.Content = form;
_request.Method = HttpMethod.Post;
var response = await _httpClient.SendAsync(_request, _cancellationToken);
return await DeserializeResultAsync<TResult>(response);
}
}
public async Task<TResult> UploadAsync<TResult>(string fileName, Stream data)
{
var buffer = new byte[data.Length];
await data.ReadExactlyAsync(buffer, 0, buffer.Length, _cancellationToken);
return await UploadAsync<TResult>(fileName, buffer);
}
public async Task<Guid> UploadInChunksAsync(long chunkSize, Stream data)
{
var initResponse = await new ApiRequestBuilder(httpClient, tokenProvider, settingsProvider)
.UseRoute("/api/Upload/Init")
.UseVersioning()
.UseAuthenticationToken()
.UseCancellationToken(_cancellationToken)
.AddBody(new UploadInitRequest())
.PostAsync<UploadInitResponse>();
if (initResponse == null || initResponse.Key == Guid.Empty)
throw new Exception("Failed to initialize upload.");
var buffer = new byte[chunkSize];
var total = data.Length;
while (data.Position < total)
{
var start = data.Position;
if (data.Position + chunkSize > total)
buffer = new byte[total - data.Position];
await data.ReadExactlyAsync(buffer, 0, buffer.Length, _cancellationToken);
var content = new MultipartFormDataContent();
content.Add(new StringContent(start.ToString()), "Start");
content.Add(new StringContent(data.Position.ToString()), "End");
content.Add(new StringContent(total.ToString()), "Total");
content.Add(new StringContent(initResponse.Key.ToString()), "Key");
content.Add(new ByteArrayContent(buffer), "File", "chunk.bin");
var chunkBuilder = new ApiRequestBuilder(httpClient, tokenProvider, settingsProvider);
chunkBuilder.UseRoute("/api/Upload/Chunk");
chunkBuilder.UseVersioning();
chunkBuilder.UseAuthenticationToken();
chunkBuilder.UseCancellationToken(_cancellationToken);
chunkBuilder._request.Content = content;
chunkBuilder._request.Method = HttpMethod.Post;
var response = await httpClient.SendAsync(chunkBuilder._request, _cancellationToken);
response.EnsureSuccessStatusCode();
}
return initResponse.Key;
}
}