From b9482a65eb42193143696804e3297edaeb7af42e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=84=E5=A5=9A=E6=A2=A6=E7=81=B5?= Date: Mon, 6 Apr 2026 13:03:21 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat(cahce):=20=E7=BC=93=E5=AD=98=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IO/Net/Http/Cache/HttpCacheHandler.cs | 49 +++ .../IO/Net/Http/Cache/HttpCacheRepository.cs | 363 ++++++++++++++++++ .../Net/Http/Cache/Models/BlockingStream.cs | 26 ++ .../IO/Net/Http/Cache/Models/CacheStream.cs | 66 ++++ .../Net/Http/Cache/Models/HttpCacheDetails.cs | 24 ++ .../Net/Http/Cache/Models/HttpCacheStatus.cs | 12 + .../Cache/Models/HttpCacheUpdateHandle.cs | 51 +++ PCL.Core/IO/Net/NetworkService.cs | 24 +- PCL.Core/PCL.Core.csproj | 1 + 9 files changed, 613 insertions(+), 3 deletions(-) create mode 100644 PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs create mode 100644 PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs create mode 100644 PCL.Core/IO/Net/Http/Cache/Models/BlockingStream.cs create mode 100644 PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs create mode 100644 PCL.Core/IO/Net/Http/Cache/Models/HttpCacheDetails.cs create mode 100644 PCL.Core/IO/Net/Http/Cache/Models/HttpCacheStatus.cs create mode 100644 PCL.Core/IO/Net/Http/Cache/Models/HttpCacheUpdateHandle.cs diff --git a/PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs b/PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs new file mode 100644 index 000000000..e8ec0a460 --- /dev/null +++ b/PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs @@ -0,0 +1,49 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using PCL.Core.IO.Net.Http.Cache.Models; + +namespace PCL.Core.IO.Net.Http.Cache; + +/// +/// HTTP 缓存处理器 +/// +public class HttpCacheHandler:DelegatingHandler +{ + private HttpCacheRepository _repository; + public HttpCacheHandler(HttpMessageHandler invoker, HttpCacheRepository repo) + { + InnerHandler = invoker; + _repository = repo; + } + + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { + return SendAsync(request, cancellationToken).GetAwaiter().GetResult(); + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if(!_repository.TryGetCacheData(request.RequestUri!.ToString(),out var details)) + return await base.SendAsync(request, cancellationToken); + if (details.ExpiredAt is not null && + details.LastUpdate.AddSeconds((double)details.ExpiredAt) < DateTimeOffset.Now + && _repository.TryGetCacheResponse(request,out var cacheResponse) && !details.EnsureValidate + ) + return cacheResponse; + + if(details.Tag is not null) request.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(details.Tag)); + if(details.LastModify is not null) request.Headers.IfModifiedSince = DateTimeOffset.Parse(details.LastModify); + var response = base.Send(request, cancellationToken); + if(response.StatusCode == HttpStatusCode.NotModified && _repository.TryGetCacheResponse(request,out cacheResponse)) + return cacheResponse; + var handle = await _repository.TryBeginUpdateAsync(request.RequestUri.ToString()); + if (handle is not null) + response.Content = new StreamContent(new CacheStream(handle, + await response.Content.ReadAsStreamAsync(cancellationToken))); + return response; + } +} \ No newline at end of file diff --git a/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs b/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs new file mode 100644 index 000000000..b9652b44d --- /dev/null +++ b/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs @@ -0,0 +1,363 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; +using PCL.Core.IO.Net.Http.Cache.Models; +using PCL.Core.IO.Storage; +using PCL.Core.Logging; +using PCL.Core.Utils.Hash; + +namespace PCL.Core.IO.Net.Http.Cache; + +/// +/// HTTP 缓存储存库,支持 LazyGC +/// +/// SQLite 数据库路径 +/// 存储位置 +public class HttpCacheRepository(string dbPath,string destLocation) +{ + + #region "预设 SQL 命令" + + private const string FindTable = "SELECT * FROM HttpCache WHERE RequestUri = @Uri"; + + private const string CreateTable = """ + + CREATE TABLE IF NOT EXISTS HttpCache ( + RequestUri TEXT NOT NULL PRIMARY KEY, + Tag TEXT NULL, + LastModify TEXT NULL, + ExpiredAt TEXT NOT NULL, + EnsureValidate INTEGER NOT NULL DEFAULT 0, + Status INTEGER NOT NULL DEFAULT 0 + LastUpdate TEXT NOT NULL + ) + """; + + private const string InsertTable = """ + INSERT OR REPLACE INTO HttpCache ( + RequestUri, Tag, LastModify, ExpiredAt, EnsureValidate, Status, LastUpdate + ) VALUES ( + @Uri, @Tag, @LastModify, @ExpiredAt, @EnsureValidate, @Status, @LastUpdate + ) + """; + + + private const string DeleteTable = "DELETE FROM HttpCache WHERE RequestUri = @Uri"; + + + #endregion + + #region "配置" + + + private readonly SqliteConnection _connection = new($"Data Source={dbPath}"); + + private readonly HashStorage _store = new(destLocation, SHA256Provider.Instance, true); + + private readonly string _taskTemp = Path.Combine(destLocation, "TaskTemp"); + #endregion + + #region "HTTP 缓存处理" + + /// + /// 初始化数据库 + /// + public void Initialize() + { + try + { + if (!Directory.Exists(destLocation)) Directory.CreateDirectory(destLocation); + } + catch (IOException) + { + File.Delete(destLocation); + Directory.CreateDirectory(destLocation); + } + + try + { + if (!Directory.Exists(_taskTemp)) Directory.CreateDirectory(_taskTemp); + } + catch (IOException) + { + File.Delete(_taskTemp); + Directory.CreateDirectory(_taskTemp); + } + + _connection.Open(); + var cmd = _connection.CreateCommand(); + cmd.CommandText = CreateTable; + cmd.ExecuteNonQuery(); + } + + /// + /// 获取缓存数据 + /// + /// + /// + /// + public bool TryGetCacheData(string uri,[NotNullWhen(true)] out HttpCacheDetails? details) + { + details = null; + using var cmd = _FindTableWithUri(uri); + using var result = cmd.ExecuteReader(); + if (!result.Read()) return false; + if ((HttpCacheStatus)result.GetInt16(6) is HttpCacheStatus.Invalid or HttpCacheStatus.Expired) + { + _DeleteTable(result.GetString(0)); + return false; + } + details = new HttpCacheDetails(this) + { + RequestUri = result.GetString(0), + Tag = result.GetString(1), + LastModify = result.GetString(2), + ExpiredAt = result.GetInt32(3), + EnsureValidate = result.GetBoolean(4), + Status = (HttpCacheStatus)result.GetInt16(5), + LastUpdate = DateTimeOffset.Parse(result.GetString(6)) + }; + return true; + } + + /// + /// 获取已缓存的响应 + /// + /// 发出的 HTTP 请求 + /// 缓存响应 + /// 如果缓存存在,返回 true + public bool TryGetCacheResponse(HttpRequestMessage request,[NotNullWhen(true)] out HttpResponseMessage? response) + { + response = null; + if (request.RequestUri is null) return false; + if (!TryGetCacheData(request.RequestUri.ToString(), out var details)) return false; + if (details.Status is HttpCacheStatus.Updating) + return false; + response = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StreamContent(_store.Get(details.Hash!) ?? throw new NullReferenceException("Hash Storage return null.")), + RequestMessage = request + }; + response.Headers.TryAddWithoutValidation("X-Cache-Repository-Status", "Hit"); + + return true; + } + + /// + /// 获取缓存更新句柄 + /// + /// URL + /// + public async ValueTask TryBeginUpdateAsync(string uri) + { + if (!TryGetCacheData(uri, out var details)) + { + Span buffer = stackalloc byte[16]; + Random.Shared.NextBytes(buffer); + details = new HttpCacheDetails(this) + { + LastUpdate = DateTimeOffset.Now, + RequestUri = uri, + EnsureValidate = false, + ExpiredAt = null, + Tag = null, + Status = HttpCacheStatus.Updating + }; + await using var cmd = _InsertDatabase(details); + cmd.ExecuteNonQuery(); + // 互斥锁,避免线程冲突 + }else if (details.Status == HttpCacheStatus.Updating) return null; + + var handle = details.GetUpdateHandle(); + await _store.PutAsync(handle.GetOutputStream()); + return handle; + } + /// + /// 异步结束更新并设置缓存状态 + /// + /// + /// + public async ValueTask TryEndUpdateAsync(HttpCacheUpdateHandle handle) + { + var details = handle.Details; + if (details is null) return false; + details.Status = HttpCacheStatus.Ok; + await using var cmd = _UpdateTable(details); + if (cmd is null) throw new ArgumentException("Cache details is not updated"); + await cmd.ExecuteNonQueryAsync(); + details.Hash = await _store.PutAsync(details.FilePath!).ConfigureAwait(false); + return true; + } + + + /// + /// 删除一个缓存 + /// + /// 请求 + /// + public bool TryRemove(HttpRequestMessage request) + { + try + { + if (!TryGetCacheData(request.RequestUri!.ToString(), out var details) && details?.Hash is null) return false; + if (request.RequestUri is null) return false; + using var cmd = _DeleteTable(request.RequestUri.ToString()); + cmd.ExecuteNonQuery(); + _store.DeleteAsync(details.Hash!).GetAwaiter().GetResult(); + return true; + } + catch(Exception ex) + { + LogWrapper.Error(ex,"Http", "删除缓存文件失败"); + } + + return false; + } + + /// + /// 删除一个缓存 + /// + /// 请求 + /// + public async ValueTask TryRemoveAsync(HttpRequestMessage request) + { + try + { + if (!TryGetCacheData(request.RequestUri!.ToString(), out var details) && details?.Hash is null) return false; + if (request.RequestUri is null) return false; + await using var cmd = _DeleteTable(request.RequestUri.ToString()); + await cmd.ExecuteNonQueryAsync(); + await _store.DeleteAsync(details.Hash!).ConfigureAwait(false); + return true; + } + catch(Exception ex) + { + LogWrapper.Error(ex,"Http", "删除缓存文件失败"); + } + + return false; + } + + /// + /// 将全部对象标记为过期 + /// + public void MarkAllObjectAsExpired() + { + using var cmd = _connection.CreateCommand(); + cmd.CommandText = "UPDATE HttpCache SET Status = 2"; + cmd.ExecuteNonQuery(); + } + + + + #endregion + + #region "SQL 执行函数" + + + private SqliteCommand _InsertDatabase(HttpCacheDetails details) + { + var cmd = _connection.CreateCommand(); + cmd.CommandText = InsertTable; + cmd.Parameters.AddWithValue("@Uri", details.RequestUri); + cmd.Parameters.AddWithValue("@Tag", details.Tag); + cmd.Parameters.AddWithValue("@LastModify", details.LastModify); + cmd.Parameters.AddWithValue("@ExpiredAt", details.ExpiredAt); + cmd.Parameters.AddWithValue("@EnsureValidate", details.EnsureValidate); + cmd.Parameters.AddWithValue("@Status", (int)details.Status); + return cmd; + } + + private SqliteCommand _DeleteTable(string uri) + { + var cmd = _connection.CreateCommand(); + cmd.CommandText = DeleteTable; + cmd.Parameters.AddWithValue("@Uri", uri); + return cmd; + } + + private SqliteCommand _FindTableWithUri(string uri) + { + var queryCmd = _connection.CreateCommand(); + queryCmd.CommandText = FindTable; + queryCmd.Parameters.AddWithValue("@Uri", uri); + return queryCmd; + } + + private SqliteCommand? _UpdateTable(HttpCacheDetails details) + { + using var queryCmd = _FindTableWithUri(details.RequestUri); + // 获取用于比较的原始内容 + using var reader = queryCmd.ExecuteReader(); + if (!reader.Read()) + { + // 可能已经被删掉了,添加就好 + return _InsertDatabase(details); + + } + var sb = new StringBuilder(); + sb.Append("UPDATE HttpCache "); + var writeCmd = _connection.CreateCommand(); + var setCount = 0; + // 按需更新以减少开销 + if (reader.GetString(0) != details.RequestUri) + { + setCount++; + sb.Append("SET RequestUri = @Uri, "); + writeCmd.Parameters.AddWithValue("@Uri", details.RequestUri); + } + + if (reader.GetString(1) != details.Tag) + { + setCount++; + sb.Append("SET Tag = @Tag, "); + writeCmd.Parameters.AddWithValue("@Tag", details.Tag); + } + if (reader.GetString(2) != details.LastModify) + { + setCount++; + sb.Append("SET LastModify = @LastModify, "); + writeCmd.Parameters.AddWithValue("@LastModify", details.LastModify); + } + if (reader.GetInt32(3) != details.ExpiredAt) + { + setCount++; + sb.Append("SET ExpiredAt = @ExpiredAt, "); + writeCmd.Parameters.AddWithValue("@ExpiredAt", details.ExpiredAt); + } + + if (reader.GetBoolean(4) != details.EnsureValidate) + { + setCount++; + sb.Append("SET EnsureValidate = @EnsureValidate,"); + writeCmd.Parameters.AddWithValue("@EnsureValidate", details.EnsureValidate); + } + if ((HttpCacheStatus)reader.GetInt16(6) != details.Status) + { + setCount++; + sb.Append("SET Status = @Status"); + writeCmd.Parameters.AddWithValue("@Status", (int)details.Status); + } + + if (reader.GetString(7) != details.LastUpdate.ToString()) + { + setCount++; + sb.Append("SET LastUpdate = @LastUpdate"); + writeCmd.Parameters.AddWithValue("@LastUpdate", details.LastUpdate.ToString()); + } + + if (setCount == 0) return null; + sb.Append("WHERE RequestUri = @Uri"); + writeCmd.CommandText = sb.ToString(); + return writeCmd; + } + + + #endregion +} \ No newline at end of file diff --git a/PCL.Core/IO/Net/Http/Cache/Models/BlockingStream.cs b/PCL.Core/IO/Net/Http/Cache/Models/BlockingStream.cs new file mode 100644 index 000000000..41df40d12 --- /dev/null +++ b/PCL.Core/IO/Net/Http/Cache/Models/BlockingStream.cs @@ -0,0 +1,26 @@ +using System; +using System.IO; +using System.Threading; + +namespace PCL.Core.IO.Net.Http.Cache.Models; + +public class BlockingStream:MemoryStream +{ + private SemaphoreSlim _lock = new(0); + public override int Read(Span buffer) + { + _lock.Wait(); + return base.Read(buffer); + } + + internal void Readable() + { + _lock.Release(); + } + + protected override void Dispose(bool disposing) + { + _lock.Dispose(); + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs b/PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs new file mode 100644 index 000000000..f5b560153 --- /dev/null +++ b/PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; + +namespace PCL.Core.IO.Net.Http.Cache.Models; + +public class CacheStream: Stream +{ + private Stream _responseStream; + private BlockingStream? _destStream; + private HttpCacheUpdateHandle _handle; + + public CacheStream(HttpCacheUpdateHandle handle, byte[] data) + { + _responseStream = new MemoryStream(data); + _destStream = handle.GetOutputStream(); + _handle = handle; + } + + public CacheStream(HttpCacheUpdateHandle handle, Stream responseStream) + { + _responseStream = responseStream; + _destStream = handle.GetOutputStream(); + _handle = handle; + } + + + public override void Flush() { } + + public override int Read(byte[] buffer, int offset, int count) + { + var read = _responseStream.Read(buffer, offset, count); + _destStream?.Write(buffer); + _destStream?.Readable(); + return read; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new InvalidOperationException("This stream is readonly."); + } + + public override void SetLength(long value) + { + throw new InvalidOperationException("This stream is readonly."); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new InvalidOperationException("This stream is readonly."); + } + + public override bool CanRead => _responseStream.CanRead; + public override bool CanSeek => _responseStream.CanSeek; + public override bool CanWrite => false; + public override long Length => _responseStream.Length; + public override long Position { get; + set => throw new InvalidOperationException("can not set position on readonly stream"); + } + + protected override void Dispose(bool disposing) + { + _destStream?.Dispose(); + _handle.Dispose(); + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/PCL.Core/IO/Net/Http/Cache/Models/HttpCacheDetails.cs b/PCL.Core/IO/Net/Http/Cache/Models/HttpCacheDetails.cs new file mode 100644 index 000000000..3f9041e50 --- /dev/null +++ b/PCL.Core/IO/Net/Http/Cache/Models/HttpCacheDetails.cs @@ -0,0 +1,24 @@ +using System; + +namespace PCL.Core.IO.Net.Http.Cache.Models; + +/// +/// HTTP 缓存信息 +/// +public class HttpCacheDetails(HttpCacheRepository repo) +{ + public required DateTimeOffset LastUpdate { get; set; } + public required string RequestUri { get; set; } + public string? Tag { get; set; } + public string? LastModify { get; set; } + public int? ExpiredAt { get; set; } + public bool EnsureValidate { get; set; } + public string? FilePath { get; set; } + public string? Hash { get; set; } + public HttpCacheStatus Status = HttpCacheStatus.Invalid; + + public HttpCacheUpdateHandle GetUpdateHandle() + { + return new HttpCacheUpdateHandle(repo); + } +} \ No newline at end of file diff --git a/PCL.Core/IO/Net/Http/Cache/Models/HttpCacheStatus.cs b/PCL.Core/IO/Net/Http/Cache/Models/HttpCacheStatus.cs new file mode 100644 index 000000000..3b3fd6653 --- /dev/null +++ b/PCL.Core/IO/Net/Http/Cache/Models/HttpCacheStatus.cs @@ -0,0 +1,12 @@ +namespace PCL.Core.IO.Net.Http.Cache.Models; + +public enum HttpCacheStatus +{ + /// + /// 缓存无效(例如文件已经删除) + /// + Invalid, + Ok, + Expired, + Updating +} \ No newline at end of file diff --git a/PCL.Core/IO/Net/Http/Cache/Models/HttpCacheUpdateHandle.cs b/PCL.Core/IO/Net/Http/Cache/Models/HttpCacheUpdateHandle.cs new file mode 100644 index 000000000..d383460b1 --- /dev/null +++ b/PCL.Core/IO/Net/Http/Cache/Models/HttpCacheUpdateHandle.cs @@ -0,0 +1,51 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace PCL.Core.IO.Net.Http.Cache.Models; + +public class HttpCacheUpdateHandle(HttpCacheRepository repo) : IDisposable +{ + + private BlockingStream? _fileStream; + private bool _disposed; + /// + /// 该对象对应的缓存信息 + /// + public HttpCacheDetails? Details { get; set; } + /// + /// 获取当前文件的写入流 + /// + /// + /// + public BlockingStream GetOutputStream() + { + ObjectDisposedException.ThrowIf(_disposed, typeof(HttpCacheUpdateHandle)); + return _fileStream ??= new BlockingStream(); + } + + ~HttpCacheUpdateHandle() + { + _Dispose(false); + } + + public void Dispose() + { + _Dispose(true); + GC.SuppressFinalize(this); + } + + private void _Dispose(bool dispose) + { + _DisposeAsync(dispose).GetAwaiter().GetResult(); + } + + private async Task _DisposeAsync(bool dispose) + { + if (_disposed) return; + await repo.TryEndUpdateAsync(this); + Details?.Status = HttpCacheStatus.Ok; + _disposed = true; + if (dispose) _fileStream?.Dispose(); + } +} \ No newline at end of file diff --git a/PCL.Core/IO/Net/NetworkService.cs b/PCL.Core/IO/Net/NetworkService.cs index 75e79625e..b7a471240 100644 --- a/PCL.Core/IO/Net/NetworkService.cs +++ b/PCL.Core/IO/Net/NetworkService.cs @@ -1,9 +1,11 @@ using System; +using System.IO; using System.Net; using System.Net.Http; using Microsoft.Extensions.DependencyInjection; using PCL.Core.App; using PCL.Core.App.IoC; +using PCL.Core.IO.Net.Http.Cache; using PCL.Core.IO.Net.Http.Client; using PCL.Core.Logging; using Polly; @@ -12,14 +14,16 @@ namespace PCL.Core.IO.Net; [LifecycleService(LifecycleState.Loading)] [LifecycleScope("network", "网络服务")] -public partial class NetworkService { - +public partial class NetworkService +{ + private static HttpCacheRepository _repo = new(Path.Combine(Paths.Temp,"cache","cache.db"),Path.Combine(Paths.Temp,"cache")); + private static ServiceProvider? _provider; private static IHttpClientFactory? _factory; [LifecycleStart] private static void _Start() - { + { _repo.Initialize(); var services = new ServiceCollection(); services.AddHttpClient("default") .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler @@ -35,6 +39,20 @@ private static void _Start() : null } ); + services.AddHttpClient("cache").ConfigurePrimaryHttpMessageHandler(() => new HttpCacheHandler( + new SocketsHttpHandler + { + UseProxy = true, + UseCookies = false, + AutomaticDecompression = DecompressionMethods.All, + Proxy = HttpProxyManager.Instance, + AllowAutoRedirect = true, + MaxAutomaticRedirections = 20, + ConnectCallback = Config.Network.EnableDoH + ? HostConnectionHandler.Instance.GetConnectionAsync + : null + },_repo)); + _provider = services.BuildServiceProvider(); _factory = _provider.GetRequiredService(); diff --git a/PCL.Core/PCL.Core.csproj b/PCL.Core/PCL.Core.csproj index e07c065d0..4b17bc1e3 100644 --- a/PCL.Core/PCL.Core.csproj +++ b/PCL.Core/PCL.Core.csproj @@ -56,6 +56,7 @@ + From 7af8c973b3510aee3e28039baa59fccc6828bd50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=84=E5=A5=9A=E6=A2=A6=E7=81=B5?= Date: Mon, 6 Apr 2026 13:11:56 +0800 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20=E7=BC=93=E5=AD=98=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=8F=AF=E8=83=BD=E4=B8=8D=E7=94=9F=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs | 9 ++++++++- PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs | 3 +-- PCL.Core/IO/Net/Http/Cache/Models/HttpCacheDetails.cs | 1 - 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs b/PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs index e8ec0a460..dc7641de7 100644 --- a/PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs +++ b/PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs @@ -37,10 +37,17 @@ protected override async Task SendAsync(HttpRequestMessage if(details.Tag is not null) request.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(details.Tag)); if(details.LastModify is not null) request.Headers.IfModifiedSince = DateTimeOffset.Parse(details.LastModify); - var response = base.Send(request, cancellationToken); + var response = await base.SendAsync(request, cancellationToken); + if (response.Headers.CacheControl?.NoStore ?? false) return response; if(response.StatusCode == HttpStatusCode.NotModified && _repository.TryGetCacheResponse(request,out cacheResponse)) return cacheResponse; var handle = await _repository.TryBeginUpdateAsync(request.RequestUri.ToString()); + var newDetails = handle?.Details; + newDetails?.RequestUri = request.RequestUri.ToString(); + newDetails?.LastUpdate = DateTimeOffset.Now; + newDetails?.EnsureValidate = response.Headers.CacheControl?.NoCache ?? false; + newDetails?.LastModify = response.Headers.Date.ToString(); + newDetails?.Tag = response.Headers.ETag?.Tag; if (handle is not null) response.Content = new StreamContent(new CacheStream(handle, await response.Content.ReadAsStreamAsync(cancellationToken))); diff --git a/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs b/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs index b9652b44d..c697d0412 100644 --- a/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs +++ b/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs @@ -189,9 +189,8 @@ public async ValueTask TryEndUpdateAsync(HttpCacheUpdateHandle handle) if (details is null) return false; details.Status = HttpCacheStatus.Ok; await using var cmd = _UpdateTable(details); - if (cmd is null) throw new ArgumentException("Cache details is not updated"); + if (cmd is null) return true; await cmd.ExecuteNonQueryAsync(); - details.Hash = await _store.PutAsync(details.FilePath!).ConfigureAwait(false); return true; } diff --git a/PCL.Core/IO/Net/Http/Cache/Models/HttpCacheDetails.cs b/PCL.Core/IO/Net/Http/Cache/Models/HttpCacheDetails.cs index 3f9041e50..a935089e5 100644 --- a/PCL.Core/IO/Net/Http/Cache/Models/HttpCacheDetails.cs +++ b/PCL.Core/IO/Net/Http/Cache/Models/HttpCacheDetails.cs @@ -13,7 +13,6 @@ public class HttpCacheDetails(HttpCacheRepository repo) public string? LastModify { get; set; } public int? ExpiredAt { get; set; } public bool EnsureValidate { get; set; } - public string? FilePath { get; set; } public string? Hash { get; set; } public HttpCacheStatus Status = HttpCacheStatus.Invalid; From eb57eb6fc7092ebf60e814cc2c830fb86bafca3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=84=E5=A5=9A=E6=A2=A6=E7=81=B5?= Date: Mon, 6 Apr 2026 14:19:38 +0800 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20=E6=9C=AA=E5=85=B3=E8=81=94=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E3=80=81=E6=AF=8F=E6=AC=A1=E6=9F=A5=E8=AF=A2=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E6=96=B0=E8=BF=9E=E6=8E=A5=E3=80=81=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IO/Net/Http/Cache/HttpCacheRepository.cs | 34 ++++++++++++------- .../IO/Net/Http/Cache/Models/CacheStream.cs | 4 +-- .../Net/Http/Cache/Models/HttpCacheDetails.cs | 5 ++- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs b/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs index c697d0412..d43d0202a 100644 --- a/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs +++ b/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs @@ -33,7 +33,7 @@ CREATE TABLE IF NOT EXISTS HttpCache ( LastModify TEXT NULL, ExpiredAt TEXT NOT NULL, EnsureValidate INTEGER NOT NULL DEFAULT 0, - Status INTEGER NOT NULL DEFAULT 0 + Status INTEGER NOT NULL DEFAULT 0, LastUpdate TEXT NOT NULL ) """; @@ -54,8 +54,13 @@ INSERT OR REPLACE INTO HttpCache ( #region "配置" - - private readonly SqliteConnection _connection = new($"Data Source={dbPath}"); + + private readonly Func _connectionFactory = () => + { + var c = new SqliteConnection($"Data Source={dbPath}"); + c.Open(); + return c; + }; private readonly HashStorage _store = new(destLocation, SHA256Provider.Instance, true); @@ -89,8 +94,8 @@ public void Initialize() Directory.CreateDirectory(_taskTemp); } - _connection.Open(); - var cmd = _connection.CreateCommand(); + using var connection = _connectionFactory.Invoke(); + var cmd = connection.CreateCommand(); cmd.CommandText = CreateTable; cmd.ExecuteNonQuery(); } @@ -248,7 +253,8 @@ public async ValueTask TryRemoveAsync(HttpRequestMessage request) /// public void MarkAllObjectAsExpired() { - using var cmd = _connection.CreateCommand(); + using var conn = _connectionFactory.Invoke(); + using var cmd = conn.CreateCommand(); cmd.CommandText = "UPDATE HttpCache SET Status = 2"; cmd.ExecuteNonQuery(); } @@ -262,7 +268,8 @@ public void MarkAllObjectAsExpired() private SqliteCommand _InsertDatabase(HttpCacheDetails details) { - var cmd = _connection.CreateCommand(); + using var conn = _connectionFactory.Invoke(); + var cmd = conn.CreateCommand(); cmd.CommandText = InsertTable; cmd.Parameters.AddWithValue("@Uri", details.RequestUri); cmd.Parameters.AddWithValue("@Tag", details.Tag); @@ -275,7 +282,8 @@ private SqliteCommand _InsertDatabase(HttpCacheDetails details) private SqliteCommand _DeleteTable(string uri) { - var cmd = _connection.CreateCommand(); + using var conn = _connectionFactory.Invoke(); + var cmd = conn.CreateCommand(); cmd.CommandText = DeleteTable; cmd.Parameters.AddWithValue("@Uri", uri); return cmd; @@ -283,7 +291,8 @@ private SqliteCommand _DeleteTable(string uri) private SqliteCommand _FindTableWithUri(string uri) { - var queryCmd = _connection.CreateCommand(); + using var conn = _connectionFactory.Invoke(); + var queryCmd = conn.CreateCommand(); queryCmd.CommandText = FindTable; queryCmd.Parameters.AddWithValue("@Uri", uri); return queryCmd; @@ -302,7 +311,8 @@ private SqliteCommand _FindTableWithUri(string uri) } var sb = new StringBuilder(); sb.Append("UPDATE HttpCache "); - var writeCmd = _connection.CreateCommand(); + using var conn = _connectionFactory.Invoke(); + var writeCmd = conn.CreateCommand(); var setCount = 0; // 按需更新以减少开销 if (reader.GetString(0) != details.RequestUri) @@ -337,14 +347,14 @@ private SqliteCommand _FindTableWithUri(string uri) sb.Append("SET EnsureValidate = @EnsureValidate,"); writeCmd.Parameters.AddWithValue("@EnsureValidate", details.EnsureValidate); } - if ((HttpCacheStatus)reader.GetInt16(6) != details.Status) + if ((HttpCacheStatus)reader.GetInt16(5) != details.Status) { setCount++; sb.Append("SET Status = @Status"); writeCmd.Parameters.AddWithValue("@Status", (int)details.Status); } - if (reader.GetString(7) != details.LastUpdate.ToString()) + if (reader.GetString(6) != details.LastUpdate.ToString()) { setCount++; sb.Append("SET LastUpdate = @LastUpdate"); diff --git a/PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs b/PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs index f5b560153..29b5f1ad2 100644 --- a/PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs +++ b/PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs @@ -29,7 +29,7 @@ public override void Flush() { } public override int Read(byte[] buffer, int offset, int count) { var read = _responseStream.Read(buffer, offset, count); - _destStream?.Write(buffer); + _destStream?.Write(buffer,0, read); _destStream?.Readable(); return read; } @@ -53,7 +53,7 @@ public override void Write(byte[] buffer, int offset, int count) public override bool CanSeek => _responseStream.CanSeek; public override bool CanWrite => false; public override long Length => _responseStream.Length; - public override long Position { get; + public override long Position { get => _responseStream.Length; set => throw new InvalidOperationException("can not set position on readonly stream"); } diff --git a/PCL.Core/IO/Net/Http/Cache/Models/HttpCacheDetails.cs b/PCL.Core/IO/Net/Http/Cache/Models/HttpCacheDetails.cs index a935089e5..3cf8867d8 100644 --- a/PCL.Core/IO/Net/Http/Cache/Models/HttpCacheDetails.cs +++ b/PCL.Core/IO/Net/Http/Cache/Models/HttpCacheDetails.cs @@ -18,6 +18,9 @@ public class HttpCacheDetails(HttpCacheRepository repo) public HttpCacheUpdateHandle GetUpdateHandle() { - return new HttpCacheUpdateHandle(repo); + return new HttpCacheUpdateHandle(repo) + { + Details = this + }; } } \ No newline at end of file From 3fcdb418d6c78f8bedad7e329a2ec86ed56c4604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=84=E5=A5=9A=E6=A2=A6=E7=81=B5?= Date: Mon, 6 Apr 2026 14:32:46 +0800 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20=E6=9C=AA=E6=B7=BB=E5=8A=A0=20Hash,?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E9=93=BE=E8=A1=A8=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IO/Net/Http/Cache/HttpCacheHandler.cs | 3 ++- .../IO/Net/Http/Cache/HttpCacheRepository.cs | 21 ++++++++++++++----- .../IO/Net/Http/Cache/Models/CacheStream.cs | 1 + 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs b/PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs index dc7641de7..1c25cac68 100644 --- a/PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs +++ b/PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -46,7 +47,7 @@ protected override async Task SendAsync(HttpRequestMessage newDetails?.RequestUri = request.RequestUri.ToString(); newDetails?.LastUpdate = DateTimeOffset.Now; newDetails?.EnsureValidate = response.Headers.CacheControl?.NoCache ?? false; - newDetails?.LastModify = response.Headers.Date.ToString(); + newDetails?.LastModify = response.Content.Headers.LastModified.ToString(); newDetails?.Tag = response.Headers.ETag?.Tag; if (handle is not null) response.Content = new StreamContent(new CacheStream(handle, diff --git a/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs b/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs index d43d0202a..766907ee1 100644 --- a/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs +++ b/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs @@ -31,18 +31,19 @@ CREATE TABLE IF NOT EXISTS HttpCache ( RequestUri TEXT NOT NULL PRIMARY KEY, Tag TEXT NULL, LastModify TEXT NULL, - ExpiredAt TEXT NOT NULL, + ExpiredAt INTEGER NOT NULL, EnsureValidate INTEGER NOT NULL DEFAULT 0, Status INTEGER NOT NULL DEFAULT 0, - LastUpdate TEXT NOT NULL + LastUpdate TEXT NOT NULL, + Hash TEXT NULL ) """; private const string InsertTable = """ INSERT OR REPLACE INTO HttpCache ( - RequestUri, Tag, LastModify, ExpiredAt, EnsureValidate, Status, LastUpdate + RequestUri, Tag, LastModify, ExpiredAt, EnsureValidate, Status, LastUpdate, Hash ) VALUES ( - @Uri, @Tag, @LastModify, @ExpiredAt, @EnsureValidate, @Status, @LastUpdate + @Uri, @Tag, @LastModify, @ExpiredAt, @EnsureValidate, @Status, @LastUpdate, @Hash ) """; @@ -172,7 +173,8 @@ public bool TryGetCacheResponse(HttpRequestMessage request,[NotNullWhen(true)] o EnsureValidate = false, ExpiredAt = null, Tag = null, - Status = HttpCacheStatus.Updating + Status = HttpCacheStatus.Updating, + Hash = null }; await using var cmd = _InsertDatabase(details); cmd.ExecuteNonQuery(); @@ -277,6 +279,7 @@ private SqliteCommand _InsertDatabase(HttpCacheDetails details) cmd.Parameters.AddWithValue("@ExpiredAt", details.ExpiredAt); cmd.Parameters.AddWithValue("@EnsureValidate", details.EnsureValidate); cmd.Parameters.AddWithValue("@Status", (int)details.Status); + cmd.Parameters.AddWithValue("@Hash", details.Hash); return cmd; } @@ -361,6 +364,14 @@ private SqliteCommand _FindTableWithUri(string uri) writeCmd.Parameters.AddWithValue("@LastUpdate", details.LastUpdate.ToString()); } + if (reader.GetString(7) != details.Hash) + { + setCount++; + sb.Append("SET Hash = @Hash"); + writeCmd.Parameters.AddWithValue("@Hash", details.Hash); + + } + if (setCount == 0) return null; sb.Append("WHERE RequestUri = @Uri"); writeCmd.CommandText = sb.ToString(); diff --git a/PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs b/PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs index 29b5f1ad2..214473996 100644 --- a/PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs +++ b/PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs @@ -29,6 +29,7 @@ public override void Flush() { } public override int Read(byte[] buffer, int offset, int count) { var read = _responseStream.Read(buffer, offset, count); + if (read == 0) return read; _destStream?.Write(buffer,0, read); _destStream?.Readable(); return read; From 1ea134518869ce233a78190ed0dc53eec01af8d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=84=E5=A5=9A=E6=A2=A6=E7=81=B5?= Date: Mon, 6 Apr 2026 15:00:13 +0800 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20=E8=AF=BB=E5=8F=96=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E6=AD=BB=E9=94=81=E3=80=81=E5=B0=86=20SqliteConnection=20?= =?UTF-8?q?=E7=9A=84=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E5=88=B0=20SqliteCommand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IO/Net/Http/Cache/HttpCacheRepository.cs | 26 +++++++------------ .../Net/Http/Cache/Models/BlockingStream.cs | 19 +++++++++++--- .../IO/Net/Http/Cache/Models/CacheStream.cs | 4 +-- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs b/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs index 766907ee1..4cbe31041 100644 --- a/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs +++ b/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs @@ -64,8 +64,7 @@ INSERT OR REPLACE INTO HttpCache ( }; private readonly HashStorage _store = new(destLocation, SHA256Provider.Instance, true); - - private readonly string _taskTemp = Path.Combine(destLocation, "TaskTemp"); + #endregion #region "HTTP 缓存处理" @@ -85,16 +84,6 @@ public void Initialize() Directory.CreateDirectory(destLocation); } - try - { - if (!Directory.Exists(_taskTemp)) Directory.CreateDirectory(_taskTemp); - } - catch (IOException) - { - File.Delete(_taskTemp); - Directory.CreateDirectory(_taskTemp); - } - using var connection = _connectionFactory.Invoke(); var cmd = connection.CreateCommand(); cmd.CommandText = CreateTable; @@ -285,19 +274,21 @@ private SqliteCommand _InsertDatabase(HttpCacheDetails details) private SqliteCommand _DeleteTable(string uri) { - using var conn = _connectionFactory.Invoke(); + var conn = _connectionFactory.Invoke(); var cmd = conn.CreateCommand(); cmd.CommandText = DeleteTable; cmd.Parameters.AddWithValue("@Uri", uri); + cmd.Disposed += (_, _) => conn.Dispose(); return cmd; } private SqliteCommand _FindTableWithUri(string uri) { - using var conn = _connectionFactory.Invoke(); + var conn = _connectionFactory.Invoke(); var queryCmd = conn.CreateCommand(); queryCmd.CommandText = FindTable; queryCmd.Parameters.AddWithValue("@Uri", uri); + queryCmd.Disposed += (_, _) => conn.Dispose(); return queryCmd; } @@ -314,8 +305,9 @@ private SqliteCommand _FindTableWithUri(string uri) } var sb = new StringBuilder(); sb.Append("UPDATE HttpCache "); - using var conn = _connectionFactory.Invoke(); + var conn = _connectionFactory.Invoke(); var writeCmd = conn.CreateCommand(); + writeCmd.Disposed += (_, _) => conn.Dispose(); var setCount = 0; // 按需更新以减少开销 if (reader.GetString(0) != details.RequestUri) @@ -353,14 +345,14 @@ private SqliteCommand _FindTableWithUri(string uri) if ((HttpCacheStatus)reader.GetInt16(5) != details.Status) { setCount++; - sb.Append("SET Status = @Status"); + sb.Append("SET Status = @Status,"); writeCmd.Parameters.AddWithValue("@Status", (int)details.Status); } if (reader.GetString(6) != details.LastUpdate.ToString()) { setCount++; - sb.Append("SET LastUpdate = @LastUpdate"); + sb.Append("SET LastUpdate = @LastUpdate,"); writeCmd.Parameters.AddWithValue("@LastUpdate", details.LastUpdate.ToString()); } diff --git a/PCL.Core/IO/Net/Http/Cache/Models/BlockingStream.cs b/PCL.Core/IO/Net/Http/Cache/Models/BlockingStream.cs index 41df40d12..c75a1226b 100644 --- a/PCL.Core/IO/Net/Http/Cache/Models/BlockingStream.cs +++ b/PCL.Core/IO/Net/Http/Cache/Models/BlockingStream.cs @@ -1,18 +1,31 @@ using System; using System.IO; using System.Threading; - +using System.Threading.Tasks; namespace PCL.Core.IO.Net.Http.Cache.Models; public class BlockingStream:MemoryStream { private SemaphoreSlim _lock = new(0); - public override int Read(Span buffer) + + [Obsolete("请使用支持取消重载的 Read", error:true)] + public new int Read(byte[] buffer, int offset, int count) + { + return base.Read(buffer, offset, count); + } + + public int Read(Span buffer, CancellationToken token) { - _lock.Wait(); + _lock.Wait(token); return base.Read(buffer); } + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = new CancellationToken()) + { + _lock.Wait(cancellationToken); + return base.ReadAsync(buffer, cancellationToken); + } + internal void Readable() { _lock.Release(); diff --git a/PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs b/PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs index 214473996..66dccc7b1 100644 --- a/PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs +++ b/PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs @@ -25,7 +25,7 @@ public CacheStream(HttpCacheUpdateHandle handle, Stream responseStream) public override void Flush() { } - + public override int Read(byte[] buffer, int offset, int count) { var read = _responseStream.Read(buffer, offset, count); @@ -34,7 +34,7 @@ public override int Read(byte[] buffer, int offset, int count) _destStream?.Readable(); return read; } - + public override long Seek(long offset, SeekOrigin origin) { throw new InvalidOperationException("This stream is readonly."); From 63a6fa00ee04a9a47f9b3b723fbe77b2f702428a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=84=E5=A5=9A=E6=A2=A6=E7=81=B5?= Date: Mon, 6 Apr 2026 15:19:41 +0800 Subject: [PATCH 6/7] fix: Memory Leak --- .../IO/Net/Http/Cache/HttpCacheRepository.cs | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs b/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs index 4cbe31041..edc930acf 100644 --- a/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs +++ b/PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs @@ -99,12 +99,13 @@ public void Initialize() public bool TryGetCacheData(string uri,[NotNullWhen(true)] out HttpCacheDetails? details) { details = null; - using var cmd = _FindTableWithUri(uri); + using var conn = _connectionFactory.Invoke(); + using var cmd = _FindTableWithUri(uri, conn); using var result = cmd.ExecuteReader(); if (!result.Read()) return false; if ((HttpCacheStatus)result.GetInt16(6) is HttpCacheStatus.Invalid or HttpCacheStatus.Expired) { - _DeleteTable(result.GetString(0)); + _DeleteTable(result.GetString(0), conn); return false; } details = new HttpCacheDetails(this) @@ -151,6 +152,7 @@ public bool TryGetCacheResponse(HttpRequestMessage request,[NotNullWhen(true)] o /// public async ValueTask TryBeginUpdateAsync(string uri) { + await using var conn = _connectionFactory.Invoke(); if (!TryGetCacheData(uri, out var details)) { Span buffer = stackalloc byte[16]; @@ -165,7 +167,7 @@ public bool TryGetCacheResponse(HttpRequestMessage request,[NotNullWhen(true)] o Status = HttpCacheStatus.Updating, Hash = null }; - await using var cmd = _InsertDatabase(details); + await using var cmd = _InsertDatabase(details, conn); cmd.ExecuteNonQuery(); // 互斥锁,避免线程冲突 }else if (details.Status == HttpCacheStatus.Updating) return null; @@ -181,10 +183,11 @@ public bool TryGetCacheResponse(HttpRequestMessage request,[NotNullWhen(true)] o /// public async ValueTask TryEndUpdateAsync(HttpCacheUpdateHandle handle) { + await using var conn = _connectionFactory.Invoke(); var details = handle.Details; if (details is null) return false; details.Status = HttpCacheStatus.Ok; - await using var cmd = _UpdateTable(details); + await using var cmd = _UpdateTable(details,conn); if (cmd is null) return true; await cmd.ExecuteNonQueryAsync(); return true; @@ -198,11 +201,12 @@ public async ValueTask TryEndUpdateAsync(HttpCacheUpdateHandle handle) /// public bool TryRemove(HttpRequestMessage request) { + using var conn = _connectionFactory.Invoke(); try { if (!TryGetCacheData(request.RequestUri!.ToString(), out var details) && details?.Hash is null) return false; if (request.RequestUri is null) return false; - using var cmd = _DeleteTable(request.RequestUri.ToString()); + using var cmd = _DeleteTable(request.RequestUri.ToString(), conn); cmd.ExecuteNonQuery(); _store.DeleteAsync(details.Hash!).GetAwaiter().GetResult(); return true; @@ -222,11 +226,12 @@ public bool TryRemove(HttpRequestMessage request) /// public async ValueTask TryRemoveAsync(HttpRequestMessage request) { + await using var conn = _connectionFactory.Invoke(); try { if (!TryGetCacheData(request.RequestUri!.ToString(), out var details) && details?.Hash is null) return false; if (request.RequestUri is null) return false; - await using var cmd = _DeleteTable(request.RequestUri.ToString()); + await using var cmd = _DeleteTable(request.RequestUri.ToString(), conn); await cmd.ExecuteNonQueryAsync(); await _store.DeleteAsync(details.Hash!).ConfigureAwait(false); return true; @@ -257,9 +262,8 @@ public void MarkAllObjectAsExpired() #region "SQL 执行函数" - private SqliteCommand _InsertDatabase(HttpCacheDetails details) + private static SqliteCommand _InsertDatabase(HttpCacheDetails details, SqliteConnection conn) { - using var conn = _connectionFactory.Invoke(); var cmd = conn.CreateCommand(); cmd.CommandText = InsertTable; cmd.Parameters.AddWithValue("@Uri", details.RequestUri); @@ -272,40 +276,35 @@ private SqliteCommand _InsertDatabase(HttpCacheDetails details) return cmd; } - private SqliteCommand _DeleteTable(string uri) + private static SqliteCommand _DeleteTable(string uri, SqliteConnection conn) { - var conn = _connectionFactory.Invoke(); var cmd = conn.CreateCommand(); cmd.CommandText = DeleteTable; cmd.Parameters.AddWithValue("@Uri", uri); - cmd.Disposed += (_, _) => conn.Dispose(); return cmd; } - private SqliteCommand _FindTableWithUri(string uri) + private static SqliteCommand _FindTableWithUri(string uri, SqliteConnection conn) { - var conn = _connectionFactory.Invoke(); var queryCmd = conn.CreateCommand(); queryCmd.CommandText = FindTable; queryCmd.Parameters.AddWithValue("@Uri", uri); - queryCmd.Disposed += (_, _) => conn.Dispose(); return queryCmd; } - private SqliteCommand? _UpdateTable(HttpCacheDetails details) + private SqliteCommand? _UpdateTable(HttpCacheDetails details, SqliteConnection conn) { - using var queryCmd = _FindTableWithUri(details.RequestUri); + using var queryCmd = _FindTableWithUri(details.RequestUri, conn); // 获取用于比较的原始内容 using var reader = queryCmd.ExecuteReader(); if (!reader.Read()) { // 可能已经被删掉了,添加就好 - return _InsertDatabase(details); + return _InsertDatabase(details,conn); } var sb = new StringBuilder(); sb.Append("UPDATE HttpCache "); - var conn = _connectionFactory.Invoke(); var writeCmd = conn.CreateCommand(); writeCmd.Disposed += (_, _) => conn.Dispose(); var setCount = 0; From 4f3c32c7456f4aa0bc2177431918dc279dd6b12d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E8=BD=BB=E8=AF=AD?= Date: Sun, 12 Apr 2026 14:38:54 +0800 Subject: [PATCH 7/7] chore: remove sync support --- PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs b/PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs index 1c25cac68..af7bd8c6c 100644 --- a/PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs +++ b/PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs @@ -21,11 +21,6 @@ public HttpCacheHandler(HttpMessageHandler invoker, HttpCacheRepository repo) _repository = repo; } - protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) - { - return SendAsync(request, cancellationToken).GetAwaiter().GetResult(); - } - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if(!_repository.TryGetCacheData(request.RequestUri!.ToString(),out var details)) @@ -54,4 +49,4 @@ protected override async Task SendAsync(HttpRequestMessage await response.Content.ReadAsStreamAsync(cancellationToken))); return response; } -} \ No newline at end of file +}