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>
|
/// 文件持久化服务实现
|
/// 将实例配置和内存数据保存到本地JSON文件
|
/// </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字符串以便JSON序列化
|
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;
|
|
// 将Base64字符串转换回字节数组
|
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}");
|
// 跳过无效的Base64数据
|
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,
|
MemoryConfig = new MemoryRegionConfigModel
|
{
|
MRegionSize = config.MemoryConfig.MRegionSize,
|
DBBlockCount = config.MemoryConfig.DBBlockCount,
|
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,
|
MemoryConfig = new MemoryRegionConfig
|
{
|
MRegionSize = model.MemoryConfig.MRegionSize,
|
DBBlockCount = model.MemoryConfig.DBBlockCount,
|
DBBlockSize = model.MemoryConfig.DBBlockSize,
|
IRegionSize = model.MemoryConfig.IRegionSize,
|
QRegionSize = model.MemoryConfig.QRegionSize,
|
TRegionCount = model.MemoryConfig.TRegionCount,
|
CRegionCount = model.MemoryConfig.CRegionCount
|
}
|
};
|
}
|
}
|
}
|