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 { /// /// 文件持久化服务实现 /// 将实例配置和内存数据保存到本地JSON文件 /// public class FilePersistenceService : IPersistenceService { /// /// 数据目录路径 /// private readonly string _dataPath; /// /// JSON序列化选项(线程安全) /// private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; /// /// 文件操作锁(线程安全) /// private readonly SemaphoreSlim _fileLock = new SemaphoreSlim(1, 1); /// /// 构造函数 /// /// 数据目录路径 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; } } /// /// 保存实例配置 /// 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(); } } /// /// 加载实例配置 /// public async Task 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(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(); } } /// /// 加载所有实例配置 /// public async Task> LoadAllInstanceConfigsAsync() { await _fileLock.WaitAsync(); try { var configs = new List(); 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(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(); } } /// /// 删除实例配置 /// 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(); } } /// /// 保存内存数据 /// 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(); } } /// /// 加载内存数据 /// 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(json, _jsonOptions); if (memoryDataModel?.MemoryData == null) return; // 将Base64字符串转换回字节数组 var importedData = new Dictionary(); 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(); } } /// /// 获取实例目录路径 /// private string GetInstanceDirectory(string instanceId) { return Path.Combine(_dataPath, $"instance-{instanceId}"); } /// /// 验证实例ID(防止路径遍历攻击) /// 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)); } /// /// 写入文件(直接写入,简化版本) /// 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); } /// /// 将实体转换为数据模型 /// 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 } }; } /// /// 将数据模型转换为实体 /// private InstanceConfig ToEntity(InstanceDataModel model) { return new InstanceConfig { Id = model.Id, Name = model.Name, PLCType = Enum.Parse(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 } }; } } }