| | |
| | | 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; |
| | |
| | | private readonly string _dataPath; |
| | | |
| | | /// <summary> |
| | | /// JSON序列化选项 |
| | | /// JSON序列化选项(线程安全) |
| | | /// </summary> |
| | | private readonly JsonSerializerOptions _jsonOptions; |
| | | private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions |
| | | { |
| | | WriteIndented = true, |
| | | PropertyNamingPolicy = JsonNamingPolicy.CamelCase |
| | | }; |
| | | |
| | | /// <summary> |
| | | /// 文件操作锁(线程安全) |
| | | /// </summary> |
| | | private readonly SemaphoreSlim _fileLock = new SemaphoreSlim(1, 1); |
| | | |
| | | /// <summary> |
| | | /// 构造函数 |
| | |
| | | /// <param name="dataPath">数据目录路径</param> |
| | | public FilePersistenceService(string dataPath = "Data") |
| | | { |
| | | _dataPath = dataPath; |
| | | _jsonOptions = new JsonSerializerOptions |
| | | { |
| | | WriteIndented = true, |
| | | PropertyNamingPolicy = JsonNamingPolicy.CamelCase |
| | | }; |
| | | // 转换为绝对路径(基于当前工作目录) |
| | | _dataPath = Path.GetFullPath(dataPath); |
| | | |
| | | // 确保数据目录存在 |
| | | if (!Directory.Exists(_dataPath)) |
| | | try |
| | | { |
| | | Directory.CreateDirectory(_dataPath); |
| | | // 确保数据目录存在 |
| | | if (!Directory.Exists(_dataPath)) |
| | | { |
| | | Directory.CreateDirectory(_dataPath); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Console.WriteLine($"Error creating data directory '{_dataPath}': {ex.Message}"); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | |
| | | if (config == null) |
| | | throw new ArgumentNullException(nameof(config)); |
| | | |
| | | var instanceDir = GetInstanceDirectory(config.Id); |
| | | if (!Directory.Exists(instanceDir)) |
| | | await _fileLock.WaitAsync(); |
| | | try |
| | | { |
| | | Directory.CreateDirectory(instanceDir); |
| | | 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); |
| | | } |
| | | |
| | | var configPath = Path.Combine(instanceDir, "config.json"); |
| | | var model = ToDataModel(config); |
| | | |
| | | var json = JsonSerializer.Serialize(model, _jsonOptions); |
| | | await File.WriteAllTextAsync(configPath, json); |
| | | catch (Exception ex) |
| | | { |
| | | Console.WriteLine($"Error saving instance config for '{config.Id}': {ex.Message}"); |
| | | throw; |
| | | } |
| | | finally |
| | | { |
| | | _fileLock.Release(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | throw new ArgumentException("实例ID不能为空", nameof(instanceId)); |
| | | |
| | | var configPath = Path.Combine(GetInstanceDirectory(instanceId), "config.json"); |
| | | if (!File.Exists(configPath)) |
| | | throw new FileNotFoundException($"实例配置文件不存在: {configPath}"); |
| | | ValidateInstanceId(instanceId); |
| | | |
| | | var json = await File.ReadAllTextAsync(configPath); |
| | | var model = JsonSerializer.Deserialize<InstanceDataModel>(json, _jsonOptions); |
| | | await _fileLock.WaitAsync(); |
| | | try |
| | | { |
| | | var configPath = Path.Combine(GetInstanceDirectory(instanceId), "config.json"); |
| | | if (!File.Exists(configPath)) |
| | | throw new FileNotFoundException($"实例配置文件不存在: {configPath}"); |
| | | |
| | | if (model == null) |
| | | throw new InvalidOperationException("无法反序列化实例配置"); |
| | | var json = await File.ReadAllTextAsync(configPath); |
| | | var model = JsonSerializer.Deserialize<InstanceDataModel>(json, _jsonOptions); |
| | | |
| | | return ToEntity(model); |
| | | 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() |
| | | { |
| | | 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) |
| | | await _fileLock.WaitAsync(); |
| | | try |
| | | { |
| | | var configPath = Path.Combine(dir, "config.json"); |
| | | if (File.Exists(configPath)) |
| | | 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) |
| | | { |
| | | try |
| | | var configPath = Path.Combine(dir, "config.json"); |
| | | if (File.Exists(configPath)) |
| | | { |
| | | var json = await File.ReadAllTextAsync(configPath); |
| | | var model = JsonSerializer.Deserialize<InstanceDataModel>(json, _jsonOptions); |
| | | if (model != null) |
| | | try |
| | | { |
| | | configs.Add(ToEntity(model)); |
| | | 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; |
| | | } |
| | | } |
| | | catch (Exception) |
| | | { |
| | | // 跳过无法加载的配置文件 |
| | | continue; |
| | | } |
| | | } |
| | | } |
| | | |
| | | return configs; |
| | | return configs; |
| | | } |
| | | finally |
| | | { |
| | | _fileLock.Release(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 删除实例配置 |
| | | /// </summary> |
| | | public Task DeleteInstanceConfigAsync(string instanceId) |
| | | public async Task DeleteInstanceConfigAsync(string instanceId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | throw new ArgumentException("实例ID不能为空", nameof(instanceId)); |
| | | |
| | | var instanceDir = GetInstanceDirectory(instanceId); |
| | | if (Directory.Exists(instanceDir)) |
| | | { |
| | | Directory.Delete(instanceDir, recursive: true); |
| | | } |
| | | ValidateInstanceId(instanceId); |
| | | |
| | | return Task.CompletedTask; |
| | | 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> |
| | |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | throw new ArgumentException("实例ID不能为空", nameof(instanceId)); |
| | | |
| | | ValidateInstanceId(instanceId); |
| | | |
| | | if (memoryStore == null) |
| | | throw new ArgumentNullException(nameof(memoryStore)); |
| | | |
| | | var instanceDir = GetInstanceDirectory(instanceId); |
| | | if (!Directory.Exists(instanceDir)) |
| | | await _fileLock.WaitAsync(); |
| | | try |
| | | { |
| | | Directory.CreateDirectory(instanceDir); |
| | | 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); |
| | | } |
| | | |
| | | var memoryPath = Path.Combine(instanceDir, "memory.json"); |
| | | var exportedData = memoryStore.Export(); |
| | | |
| | | // 将字节数组转换为Base64字符串以便JSON序列化 |
| | | var memoryDataModel = new MemoryDataModel |
| | | catch (Exception ex) |
| | | { |
| | | MemoryData = exportedData.ToDictionary( |
| | | kvp => kvp.Key, |
| | | kvp => Convert.ToBase64String(kvp.Value) |
| | | ) |
| | | }; |
| | | |
| | | var json = JsonSerializer.Serialize(memoryDataModel, _jsonOptions); |
| | | await File.WriteAllTextAsync(memoryPath, json); |
| | | Console.WriteLine($"Error saving memory data for instance '{instanceId}': {ex.Message}"); |
| | | throw; |
| | | } |
| | | finally |
| | | { |
| | | _fileLock.Release(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | throw new ArgumentException("实例ID不能为空", nameof(instanceId)); |
| | | |
| | | ValidateInstanceId(instanceId); |
| | | |
| | | if (memoryStore == null) |
| | | throw new ArgumentNullException(nameof(memoryStore)); |
| | | |
| | | 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) |
| | | await _fileLock.WaitAsync(); |
| | | try |
| | | { |
| | | try |
| | | { |
| | | importedData[kvp.Key] = Convert.FromBase64String(kvp.Value); |
| | | } |
| | | catch (FormatException) |
| | | { |
| | | // 跳过无效的Base64数据 |
| | | continue; |
| | | } |
| | | } |
| | | var memoryPath = Path.Combine(GetInstanceDirectory(instanceId), "memory.json"); |
| | | if (!File.Exists(memoryPath)) |
| | | return; // 内存文件不存在,跳过加载 |
| | | |
| | | memoryStore.Import(importedData); |
| | | 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> |
| | | /// 验证实例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) |