Skip to content

Commit d2e5927

Browse files
committed
rewrite file cache service
1 parent 40f468a commit d2e5927

9 files changed

Lines changed: 246 additions & 966 deletions

File tree

src/Starward/AppConfig.Configuration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public static void LoadConfiguration()
8181
CacheFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Starward");
8282
}
8383
Directory.CreateDirectory(CacheFolder);
84+
FileCache.Initialize(Path.Combine(CacheFolder, "cache"));
8485
var webviewFolder = Path.Combine(CacheFolder, "webview");
8586
Environment.SetEnvironmentVariable("WEBVIEW2_USER_DATA_FOLDER", webviewFolder, EnvironmentVariableTarget.Process);
8687

src/Starward/Controls/CachedImage.cs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using Microsoft.UI.Xaml.Media.Imaging;
44
using Scighost.WinUI.ImageEx;
55
using Starward.Features.Codec;
6-
using Starward.Services.Cache;
6+
using Starward.Helpers;
77
using System;
88
using System.IO;
99
using System.Threading;
@@ -49,9 +49,7 @@ public bool IsThumbnail
4949
}
5050
else
5151
{
52-
53-
54-
var file = await FileCacheService.Instance.GetFromCacheAsync(imageUri, false, token);
52+
var file = await FileCache.GetFromCacheAsync(imageUri, false, token);
5553
if (token.IsCancellationRequested)
5654
{
5755
throw new TaskCanceledException("Image source has changed.");
@@ -60,7 +58,10 @@ public bool IsThumbnail
6058
{
6159
throw new FileNotFoundException(imageUri.ToString());
6260
}
63-
return new BitmapImage(new Uri(file.Path));
61+
var bitmap = new BitmapImage(new Uri(file));
62+
bitmap.ImageOpened += BitmapImage_ImageOpened;
63+
bitmap.ImageFailed += BitmapImage_ImageFailed;
64+
return bitmap;
6465
}
6566
}
6667
catch (TaskCanceledException)
@@ -71,12 +72,33 @@ public bool IsThumbnail
7172
{
7273
throw;
7374
}
74-
catch (Exception ex)
75+
catch (Exception)
7576
{
76-
await FileCacheService.Instance.RemoveAsync([imageUri]);
7777
throw;
7878
}
7979
}
8080

8181

82+
83+
private void BitmapImage_ImageOpened(object sender, RoutedEventArgs e)
84+
{
85+
if (sender is BitmapImage image)
86+
{
87+
image.ImageOpened -= BitmapImage_ImageOpened;
88+
image.ImageFailed -= BitmapImage_ImageFailed;
89+
}
90+
}
91+
92+
93+
private void BitmapImage_ImageFailed(object sender, ExceptionRoutedEventArgs e)
94+
{
95+
if (sender is BitmapImage image)
96+
{
97+
image.ImageOpened -= BitmapImage_ImageOpened;
98+
image.ImageFailed -= BitmapImage_ImageFailed;
99+
FileCache.DeleteCacheFile(image.UriSource);
100+
}
101+
}
102+
103+
82104
}

