using System;
|
using System.Collections.Generic;
|
using System.IO;
|
using System.Linq;
|
using System.Text;
|
using System.Text.Json;
|
using System.Threading;
|
using System.Threading.Tasks;
|
using WIDESEAWCS_S7Simulator.Core.Entities;
|
using WIDESEAWCS_S7Simulator.Core.Enums;
|
using WIDESEAWCS_S7Simulator.Core.Interfaces;
|
using WIDESEAWCS_S7Simulator.Core.Persistence.Models;
|
|
namespace WIDESEAWCS_S7Simulator.Core.Persistence
|
{
|
/// <summary>
|
/// 鏂囦欢鎸佷箙鍖栨湇鍔″疄鐜?
|
/// 灏嗗疄渚嬮厤缃拰鍐呭瓨鏁版嵁淇濆瓨鍒版湰鍦癑SON鏂囦欢
|
/// </summary>
|
public class FilePersistenceService : IPersistenceService
|
{
|
/// <summary>
|
/// 鏁版嵁鐩綍璺緞
|
/// </summary>
|
private readonly string _dataPath;
|
|
/// <summary>
|
/// JSON搴忓垪鍖栭€夐」锛堢嚎绋嬪畨鍏級
|
/// </summary>
|
private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
|
{
|
WriteIndented = true,
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
};
|
|
/// <summary>
|
/// 鏂囦欢鎿嶄綔閿侊紙绾跨▼瀹夊叏锛?
|
/// </summary>
|
private readonly SemaphoreSlim _fileLock = new SemaphoreSlim(1, 1);
|
|
/// <summary>
|
/// 鏋勯€犲嚱鏁?
|
/// </summary>
|
/// <param name="dataPath">鏁版嵁鐩綍璺緞</param>
|
public FilePersistenceService(string dataPath = "Data")
|
{
|
// 杞崲涓虹粷瀵硅矾寰勶紙鍩轰簬褰撳墠宸ヤ綔鐩綍锛?
|
_dataPath = Path.GetFullPath(dataPath);
|
|
try
|
{
|
// 纭繚鏁版嵁鐩綍瀛樺湪
|
if (!Directory.Exists(_dataPath))
|
{
|
Directory.CreateDirectory(_dataPath);
|
}
|
}
|
catch (Exception ex)
|
{
|
Console.WriteLine($"Error creating data directory '{_dataPath}': {ex.Message}");
|
throw;
|
}
|
}
|
|
/// <summary>
|
/// 淇濆瓨瀹炰緥閰嶇疆
|
/// </summary>
|
public async Task SaveInstanceConfigAsync(InstanceConfig config)
|
{
|
if (config == null)
|
throw new ArgumentNullException(nameof(config));
|
|
await _fileLock.WaitAsync();
|
try
|
{
|
var instanceDir = GetInstanceDirectory(config.Id);
|
if (!Directory.Exists(instanceDir))
|
{
|
Directory.CreateDirectory(instanceDir);
|
}
|
|
var configPath = Path.Combine(instanceDir, "config.json");
|
var model = ToDataModel(config);
|
|
var json = JsonSerializer.Serialize(model, _jsonOptions);
|
await WriteFileAtomicAsync(configPath, json);
|
}
|
catch (Exception ex)
|
{
|
Console.WriteLine($"Error saving instance config for '{config.Id}': {ex.Message}");
|
throw;
|
}
|
finally
|
{
|
_fileLock.Release();
|
}
|
}
|
|
/// <summary>
|
/// 鍔犺浇瀹炰緥閰嶇疆
|
/// </summary>
|
public async Task<InstanceConfig> LoadInstanceConfigAsync(string instanceId)
|
{
|
if (string.IsNullOrWhiteSpace(instanceId))
|
throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId));
|
|
ValidateInstanceId(instanceId);
|
|
await _fileLock.WaitAsync();
|
try
|
{
|
var configPath = Path.Combine(GetInstanceDirectory(instanceId), "config.json");
|
if (!File.Exists(configPath))
|
throw new FileNotFoundException($"瀹炰緥閰嶇疆鏂囦欢涓嶅瓨鍦? {configPath}");
|
|
var json = await File.ReadAllTextAsync(configPath);
|
var model = JsonSerializer.Deserialize<InstanceDataModel>(json, _jsonOptions);
|
|
if (model == null)
|
throw new InvalidOperationException("鏃犳硶鍙嶅簭鍒楀寲瀹炰緥閰嶇疆");
|
|
return ToEntity(model);
|
}
|
catch (Exception ex) when (!(ex is FileNotFoundException || ex is InvalidOperationException || ex is ArgumentException))
|
{
|
Console.WriteLine($"Error loading instance config for '{instanceId}': {ex.Message}");
|
throw;
|
}
|
finally
|
{
|
_fileLock.Release();
|
}
|
}
|
|
/// <summary>
|
/// 鍔犺浇鎵€鏈夊疄渚嬮厤缃?
|
/// </summary>
|
public async Task<List<InstanceConfig>> LoadAllInstanceConfigsAsync()
|
{
|
await _fileLock.WaitAsync();
|
try
|
{
|
var configs = new List<InstanceConfig>();
|
|
if (!Directory.Exists(_dataPath))
|
return configs;
|
|
var instanceDirs = Directory.GetDirectories(_dataPath)
|
.Where(d => Path.GetFileName(d).StartsWith("instance-"))
|
.ToList();
|
|
foreach (var dir in instanceDirs)
|
{
|
var configPath = Path.Combine(dir, "config.json");
|
if (File.Exists(configPath))
|
{
|
try
|
{
|
var json = await File.ReadAllTextAsync(configPath);
|
var model = JsonSerializer.Deserialize<InstanceDataModel>(json, _jsonOptions);
|
if (model != null)
|
{
|
configs.Add(ToEntity(model));
|
}
|
}
|
catch (Exception ex)
|
{
|
Console.WriteLine($"Error loading instance config from '{configPath}': {ex.Message}");
|
// 璺宠繃鏃犳硶鍔犺浇鐨勯厤缃枃浠?
|
continue;
|
}
|
}
|
}
|
|
return configs;
|
}
|
finally
|
{
|
_fileLock.Release();
|
}
|
}
|
|
/// <summary>
|
/// 鍒犻櫎瀹炰緥閰嶇疆
|
/// </summary>
|
public async Task DeleteInstanceConfigAsync(string instanceId)
|
{
|
if (string.IsNullOrWhiteSpace(instanceId))
|
throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId));
|
|
ValidateInstanceId(instanceId);
|
|
await _fileLock.WaitAsync();
|
try
|
{
|
var instanceDir = GetInstanceDirectory(instanceId);
|
if (Directory.Exists(instanceDir))
|
{
|
Directory.Delete(instanceDir, recursive: true);
|
}
|
}
|
catch (Exception ex)
|
{
|
Console.WriteLine($"Error deleting instance '{instanceId}': {ex.Message}");
|
throw;
|
}
|
finally
|
{
|
_fileLock.Release();
|
}
|
}
|
|
/// <summary>
|
/// 淇濆瓨鍐呭瓨鏁版嵁
|
/// </summary>
|
public async Task SaveMemoryDataAsync(string instanceId, IMemoryStore memoryStore)
|
{
|
if (string.IsNullOrWhiteSpace(instanceId))
|
throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId));
|
|
ValidateInstanceId(instanceId);
|
|
if (memoryStore == null)
|
throw new ArgumentNullException(nameof(memoryStore));
|
|
await _fileLock.WaitAsync();
|
try
|
{
|
var instanceDir = GetInstanceDirectory(instanceId);
|
if (!Directory.Exists(instanceDir))
|
{
|
Directory.CreateDirectory(instanceDir);
|
}
|
|
var memoryPath = Path.Combine(instanceDir, "memory.json");
|
var exportedData = memoryStore.Export();
|
|
// 灏嗗瓧鑺傛暟缁勮浆鎹负Base64瀛楃涓蹭互渚縅SON搴忓垪鍖?
|
var memoryDataModel = new MemoryDataModel
|
{
|
MemoryData = exportedData.ToDictionary(
|
kvp => kvp.Key,
|
kvp => Convert.ToBase64String(kvp.Value)
|
)
|
};
|
|
var json = JsonSerializer.Serialize(memoryDataModel, _jsonOptions);
|
await WriteFileAtomicAsync(memoryPath, json);
|
}
|
catch (Exception ex)
|
{
|
Console.WriteLine($"Error saving memory data for instance '{instanceId}': {ex.Message}");
|
throw;
|
}
|
finally
|
{
|
_fileLock.Release();
|
}
|
}
|
|
/// <summary>
|
/// 鍔犺浇鍐呭瓨鏁版嵁
|
/// </summary>
|
public async Task LoadMemoryDataAsync(string instanceId, IMemoryStore memoryStore)
|
{
|
if (string.IsNullOrWhiteSpace(instanceId))
|
throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId));
|
|
ValidateInstanceId(instanceId);
|
|
if (memoryStore == null)
|
throw new ArgumentNullException(nameof(memoryStore));
|
|
await _fileLock.WaitAsync();
|
try
|
{
|
var memoryPath = Path.Combine(GetInstanceDirectory(instanceId), "memory.json");
|
if (!File.Exists(memoryPath))
|
return; // 鍐呭瓨鏂囦欢涓嶅瓨鍦紝璺宠繃鍔犺浇
|
|
var json = await File.ReadAllTextAsync(memoryPath);
|
var memoryDataModel = JsonSerializer.Deserialize<MemoryDataModel>(json, _jsonOptions);
|
|
if (memoryDataModel?.MemoryData == null)
|
return;
|
|
// 灏咮ase64瀛楃涓茶浆鎹㈠洖瀛楄妭鏁扮粍
|
var importedData = new Dictionary<string, byte[]>();
|
foreach (var kvp in memoryDataModel.MemoryData)
|
{
|
try
|
{
|
importedData[kvp.Key] = Convert.FromBase64String(kvp.Value);
|
}
|
catch (FormatException ex)
|
{
|
Console.WriteLine($"Warning: Invalid Base64 data for memory region '{kvp.Key}' in instance '{instanceId}': {ex.Message}");
|
// 璺宠繃鏃犳晥鐨凚ase64鏁版嵁
|
continue;
|
}
|
}
|
|
memoryStore.Import(importedData);
|
}
|
catch (Exception ex) when (!(ex is FileNotFoundException))
|
{
|
Console.WriteLine($"Error loading memory data for instance '{instanceId}': {ex.Message}");
|
throw;
|
}
|
finally
|
{
|
_fileLock.Release();
|
}
|
}
|
|
/// <summary>
|
/// 鑾峰彇瀹炰緥鐩綍璺緞
|
/// </summary>
|
private string GetInstanceDirectory(string instanceId)
|
{
|
return Path.Combine(_dataPath, $"instance-{instanceId}");
|
}
|
|
/// <summary>
|
/// 楠岃瘉瀹炰緥ID锛堥槻姝㈣矾寰勯亶鍘嗘敾鍑伙級
|
/// </summary>
|
private void ValidateInstanceId(string instanceId)
|
{
|
if (string.IsNullOrWhiteSpace(instanceId))
|
throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId));
|
|
// 妫€鏌ヨ矾寰勯亶鍘嗗瓧绗?
|
if (instanceId.Contains("..") || instanceId.Contains("/") || instanceId.Contains("\\"))
|
throw new ArgumentException("瀹炰緥ID鍖呭惈闈炴硶瀛楃", nameof(instanceId));
|
|
// 妫€鏌ユ棤鏁堣矾寰勫瓧绗?
|
var invalidChars = Path.GetInvalidFileNameChars();
|
if (instanceId.IndexOfAny(invalidChars) >= 0)
|
throw new ArgumentException("瀹炰緥ID鍖呭惈闈炴硶瀛楃", nameof(instanceId));
|
}
|
|
/// <summary>
|
/// 鍐欏叆鏂囦欢锛堢洿鎺ュ啓鍏ワ紝绠€鍖栫増鏈級
|
/// </summary>
|
private async Task WriteFileAtomicAsync(string filePath, string content)
|
{
|
// 纭繚鐩爣鏂囦欢鐨勭埗鐩綍瀛樺湪
|
var directory = Path.GetDirectoryName(filePath);
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
{
|
Directory.CreateDirectory(directory);
|
}
|
|
// 鐩存帴鍐欏叆鏂囦欢
|
await File.WriteAllTextAsync(filePath, content, Encoding.UTF8);
|
}
|
|
/// <summary>
|
/// 灏嗗疄浣撹浆鎹负鏁版嵁妯″瀷
|
/// </summary>
|
private InstanceDataModel ToDataModel(InstanceConfig config)
|
{
|
return new InstanceDataModel
|
{
|
Id = config.Id,
|
Name = config.Name,
|
PlcType = config.PLCType.ToString(),
|
Port = config.Port,
|
ActivationKey = config.ActivationKey,
|
AutoStart = config.AutoStart,
|
ProtocolTemplateId = config.ProtocolTemplateId,
|
MemoryConfig = new MemoryRegionConfigModel
|
{
|
MRegionSize = config.MemoryConfig.MRegionSize,
|
DBBlockCount = config.MemoryConfig.DBBlockCount,
|
DBBlockNumbers = config.MemoryConfig.DBBlockNumbers.ToList(),
|
DBBlockSize = config.MemoryConfig.DBBlockSize,
|
IRegionSize = config.MemoryConfig.IRegionSize,
|
QRegionSize = config.MemoryConfig.QRegionSize,
|
TRegionCount = config.MemoryConfig.TRegionCount,
|
CRegionCount = config.MemoryConfig.CRegionCount
|
}
|
};
|
}
|
|
/// <summary>
|
/// 灏嗘暟鎹ā鍨嬭浆鎹负瀹炰綋
|
/// </summary>
|
private InstanceConfig ToEntity(InstanceDataModel model)
|
{
|
return new InstanceConfig
|
{
|
Id = model.Id,
|
Name = model.Name,
|
PLCType = Enum.Parse<SiemensPLCType>(model.PlcType),
|
Port = model.Port,
|
ActivationKey = model.ActivationKey,
|
AutoStart = model.AutoStart,
|
ProtocolTemplateId = model.ProtocolTemplateId,
|
MemoryConfig = new MemoryRegionConfig
|
{
|
MRegionSize = model.MemoryConfig.MRegionSize,
|
DBBlockCount = model.MemoryConfig.DBBlockCount,
|
DBBlockNumbers = model.MemoryConfig.DBBlockNumbers?.ToList() ?? new List<int>(),
|
DBBlockSize = model.MemoryConfig.DBBlockSize,
|
IRegionSize = model.MemoryConfig.IRegionSize,
|
QRegionSize = model.MemoryConfig.QRegionSize,
|
TRegionCount = model.MemoryConfig.TRegionCount,
|
CRegionCount = model.MemoryConfig.CRegionCount
|
}
|
};
|
}
|
}
|
}
|