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
}