src/Starward/Helpers/FileCache.cs

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
using System;
2+
using System.Buffers;
3+
using System.Collections.Concurrent;
4+
using System.Diagnostics;
5+
using System.IO;
6+
using System.IO.Hashing;
7+
using System.Net;
8+
using System.Net.Http;
9+
using System.Net.Http.Headers;
10+
using System.Runtime.InteropServices;
11+
using System.Security.Cryptography;
12+
using System.Threading;
13+
using System.Threading.Tasks;
14+
15+
namespace Starward.Helpers;
16+
17+
internal static class FileCache
18+
{
19+
20+
21+
private static readonly HttpClient _httpClient;
22+
23+
private static readonly ConcurrentDictionary<string, Task<string?>> _concurrentTasks;
24+
25+
26+
static FileCache()
27+
{
28+
_httpClient = new HttpClient(new SocketsHttpHandler
29+
{
30+
AutomaticDecompression = DecompressionMethods.All,
31+
EnableMultipleHttp2Connections = true,
32+
EnableMultipleHttp3Connections = true,
33+
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
34+
});
35+
_httpClient.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
36+
_concurrentTasks = new();
37+
}
38+
39+
40+
41+
public static int RetryCount { get; set; } = 3;
42+
43+
public static TimeSpan CacheDuration { get; set; } = TimeSpan.FromDays(90);
44+
45+
46+
public static string CacheFolder { get; private set; }
47+
48+
49+
50+
public static bool Initialize(string folder)
51+
{
52+
try
53+
{
54+
Directory.CreateDirectory(folder);
55+
CacheFolder = folder;
56+
return true;
57+
}
58+
catch (Exception ex)
59+
{
60+
Debug.WriteLine($"Error initializing FileCache: {ex.Message}");
61+
}
62+
return false;
63+
}
64+
65+
66+
67+
68+
public static async Task<string?> GetFromCacheAsync(Uri uri, bool throwOnError = false, CancellationToken cancellationToken = default)
69+
{
70+
return await GetItemAsync(uri, throwOnError, cancellationToken);
71+
}
72+
73+
74+
private static async Task<string?> GetItemAsync(Uri uri, bool throwOnError, CancellationToken cancellationToken)
75+
{
76+
string fileName = GetCacheFileName(uri);
77+
if (_concurrentTasks.TryGetValue(fileName, out var request))
78+
{
79+
return await request.ConfigureAwait(false);
80+
}
81+
82+
request = GetFromCacheOrDownloadAsync(uri, fileName, cancellationToken);
83+
_concurrentTasks.TryAdd(fileName, request);
84+
85+
try
86+
{
87+
return await request.ConfigureAwait(false);
88+
}
89+
catch (Exception ex)
90+
{
91+
Debug.WriteLine($"Error retrieving file from cache: {ex.Message}");
92+
if (throwOnError)
93+
{
94+
throw;
95+
}
96+
}
97+
finally
98+
{
99+
_concurrentTasks.TryRemove(fileName, out _);
100+
}
101+
102+
return null;
103+
}
104+
105+
106+
107+
private static async Task<string?> GetFromCacheOrDownloadAsync(Uri uri, string fileName, CancellationToken cancellationToken)
108+
{
109+
if (CacheFolder is null)
110+
{
111+
throw new DirectoryNotFoundException("Cache folder not initialized.");
112+
}
113+
114+
string filePath = Path.Combine(CacheFolder, fileName);
115+
116+
await Task.Delay(1, CancellationToken.None).ConfigureAwait(false);
117+
if (IsFileCacheAvailable(filePath, CacheDuration))
118+
{
119+
return filePath;
120+
}
121+
122+
uint retries = 0;
123+
while (retries < RetryCount)
124+
{
125+
try
126+
{
127+
await DownloadFileAsync(uri, filePath, cancellationToken).ConfigureAwait(false);
128+
}
129+
catch (HttpRequestException) { }
130+
retries++;
131+
}
132+
133+
return filePath;
134+
}
135+
136+
137+
private static async Task DownloadFileAsync(Uri uri, string path, CancellationToken cancellationToken)
138+
{
139+
string path_tmp = path + "_tmp";
140+
using var fs = File.Open(path_tmp, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
141+
142+
using var request = new HttpRequestMessage(HttpMethod.Get, uri);
143+
request.Headers.Range = new RangeHeaderValue(fs.Length, null);
144+
request.VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
145+
146+
using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
147+
response.EnsureSuccessStatusCode();
148+
if (response.Content.Headers.ContentRange?.From > 0)
149+
{
150+
fs.Position = response.Content.Headers.ContentRange.From.Value;
151+
}
152+
153+
using var hs = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
154+
await hs.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
155+
await fs.FlushAsync(cancellationToken).ConfigureAwait(false);
156+
fs.Dispose();
157+
158+
File.Move(path_tmp, path, true);
159+
}
160+
161+
162+
163+
164+
private static string GetCacheFileName(Uri uri)
165+
{
166+
byte[] hashBytes = ArrayPool<byte>.Shared.Rent(24);
167+
try
168+
{
169+
ReadOnlySpan<byte> pathSpan = MemoryMarshal.AsBytes(uri.ToString().AsSpan());
170+
XxHash64.Hash(pathSpan, hashBytes.AsSpan(0, 8));
171+
MD5.HashData(pathSpan, hashBytes.AsSpan(8, 16));
172+
return Convert.ToHexString(hashBytes.AsSpan(0, 24));
173+
}
174+
finally
175+
{
176+
ArrayPool<byte>.Shared.Return(hashBytes);
177+
}
178+
}
179+
180+
181+
182+
private static bool IsFileCacheAvailable(string path, TimeSpan duration)
183+
{
184+
if (File.Exists(path))
185+
{
186+
var fileInfo = new FileInfo(path);
187+
return fileInfo.Length > 0 && (DateTime.Now - fileInfo.LastWriteTime <= duration);
188+
}
189+
return false;
190+
}
191+
192+
193+
194+
public static async void DeleteCacheFile(Uri uri)
195+
{
196+
await Task.Run(() =>
197+
{
198+
string fileName = GetCacheFileName(uri);
199+
string filePath = Path.Join(CacheFolder, fileName);
200+
if (File.Exists(filePath))
201+
{
202+
try
203+
{
204+
File.Delete(filePath);
205+
}
206+
catch (Exception ex)
207+
{
208+
Debug.WriteLine($"Error deleting cache file: {ex.Message}");
209+
}
210+
}
211+
}).ConfigureAwait(false);
212+
}
213+
214+
215+
216+
}

0 commit comments

Comments
 (0)