feat(upserver): 接入认证与KV存档并完成限定钓鱼闭环
This commit is contained in:
105
Client/Assets/Scripts/Services/PlayerData/PlayerKVCacheStore.cs
Normal file
105
Client/Assets/Scripts/Services/PlayerData/PlayerKVCacheStore.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace game.PlayerDataStorage
|
||||
{
|
||||
// Internal seam for deterministic disk-failure tests; no production callback is supplied.
|
||||
internal enum KVCacheWriteStage { BeforeWrite, AfterWrite, AfterFlush, BeforeReplace, AfterReplace }
|
||||
|
||||
internal sealed class PlayerKVCacheStore : IDisposable
|
||||
{
|
||||
const long MaxBytes = 64L * 1024 * 1024;
|
||||
readonly KVIdentity identity;
|
||||
readonly FileStream lease;
|
||||
readonly Action<KVCacheWriteStage> fault;
|
||||
readonly string main, temporary, backup;
|
||||
bool disposed;
|
||||
|
||||
PlayerKVCacheStore(KVIdentity identity, FileStream lease, Action<KVCacheWriteStage> fault)
|
||||
{
|
||||
this.identity = identity; this.lease = lease; this.fault = fault;
|
||||
main = Path.Combine(identity.DirectoryPath, "state.json");
|
||||
temporary = main + ".tmp"; backup = main + ".bak";
|
||||
}
|
||||
|
||||
internal static PlayerKVOpenError Open(KVIdentity identity, Action<KVCacheWriteStage> fault,
|
||||
out PlayerKVCacheStore store, out KVSnapshot snapshot)
|
||||
{
|
||||
store = null; snapshot = null;
|
||||
FileStream lease = null;
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(identity.DirectoryPath);
|
||||
try { lease = new FileStream(Path.Combine(identity.DirectoryPath, "state.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); }
|
||||
catch (IOException ex) when ((ex.HResult & 0xffff) == 32 || (ex.HResult & 0xffff) == 33)
|
||||
{ return PlayerKVOpenError.Busy; }
|
||||
store = new PlayerKVCacheStore(identity, lease, fault);
|
||||
if (!File.Exists(store.main))
|
||||
{
|
||||
if (File.Exists(store.backup) || File.Exists(store.temporary)) throw KVSnapshot.Invalid();
|
||||
snapshot = new KVSnapshot();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (new FileInfo(store.main).Length > MaxBytes) throw KVSnapshot.Invalid();
|
||||
snapshot = KVSnapshot.Decode(File.ReadAllText(store.main, new UTF8Encoding(false, true)), identity);
|
||||
}
|
||||
return PlayerKVOpenError.None;
|
||||
}
|
||||
catch (Exception ex) when (ex is JsonException || ex is OverflowException || ex is FormatException || ex is DecoderFallbackException)
|
||||
{
|
||||
store?.Dispose(); lease?.Dispose(); store = null;
|
||||
return PlayerKVOpenError.RecoveryRequired;
|
||||
}
|
||||
catch (Exception ex) when (DiskError(ex))
|
||||
{
|
||||
store?.Dispose(); lease?.Dispose(); store = null;
|
||||
return PlayerKVOpenError.StorageError;
|
||||
}
|
||||
}
|
||||
|
||||
internal bool Save(KVSnapshot snapshot, out bool mayHaveCommitted)
|
||||
{
|
||||
mayHaveCommitted = false;
|
||||
try
|
||||
{
|
||||
if (disposed) throw new ObjectDisposedException(nameof(PlayerKVCacheStore));
|
||||
snapshot.Validate();
|
||||
var bytes = new UTF8Encoding(false, true).GetBytes(snapshot.Encode(identity));
|
||||
if (bytes.LongLength > MaxBytes) throw new IOException("KV cache capacity exceeded.");
|
||||
fault?.Invoke(KVCacheWriteStage.BeforeWrite);
|
||||
using (var stream = new FileStream(temporary, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
{
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
fault?.Invoke(KVCacheWriteStage.AfterWrite);
|
||||
stream.Flush(true);
|
||||
fault?.Invoke(KVCacheWriteStage.AfterFlush);
|
||||
}
|
||||
fault?.Invoke(KVCacheWriteStage.BeforeReplace);
|
||||
mayHaveCommitted = true;
|
||||
// The temp file is on the same filesystem. Never delete the main file first.
|
||||
if (File.Exists(main)) File.Replace(temporary, main, backup);
|
||||
else File.Move(temporary, main);
|
||||
fault?.Invoke(KVCacheWriteStage.AfterReplace);
|
||||
mayHaveCommitted = false;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (DiskError(ex) || ex is JsonException || ex is EncoderFallbackException)
|
||||
{ return false; }
|
||||
}
|
||||
|
||||
static bool DiskError(Exception ex) => ex is IOException || ex is UnauthorizedAccessException ||
|
||||
ex is SecurityException || ex is NotSupportedException || ex is ObjectDisposedException;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
lease.Dispose();
|
||||
// Keep files, including failed temporary writes, for explicit recovery diagnosis.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user