refactor CacheFile

This commit is contained in:
HolographicHat
2025-04-09 01:09:47 +08:00
parent 4ff2b454f3
commit 4b052cf6c7
9 changed files with 76 additions and 118 deletions

18
.gitignore vendored
View File

@@ -1,15 +1,5 @@
bin/ bin
obj/ obj
/packages/
riderModule.iml
/_ReSharper.Caches/
.idea .idea
.vs
# User-specific files publish
*.suo
*.user
*.userosscache
*.sln.docstates
.vs/
src/Proto/*

View File

@@ -1,32 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<LangVersion>preview</LangVersion> <LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ApplicationManifest>res\app.manifest</ApplicationManifest> <TargetFramework>net9.0-windows</TargetFramework>
<AssemblyVersion>5.3.0</AssemblyVersion>
<FileVersion>5.3.0</FileVersion> <FileVersion>5.3.0</FileVersion>
<AssemblyVersion>5.3.0</AssemblyVersion>
<ApplicationIcon>res\icon.ico</ApplicationIcon> <ApplicationIcon>res\icon.ico</ApplicationIcon>
<ApplicationManifest>res\app.manifest</ApplicationManifest>
</PropertyGroup>
<DebugType>embedded</DebugType> <PropertyGroup>
<SelfContained>false</SelfContained> <PublishAot>true</PublishAot>
<PublishSingleFile>true</PublishSingleFile>
<PublishReadyToRun>true</PublishReadyToRun>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<IlcOptimizationPreference>Size</IlcOptimizationPreference>
<CETCompat>false</CETCompat> <IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.29.0"/>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106"> <PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Google.Protobuf" Version="3.29.0"/>
<PackageReference Include="Grpc.Tools" Version="2.67.0"> <PackageReference Include="Grpc.Tools" Version="2.67.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -55,4 +53,8 @@
<Protobuf Include="res/proto/*.proto" ProtoRoot="res/proto" GrpcServices="None"/> <Protobuf Include="res/proto/*.proto" ProtoRoot="res/proto" GrpcServices="None"/>
</ItemGroup> </ItemGroup>
<PropertyGroup>
<CETCompat>false</CETCompat>
</PropertyGroup>
</Project> </Project>

View File

@@ -15,9 +15,8 @@ public static partial class AppConfig {
GamePath = argumentPath; GamePath = argumentPath;
return; return;
} }
var pathCacheFile = new CacheFile("genshin_impact_game_path"); if (CacheFile.TryRead("genshin_impact_game_path", out var cache)) {
if (pathCacheFile.Exists()) { var path = cache.Content.ToStringUtf8();
var path = pathCacheFile.Read().Content.ToStringUtf8();
if (path != null && File.Exists(path)) { if (path != null && File.Exists(path)) {
GamePath = path; GamePath = path;
return; return;
@@ -40,12 +39,7 @@ public static partial class AppConfig {
if (!File.Exists(path)) { if (!File.Exists(path)) {
return null; return null;
} }
var copiedLogFilePath = Path.GetTempFileName(); var content = File.ReadAllText(path);
File.Copy(path, copiedLogFilePath, true);
var content = File.ReadAllText(copiedLogFilePath);
try {
File.Delete(copiedLogFilePath);
} catch (Exception) { /* ignore */ }
var matchResult = GamePathRegex().Match(content); var matchResult = GamePathRegex().Match(content);
if (!matchResult.Success) { if (!matchResult.Success) {
return null; return null;
@@ -54,7 +48,7 @@ public static partial class AppConfig {
return Path.GetFullPath(Path.Combine(matchResult.Value, "..", entryName)); return Path.GetFullPath(Path.Combine(matchResult.Value, "..", entryName));
} }
[GeneratedRegex(@"(?m).:/.+(GenshinImpact_Data|YuanShen_Data)", RegexOptions.IgnoreCase)] [GeneratedRegex(@"(?m).:(?:\\|/).+(GenshinImpact_Data|YuanShen_Data)", RegexOptions.IgnoreCase)]
private static partial Regex GamePathRegex(); private static partial Regex GamePathRegex();
} }

View File

@@ -1,41 +0,0 @@
using System.Security.Cryptography;
using System.Text;
namespace YaeAchievement;
// ReSharper disable MemberCanBePrivate.Global
public static class Extensions {
// ReSharper disable once InconsistentNaming
private static readonly Lazy<MD5> md5 = new (MD5.Create);
// ReSharper disable once InconsistentNaming
private static readonly Lazy<SHA1> sha1 = new (SHA1.Create);
public static byte[] ToBytes(this string text) {
return Encoding.UTF8.GetBytes(text);
}
// ReSharper disable once InconsistentNaming
public static string MD5Hash(this string text) {
return text.ToBytes().MD5Hash();
}
// ReSharper disable once InconsistentNaming
public static string MD5Hash(this byte[] data) {
return md5.Value.ComputeHash(data).ToHex().ToLower();
}
// ReSharper disable once InconsistentNaming
public static string SHA1Hash(this string text, bool base64 = true) {
var bytes = sha1.Value.ComputeHash(text.ToBytes());
return base64 ? bytes.ToBase64() : bytes.ToHex();
}
public static string ToHex(this byte[] bytes) {
return Convert.ToHexString(bytes);
}
public static string ToBase64(this byte[] bytes) {
return Convert.ToBase64String(bytes);
}
}

