Skip to content

Commit 8b1b5f2

Browse files
committed
🎨 ImageHttpClientService
1 parent 7db2255 commit 8b1b5f2

File tree

7 files changed

+541
-217
lines changed

7 files changed

+541
-217
lines changed

src/BD.Common8.Bcl/Security/Cryptography/X509Certificates/X509Certificate2Generator.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,8 @@ public static X509Certificate2 CreateServerCertificate(CreateServerCertificateOp
371371
notBefore, notAfter, serialNumber); // 创建证书
372372
using var serverCertificateCopyWithPrivateKey = serverCertificate.CopyWithPrivateKey(rsa); // 将私钥复制进去
373373
var serverCertificateCopyWithPrivateKeyExport = serverCertificateCopyWithPrivateKey.Export(X509ContentType.Pfx); // 导出 Pfx 数据
374-
return new X509Certificate2(serverCertificateCopyWithPrivateKeyExport, (string?)null, X509KeyStorageFlags.EphemeralKeySet | X509KeyStorageFlags.Exportable); // 重新创建证书,否则无法应用在 Kestrel 中
374+
X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.Exportable;
375+
//X509KeyStorageFlags.EphemeralKeySet | X509KeyStorageFlags.Exportable;
376+
return X509CertificateLoader.LoadPkcs12(serverCertificateCopyWithPrivateKeyExport, null, keyStorageFlags); // 重新创建证书,否则无法应用在 Kestrel 中
375377
}
376378
}

src/BD.Common8.Http.ClientFactory/Models/GetImageArgs.cs

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace BD.Common8.Http.ClientFactory.Models;
88
/// <item><para>设置 <see cref="HashValue"/> + <see cref="UseCache"/> 与 <see cref="CacheFirst"/> 为 <see langword="true"/> 时,优先从本地根据哈希值加载缓存</para></item>
99
/// </list>
1010
/// </summary>
11-
public record struct GetImageArgs
11+
public readonly record struct GetImageArgs
1212
{
1313
/// <summary>
1414
/// Initializes a new instance of the <see cref="GetImageArgs"/> struct.
@@ -23,31 +23,49 @@ public GetImageArgs()
2323
public required string RequestUri { get; init; }
2424

2525
/// <summary>
26-
/// 是否使用 <see cref="Polly"/> 重试
26+
/// 是否使用 <see cref="Polly"/> 重试次数,为 0 时不重试
2727
/// </summary>
28-
public bool IsPolly { get; set; }
28+
public int IsPollyNum { get; init; } = 3;
2929

3030
/// <summary>
3131
/// 是否进行缓存
3232
/// </summary>
33-
public bool UseCache { get; set; }
33+
public bool UseCache { get; init; } = true;
3434

3535
/// <summary>
3636
/// 是否优先使用缓存加载
3737
/// </summary>
38-
public bool CacheFirst { get; set; }
38+
public bool CacheFirst { get; init; } = true;
3939

4040
/// <summary>
4141
/// <see cref="HttpHandlerCategory"/> 的默认值
4242
/// </summary>
43-
public const HttpHandlerCategory DefaultHttpHandlerCategory = HttpHandlerCategory.UserInitiated;
43+
public const HttpHandlerCategory DefaultHttpHandlerCategory = IImageHttpClientService.DefaultHttpHandlerCategory;
4444

4545
/// <inheritdoc cref="HttpHandlerCategory"/>
46-
public HttpHandlerCategory Category { get; set; } = DefaultHttpHandlerCategory;
46+
public HttpHandlerCategory Category { get; init; } = DefaultHttpHandlerCategory;
4747

4848
/// <summary>
4949
/// 哈希值
5050
/// </summary>
51-
public string? HashValue { get; set; }
51+
public string? HashValue { get; init; }
52+
53+
public void Deconstruct(out string requestUri, out int isPollyNum, out bool cache, out bool cacheFirst, out HttpHandlerCategory category)
54+
{
55+
requestUri = RequestUri;
56+
isPollyNum = IsPollyNum;
57+
cache = UseCache;
58+
cacheFirst = CacheFirst;
59+
category = Category;
60+
61+
if (!cache)
62+
{
63+
category = HttpHandlerCategory.Default;
64+
}
65+
else
66+
{
67+
isPollyNum = 0;
68+
}
69+
}
5270
}
5371
#endif

src/BD.Common8.Http.ClientFactory/Services/IImageHttpClientService.cs

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,52 @@
22
namespace BD.Common8.Http.ClientFactory.Services;
33

