using GFramework.Core.Abstractions.Concurrency;
using GFramework.Core.Abstractions.Serializer;
using GFramework.Core.Abstractions.Storage;
namespace GFramework.Godot.Storage;
///
/// Godot 特化的文件存储实现,支持 res://、user:// 和普通文件路径
/// 支持按 key 细粒度锁保证线程安全,使用异步安全的锁机制
///
public sealed class GodotFileStorage : IStorage, IDisposable
{
private readonly IAsyncKeyLockManager _lockManager;
private readonly bool _ownsLockManager;
private readonly ISerializer _serializer;
private bool _disposed;
///
/// 初始化 Godot 文件存储
///
/// 序列化器实例
/// 可选的锁管理器,用于依赖注入
public GodotFileStorage(ISerializer serializer, IAsyncKeyLockManager? lockManager = null)
{
_serializer = serializer ?? throw new ArgumentNullException(nameof(serializer));
if (lockManager == null)
{
_lockManager = new AsyncKeyLockManager();
_ownsLockManager = true;
}
else
{
_lockManager = lockManager;
_ownsLockManager = false;
}
}
///
/// 释放资源
///
public void Dispose()
{
if (_disposed) return;
_disposed = true;
// 只释放内部创建的锁管理器
if (_ownsLockManager)
{
_lockManager.Dispose();
}
}
#region Delete
///
/// 删除指定键对应的文件
///
/// 存储键
///
/// 此方法通过同步等待异步操作完成,可能在具有同步上下文的环境(例如 UI 线程、经典 ASP.NET)中导致死锁。
/// 仅在无法使用异步 API 时使用。如果可能,请优先使用 。
///
public void Delete(string key)
{
DeleteAsync(key).ConfigureAwait(false).GetAwaiter().GetResult();
}
///
/// 异步删除指定键对应的文件
///
/// 存储键
/// 异步任务
public async Task DeleteAsync(string key)
{
ObjectDisposedException.ThrowIf(_disposed, this);
var path = ToAbsolutePath(key);
await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false))
{
// 处理Godot文件系统路径的删除操作
if (path.IsGodotPath())
{
if (FileAccess.FileExists(path))
{
var err = DirAccess.RemoveAbsolute(path);
if (err != Error.Ok)
throw new IOException($"Failed to delete Godot file: {path}, error: {err}");
}
}
// 处理标准文件系统路径的删除操作
else
{
if (File.Exists(path)) File.Delete(path);
}
}
}
#endregion
#region Helpers
///
/// 清理路径段中的无效字符,将无效文件名字符替换为下划线
///
/// 要清理的路径段
/// 清理后的路径段
private static string SanitizeSegment(string segment)
{
return Path.GetInvalidFileNameChars().Aggregate(segment, (current, c) => current.Replace(c, '_'));
}
///
/// 将存储键转换为绝对路径,处理 Godot 虚拟路径和普通文件系统路径
///
/// 存储键
/// 绝对路径字符串
private static string ToAbsolutePath(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));
// Godot 虚拟路径直接使用 FileAccess 支持
if (key.IsGodotPath())
return key;
// 普通文件系统路径
var segments = key.Split('/', StringSplitOptions.RemoveEmptyEntries)
.Select(SanitizeSegment)
.ToArray();
if (segments.Length == 0)
throw new ArgumentException("Invalid storage key", nameof(key));
var dir = Path.Combine(segments[..^1]);
var fileName = segments[^1];
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
return Path.Combine(dir, fileName);
}
#endregion
#region Exists
///
/// 检查指定键对应的文件是否存在
///
/// 存储键
/// 文件存在返回 true,否则返回 false
///
/// 此方法通过同步等待异步操作完成,可能在具有同步上下文的环境(例如 UI 线程、经典 ASP.NET)中导致死锁。
/// 仅在无法使用异步 API 时使用。如果可能,请优先使用 。
///
public bool Exists(string key)
{
return ExistsAsync(key).ConfigureAwait(false).GetAwaiter().GetResult();
}
///
/// 异步检查指定键对应的文件是否存在
///
/// 存储键
/// 表示异步操作的任务,结果为布尔值表示文件是否存在
public async Task ExistsAsync(string key)
{
ObjectDisposedException.ThrowIf(_disposed, this);
var path = ToAbsolutePath(key);
await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false))
{
if (!path.IsGodotPath()) return File.Exists(path);
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
return file != null;
}
}
#endregion
#region Read
///
/// 读取指定键对应的序列化数据并反序列化为指定类型
///
/// 要反序列化的类型
/// 存储键
/// 反序列化后的对象实例
/// 当指定键对应的文件不存在时抛出
///
/// 此方法通过同步等待异步操作完成,可能在具有同步上下文的环境(例如 UI 线程、经典 ASP.NET)中导致死锁。
/// 仅在无法使用异步 API 时使用。如果可能,请优先使用 。
///
public T Read(string key)
{
return ReadAsync(key).ConfigureAwait(false).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 = ToAbsolutePath(key);
await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false))
{
string content;
if (path.IsGodotPath())
{
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
if (file == null) throw new FileNotFoundException($"Storage key not found: {key}", path);
content = file.GetAsText();
}
else
{
if (!File.Exists(path))
throw new FileNotFoundException($"Storage key not found: {key}", path);
content = await File.ReadAllTextAsync(path, Encoding.UTF8).ConfigureAwait(false);
}
return _serializer.Deserialize(content);
}
}
#endregion
#region Directory Operations
///
/// 列举指定路径下的所有子目录名称
///
/// 要列举的路径,空字符串表示根目录
/// 子目录名称列表
public async Task> ListDirectoriesAsync(string path = "")
{
return await Task.Run(() =>
{
var fullPath = string.IsNullOrEmpty(path) ? "user://" : ToAbsolutePath(path);
var dir = DirAccess.Open(fullPath);
if (dir == null) return Array.Empty();
dir.ListDirBegin();
var result = new List();
while (true)
{
var name = dir.GetNext();
if (string.IsNullOrEmpty(name)) break;
if (dir.CurrentIsDir() && !name.StartsWith(".", StringComparison.Ordinal))
result.Add(name);
}
dir.ListDirEnd();
return (IReadOnlyList)result;
});
}
///
/// 列举指定路径下的所有文件名称
///
/// 要列举的路径,空字符串表示根目录
/// 文件名称列表
public async Task> ListFilesAsync(string path = "")
{
return await Task.Run(() =>
{
var fullPath = string.IsNullOrEmpty(path) ? "user://" : ToAbsolutePath(path);
var dir = DirAccess.Open(fullPath);
if (dir == null) return Array.Empty();
dir.ListDirBegin();
var result = new List();
while (true)
{
var name = dir.GetNext();
if (string.IsNullOrEmpty(name)) break;
if (!dir.CurrentIsDir())
result.Add(name);
}
dir.ListDirEnd();
return (IReadOnlyList)result;
});
}
///
/// 检查指定路径的目录是否存在
///
/// 要检查的目录路径
/// 如果目录存在则返回true,否则返回false
public Task DirectoryExistsAsync(string path)
{
var fullPath = ToAbsolutePath(path);
return Task.FromResult(DirAccess.DirExistsAbsolute(fullPath));
}
///
/// 创建目录(递归创建父目录)
///
/// 要创建的目录路径
/// 表示异步操作的Task
public async Task CreateDirectoryAsync(string path)
{
await Task.Run(() =>
{
var fullPath = ToAbsolutePath(path);
if (!DirAccess.DirExistsAbsolute(fullPath))
DirAccess.MakeDirRecursiveAbsolute(fullPath);
});
}
#endregion
#region Write
///
/// 将指定对象序列化并写入到指定键对应的文件中
///
/// 要序列化的对象类型
/// 存储键
/// 要写入的对象实例
///
/// 此方法通过同步等待异步操作完成,可能在具有同步上下文的环境(例如 UI 线程、经典 ASP.NET)中导致死锁。
/// 仅在无法使用异步 API 时使用。如果可能,请优先使用 。
///
public void Write(string key, T value)
{
WriteAsync(key, value).ConfigureAwait(false).GetAwaiter().GetResult();
}
///
/// 异步将指定对象序列化并写入到指定键对应的文件中
///
/// 要序列化的对象类型
/// 存储键
/// 要写入的对象实例
/// 表示异步操作的任务
public async Task WriteAsync(string key, T value)
{
ObjectDisposedException.ThrowIf(_disposed, this);
var path = ToAbsolutePath(key);
await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false))
{
var content = _serializer.Serialize(value);
if (path.IsGodotPath())
{
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write);
if (file == null) throw new IOException($"Cannot write file: {path}");
file.StoreString(content);
}
else
{
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
await File.WriteAllTextAsync(path, content, Encoding.UTF8).ConfigureAwait(false);
}
}
}
#endregion
}