using System.Globalization;
using System.Net;
using System.Net.Http.Json;
using System.Text.RegularExpressions;
namespace OpenNest.Data;
///
/// The server worker's side of library measurement (/api/drawing-packages): lists the
/// packages of current drawings not yet measured at a version, downloads one by its hash and
/// stores its . A server without these routes raises
/// ; other failures surface as .
///
public sealed partial class RemoteDrawingGeometry : NestServerClient
{
public const int MaxBatch = 500;
public RemoteDrawingGeometry(string baseUrl)
: base(new HttpClient(), baseUrl, ownsClient: true)
{
}
public RemoteDrawingGeometry(HttpClient httpClient, string baseUrl)
: base(httpClient, baseUrl, ownsClient: false)
{
}
/// Up to package hashes with no summary at or later.
public async Task> GetUnmeasuredAsync(int version, int limit, CancellationToken cancellationToken = default)
{
ArgumentOutOfRangeException.ThrowIfLessThan(version, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(limit, 1);
ArgumentOutOfRangeException.ThrowIfGreaterThan(limit, MaxBatch);
using var response = await _httpClient.GetAsync(Url(
$"api/drawing-packages/unmeasured?version={version.ToString(CultureInfo.InvariantCulture)}&limit={limit.ToString(CultureInfo.InvariantCulture)}"),
cancellationToken);
if (response.StatusCode == HttpStatusCode.NotFound)
throw new ServerTooOldException();
await NestServerHttp.EnsureSuccessAsync(response, "list unmeasured library drawings", cancellationToken);
var hashes = await response.Content.ReadFromJsonAsync>(NestServerHttp.JsonOptions, cancellationToken)
?? throw new IOException("Nest server returned an empty list of unmeasured drawings.");
if (hashes.Count > limit || hashes.Any(hash => hash is null || !IsSha256(hash)))
throw new IOException("Nest server returned an invalid list of unmeasured drawings.");
return hashes;
}
/// The package stored under , or null when there is none.
public async Task GetPackageAsync(string sha256, CancellationToken cancellationToken = default)
{
ValidateSha(sha256);
using var response = await _httpClient.GetAsync(Url($"api/drawing-packages/{sha256}"), cancellationToken);
if (response.StatusCode == HttpStatusCode.NotFound)
return null;
await NestServerHttp.EnsureSuccessAsync(response, $"download library package {sha256}", cancellationToken);
return await response.Content.ReadAsByteArrayAsync(cancellationToken);
}
/// Stores a package's summary; false when the server no longer has the package.
public async Task PutAsync(string sha256, DrawingGeometrySummary summary, CancellationToken cancellationToken = default)
{
ValidateSha(sha256);
ArgumentNullException.ThrowIfNull(summary);
var error = summary.GetValidationError();
if (error is not null)
throw new ArgumentException(error, nameof(summary));
using var response = await _httpClient.PutAsJsonAsync(
Url($"api/drawing-packages/{sha256}/geometry"), summary, NestServerHttp.JsonOptions, cancellationToken);
if (response.StatusCode == HttpStatusCode.NotFound)
return false;
await NestServerHttp.EnsureSuccessAsync(response, $"store the size of library package {sha256}", cancellationToken);
return true;
}
/// True for a lower-case hex SHA-256, the form the server stores.
public static bool IsSha256(string text) => Sha256Pattern().IsMatch(text);
private static void ValidateSha(string sha256)
{
if (sha256 is null || !IsSha256(sha256))
throw new ArgumentException("A package hash is 64 lower-case hex characters.", nameof(sha256));
}
[GeneratedRegex("^[0-9a-f]{64}$")]
private static partial Regex Sha256Pattern();
}