using System.Text.Json; using LumaTunnel.Shared.Protocol; namespace LumaTunnel.Client.Profiles; public sealed class ClientProfileStore : IDisposable { private readonly SemaphoreSlim _gate = new(1, 1); public string RootPath { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "LumaTunnel"); public string ProfilePath => Path.Combine(RootPath, "config", "client.json"); public string LogPath => Path.Combine(RootPath, "logs"); public string StatePath => Path.Combine(RootPath, "state"); public async Task LoadAsync(CancellationToken cancellationToken = default) { Directory.CreateDirectory(Path.GetDirectoryName(ProfilePath)!); Directory.CreateDirectory(LogPath); Directory.CreateDirectory(StatePath); if (!File.Exists(ProfilePath)) { var profile = new ClientProfile(); await SaveAsync(profile, cancellationToken).ConfigureAwait(false); return profile; } await using var stream = File.OpenRead(ProfilePath); return await JsonSerializer.DeserializeAsync(stream, TunnelMessageSerializer.Options, cancellationToken).ConfigureAwait(false) ?? new ClientProfile(); } public async Task SaveAsync(ClientProfile profile, CancellationToken cancellationToken = default) { await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); try { Directory.CreateDirectory(Path.GetDirectoryName(ProfilePath)!); var temp = ProfilePath + ".tmp"; try { await using (var stream = new FileStream(temp, FileMode.Create, FileAccess.Write, FileShare.None, 8192, FileOptions.WriteThrough | FileOptions.Asynchronous)) await JsonSerializer.SerializeAsync(stream, profile, TunnelMessageSerializer.Options, cancellationToken).ConfigureAwait(false); File.Move(temp, ProfilePath, true); } finally { if (File.Exists(temp)) File.Delete(temp); } } finally { _gate.Release(); } } public void Dispose() { _gate.Dispose(); GC.SuppressFinalize(this); } }