44
/// <summary>
5-
/// Http 图片客户端请求服务
5+
/// 图片的 HTTP 请求服务
66
/// </summary>
77
public interface IImageHttpClientService
88
{
9+
// 从网络中加载图片至 View 层控件
10+
// 由文件路径直接传递 Skia 构建 Bitmap 对象
11+
// ----- 网络请求 -----
12+
// Http 请求响应内容流读取 RateLimitedHttpMessageHandler2.SendAsync
13+
// 复制到内存流中 RecyclableMemoryStream
14+
// 由 cacheResult RequestCacheRepository.Save 方法保存到本地文件中
15+
// 通过 FusilladeClientHttpClientFactory.ReadAsStreamAsync 与类型 RecyclableMemoryStreamContent 直接获取内存流对象实例,避免复制流产生开销
16+
// 通过 KeyFilePath 设置文件路径
17+
// 响应内容替换为 BD.Common8.Http.ClientFactory.Services.Implementation.FileContent 由路径提供内容
18+
// ----- 本地缓存 -----
19+
// 由 retrieveBody RequestCacheRepository.Fetch 方法读取响应内容并设置 KeyFilePath 值
20+
// 通过 KeyFilePath 值直接返回文件路径或文件流
21+
22+
// ImageValueConverter.GetDecodeBitmap
23+
// 使用 Bitmap(string fileName) OR Bitmap(Stream stream) OR Bitmap.DecodeToWidth
24+
// https://github.com/AvaloniaUtils/AsyncImageLoader.Avalonia/blob/master/AsyncImageLoader.Avalonia/Loaders/DiskCachedWebImageLoader.cs#L32
25+
// https://github.com/AvaloniaUtils/AsyncImageLoader.Avalonia/blob/master/AsyncImageLoader.Avalonia/Loaders/BaseWebImageLoader.cs#L68
26+
927
/// <summary>
1028
/// <see cref="HttpHandlerCategory"/> 的默认值
1129
/// </summary>
12-
protected const HttpHandlerCategory DefaultHttpHandlerCategory = GetImageArgs.DefaultHttpHandlerCategory;
30+
public const HttpHandlerCategory DefaultHttpHandlerCategory = HttpHandlerCategory.UserInitiated;
31+
32+
/// <summary>
33+
/// 图片本地缓存路径的 HTTP 请求键
34+
/// </summary>
35+
public static readonly HttpRequestOptionsKey<string> KeyFilePath = new("ResponseContentFilePath");
36+
37+
///// <summary>
38+
///// 自定义返回流的 HTTP 请求键
39+
///// </summary>
40+
//public static readonly HttpRequestOptionsKey<Stream> KeyStream = new("ResponseContentStream");
41+
42+
///// <summary>
43+
///// 自定义返回字节数组的 HTTP 请求键
44+
///// </summary>
45+
//public static readonly HttpRequestOptionsKey<byte[]> KeyByteArray = new("ResponseContentByteArray");
46+
47+
/// <summary>
48+
/// 原始请求地址的 HTTP 请求键,因某些请求 301/302 跳转会改变地址
49+
/// </summary>
50+
protected static readonly HttpRequestOptionsKey<string> KeyOriginalRequestUri = new("OriginalRequestUri");
1351

1452
/// <summary>
1553
/// 通过 Get 请求 Image <see cref="MemoryStream"/>
@@ -21,7 +59,8 @@ public interface IImageHttpClientService
2159
/// <param name="category">使用的调度器种类</param>
2260
/// <param name="cancellationToken">取消操作标记</param>
2361
/// <returns></returns>
24-
Task<MemoryStream?> GetImageMemoryStreamAsync(
62+
[Obsolete("use GetImageFilePathAsync OR GetImageAsync")]
63+
Task<Stream?> GetImageMemoryStreamAsync(
2564
string requestUri,
2665
bool isPolly = true,
2766
bool cache = false,
@@ -30,7 +69,7 @@ public interface IImageHttpClientService
3069
CancellationToken cancellationToken = default) => GetImageMemoryStreamAsync(new()
3170
{
3271
RequestUri = requestUri,
33-
IsPolly = isPolly,
72+
IsPollyNum = isPolly ? 2 : 0,
3473
UseCache = cache,
3574
CacheFirst = cacheFirst,
3675
Category = category,
@@ -42,7 +81,28 @@ public interface IImageHttpClientService
4281
/// <param name="args"></param>
4382
/// <param name="cancellationToken">取消操作标记</param>
4483
/// <returns></returns>
45-
Task<MemoryStream?> GetImageMemoryStreamAsync(GetImageArgs args,
84+
[Obsolete("use GetImageFilePathAsync OR GetImageAsync")]
85+
Task<Stream?> GetImageMemoryStreamAsync(GetImageArgs args,
4686
CancellationToken cancellationToken = default);
87+
88+
/// <summary>
89+
/// 通过 Get 请求获取网络图片并保存到本地返回路径
90+
/// </summary>
91+
/// <param name="args"></param>
92+
/// <param name="cancellationToken"></param>
93+
/// <returns></returns>
94+
Task<string?> GetImageFilePathAsync(GetImageArgs args,
95+
CancellationToken cancellationToken = default);
96+
97+
/// <summary>
98+
/// 通过 Get 请求获取网络图片并保存到本地返回路径或流泛型自定义类型转换返回
99+
/// </summary>
100+
/// <typeparam name="T"></typeparam>
101+
/// <param name="args"></param>
102+
/// <param name="filePathConvert"></param>
103+
/// <param name="responseConvert"></param>
104+
/// <param name="cancellationToken"></param>
105+
/// <returns></returns>
106+
Task<T?> GetImageAsync<T>(GetImageArgs args, Func<string, T?> filePathConvert, Func<HttpResponseImageContent, T?> responseConvert, CancellationToken cancellationToken) where T : notnull;
47107
}
48108
#endif

src/BD.Common8.Http.ClientFactory/Services/Implementation/FusilladeClientHttpClientFactory.cs

Lines changed: 130 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,21 @@ namespace BD.Common8.Http.ClientFactory.Services.Implementation;
1313
/// </summary>
1414
public class FusilladeClientHttpClientFactory : IClientHttpClientFactory, IDisposable
1515
{
16+
internal static readonly global::Microsoft.IO.RecyclableMemoryStreamManager MemoryStreamManager = new();
17+
18+
internal static async Task<global::Microsoft.IO.RecyclableMemoryStream> ReadAsStreamAsync(HttpContent content, CancellationToken cancellationToken = default)
19+
{
20+
if (content is RecyclableMemoryStreamContent streamContent)
21+
{
22+
return streamContent.Stream;
23+
}
24+
25+
var stream = await content.ReadAsStreamAsync(cancellationToken);
26+
var memoryStream = MemoryStreamManager.GetStream();
27+
await stream.CopyToAsync(memoryStream, cancellationToken);
28+
return memoryStream;
29+
}
30+
1631
bool disposedValue;
1732

1833
readonly HttpMessageHandler handler;
@@ -301,7 +316,7 @@ static void Write(Stream s, IEnumerable<object> items)
301316
/// <returns>The unique key.</returns>
302317
public static string UniqueKeyForRequest(HttpRequestMessage request)
303318
{
304-
var requestUriString = ImageHttpClientServiceImpl.ImageHttpRequestMessage.GetOriginalRequestUri(request);
319+
var requestUriString = ImageHttpClientServiceImpl.GetOriginalRequestUri(request);
305320
return UniqueKeyForRequest(requestUriString, request);
306321
}
307322

@@ -385,32 +400,58 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
385400

386401
if (cacheResult != null && resp.Content != null)
387402
{
388-
var ms = new MemoryStream();
403+
using var ms = FusilladeClientHttpClientFactory.MemoryStreamManager.GetStream();
389404
#if NET5_0_OR_GREATER
390405
var stream = await resp.Content.ReadAsStreamAsync(realToken.Token).ConfigureAwait(false);
391406
#else
392407
var stream = await resp.Content.ReadAsStreamAsync().ConfigureAwait(false);
393408
#endif
394-
await stream.CopyToAsync(ms, 32 * 1024, realToken.Token).ConfigureAwait(false);
409+
await stream.CopyToAsync(ms, realToken.Token).ConfigureAwait(false);
395410

396411
realToken.Token.ThrowIfCancellationRequested();
397412

398-
var newResp = new HttpResponseMessage();
413+
var newResp = new HttpResponseMessage()
414+
{
415+
RequestMessage = request,
416+
};
399417
foreach (var kvp in resp.Headers)
400418
{
401419
newResp.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value);
402420
}
403421

404-
var newContent = new ByteArrayContent(ms.ToArray());
405-
foreach (var kvp in resp.Content.Headers)
422+
if (request.RequestUri?.ToString() == "https://picsum.photos/360/202?image=883")
406423
{
407-
newContent.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value);
424+
//TODO
408425
}
409426

410-
newResp.Content = newContent;
411-
427+
{
428+
var newContent = new RecyclableMemoryStreamContent(ms);
429+
foreach (var kvp in resp.Content.Headers)
430+
{
431+
newContent.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value);
432+
}
433+
newResp.Content = newContent;
434+
}
412435
resp = newResp;
413436
await cacheResult(request, resp, key, realToken.Token).ConfigureAwait(false);
437+
if (request.Options.TryGetValue(IImageHttpClientService.KeyFilePath, out var filePath))
438+
{
439+
var newContent = new FileContent(filePath);
440+
foreach (var kvp in resp.Content.Headers)
441+
{
442+
newContent.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value);
443+
}
444+
newResp.Content = newContent;
445+
}
446+
else
447+
{
448+
var newContent = new ByteArrayContent(ms.GetBuffer());
449+
foreach (var kvp in resp.Content.Headers)
450+
{
451+
newContent.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value);
452+
}
453+
newResp.Content = newContent;
454+
}
414455
}
415456

