| | |
| | | using System; |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using System.IO; |
| | | using System.Linq; |
| | |
| | | namespace WIDESEAWCS_S7Simulator.Core.Persistence |
| | | { |
| | | /// <summary> |
| | | /// 文件持久化服务实现 |
| | | /// 将实例配置和内存数据保存到本地JSON文件 |
| | | /// 鏂囦欢鎸佷箙鍖栨湇鍔″疄鐜? |
| | | /// 灏嗗疄渚嬮厤缃拰鍐呭瓨鏁版嵁淇濆瓨鍒版湰鍦癑SON鏂囦欢 |
| | | /// </summary> |
| | | public class FilePersistenceService : IPersistenceService |
| | | { |
| | | /// <summary> |
| | | /// 数据目录路径 |
| | | /// 鏁版嵁鐩綍璺緞 |
| | | /// </summary> |
| | | private readonly string _dataPath; |
| | | |
| | | /// <summary> |
| | | /// JSON序列化选项(线程安全) |
| | | /// JSON搴忓垪鍖栭€夐」锛堢嚎绋嬪畨鍏級 |
| | | /// </summary> |
| | | private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions |
| | | { |
| | |
| | | }; |
| | | |
| | | /// <summary> |
| | | /// 文件操作锁(线程安全) |
| | | /// 鏂囦欢鎿嶄綔閿侊紙绾跨▼瀹夊叏锛? |
| | | /// </summary> |
| | | private readonly SemaphoreSlim _fileLock = new SemaphoreSlim(1, 1); |
| | | |
| | | /// <summary> |
| | | /// 构造函数 |
| | | /// 鏋勯€犲嚱鏁? |
| | | /// </summary> |
| | | /// <param name="dataPath">数据目录路径</param> |
| | | /// <param name="dataPath">鏁版嵁鐩綍璺緞</param> |
| | | public FilePersistenceService(string dataPath = "Data") |
| | | { |
| | | _dataPath = dataPath; |
| | | // 杞崲涓虹粷瀵硅矾寰勶紙鍩轰簬褰撳墠宸ヤ綔鐩綍锛? |
| | | _dataPath = Path.GetFullPath(dataPath); |
| | | |
| | | try |
| | | { |
| | | // 确保数据目录存在 |
| | | // 纭繚鏁版嵁鐩綍瀛樺湪 |
| | | if (!Directory.Exists(_dataPath)) |
| | | { |
| | | Directory.CreateDirectory(_dataPath); |
| | |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 保存实例配置 |
| | | /// 淇濆瓨瀹炰緥閰嶇疆 |
| | | /// </summary> |
| | | public async Task SaveInstanceConfigAsync(InstanceConfig config) |
| | | { |
| | |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 加载实例配置 |
| | | /// 鍔犺浇瀹炰緥閰嶇疆 |
| | | /// </summary> |
| | | public async Task<InstanceConfig> LoadInstanceConfigAsync(string instanceId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | throw new ArgumentException("实例ID不能为空", nameof(instanceId)); |
| | | throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId)); |
| | | |
| | | ValidateInstanceId(instanceId); |
| | | |
| | |
| | | { |
| | | var configPath = Path.Combine(GetInstanceDirectory(instanceId), "config.json"); |
| | | if (!File.Exists(configPath)) |
| | | throw new FileNotFoundException($"实例配置文件不存在: {configPath}"); |
| | | throw new FileNotFoundException($"瀹炰緥閰嶇疆鏂囦欢涓嶅瓨鍦? {configPath}"); |
| | | |
| | | var json = await File.ReadAllTextAsync(configPath); |
| | | var model = JsonSerializer.Deserialize<InstanceDataModel>(json, _jsonOptions); |
| | | |
| | | if (model == null) |
| | | throw new InvalidOperationException("无法反序列化实例配置"); |
| | | throw new InvalidOperationException("鏃犳硶鍙嶅簭鍒楀寲瀹炰緥閰嶇疆"); |
| | | |
| | | return ToEntity(model); |
| | | } |
| | |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 加载所有实例配置 |
| | | /// 鍔犺浇鎵€鏈夊疄渚嬮厤缃? |
| | | /// </summary> |
| | | public async Task<List<InstanceConfig>> LoadAllInstanceConfigsAsync() |
| | | { |
| | |
| | | catch (Exception ex) |
| | | { |
| | | Console.WriteLine($"Error loading instance config from '{configPath}': {ex.Message}"); |
| | | // 跳过无法加载的配置文件 |
| | | // 璺宠繃鏃犳硶鍔犺浇鐨勯厤缃枃浠? |
| | | continue; |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 删除实例配置 |
| | | /// 鍒犻櫎瀹炰緥閰嶇疆 |
| | | /// </summary> |
| | | public async Task DeleteInstanceConfigAsync(string instanceId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | throw new ArgumentException("实例ID不能为空", nameof(instanceId)); |
| | | throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId)); |
| | | |
| | | ValidateInstanceId(instanceId); |
| | | |
| | |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 保存内存数据 |
| | | /// 淇濆瓨鍐呭瓨鏁版嵁 |
| | | /// </summary> |
| | | public async Task SaveMemoryDataAsync(string instanceId, IMemoryStore memoryStore) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | throw new ArgumentException("实例ID不能为空", nameof(instanceId)); |
| | | throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId)); |
| | | |
| | | ValidateInstanceId(instanceId); |
| | | |
| | |
| | | var memoryPath = Path.Combine(instanceDir, "memory.json"); |
| | | var exportedData = memoryStore.Export(); |
| | | |
| | | // 将字节数组转换为Base64字符串以便JSON序列化 |
| | | // 灏嗗瓧鑺傛暟缁勮浆鎹负Base64瀛楃涓蹭互渚縅SON搴忓垪鍖? |
| | | var memoryDataModel = new MemoryDataModel |
| | | { |
| | | MemoryData = exportedData.ToDictionary( |
| | |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 加载内存数据 |
| | | /// 鍔犺浇鍐呭瓨鏁版嵁 |
| | | /// </summary> |
| | | public async Task LoadMemoryDataAsync(string instanceId, IMemoryStore memoryStore) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | throw new ArgumentException("实例ID不能为空", nameof(instanceId)); |
| | | throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId)); |
| | | |
| | | ValidateInstanceId(instanceId); |
| | | |
| | |
| | | { |
| | | var memoryPath = Path.Combine(GetInstanceDirectory(instanceId), "memory.json"); |
| | | if (!File.Exists(memoryPath)) |
| | | return; // 内存文件不存在,跳过加载 |
| | | return; // 鍐呭瓨鏂囦欢涓嶅瓨鍦紝璺宠繃鍔犺浇 |
| | | |
| | | var json = await File.ReadAllTextAsync(memoryPath); |
| | | var memoryDataModel = JsonSerializer.Deserialize<MemoryDataModel>(json, _jsonOptions); |
| | |
| | | if (memoryDataModel?.MemoryData == null) |
| | | return; |
| | | |
| | | // 将Base64字符串转换回字节数组 |
| | | // 灏咮ase64瀛楃涓茶浆鎹㈠洖瀛楄妭鏁扮粍 |
| | | var importedData = new Dictionary<string, byte[]>(); |
| | | foreach (var kvp in memoryDataModel.MemoryData) |
| | | { |
| | |
| | | catch (FormatException ex) |
| | | { |
| | | Console.WriteLine($"Warning: Invalid Base64 data for memory region '{kvp.Key}' in instance '{instanceId}': {ex.Message}"); |
| | | // 跳过无效的Base64数据 |
| | | // 璺宠繃鏃犳晥鐨凚ase64鏁版嵁 |
| | | continue; |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取实例目录路径 |
| | | /// 鑾峰彇瀹炰緥鐩綍璺緞 |
| | | /// </summary> |
| | | private string GetInstanceDirectory(string instanceId) |
| | | { |
| | |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 验证实例ID(防止路径遍历攻击) |
| | | /// 楠岃瘉瀹炰緥ID锛堥槻姝㈣矾寰勯亶鍘嗘敾鍑伙級 |
| | | /// </summary> |
| | | private void ValidateInstanceId(string instanceId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | throw new ArgumentException("实例ID不能为空", nameof(instanceId)); |
| | | throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId)); |
| | | |
| | | // 检查路径遍历字符 |
| | | // 妫€鏌ヨ矾寰勯亶鍘嗗瓧绗? |
| | | if (instanceId.Contains("..") || instanceId.Contains("/") || instanceId.Contains("\\")) |
| | | throw new ArgumentException("实例ID包含非法字符", nameof(instanceId)); |
| | | throw new ArgumentException("瀹炰緥ID鍖呭惈闈炴硶瀛楃", nameof(instanceId)); |
| | | |
| | | // 检查无效路径字符 |
| | | // 妫€鏌ユ棤鏁堣矾寰勫瓧绗? |
| | | var invalidChars = Path.GetInvalidFileNameChars(); |
| | | if (instanceId.IndexOfAny(invalidChars) >= 0) |
| | | throw new ArgumentException("实例ID包含非法字符", nameof(instanceId)); |
| | | throw new ArgumentException("瀹炰緥ID鍖呭惈闈炴硶瀛楃", nameof(instanceId)); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 原子性写入文件(使用临时文件+替换模式) |
| | | /// 鍐欏叆鏂囦欢锛堢洿鎺ュ啓鍏ワ紝绠€鍖栫増鏈級 |
| | | /// </summary> |
| | | private async Task WriteFileAtomicAsync(string filePath, string content) |
| | | { |
| | | var tempPath = filePath + ".tmp"; |
| | | |
| | | try |
| | | // 纭繚鐩爣鏂囦欢鐨勭埗鐩綍瀛樺湪 |
| | | var directory = Path.GetDirectoryName(filePath); |
| | | if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) |
| | | { |
| | | // 写入临时文件 |
| | | await File.WriteAllTextAsync(tempPath, content, Encoding.UTF8); |
| | | Directory.CreateDirectory(directory); |
| | | } |
| | | |
| | | // 原子性替换目标文件 |
| | | File.Replace(tempPath, filePath, null); |
| | | } |
| | | catch |
| | | { |
| | | // 清理临时文件 |
| | | if (File.Exists(tempPath)) |
| | | { |
| | | try |
| | | { |
| | | File.Delete(tempPath); |
| | | } |
| | | catch |
| | | { |
| | | // 忽略清理失败 |
| | | } |
| | | } |
| | | throw; |
| | | } |
| | | // 鐩存帴鍐欏叆鏂囦欢 |
| | | await File.WriteAllTextAsync(filePath, content, Encoding.UTF8); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 将实体转换为数据模型 |
| | | /// 灏嗗疄浣撹浆鎹负鏁版嵁妯″瀷 |
| | | /// </summary> |
| | | private InstanceDataModel ToDataModel(InstanceConfig config) |
| | | { |
| | |
| | | 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, |
| | |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 将数据模型转换为实体 |
| | | /// 灏嗘暟鎹ā鍨嬭浆鎹负瀹炰綋 |
| | | /// </summary> |
| | | private InstanceConfig ToEntity(InstanceDataModel model) |
| | | { |
| | |
| | | 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, |
| | |
| | | } |
| | | } |
| | | } |
| | | |