246 lines
8.1 KiB
C#
246 lines
8.1 KiB
C#
namespace GeoSus.Server;
|
|
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
// Write-Ahead Log a Snapshoty pro persistenci
|
|
public class Persistence
|
|
{
|
|
private readonly ServerConfig _config;
|
|
private readonly ILogger<Persistence> _logger;
|
|
private readonly object _walLock = new();
|
|
|
|
public Persistence(ServerConfig config, ILogger<Persistence> logger)
|
|
{
|
|
_config = config;
|
|
_logger = logger;
|
|
|
|
// Vytvoříme adresář pro data
|
|
Directory.CreateDirectory(_config.DataPath);
|
|
}
|
|
|
|
#region WAL
|
|
|
|
public void AppendToWal(string lobbyId, GameEvent evt)
|
|
{
|
|
var lobbyDir = GetLobbyDir(lobbyId);
|
|
Directory.CreateDirectory(lobbyDir);
|
|
|
|
var walPath = GetCurrentWalPath(lobbyDir);
|
|
var json = JsonSerializer.Serialize(evt, JsonOptions.Default);
|
|
|
|
lock (_walLock)
|
|
{
|
|
// Kontrola velikosti WAL
|
|
if (File.Exists(walPath))
|
|
{
|
|
var fileInfo = new FileInfo(walPath);
|
|
if (fileInfo.Length > _config.WalMaxSizeMb * 1024 * 1024)
|
|
{
|
|
// Rotace - nový soubor
|
|
var newWalPath = Path.Combine(lobbyDir, $"wal_{DateTime.UtcNow:yyyyMMddHHmmss}.ndjson");
|
|
walPath = newWalPath;
|
|
}
|
|
}
|
|
|
|
using var fs = new FileStream(walPath, FileMode.Append, FileAccess.Write, FileShare.Read);
|
|
using var writer = new StreamWriter(fs, Encoding.UTF8);
|
|
writer.WriteLine(json);
|
|
writer.Flush();
|
|
fs.Flush(true); // fsync
|
|
}
|
|
}
|
|
|
|
public List<GameEvent> ReadWal(string lobbyId, long fromEventId = 0)
|
|
{
|
|
var lobbyDir = GetLobbyDir(lobbyId);
|
|
var events = new List<GameEvent>();
|
|
|
|
if (!Directory.Exists(lobbyDir))
|
|
return events;
|
|
|
|
// Načteme všechny WAL soubory seřazené podle času
|
|
var walFiles = Directory.GetFiles(lobbyDir, "wal_*.ndjson")
|
|
.OrderBy(f => f)
|
|
.ToList();
|
|
|
|
foreach (var walPath in walFiles)
|
|
{
|
|
try
|
|
{
|
|
var lines = File.ReadAllLines(walPath, Encoding.UTF8);
|
|
foreach (var line in lines)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(line)) continue;
|
|
|
|
var evt = JsonSerializer.Deserialize<GameEvent>(line, JsonOptions.Default);
|
|
if (evt != null && evt.EventId > fromEventId)
|
|
{
|
|
events.Add(evt);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Chyba při čtení WAL {Path}", walPath);
|
|
}
|
|
}
|
|
|
|
return events.OrderBy(e => e.EventId).ToList();
|
|
}
|
|
|
|
private string GetCurrentWalPath(string lobbyDir)
|
|
{
|
|
// Najdeme nejnovější WAL nebo vytvoříme nový
|
|
var walFiles = Directory.GetFiles(lobbyDir, "wal_*.ndjson");
|
|
if (walFiles.Length == 0)
|
|
{
|
|
return Path.Combine(lobbyDir, $"wal_{DateTime.UtcNow:yyyyMMddHHmmss}.ndjson");
|
|
}
|
|
return walFiles.OrderByDescending(f => f).First();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Snapshots
|
|
|
|
public void SaveSnapshot(string lobbyId, LobbySnapshot snapshot)
|
|
{
|
|
var lobbyDir = GetLobbyDir(lobbyId);
|
|
Directory.CreateDirectory(lobbyDir);
|
|
|
|
// Serializujeme bez checksumu
|
|
snapshot.Timestamp = DateTime.UtcNow;
|
|
var json = JsonSerializer.Serialize(snapshot, JsonOptions.Indented);
|
|
|
|
// Spočítáme checksum
|
|
var checksum = ComputeChecksum(json);
|
|
snapshot.Checksum = checksum;
|
|
|
|
// Serializujeme znovu s checksumem
|
|
json = JsonSerializer.Serialize(snapshot, JsonOptions.Indented);
|
|
|
|
var snapshotPath = Path.Combine(lobbyDir, $"snapshot_{snapshot.LastEventId}.json");
|
|
var tempPath = snapshotPath + ".tmp";
|
|
|
|
// Atomic write - temp + rename
|
|
File.WriteAllText(tempPath, json, Encoding.UTF8);
|
|
File.Move(tempPath, snapshotPath, overwrite: true);
|
|
|
|
_logger.LogInformation("Snapshot uložen: {Path}, eventId: {EventId}", snapshotPath, snapshot.LastEventId);
|
|
|
|
// Čištění starých snapshotů
|
|
CleanupOldSnapshots(lobbyDir);
|
|
}
|
|
|
|
public LobbySnapshot? LoadLatestSnapshot(string lobbyId)
|
|
{
|
|
var lobbyDir = GetLobbyDir(lobbyId);
|
|
|
|
if (!Directory.Exists(lobbyDir))
|
|
return null;
|
|
|
|
var snapshotFiles = Directory.GetFiles(lobbyDir, "snapshot_*.json")
|
|
.OrderByDescending(f => f)
|
|
.ToList();
|
|
|
|
foreach (var snapshotPath in snapshotFiles)
|
|
{
|
|
try
|
|
{
|
|
var json = File.ReadAllText(snapshotPath, Encoding.UTF8);
|
|
var snapshot = JsonSerializer.Deserialize<LobbySnapshot>(json, JsonOptions.Default);
|
|
|
|
if (snapshot == null) continue;
|
|
|
|
// Validace checksumu
|
|
var storedChecksum = snapshot.Checksum;
|
|
snapshot.Checksum = "";
|
|
var jsonWithoutChecksum = JsonSerializer.Serialize(snapshot, JsonOptions.Indented);
|
|
var computedChecksum = ComputeChecksum(jsonWithoutChecksum);
|
|
|
|
if (storedChecksum != computedChecksum)
|
|
{
|
|
_logger.LogWarning("Checksum mismatch pro snapshot {Path}", snapshotPath);
|
|
continue;
|
|
}
|
|
|
|
snapshot.Checksum = storedChecksum;
|
|
return snapshot;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Chyba při čtení snapshot {Path}", snapshotPath);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void CleanupOldSnapshots(string lobbyDir, int keepCount = 5)
|
|
{
|
|
var snapshotFiles = Directory.GetFiles(lobbyDir, "snapshot_*.json")
|
|
.OrderByDescending(f => f)
|
|
.Skip(keepCount)
|
|
.ToList();
|
|
|
|
foreach (var oldSnapshot in snapshotFiles)
|
|
{
|
|
try
|
|
{
|
|
File.Delete(oldSnapshot);
|
|
_logger.LogDebug("Smazán starý snapshot: {Path}", oldSnapshot);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Nelze smazat snapshot {Path}", oldSnapshot);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string ComputeChecksum(string content)
|
|
{
|
|
var bytes = Encoding.UTF8.GetBytes(content);
|
|
var hash = SHA256.HashData(bytes);
|
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Archive
|
|
|
|
public void ArchiveLobby(string lobbyId)
|
|
{
|
|
var lobbyDir = GetLobbyDir(lobbyId);
|
|
if (!Directory.Exists(lobbyDir))
|
|
return;
|
|
|
|
var archiveDir = Path.Combine(_config.DataPath, "archive");
|
|
Directory.CreateDirectory(archiveDir);
|
|
|
|
var archivePath = Path.Combine(archiveDir, $"{lobbyId}_{DateTime.UtcNow:yyyyMMddHHmmss}");
|
|
Directory.Move(lobbyDir, archivePath);
|
|
|
|
_logger.LogInformation("Lobby archivováno: {LobbyId} -> {ArchivePath}", lobbyId, archivePath);
|
|
}
|
|
|
|
public void DeleteLobbyData(string lobbyId)
|
|
{
|
|
var lobbyDir = GetLobbyDir(lobbyId);
|
|
if (Directory.Exists(lobbyDir))
|
|
{
|
|
Directory.Delete(lobbyDir, recursive: true);
|
|
_logger.LogInformation("Data lobby smazána: {LobbyId}", lobbyId);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
private string GetLobbyDir(string lobbyId)
|
|
{
|
|
return Path.Combine(_config.DataPath, "lobbies", lobbyId);
|
|
}
|
|
}
|