From bec952841656d043adcfec42c4f5615b86057868 Mon Sep 17 00:00:00 2001 From: Gerard Smit Date: Tue, 1 Oct 2019 23:18:37 +0200 Subject: [PATCH] Use System.Text.Json to serialize and deserialize JSON text --- src/Imghoard.Tests/ClientTests.cs | 18 -- .../Formatters/Base64FileFormatterTest.cs | 30 +++ src/Imghoard.Tests/Imghoard.Tests.csproj | 12 +- src/Imghoard.Tests/Program.cs | 14 -- src/Imghoard.Tests/Resources/BlackPixel.png | Bin 0 -> 67 bytes .../Formatters/Base64FileFormatter.cs | 90 ++++++++ src/Imghoard/Imghoard.csproj | 3 +- src/Imghoard/ImghoardClient.cs | 192 +++++++++--------- src/Imghoard/Models/Image.cs | 10 +- src/Imghoard/Models/ImagePostResult.cs | 10 + src/Imghoard/Models/PostImage.cs | 14 +- src/Imghoard/Properties/AssemblyInfo.cs | 3 + 12 files changed, 252 insertions(+), 144 deletions(-) delete mode 100644 src/Imghoard.Tests/ClientTests.cs create mode 100644 src/Imghoard.Tests/Formatters/Base64FileFormatterTest.cs delete mode 100644 src/Imghoard.Tests/Program.cs create mode 100644 src/Imghoard.Tests/Resources/BlackPixel.png create mode 100644 src/Imghoard/Formatters/Base64FileFormatter.cs create mode 100644 src/Imghoard/Models/ImagePostResult.cs create mode 100644 src/Imghoard/Properties/AssemblyInfo.cs diff --git a/src/Imghoard.Tests/ClientTests.cs b/src/Imghoard.Tests/ClientTests.cs deleted file mode 100644 index 40d896a..0000000 --- a/src/Imghoard.Tests/ClientTests.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Xunit; - -namespace Imghoard.Tests -{ - public class ClientTests - { - [Fact] - public void ConstructClientTest() - { - var i = new ImghoardClient(ImghoardClient.Config.Default()); - - Assert.NotNull(i); - } - } -} diff --git a/src/Imghoard.Tests/Formatters/Base64FileFormatterTest.cs b/src/Imghoard.Tests/Formatters/Base64FileFormatterTest.cs new file mode 100644 index 0000000..713307e --- /dev/null +++ b/src/Imghoard.Tests/Formatters/Base64FileFormatterTest.cs @@ -0,0 +1,30 @@ +using System.Text; +using System.Text.Json; +using Imghoard.Models; +using Xunit; + +namespace Imghoard.Tests.Formatters +{ + public class Base64FileFormatterTest + { + [Fact] + public void FormatTest() + { + using var pixel = typeof(Base64FileFormatterTest).Assembly + .GetManifestResourceStream("Imghoard.Tests.Resources.BlackPixel.png"); + + var bytes = JsonSerializer.SerializeToUtf8Bytes( + new PostImage + { + Stream = pixel + }, + ImghoardClient.JsonSerializerOptions + ); + + Assert.Equal( + @"{""Data"":""data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==""}", + Encoding.UTF8.GetString(bytes) + ); + } + } +} diff --git a/src/Imghoard.Tests/Imghoard.Tests.csproj b/src/Imghoard.Tests/Imghoard.Tests.csproj index 787828d..e3f4c60 100644 --- a/src/Imghoard.Tests/Imghoard.Tests.csproj +++ b/src/Imghoard.Tests/Imghoard.Tests.csproj @@ -1,12 +1,18 @@  - WinExe netcoreapp3.0 - - + false + + + + + + + + diff --git a/src/Imghoard.Tests/Program.cs b/src/Imghoard.Tests/Program.cs deleted file mode 100644 index eabdc22..0000000 --- a/src/Imghoard.Tests/Program.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; - -namespace Imghoard.Tests -{ - class Program - { - static async Task RunAsync() - { - var cli = new ImghoardClient(); - } - } -} diff --git a/src/Imghoard.Tests/Resources/BlackPixel.png b/src/Imghoard.Tests/Resources/BlackPixel.png new file mode 100644 index 0000000000000000000000000000000000000000..17f1d2ed7807fce394675e37973e22ee9928be0e GIT binary patch literal 67 zcmeAS@N?(olHy`uVBq!ia0vp^j3CSbBp9sfW`_bPE>9Q7kcv6UNkBFm1GAZV%?gmL Mr>mdKI;Vst09< + { + public override Stream Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotSupportedException("Reading a Base64 stream is not supported."); + } + + public override void Write(Utf8JsonWriter writer, Stream value, JsonSerializerOptions options) + { + byte[] data; + + if (value is MemoryStream memoryStream) + { + data = memoryStream.ToArray(); + } + else + { + // TODO: Check if this can be improved. + using var ms = new MemoryStream(); + value.Position = 0; + value.CopyTo(ms); + data = ms.ToArray(); + } + + var (supported, name) = IsSupported(data); + + if (!supported) + { + throw new NotSupportedException($"File extension {name} is not supported. Supported extensions: png, jpeg, gif."); + } + + var prefix = $"data:image/{name};base64,"; + + var prefixLength = Encoding.UTF8.GetByteCount(prefix); + var encodingLength = Base64.GetMaxEncodedToUtf8Length(data.Length); + var length = prefixLength + encodingLength; + + var destination = ArrayPool.Shared.Rent(length); + var span = destination.AsSpan(); + + try + { + Encoding.UTF8.GetBytes(prefix, span); + + var status = Base64.EncodeToUtf8(data, span.Slice(prefixLength), out var consumed, out var written); + Debug.Assert(status == OperationStatus.Done); + Debug.Assert(consumed == data.Length); + + writer.WriteStringValue(span.Slice(0, prefixLength + written)); + } + finally + { + ArrayPool.Shared.Return(destination); + } + } + + private static (bool supported, string name) IsSupported(byte[] image) + { + if (ImageHeaders.Validate(image, ImageType.Png)) + { + return (true, "png"); + } + + if (ImageHeaders.Validate(image, ImageType.Jpeg)) + { + return (true, "jpeg"); + } + + if (ImageHeaders.Validate(image, ImageType.Gif89a) || ImageHeaders.Validate(image, ImageType.Gif87a)) + { + return (true, "gif"); + } + + return (false, null); + } + } +} \ No newline at end of file diff --git a/src/Imghoard/Imghoard.csproj b/src/Imghoard/Imghoard.csproj index c899b6f..fafb819 100644 --- a/src/Imghoard/Imghoard.csproj +++ b/src/Imghoard/Imghoard.csproj @@ -5,10 +5,9 @@ - - + diff --git a/src/Imghoard/ImghoardClient.cs b/src/Imghoard/ImghoardClient.cs index a5c10e4..c9245c8 100644 --- a/src/Imghoard/ImghoardClient.cs +++ b/src/Imghoard/ImghoardClient.cs @@ -1,157 +1,153 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using Imghoard.Models; -using Miki.Net.Http; -using Miki.Utils.Imaging.Headers; -using Miki.Utils.Imaging.Headers.Models; -using Newtonsoft.Json; namespace Imghoard { - public class ImghoardClient + public class ImghoardClient : IDisposable { - private HttpClient apiClient; - private Config config; + internal static readonly MediaTypeHeaderValue JsonHeaderValue = new MediaTypeHeaderValue("application/json"); + internal static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions + { + IgnoreNullValues = true + }; + + private readonly HttpClient apiClient; - public ImghoardClient(ImghoardClient.Config config) { - this.config = config; + public ImghoardClient(Config config) + { + apiClient = new HttpClient + { + BaseAddress = config.Endpoint, + DefaultRequestHeaders = + { + {"x-miki-tenancy", config.Tenancy} + } + }; } - public ImghoardClient(Uri Endpoint) + public ImghoardClient(Uri endpoint) + : this (new Config { Endpoint = endpoint }) { - apiClient = new HttpClientFactory() - .HasBaseUri(config.Endpoint) - .CreateNew(); - apiClient.AddHeader("x-miki-tenancy", config.Tenancy); } - public async Task> GetImagesAsync(params string[] Tags) + public ImghoardClient() + : this(Config.Default()) { - StringBuilder query = null; - if (Tags.Any()) + } + + private static void ValidateResponse(HttpResponseMessage response) + { + if (!response.IsSuccessStatusCode) + { + throw new Exception(response.ReasonPhrase); + } + } + + public async Task> GetImagesAsync(params string[] tags) + { + string url; + + if (tags.Length == 0) { - query = new StringBuilder(); - foreach (string tag in Tags) + url = "/images"; + } + else + { + var query = new StringBuilder(); + + for (var i = 0; i < tags.Length; i++) { - if (tag.StartsWith("-")) + var tag = tags[i]; + + if (string.IsNullOrEmpty(tag)) + { + throw new ArgumentException("Cannot provide an empty tag name.", nameof(tags)); + } + + if (tag[0] == '-') { query.Append(tag); - continue; + } + else if (i > 0) + { + query.Append($"+{tag}"); } else { - if (tag != Tags.FirstOrDefault()) - { - query.Append($"+{tag}"); - continue; - } - query.Append(tag); } } - } - - StringBuilder urlBuilder = new StringBuilder("/images"); - if(query != null) - { - urlBuilder.Append($"?tags={query}"); + url = $"/images?tags={query}"; } - var response = await apiClient.GetAsync(urlBuilder.ToString()); - if (response.Success) - { - return JsonConvert.DeserializeObject>(response.Body); - } + var response = await apiClient.GetAsync(url); + ValidateResponse(response); - // TODO(velddev): Add better error handling. - throw new Exception(response.HttpResponseMessage.ReasonPhrase); - } - - public async Task GetImageAsync(ulong Id) - { - var response = await apiClient.GetAsync("/images?id={Id}"); - if (response.Success) - { - return JsonConvert.DeserializeObject(response.Body); - } - throw new Exception(response.HttpResponseMessage.ReasonPhrase); + var stream = await response.Content.ReadAsStreamAsync(); + + return await JsonSerializer.DeserializeAsync(stream); } - public async Task PostImageAsync(Stream image, params string[] Tags) + public async Task GetImageAsync(ulong id) { - byte[] bytes; + var response = await apiClient.GetAsync($"/images/{id}"); + ValidateResponse(response); - using (var mStream = new MemoryStream()) - { - await image.CopyToAsync(mStream); - bytes = mStream.ToArray(); - } + var stream = await response.Content.ReadAsStreamAsync(); - var imgd = IsSupported(bytes); + return await JsonSerializer.DeserializeAsync(stream); + } - if (!imgd.Item1) - { - throw new NotSupportedException( - "You have given an incorrect image format, currently supported formats are: png, jpeg, gif"); - } + public async Task PostImageAsync(Stream image, params string[] tags) + { + if (image == null) throw new ArgumentNullException(nameof(image)); - var b64 = Convert.ToBase64String(bytes); - image.Position = 0; + // TODO: Check if we can stream this. - var body = JsonConvert.SerializeObject( + var body = JsonSerializer.SerializeToUtf8Bytes( new PostImage { - Data = $"data:image/{imgd.Item2};base64,{b64}", - Tags = Tags + Stream = image, + Tags = tags }, - new JsonSerializerSettings - { - DefaultValueHandling = DefaultValueHandling.Ignore, - NullValueHandling = NullValueHandling.Ignore - } + JsonSerializerOptions ); - var response = await apiClient.PostAsync("/images", body); + var content = new ByteArrayContent(body); + content.Headers.ContentType = JsonHeaderValue; - if (response.Success) - { - return JsonConvert.DeserializeObject(response.Body); - } - return null; - } + var response = await apiClient.PostAsync("/images", content); + ValidateResponse(response); - (bool, string) IsSupported(byte[] image) - { - if(ImageHeaders.Validate(image, ImageType.Png)) - { - return (true, "png"); - } - if(ImageHeaders.Validate(image, ImageType.Jpeg)) - { - return (true, "jpeg"); - } - if(ImageHeaders.Validate(image, ImageType.Gif89a) - || ImageHeaders.Validate(image, ImageType.Gif87a)) - { - return (true, "gif"); - } - return (false, null); + var stream = await response.Content.ReadAsStreamAsync(); + var result = await JsonSerializer.DeserializeAsync(stream); + + return new Uri(result.File); } public class Config { public string Tenancy { get; set; } = "prod"; - public string Endpoint { get; set; } = "https://imgh.miki.ai/"; + + public Uri Endpoint { get; set; } = new Uri("https://imgh.miki.ai/"); public static Config Default() { return new Config(); } } + + public void Dispose() + { + apiClient.Dispose(); + } } } diff --git a/src/Imghoard/Models/Image.cs b/src/Imghoard/Models/Image.cs index 6289198..735cc10 100644 --- a/src/Imghoard/Models/Image.cs +++ b/src/Imghoard/Models/Image.cs @@ -1,14 +1,16 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Imghoard.Models { public class Image { - [JsonProperty("ID")] + [JsonPropertyName("id")] public ulong Id { get; internal set; } - [JsonProperty("Tags")] + + [JsonPropertyName("tags")] public string[] Tags { get; internal set; } - [JsonProperty("URL")] + + [JsonPropertyName("url")] public string Url { get; internal set; } } } diff --git a/src/Imghoard/Models/ImagePostResult.cs b/src/Imghoard/Models/ImagePostResult.cs new file mode 100644 index 0000000..2ab1382 --- /dev/null +++ b/src/Imghoard/Models/ImagePostResult.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace Imghoard.Models +{ + public class ImagePostResult + { + [JsonPropertyName("file")] + public string File { get; set; } + } +} \ No newline at end of file diff --git a/src/Imghoard/Models/PostImage.cs b/src/Imghoard/Models/PostImage.cs index 61c0f84..7de58be 100644 --- a/src/Imghoard/Models/PostImage.cs +++ b/src/Imghoard/Models/PostImage.cs @@ -1,12 +1,16 @@ -using Newtonsoft.Json; +using System.IO; +using System.Text.Json.Serialization; +using Imghoard.Formatters; namespace Imghoard.Models { - internal class PostImage + internal struct PostImage { - [JsonProperty("Tags")] + [JsonPropertyName("Tags")] public string[] Tags { get; internal set; } - [JsonProperty("Data")] - public string Data { get; internal set; } + + [JsonPropertyName("Data")] + [JsonConverter(typeof(Base64FileFormatter))] + public Stream Stream { get; internal set; } } } diff --git a/src/Imghoard/Properties/AssemblyInfo.cs b/src/Imghoard/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..36f3362 --- /dev/null +++ b/src/Imghoard/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("Imghoard.Tests")] \ No newline at end of file