using System.IO; using System.Text; using GFramework.Core.Abstractions.Concurrency; using GFramework.Core.Abstractions.Serializer; using GFramework.Core.Concurrency; using GFramework.Game.Abstractions.Storage; namespace GFramework.Game.Storage; /// /// 基于文件系统的存储实现,实现了IFileStorage接口,支持按key细粒度锁保证线程安全 /// 使用异步安全的锁机制、原子写入和自动清理 /// public sealed class FileStorage : IFileStorage, IDisposable { private readonly int _bufferSize; private readonly string _extension; private readonly IAsyncKeyLockManager _lockManager; private readonly string _rootPath; private readonly ISerializer _serializer; private bool _disposed; /// /// 初始化FileStorage实例 /// /// 存储根目录路径 /// 序列化器实例 /// 存储文件的扩展名 /// IO 缓冲区大小,默认 8KB /// 可选的锁管理器,用于依赖注入 public FileStorage(string rootPath, ISerializer serializer, string extension = ".dat", int bufferSize = 8192, IAsyncKeyLockManager? lockManager = null) { _rootPath = rootPath; _serializer = serializer; _extension = extension; _bufferSize = bufferSize; _lockManager = lockManager ?? new AsyncKeyLockManager(); Directory.CreateDirectory(_rootPath); } /// /// 释放资源 /// public void Dispose() { if (_disposed) return; _disposed = true; _lockManager.Dispose(); } /// /// 清理文件段字符串,将其中的无效文件名字符替换为下划线 /// /// 需要清理的文件段字符串 /// 清理后的字符串,其中所有无效文件名字符都被替换为下划线 private static string SanitizeSegment(string segment) { return Path.GetInvalidFileNameChars().Aggregate(segment, (current, c) => current.Replace(c, '_')); } #region Helpers /// /// 将存储键转换为文件路径 /// /// 存储键 /// 对应的文件路径 private string ToPath(string key) { if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException("Storage key cannot be empty", nameof(key)); // 统一分隔符 key = key.Replace('\\', '/'); // 防止路径逃逸 if (key.Contains("..")) throw new ArgumentException("Storage key cannot contain '..'", nameof(key)); var segments = key .Split('/', StringSplitOptions.RemoveEmptyEntries) .Select(SanitizeSegment) .ToArray(); if (segments.Length == 0) throw new ArgumentException("Invalid storage key", nameof(key)); // 目录部分 var dirSegments = segments[..^1]; var fileName = segments[^1] + _extension; var dirPath = dirSegments.Length == 0 ? _rootPath : Path.Combine(_rootPath, Path.Combine(dirSegments)); Directory.CreateDirectory(dirPath); return Path.Combine(dirPath, fileName); } #endregion #region Delete /// /// 删除指定键的存储项 /// /// 存储键,用于标识要删除的存储项 public void Delete(string key) { DeleteAsync(key).GetAwaiter().GetResult(); } /// /// 异步删除指定键的存储项 /// /// 存储键,用于标识要删除的存储项 /// 表示异步操作的任务 public async Task DeleteAsync(string key) { ObjectDisposedException.ThrowIf(_disposed, this); var path = ToPath(key); await using (await _lockManager.AcquireLockAsync(path)) { if (File.Exists(path)) File.Delete(path); } } #endregion #region Exists /// /// 检查指定键的存储项是否存在 /// /// 存储键 /// 如果存储项存在则返回true,否则返回false public bool Exists(string key) { return ExistsAsync(key).GetAwaiter().GetResult(); } /// /// 异步检查指定键的存储项是否存在 /// /// 存储键 /// 如果存储项存在则返回true,否则返回false public async Task ExistsAsync(string key) { ObjectDisposedException.ThrowIf(_disposed, this); var path = ToPath(key); await using (await _lockManager.AcquireLockAsync(path)) { return File.Exists(path); } } #endregion #region Read /// /// 读取指定键的存储项 /// /// 要反序列化的类型 /// 存储键 /// 反序列化后的对象 /// 当存储键不存在时抛出 public T Read(string key) { return ReadAsync(key).GetAwaiter().GetResult(); } /// /// 读取指定键的存储项,如果不存在则返回默认值 /// /// 要反序列化的类型 /// 存储键 /// 当存储键不存在时返回的默认值 /// 反序列化后的对象或默认值 public T Read(string key, T defaultValue) { ObjectDisposedException.ThrowIf(_disposed, this); try { return Read(key); } catch (FileNotFoundException) { return defaultValue; } } /// /// 异步读取指定键的存储项 /// /// 要反序列化的类型 /// 存储键 /// 反序列化后的对象 /// 当存储键不存在时抛出 public async Task ReadAsync(string key) { ObjectDisposedException.ThrowIf(_disposed, this); var path = ToPath(key); await using (await _lockManager.AcquireLockAsync(path)) { if (!File.Exists(path)) throw new FileNotFoundException($"Storage key not found: {key}", path); await using var fs = new FileStream( path, FileMode.Open, FileAccess.Read, FileShare.Read, _bufferSize, useAsync: true); using var sr = new StreamReader(fs, Encoding.UTF8); var content = await sr.ReadToEndAsync(); return _serializer.Deserialize(content); } } #endregion #region Directory Operations /// /// 列举指定路径下的所有子目录名称 /// /// 要列举的路径,空字符串表示根目录 /// 子目录名称列表 public Task> ListDirectoriesAsync(string path = "") { var fullPath = string.IsNullOrEmpty(path) ? _rootPath : Path.Combine(_rootPath, path); if (!Directory.Exists(fullPath)) return Task.FromResult>([]); var dirs = Directory.GetDirectories(fullPath) .Select(Path.GetFileName) .OfType() .Where(name => !string.IsNullOrEmpty(name) && !name.StartsWith('.')) .ToList(); return Task.FromResult>(dirs); } /// /// 列举指定路径下的所有文件名称 /// /// 要列举的路径,空字符串表示根目录 /// 文件名称列表 public Task> ListFilesAsync(string path = "") { var fullPath = string.IsNullOrEmpty(path) ? _rootPath : Path.Combine(_rootPath, path); if (!Directory.Exists(fullPath)) return Task.FromResult>(Array.Empty()); var files = Directory.GetFiles(fullPath) .Select(Path.GetFileName) .OfType() .Where(name => !string.IsNullOrEmpty(name)) .ToList(); return Task.FromResult>(files); } /// /// 检查指定路径的目录是否存在 /// /// 要检查的目录路径 /// 如果目录存在则返回true,否则返回false public Task DirectoryExistsAsync(string path) { var fullPath = string.IsNullOrEmpty(path) ? _rootPath : Path.Combine(_rootPath, path); return Task.FromResult(Directory.Exists(fullPath)); } /// /// 创建目录(递归创建父目录) /// /// 要创建的目录路径 /// 表示异步操作的Task public Task CreateDirectoryAsync(string path) { var fullPath = string.IsNullOrEmpty(path) ? _rootPath : Path.Combine(_rootPath, path); Directory.CreateDirectory(fullPath); return Task.CompletedTask; } #endregion #region Write /// /// 写入指定键的存储项 /// /// 要序列化的对象类型 /// 存储键 /// 要存储的对象 public void Write(string key, T value) { WriteAsync(key, value).GetAwaiter().GetResult(); } /// /// 异步写入指定键的存储项,使用原子写入防止文件损坏 /// /// 要序列化的对象类型 /// 存储键 /// 要存储的对象 /// 表示异步操作的任务 public async Task WriteAsync(string key, T value) { ObjectDisposedException.ThrowIf(_disposed, this); var path = ToPath(key); var tempPath = path + ".tmp"; await using (await _lockManager.AcquireLockAsync(path)) { try { var content = _serializer.Serialize(value); // 先写入临时文件 await using (var fs = new FileStream( tempPath, FileMode.Create, FileAccess.Write, FileShare.None, _bufferSize, useAsync: true)) { await using var sw = new StreamWriter(fs, Encoding.UTF8); await sw.WriteAsync(content); await sw.FlushAsync(); } // 原子性替换目标文件 File.Move(tempPath, path, overwrite: true); } catch { // 清理临时文件 if (File.Exists(tempPath)) File.Delete(tempPath); throw; } } } #endregion }