416457
return resp;
@@ -429,6 +470,53 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
429470
}
430471
}
431472

473+
file sealed class FileContent(string filePath) : HttpContent
474+
{
475+
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
476+
{
477+
try
478+
{
479+
using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
480+
await fileStream.CopyToAsync(stream);
481+
}
482+
catch (DirectoryNotFoundException)
483+
{
484+
}
485+
catch (FileNotFoundException)
486+
{
487+
}
488+
}
489+
490+
protected override bool TryComputeLength(out long length)
491+
{
492+
try
493+
{
494+
length = new FileInfo(filePath).Length;
495+
return true;
496+
}
497+
catch (DirectoryNotFoundException)
498+
{
499+
}
500+
catch (FileNotFoundException)
501+
{
502+
}
503+
length = 0;
504+
return false;
505+
}
506+
}
507+
508+
file sealed class RecyclableMemoryStreamContent : StreamContent
509+
{
510+
readonly global::Microsoft.IO.RecyclableMemoryStream stream;
511+
512+
internal global::Microsoft.IO.RecyclableMemoryStream Stream => stream;
513+
514+
internal RecyclableMemoryStreamContent(global::Microsoft.IO.RecyclableMemoryStream stream) : base(stream)
515+
{
516+
this.stream = stream;
517+
}
518+
}
519+
432520
/// <summary>
433521
/// A http handler that will make a response even if the HttpClient is offline.
434522
/// </summary>
@@ -439,6 +527,8 @@ file sealed class OfflineHttpMessageHandler2() : HttpMessageHandler
439527
{
440528
const HttpStatusCode OfflineCacheMiss = (HttpStatusCode)599;
441529

530+
HttpResponseMessage ResponseOfflineCacheMiss() => new(OfflineCacheMiss);
531+
442532
/// <inheritdoc />
443533
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
444534
{
@@ -453,14 +543,40 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
453543
throw new Exception("Configure NetCache.RequestCache before calling this!");
454544
}
455545

456-
var body = await retrieveBody(request, RateLimitedHttpMessageHandler2.UniqueKeyForRequest(request), cancellationToken).ConfigureAwait(false);
546+
var uniqueKey = RateLimitedHttpMessageHandler2.UniqueKeyForRequest(request);
547+
var body = await retrieveBody(request, uniqueKey, cancellationToken).ConfigureAwait(false);
457548
if (body == null)
458549
{
459-
return new HttpResponseMessage(OfflineCacheMiss);
550+
if (request.Options.TryGetValue(IImageHttpClientService.KeyFilePath, out var filePath))
551+
{
552+
if (File.Exists(filePath))
553+
{
554+
return new HttpResponseMessage(HttpStatusCode.OK)
555+
{
556+
Content = new FileContent(filePath),
557+
};
558+
}
559+
else
560+
{
561+
return ResponseOfflineCacheMiss();
562+
}
563+
}
564+
//else if (request.Options.TryGetValue(IImageHttpClientService.KeyStream, out var stream))
565+
//{
566+
// return new HttpResponseMessage(HttpStatusCode.OK)
567+
// {
568+
// Content = new StreamContent(stream),
569+
// };
570+
//}
571+
else
572+
{
573+
return ResponseOfflineCacheMiss();
574+
}
460575
}
461-
462-
var byteContent = new ByteArrayContent(body);
463-
return new HttpResponseMessage(HttpStatusCode.OK) { Content = byteContent };
576+
return new HttpResponseMessage(HttpStatusCode.OK)
577+
{
578+
Content = new ByteArrayContent(body),
579+
};
464580
}
465581
}
466582
#endif

0 commit comments

Comments
 (0)