View File

@@ -1,7 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Reflection; using System.Reflection;
using Proto; using Proto;
using YaeAchievement.Utilities;
namespace YaeAchievement; namespace YaeAchievement;
@@ -28,8 +27,6 @@ public static class GlobalVars {
public const string RinBucketHost = "https://rin.holohat.work"; public const string RinBucketHost = "https://rin.holohat.work";
public const string SakuraBucketHost = "https://cn-cd-1259389942.file.myqcloud.com"; public const string SakuraBucketHost = "https://cn-cd-1259389942.file.myqcloud.com";
public static CacheFile AchievementDataCache { get; } = new ("achievement_data");
[field:MaybeNull] [field:MaybeNull]
public static AchievementInfo AchievementInfo => public static AchievementInfo AchievementInfo =>
field ??= AchievementInfo.Parser.ParseFrom(Utils.GetBucketFile("schicksal/metadata").GetAwaiter().GetResult()); field ??= AchievementInfo.Parser.ParseFrom(Utils.GetBucketFile("schicksal/metadata").GetAwaiter().GetResult());

View File

@@ -2,6 +2,7 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Google.Protobuf; using Google.Protobuf;
using YaeAchievement.res; using YaeAchievement.res;
using YaeAchievement.Utilities;
namespace YaeAchievement.Parsers; namespace YaeAchievement.Parsers;
@@ -30,7 +31,7 @@ public class AchievementAllDataNotify {
public static bool OnReceive(BinaryReader reader) { public static bool OnReceive(BinaryReader reader) {
var bytes = reader.ReadBytes(reader.ReadInt32()); var bytes = reader.ReadBytes(reader.ReadInt32());
GlobalVars.AchievementDataCache.Write(bytes); CacheFile.Write("achievement_data", bytes);
Instance = ParseFrom(bytes); Instance = ParseFrom(bytes);
return true; return true;
} }

View File

@@ -5,6 +5,7 @@ using Windows.Win32;
using Windows.Win32.System.Console; using Windows.Win32.System.Console;
using YaeAchievement.Parsers; using YaeAchievement.Parsers;
using YaeAchievement.res; using YaeAchievement.res;
using YaeAchievement.Utilities;
using static YaeAchievement.Utils; using static YaeAchievement.Utils;
namespace YaeAchievement; namespace YaeAchievement;
@@ -33,14 +34,14 @@ internal static class Program {
await CheckUpdate(ToBooleanOrFalse(args.GetOrNull(2))); await CheckUpdate(ToBooleanOrFalse(args.GetOrNull(2)));
var historyCache = GlobalVars.AchievementDataCache;
AchievementAllDataNotify? data = null; AchievementAllDataNotify? data = null;
try { try {
data = AchievementAllDataNotify.ParseFrom(historyCache.Read().Content.ToByteArray()); if (CacheFile.TryRead("achievement_data", out var cache)) {
data = AchievementAllDataNotify.ParseFrom(cache.Content.ToByteArray());
}
} catch (Exception) { /* ignored */ } } catch (Exception) { /* ignored */ }
if (historyCache.LastWriteTime.AddMinutes(60) > DateTime.UtcNow && data != null) { if (CacheFile.GetLastWriteTime("achievement_data").AddMinutes(60) > DateTime.UtcNow && data != null) {
Console.WriteLine(App.UsePreviousData); Console.WriteLine(App.UsePreviousData);
if (Console.ReadLine()?.ToUpper() is "Y" or "YES") { if (Console.ReadLine()?.ToUpper() is "Y" or "YES") {
Export.Choose(data); Export.Choose(data);

View File

@@ -1,39 +1,53 @@
using System.IO.Compression; using System.Diagnostics.CodeAnalysis;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
using Google.Protobuf; using Google.Protobuf;
using Proto; using Proto;
namespace YaeAchievement.Utilities; namespace YaeAchievement.Utilities;
public class CacheFile(string identifier) { public static class CacheFile {
private readonly string _cacheName = Path.Combine(GlobalVars.CachePath, $"{identifier.MD5Hash()[..16]}.miko"); static CacheFile() {
private CacheItem? _content; // remove deprecated cache
foreach (var file in Directory.EnumerateFiles(GlobalVars.CachePath, "*.miko")) {
public DateTime LastWriteTime => Exists() ? File.GetLastWriteTimeUtc(_cacheName) : DateTime.UnixEpoch; File.Delete(file);
public bool Exists() => File.Exists(_cacheName);
public CacheItem Read() {
if (_content == null) {
using var fInput = File.OpenRead(_cacheName);
using var dInput = new GZipStream(fInput, CompressionMode.Decompress);
_content = CacheItem.Parser.ParseFrom(dInput);
} }
return _content;
} }
public void Write(string data, string? etag = null) => Write(ByteString.CopyFromUtf8(data), data.MD5Hash(), etag); public static DateTime GetLastWriteTime(string id) {
var fileName = Path.Combine(GlobalVars.CachePath, $"{GetStrHash(id)}.nyan");
return File.Exists(fileName) ? File.GetLastWriteTimeUtc(fileName) : DateTime.UnixEpoch;
}
public void Write(byte[] data, string? etag = null) => Write(ByteString.CopyFrom(data), data.MD5Hash(), etag); public static bool TryRead(string id, [NotNullWhen(true)] out CacheItem? item) {
item = null;
try {
var fileName = Path.Combine(GlobalVars.CachePath, $"{GetStrHash(id)}.nyan");
using var fileStream = File.OpenRead(fileName);
using var zipStream = new GZipStream(fileStream, CompressionMode.Decompress);
item = CacheItem.Parser.ParseFrom(zipStream);
return true;
} catch (Exception) {
return false;
}
}
private void Write(ByteString data, string hash, string? etag) { public static void Write(string id, byte[] data, string? etag = null) {
using var fOut = File.OpenWrite(_cacheName); var fileName = Path.Combine(GlobalVars.CachePath, $"{GetStrHash(id)}.nyan");
using var cOut = new GZipStream(fOut, CompressionLevel.SmallestSize); using var fileStream = File.Open(fileName, FileMode.Create);
using var zipStream = new GZipStream(fileStream, CompressionLevel.SmallestSize);
new CacheItem { new CacheItem {
Etag = etag ?? string.Empty, Etag = etag ?? string.Empty,
Version = 3, Version = 3,
Checksum = hash, Checksum = GetBinHash(data),
Content = data Content = ByteString.CopyFrom(data)
}.WriteTo(cOut); }.WriteTo(zipStream);
} }
private static string GetStrHash(string value) => GetBinHash(Encoding.UTF8.GetBytes(value));
private static string GetBinHash(byte[] value) => Convert.ToHexStringLower(MD5.HashData(value));
} }

View File

@@ -15,7 +15,7 @@ namespace YaeAchievement;
public static class Utils { public static class Utils {
public static readonly HttpClient CHttpClient = new (new HttpClientHandler { public static HttpClient CHttpClient { get; } = new (new HttpClientHandler {
AutomaticDecompression = DecompressionMethods.Brotli | DecompressionMethods.GZip AutomaticDecompression = DecompressionMethods.Brotli | DecompressionMethods.GZip
}) { }) {
DefaultRequestHeaders = { DefaultRequestHeaders = {
@@ -25,7 +25,7 @@ public static class Utils {
} }
}; };
public static async Task<byte[]> GetBucketFile(string path, bool cache = true) { public static async Task<byte[]> GetBucketFile(string path, bool useCache = true) {
try { try {
return await await Task.WhenAny(GetFile(GlobalVars.RinBucketHost), GetFile(GlobalVars.SakuraBucketHost)); return await await Task.WhenAny(GetFile(GlobalVars.RinBucketHost), GetFile(GlobalVars.SakuraBucketHost));
} catch (Exception e) when(e is SocketException or TaskCanceledException) { } catch (Exception e) when(e is SocketException or TaskCanceledException) {
@@ -37,19 +37,19 @@ public static class Utils {
using var msg = new HttpRequestMessage(); using var msg = new HttpRequestMessage();
msg.Method = HttpMethod.Get; msg.Method = HttpMethod.Get;
msg.RequestUri = new Uri($"{host}/{path}"); msg.RequestUri = new Uri($"{host}/{path}");
var cacheFile = new CacheFile(path); CacheItem? cache = null;
if (cache && cacheFile.Exists()) { if (useCache && CacheFile.TryRead(path, out cache)) {
msg.Headers.TryAddWithoutValidation("If-None-Match", $"{cacheFile.Read().Etag}"); msg.Headers.TryAddWithoutValidation("If-None-Match", $"{cache.Etag}");
} }
using var response = await CHttpClient.SendAsync(msg); using var response = await CHttpClient.SendAsync(msg);
if (cache && response.StatusCode == HttpStatusCode.NotModified) { if (cache != null && response.StatusCode == HttpStatusCode.NotModified) {
return cacheFile.Read().Content.ToByteArray(); return cache.Content.ToByteArray();
} }
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var responseBytes = await response.Content.ReadAsByteArrayAsync(); var responseBytes = await response.Content.ReadAsByteArrayAsync();
if (cache) { if (useCache) {
var etag = response.Headers.ETag!.Tag; var etag = response.Headers.ETag!.Tag;
cacheFile.Write(responseBytes, etag); CacheFile.Write(path, responseBytes, etag);
} }
return responseBytes; return responseBytes;
} }