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/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
bin
obj
.idea
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
.vs/
src/Proto/*
.vs
publish

View File

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

View File

@@ -15,9 +15,8 @@ public static partial class AppConfig {
GamePath = argumentPath;
return;
}
var pathCacheFile = new CacheFile("genshin_impact_game_path");
if (pathCacheFile.Exists()) {
var path = pathCacheFile.Read().Content.ToStringUtf8();
if (CacheFile.TryRead("genshin_impact_game_path", out var cache)) {
var path = cache.Content.ToStringUtf8();
if (path != null && File.Exists(path)) {
GamePath = path;
return;
@@ -40,12 +39,7 @@ public static partial class AppConfig {
if (!File.Exists(path)) {
return null;
}
var copiedLogFilePath = Path.GetTempFileName();
File.Copy(path, copiedLogFilePath, true);
var content = File.ReadAllText(copiedLogFilePath);
try {
File.Delete(copiedLogFilePath);
} catch (Exception) { /* ignore */ }
var content = File.ReadAllText(path);
var matchResult = GamePathRegex().Match(content);
if (!matchResult.Success) {
return null;
@@ -54,7 +48,7 @@ public static partial class AppConfig {
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();
}

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

View File

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

View File

@@ -5,6 +5,7 @@ using Windows.Win32;
using Windows.Win32.System.Console;
using YaeAchievement.Parsers;
using YaeAchievement.res;
using YaeAchievement.Utilities;
using static YaeAchievement.Utils;
namespace YaeAchievement;
@@ -33,14 +34,14 @@ internal static class Program {
await CheckUpdate(ToBooleanOrFalse(args.GetOrNull(2)));
var historyCache = GlobalVars.AchievementDataCache;
AchievementAllDataNotify? data = null;
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 */ }
if (historyCache.LastWriteTime.AddMinutes(60) > DateTime.UtcNow && data != null) {
if (CacheFile.GetLastWriteTime("achievement_data").AddMinutes(60) > DateTime.UtcNow && data != null) {
Console.WriteLine(App.UsePreviousData);
if (Console.ReadLine()?.ToUpper() is "Y" or "YES") {
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 Proto;
namespace YaeAchievement.Utilities;
public class CacheFile(string identifier) {
public static class CacheFile {
private readonly string _cacheName = Path.Combine(GlobalVars.CachePath, $"{identifier.MD5Hash()[..16]}.miko");
private CacheItem? _content;
public DateTime LastWriteTime => Exists() ? File.GetLastWriteTimeUtc(_cacheName) : DateTime.UnixEpoch;
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);
static CacheFile() {
// remove deprecated cache
foreach (var file in Directory.EnumerateFiles(GlobalVars.CachePath, "*.miko")) {
File.Delete(file);
}
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) {
using var fOut = File.OpenWrite(_cacheName);
using var cOut = new GZipStream(fOut, CompressionLevel.SmallestSize);
public static void Write(string id, byte[] data, string? etag = null) {
var fileName = Path.Combine(GlobalVars.CachePath, $"{GetStrHash(id)}.nyan");
using var fileStream = File.Open(fileName, FileMode.Create);
using var zipStream = new GZipStream(fileStream, CompressionLevel.SmallestSize);
new CacheItem {
Etag = etag ?? string.Empty,
Version = 3,
Checksum = hash,
Content = data
}.WriteTo(cOut);
Checksum = GetBinHash(data),
Content = ByteString.CopyFrom(data)
}.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 readonly HttpClient CHttpClient = new (new HttpClientHandler {
public static HttpClient CHttpClient { get; } = new (new HttpClientHandler {
AutomaticDecompression = DecompressionMethods.Brotli | DecompressionMethods.GZip
}) {
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 {
return await await Task.WhenAny(GetFile(GlobalVars.RinBucketHost), GetFile(GlobalVars.SakuraBucketHost));
} catch (Exception e) when(e is SocketException or TaskCanceledException) {
@@ -37,19 +37,19 @@ public static class Utils {
using var msg = new HttpRequestMessage();
msg.Method = HttpMethod.Get;
msg.RequestUri = new Uri($"{host}/{path}");
var cacheFile = new CacheFile(path);
if (cache && cacheFile.Exists()) {
msg.Headers.TryAddWithoutValidation("If-None-Match", $"{cacheFile.Read().Etag}");
CacheItem? cache = null;
if (useCache && CacheFile.TryRead(path, out cache)) {
msg.Headers.TryAddWithoutValidation("If-None-Match", $"{cache.Etag}");
}
using var response = await CHttpClient.SendAsync(msg);
if (cache && response.StatusCode == HttpStatusCode.NotModified) {
return cacheFile.Read().Content.ToByteArray();
if (cache != null && response.StatusCode == HttpStatusCode.NotModified) {
return cache.Content.ToByteArray();
}
response.EnsureSuccessStatusCode();
var responseBytes = await response.Content.ReadAsByteArrayAsync();
if (cache) {
if (useCache) {
var etag = response.Headers.ETag!.Tag;
cacheFile.Write(responseBytes, etag);
CacheFile.Write(path, responseBytes, etag);
}
return responseBytes;
}