-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathInternetFileCache.cs
More file actions
262 lines (221 loc) · 8.58 KB
/
Copy pathInternetFileCache.cs
File metadata and controls
262 lines (221 loc) · 8.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
using System.Text;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
namespace NuLigaViewer;
public class InternetFileCache
{
public static readonly InternetFileCache Instance;
static InternetFileCache()
{
_urlRegexe = new();
_urlRegexe.Add((new Regex(@"^https://www.schachbund.de/php/dewis/verein.php\?zps=(\w+)&format=csv$"), "dewis"));
_urlRegexe.Add((new Regex(@"^https://bsv-schach.liga.nu/cgi-bin/WebObjects/nuLigaSCHACHDE.woa/wa/clubInfoDisplay\?club=(\d+)$"), "cID"));
_preferredFilesRegex = new Regex("^(dewis|cID)");
Instance = new InternetFileCache(TimeSpan.FromDays(1), TimeSpan.FromDays(7), TimeSpan.FromDays(200), 5000, 100);
}
/// <summary>
/// Erstellt einen FileCache mit drei unterschiedlichen Altersgrenzen.
///
/// Die drei Stufen steuern:
/// - wann ein Cache-Eintrag direkt verwendet wird,
/// - wann er im Hintergrund aktualisiert wird,
/// - und wann er endgültig gelöscht wird.
/// </summary>
/// <param name="maxAge1">
/// Wenn die Datei jünger als maxAge1 ist, wird sie direkt von der Platte
/// zurückgegeben, ohne dass ein Download erfolgt.
/// </param>
/// <param name="maxAge2">
/// Wenn die Datei älter als maxAge1, aber jünger als maxAge2 ist,
/// wird sie von der Platte zurückgegeben und gleichzeitig im Hintergrund
/// neu von der URL geladen.
/// </param>
/// <param name="maxAge3">
/// Dateien, die älter als maxAge3 sind, werden beim Cleanup gelöscht.
/// Sie gelten als veraltet und werden beim nächsten Zugriff neu geladen.
/// </param>
/// <param name="maxSizeKB">
/// Maximale Größe des gesamten Cache-Verzeichnisses in Kilobyte.
/// Wird diese Größe überschritten, löscht das Cleanup auch jüngere Dateien
/// (beginnend mit den ältesten), bis die Größe wieder unterhalb des Limits liegt.
/// </param>
/// <param name="cleanupThreshold"> Alle cleanupThreshold Zugriffe wird ein Cleanup des Caches durchgeführt. </param>
private InternetFileCache(TimeSpan maxAge1, TimeSpan maxAge2, TimeSpan maxAge3, int maxSizeKB, int cleanupThreshold)
{
_maxAge1 = maxAge1;
_maxAge2 = maxAge2;
_maxAge3 = maxAge3;
_maxSizeBytes = maxSizeKB * 1024L;
_cleanupThreshold = Math.Max(1, cleanupThreshold);
_cacheDir = Path.Combine(FileSystem.AppDataDirectory, "inetcache");
Directory.CreateDirectory(_cacheDir);
// Cleanup im Hintergrund
_ = Task.Run(CleanupOldFiles);
}
public async Task<string> GetAsync(string url)
{
MaybeStartCleanup();
string path = GetPathForUrl(url);
SemaphoreSlim sem = GetLock(path);
await sem.WaitAsync();
try
{
if (File.Exists(path))
{
var age = DateTime.Now - File.GetLastWriteTime(path);
// 1. Frisch genug => direkt zurückgeben
if (age < _maxAge1)
{
return await File.ReadAllTextAsync(path);
}
// 2. Mittelalt => zurückgeben + Hintergrund-Refresh
if (age < _maxAge2)
{
string content = await File.ReadAllTextAsync(path);
// Hintergrund-Refresh
_ = Task.Run(async () =>
{
try
{
using var client = new HttpClient();
string fresh = await client.GetStringAsync(url);
await File.WriteAllTextAsync(path, fresh);
}
catch
{
// Fehler ignorieren – Cache bleibt stabil
}
});
return content;
}
// 3. Älter als maxAge2 → neu laden
}
// Datei existiert nicht oder ist zu alt => neu laden
using var httpClient = new HttpClient();
string downloaded = await httpClient.GetStringAsync(url);
await File.WriteAllTextAsync(path, downloaded);
return downloaded;
}
finally
{
sem.Release();
}
}
public string Get(string url)
{
// Ruft die async-Methode synchron auf
return GetAsync(url).GetAwaiter().GetResult();
}
private SemaphoreSlim GetLock(string path)
{
lock (_locks)
{
if (!_locks.TryGetValue(path, out var sem))
{
sem = new SemaphoreSlim(1, 1);
_locks[path] = sem;
}
return sem;
}
}
private string GetPathForUrl(string url)
{
string? file = null;
System.Diagnostics.Debug.WriteLine(url);
foreach (var re in _urlRegexe)
{
var m = re.Item1.Match(url);
if (m.Success)
{
file = re.Item2 + m.Groups[1].Value;
break;
}
}
if (file == null)
{
using var sha = SHA256.Create();
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(url));
file = Convert.ToHexString(hash) + ".cache";
}
return Path.Combine(_cacheDir, file);
}
/// <summary> Erhöht den Zugriffszähler atomar und startet asynchron ein Cleanup,
/// wenn die Schwelle erreicht ist. Verhindert parallele Cleanups. </summary>
private void MaybeStartCleanup()
{
long count = Interlocked.Increment(ref _accessCounter); // atomar erhöhen
// noch nicht an der Schwelle
if (count < _cleanupThreshold)
return;
// Versuche, das Cleanup zu starten; nur einer darf starten
if (Interlocked.CompareExchange(ref _cleanupRunning, 1, 0) == 0)
{
// Zähler zurücksetzen (kleine Race-Conditions sind unkritisch)
Interlocked.Exchange(ref _accessCounter, 0);
// Cleanup im Hintergrund starten, nicht awaiten
_ = Task.Run(async () =>
{
try
{
await CleanupOldFiles();
}
finally
{
// Flag zurücksetzen
Interlocked.Exchange(ref _cleanupRunning, 0);
}
});
}
}
private async Task CleanupOldFiles()
{
var files = Directory.GetFiles(_cacheDir).Select(f => new FileInfo(f)).ToList();
// 1. Alte Dateien löschen (älter als maxAge3)
foreach (var file in files)
{
try
{
var age = DateTime.Now - file.LastWriteTime;
if (age > _maxAge3)
{
file.Delete();
}
}
catch { }
}
// Liste nach Löschungen neu laden
files = Directory.GetFiles(_cacheDir).Select(f => new FileInfo(f)).ToList();
// 2. Größe prüfen
long totalSize = files.Sum(f => f.Length);
if (totalSize <= _maxSizeBytes)
return;
// 3. Zu groß => nach Preferredness und Alter sortieren und löschen
var preferred = files.Where(fi => _preferredFilesRegex.IsMatch(fi.Name)).
OrderBy(f => f.LastWriteTime).ToList();
var nonpreferred = files.Except(preferred).
OrderBy(f => f.LastWriteTime).ToList();
nonpreferred.AddRange(preferred);
var ordered = nonpreferred;
foreach (var file in ordered)
{
try
{
totalSize -= file.Length;
file.Delete();
if (totalSize <= _maxSizeBytes)
break;
}
catch { }
}
await Task.CompletedTask;
}
private readonly TimeSpan _maxAge1, _maxAge2, _maxAge3;
private readonly long _maxSizeBytes;
private readonly string _cacheDir;
private readonly Dictionary<string, SemaphoreSlim> _locks = new();
private readonly int _cleanupThreshold;
private long _accessCounter = 0;
private int _cleanupRunning = 0; // 0 = nicht laufend, 1 = läuft
static readonly List<(Regex, string)> _urlRegexe;
static readonly Regex _preferredFilesRegex;
}