161 lines
6.2 KiB
C#
161 lines
6.2 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text.Json;
|
|
using KronaStaffTerminal.Models;
|
|
|
|
namespace KronaStaffTerminal.Services;
|
|
|
|
public sealed class TerminalStore
|
|
{
|
|
private static readonly byte[] Entropy = "Krona.Staff.Terminal.v1"u8.ToArray();
|
|
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
|
private readonly SemaphoreSlim _saveGate = new(1, 1);
|
|
private readonly string _filePath = Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
"Krona",
|
|
"StaffTerminal",
|
|
"terminal-state.dat");
|
|
|
|
public async Task<TerminalState> LoadAsync()
|
|
{
|
|
var sourcePath = File.Exists(_filePath) ? _filePath : FindLegacyPackagedState();
|
|
if (sourcePath is null) return new TerminalState();
|
|
|
|
try
|
|
{
|
|
var state = await ReadStateAsync(sourcePath);
|
|
var migrated = MigrateFingerprints(state);
|
|
if (migrated || !string.Equals(sourcePath, _filePath, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
await SaveAsync(state);
|
|
}
|
|
return state;
|
|
}
|
|
catch (Exception primaryException) when (string.Equals(sourcePath, _filePath, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var corruptPath = _filePath + $".corrupt-{DateTimeOffset.UtcNow:yyyyMMdd-HHmmss}";
|
|
try
|
|
{
|
|
File.Move(_filePath, corruptPath, false);
|
|
}
|
|
catch (Exception moveException)
|
|
{
|
|
throw new InvalidDataException(
|
|
"Хранилище терминала повреждено и не может быть сохранено отдельно. Работа заблокирована, чтобы не потерять данные.",
|
|
new AggregateException(primaryException, moveException));
|
|
}
|
|
|
|
foreach (var backupPath in BackupPaths())
|
|
{
|
|
if (!File.Exists(backupPath)) continue;
|
|
try
|
|
{
|
|
var recovered = await ReadStateAsync(backupPath);
|
|
MigrateFingerprints(recovered);
|
|
await SaveAsync(recovered);
|
|
return recovered;
|
|
}
|
|
catch (Exception backupException)
|
|
{
|
|
CrashLogService.Write(backupException);
|
|
}
|
|
}
|
|
|
|
throw new InvalidDataException(
|
|
$"Хранилище терминала повреждено. Исходный файл сохранён как {Path.GetFileName(corruptPath)}. Работа заблокирована до восстановления резервной копии.",
|
|
primaryException);
|
|
}
|
|
}
|
|
|
|
private static async Task<TerminalState> ReadStateAsync(string path)
|
|
{
|
|
var encrypted = await File.ReadAllBytesAsync(path);
|
|
var clear = ProtectedData.Unprotect(encrypted, Entropy, DataProtectionScope.CurrentUser);
|
|
return JsonSerializer.Deserialize<TerminalState>(clear, JsonOptions)
|
|
?? throw new InvalidDataException("Файл состояния не содержит данных.");
|
|
}
|
|
|
|
private static bool MigrateFingerprints(TerminalState state)
|
|
{
|
|
var changed = false;
|
|
foreach (var employee in state.Employees)
|
|
{
|
|
employee.Fingerprints ??= [];
|
|
if (employee.FingerprintId <= 0 || string.IsNullOrWhiteSpace(employee.FingerprintTemplate))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (employee.Fingerprints.All(item => item.Id != employee.FingerprintId))
|
|
{
|
|
employee.Fingerprints.Add(new FingerprintRecord
|
|
{
|
|
Id = employee.FingerprintId,
|
|
Template = employee.FingerprintTemplate,
|
|
Label = "Отпечаток 1"
|
|
});
|
|
}
|
|
employee.FingerprintId = 0;
|
|
employee.FingerprintTemplate = null;
|
|
changed = true;
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
public async Task SaveAsync(TerminalState state)
|
|
{
|
|
await _saveGate.WaitAsync();
|
|
try
|
|
{
|
|
var directory = Path.GetDirectoryName(_filePath)!;
|
|
Directory.CreateDirectory(directory);
|
|
var clear = JsonSerializer.SerializeToUtf8Bytes(state, JsonOptions);
|
|
var encrypted = ProtectedData.Protect(clear, Entropy, DataProtectionScope.CurrentUser);
|
|
var temporaryPath = _filePath + ".tmp";
|
|
await using (var stream = new FileStream(
|
|
temporaryPath,
|
|
FileMode.Create,
|
|
FileAccess.Write,
|
|
FileShare.None,
|
|
4096,
|
|
FileOptions.Asynchronous | FileOptions.WriteThrough))
|
|
{
|
|
await stream.WriteAsync(encrypted);
|
|
await stream.FlushAsync();
|
|
stream.Flush(true);
|
|
}
|
|
|
|
RotateBackups();
|
|
File.Move(temporaryPath, _filePath, true);
|
|
}
|
|
finally
|
|
{
|
|
_saveGate.Release();
|
|
}
|
|
}
|
|
|
|
private IEnumerable<string> BackupPaths()
|
|
{
|
|
for (var index = 1; index <= 3; index++) yield return _filePath + $".bak{index}";
|
|
}
|
|
|
|
private void RotateBackups()
|
|
{
|
|
var backups = BackupPaths().ToArray();
|
|
for (var index = backups.Length - 1; index > 0; index--)
|
|
{
|
|
if (File.Exists(backups[index - 1])) File.Copy(backups[index - 1], backups[index], true);
|
|
}
|
|
if (File.Exists(_filePath)) File.Copy(_filePath, backups[0], true);
|
|
}
|
|
|
|
private static string? FindLegacyPackagedState()
|
|
{
|
|
var packagesPath = Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
"Packages");
|
|
if (!Directory.Exists(packagesPath)) return null;
|
|
return Directory.EnumerateDirectories(packagesPath, "223A9635-262B-4298-9F19-1D2FE0A6B5F7_*")
|
|
.Select(directory => Path.Combine(directory, "LocalCache", "Local", "Krona", "StaffTerminal", "terminal-state.dat"))
|
|
.FirstOrDefault(File.Exists);
|
|
}
|
|
}
|