mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 19:03:29 +08:00
- 将所有小写的命名空间导入更正为首字母大写格式 - 统一 GFramework 框架的命名空间引用规范 - 修复 core、ecs、godot 等模块的命名空间导入错误 - 标准化文档示例代码中的 using 语句格式 - 确保所有文档中的命名空间引用保持一致性 - 更新 global using 语句以匹配正确的命名空间格式
397 lines
13 KiB
C#
397 lines
13 KiB
C#
using System.Collections.Concurrent;
|
||
using System.IO;
|
||
using System.Text;
|
||
using GFramework.Core.Abstractions.Serializer;
|
||
using GFramework.Core.Abstractions.Storage;
|
||
using GFramework.Godot.Extensions;
|
||
using Godot;
|
||
using Error = Godot.Error;
|
||
using FileAccess = Godot.FileAccess;
|
||
|
||
namespace GFramework.Godot.Storage;
|
||
|
||
/// <summary>
|
||
/// Godot 特化的文件存储实现,支持 res://、user:// 和普通文件路径
|
||
/// 支持按 key 细粒度锁保证线程安全
|
||
/// </summary>
|
||
public sealed class GodotFileStorage : IStorage
|
||
{
|
||
/// <summary>
|
||
/// 每个 key 对应的锁对象
|
||
/// </summary>
|
||
private readonly ConcurrentDictionary<string, object> _keyLocks = new();
|
||
|
||
private readonly ISerializer _serializer;
|
||
|
||
/// <summary>
|
||
/// 初始化 Godot 文件存储
|
||
/// </summary>
|
||
/// <param name="serializer">序列化器实例</param>
|
||
public GodotFileStorage(ISerializer serializer)
|
||
{
|
||
_serializer = serializer ?? throw new ArgumentNullException(nameof(serializer));
|
||
}
|
||
|
||
#region Delete
|
||
|
||
/// <summary>
|
||
/// 删除指定键对应的文件
|
||
/// </summary>
|
||
/// <param name="key">存储键</param>
|
||
public void Delete(string key)
|
||
{
|
||
var path = ToAbsolutePath(key);
|
||
var keyLock = GetLock(path);
|
||
|
||
lock (keyLock)
|
||
{
|
||
// 处理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);
|
||
}
|
||
}
|
||
|
||
// 删除完成后尝试移除锁,防止锁字典无限增长
|
||
_keyLocks.TryRemove(path, out _);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步删除指定键对应的文件
|
||
/// </summary>
|
||
/// <param name="key">存储键</param>
|
||
/// <returns>异步任务</returns>
|
||
public async Task DeleteAsync(string key)
|
||
{
|
||
await Task.Run(() => Delete(key));
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Helpers
|
||
|
||
/// <summary>
|
||
/// 清理路径段中的无效字符,将无效文件名字符替换为下划线
|
||
/// </summary>
|
||
/// <param name="segment">要清理的路径段</param>
|
||
/// <returns>清理后的路径段</returns>
|
||
private static string SanitizeSegment(string segment)
|
||
{
|
||
return Path.GetInvalidFileNameChars().Aggregate(segment, (current, c) => current.Replace(c, '_'));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将存储键转换为绝对路径,处理 Godot 虚拟路径和普通文件系统路径
|
||
/// </summary>
|
||
/// <param name="key">存储键</param>
|
||
/// <returns>绝对路径字符串</returns>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取指定路径对应的锁对象,如果不存在则创建新的锁对象
|
||
/// </summary>
|
||
/// <param name="path">文件路径</param>
|
||
/// <returns>对应路径的锁对象</returns>
|
||
private object GetLock(string path)
|
||
{
|
||
return _keyLocks.GetOrAdd(path, _ => new object());
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Exists
|
||
|
||
/// <summary>
|
||
/// 检查指定键对应的文件是否存在
|
||
/// </summary>
|
||
/// <param name="key">存储键</param>
|
||
/// <returns>文件存在返回 true,否则返回 false</returns>
|
||
public bool Exists(string key)
|
||
{
|
||
var path = ToAbsolutePath(key);
|
||
var keyLock = GetLock(path);
|
||
|
||
lock (keyLock)
|
||
{
|
||
if (!path.IsGodotPath()) return File.Exists(path);
|
||
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
|
||
return file != null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步检查指定键对应的文件是否存在
|
||
/// </summary>
|
||
/// <param name="key">存储键</param>
|
||
/// <returns>表示异步操作的任务,结果为布尔值表示文件是否存在</returns>
|
||
public Task<bool> ExistsAsync(string key)
|
||
{
|
||
return Task.FromResult(Exists(key));
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Read
|
||
|
||
/// <summary>
|
||
/// 读取指定键对应的序列化数据并反序列化为指定类型
|
||
/// </summary>
|
||
/// <typeparam name="T">要反序列化的类型</typeparam>
|
||
/// <param name="key">存储键</param>
|
||
/// <returns>反序列化后的对象实例</returns>
|
||
/// <exception cref="FileNotFoundException">当指定键对应的文件不存在时抛出</exception>
|
||
public T Read<T>(string key)
|
||
{
|
||
var path = ToAbsolutePath(key);
|
||
var keyLock = GetLock(path);
|
||
|
||
lock (keyLock)
|
||
{
|
||
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 = File.ReadAllText(path, Encoding.UTF8);
|
||
}
|
||
|
||
return _serializer.Deserialize<T>(content);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 读取指定键对应的序列化数据,如果文件不存在则返回默认值
|
||
/// </summary>
|
||
/// <typeparam name="T">要反序列化的类型</typeparam>
|
||
/// <param name="key">存储键</param>
|
||
/// <param name="defaultValue">当文件不存在时返回的默认值</param>
|
||
/// <returns>反序列化后的对象实例或默认值</returns>
|
||
public T Read<T>(string key, T defaultValue)
|
||
{
|
||
var path = ToAbsolutePath(key);
|
||
var keyLock = GetLock(path);
|
||
|
||
lock (keyLock)
|
||
{
|
||
if ((path.IsGodotPath() && !FileAccess.FileExists(path)) || (!path.IsGodotPath() && !File.Exists(path)))
|
||
return defaultValue;
|
||
|
||
return Read<T>(key);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步读取指定键对应的序列化数据并反序列化为指定类型
|
||
/// </summary>
|
||
/// <typeparam name="T">要反序列化的类型</typeparam>
|
||
/// <param name="key">存储键</param>
|
||
/// <returns>表示异步操作的任务,结果为反序列化后的对象实例</returns>
|
||
public async Task<T> ReadAsync<T>(string key)
|
||
{
|
||
var path = ToAbsolutePath(key);
|
||
var keyLock = GetLock(path);
|
||
|
||
return await Task.Run(() =>
|
||
{
|
||
lock (keyLock)
|
||
{
|
||
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 = File.ReadAllText(path, Encoding.UTF8);
|
||
}
|
||
|
||
return _serializer.Deserialize<T>(content);
|
||
}
|
||
});
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Directory Operations
|
||
|
||
/// <summary>
|
||
/// 列举指定路径下的所有子目录名称
|
||
/// </summary>
|
||
/// <param name="path">要列举的路径,空字符串表示根目录</param>
|
||
/// <returns>子目录名称列表</returns>
|
||
public async Task<IReadOnlyList<string>> 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<string>();
|
||
|
||
dir.ListDirBegin();
|
||
var result = new List<string>();
|
||
|
||
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<string>)result;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 列举指定路径下的所有文件名称
|
||
/// </summary>
|
||
/// <param name="path">要列举的路径,空字符串表示根目录</param>
|
||
/// <returns>文件名称列表</returns>
|
||
public async Task<IReadOnlyList<string>> 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<string>();
|
||
|
||
dir.ListDirBegin();
|
||
var result = new List<string>();
|
||
|
||
while (true)
|
||
{
|
||
var name = dir.GetNext();
|
||
if (string.IsNullOrEmpty(name)) break;
|
||
if (!dir.CurrentIsDir())
|
||
result.Add(name);
|
||
}
|
||
|
||
dir.ListDirEnd();
|
||
return (IReadOnlyList<string>)result;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查指定路径的目录是否存在
|
||
/// </summary>
|
||
/// <param name="path">要检查的目录路径</param>
|
||
/// <returns>如果目录存在则返回true,否则返回false</returns>
|
||
public Task<bool> DirectoryExistsAsync(string path)
|
||
{
|
||
var fullPath = ToAbsolutePath(path);
|
||
return Task.FromResult(DirAccess.DirExistsAbsolute(fullPath));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建目录(递归创建父目录)
|
||
/// </summary>
|
||
/// <param name="path">要创建的目录路径</param>
|
||
/// <returns>表示异步操作的Task</returns>
|
||
public async Task CreateDirectoryAsync(string path)
|
||
{
|
||
await Task.Run(() =>
|
||
{
|
||
var fullPath = ToAbsolutePath(path);
|
||
if (!DirAccess.DirExistsAbsolute(fullPath))
|
||
DirAccess.MakeDirRecursiveAbsolute(fullPath);
|
||
});
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Write
|
||
|
||
/// <summary>
|
||
/// 将指定对象序列化并写入到指定键对应的文件中
|
||
/// </summary>
|
||
/// <typeparam name="T">要序列化的对象类型</typeparam>
|
||
/// <param name="key">存储键</param>
|
||
/// <param name="value">要写入的对象实例</param>
|
||
public void Write<T>(string key, T value)
|
||
{
|
||
var path = ToAbsolutePath(key);
|
||
var keyLock = GetLock(path);
|
||
|
||
lock (keyLock)
|
||
{
|
||
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)!);
|
||
File.WriteAllText(path, content, Encoding.UTF8);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步将指定对象序列化并写入到指定键对应的文件中
|
||
/// </summary>
|
||
/// <typeparam name="T">要序列化的对象类型</typeparam>
|
||
/// <param name="key">存储键</param>
|
||
/// <param name="value">要写入的对象实例</param>
|
||
/// <returns>表示异步操作的任务</returns>
|
||
public async Task WriteAsync<T>(string key, T value)
|
||
{
|
||
await Task.Run(() => Write(key, value));
|
||
}
|
||
|
||
#endregion
|
||
} |