diff --git a/GFramework.Game/storage/FileStorage.cs b/GFramework.Game/storage/FileStorage.cs index e5c1d63..cb92eeb 100644 --- a/GFramework.Game/storage/FileStorage.cs +++ b/GFramework.Game/storage/FileStorage.cs @@ -43,6 +43,17 @@ public sealed class FileStorage : IStorage #endregion + /// + /// 清理文件段字符串,将其中的无效文件名字符替换为下划线 + /// + /// 需要清理的文件段字符串 + /// 清理后的字符串,其中所有无效文件名字符都被替换为下划线 + private static string SanitizeSegment(string segment) + { + return Path.GetInvalidFileNameChars().Aggregate(segment, (current, c) => current.Replace(c, '_')); + } + + #region Helpers /// @@ -52,9 +63,35 @@ public sealed class FileStorage : IStorage /// 对应的文件路径 private string ToPath(string key) { - // 防止非法路径 - key = Path.GetInvalidFileNameChars().Aggregate(key, (current, c) => current.Replace(c, '_')); - return Path.Combine(_rootPath, $"{key}{_extension}"); + 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 diff --git a/GFramework.Game/storage/ReadMe.md b/GFramework.Game/storage/ReadMe.md new file mode 100644 index 0000000..68ffb2f --- /dev/null +++ b/GFramework.Game/storage/ReadMe.md @@ -0,0 +1,130 @@ +# GFramework 存储模块使用指南 + +本模块提供了基于文件系统的存储功能,包括两个主要类:[FileStorage](./FileStorage.cs) +和 [ScopedStorage](./ScopedStorage.cs)。 + +## FileStorage + +[FileStorage](./FileStorage.cs) 是一个基于文件系统的存储实现,它将数据以序列化形式保存到磁盘上的指定目录。 + +### 特性 + +- 将数据保存为文件(默认扩展名为 `.dat`) +- 支持同步和异步操作 +- 自动创建存储目录 +- 防止路径遍历攻击(过滤非法文件名字符) + +### 构造函数 + +```csharp +FileStorage(string rootPath, ISerializer serializer, string extension = ".dat") +``` + +### 使用示例 + +```csharp +// 创建 JSON 序列化器 +var serializer = new JsonSerializer(); + +// 创建文件存储实例 +var storage = new FileStorage(@"C:\MyGame\Data", serializer); + +// 写入数据 +storage.Write("player_score", 1000); +storage.Write("player_settings", new { Volume = 0.8f, Difficulty = "Hard" }); + +// 读取数据 +int score = storage.Read("player_score"); +var settings = storage.Read("player_settings", new { Volume = 0.5f, Difficulty = "Normal" }); + +// 异步读取数据 +Task scoreTask = storage.ReadAsync("player_score"); + +// 检查数据是否存在 +if (storage.Exists("player_score")) +{ + // 数据存在 +} + +// 异步检查数据是否存在 +Task existsTask = storage.ExistsAsync("player_score"); + +// 删除数据 +storage.Delete("player_score"); + +// 异步写入数据 +storage.WriteAsync("player_score", 1200); +``` + +## ScopedStorage + +[ScopedStorage](./ScopedStorage.cs) 是一个装饰器模式的实现,它为所有存储键添加前缀,从而实现逻辑分组和命名空间隔离。 + +### 特性 + +- 为所有键添加指定前缀 +- 透明地包装底层存储实现 +- 支持嵌套作用域 +- 与底层存储共享物理存储 +- 支持同步和异步操作 + +### 构造函数 + +```csharp +ScopedStorage(IStorage inner, string prefix) +``` + +### 使用示例 + +```csharp +// 基于文件存储创建带作用域的存储 +var scopedStorage = new ScopedStorage(fileStorage, "game_settings"); + +// 所有操作都会添加前缀 "game_settings/" +scopedStorage.Write("volume", 0.8f); // 实际存储为 "game_settings/volume.dat" +scopedStorage.Write("theme", "dark"); // 实际存储为 "game_settings/theme.dat" + +// 读取操作同样适用前缀 +float volume = scopedStorage.Read("volume"); // 从 "game_settings/volume.dat" 读取 + +// 创建嵌套作用域 +var nestedStorage = scopedStorage.Scope("graphics"); // 前缀变为 "game_settings/graphics/" +nestedStorage.Write("resolution", "1920x1080"); // 实际存储为 "game_settings/graphics/resolution.dat" + +// 异步操作 +await scopedStorage.WriteAsync("audio", new { MasterVolume = 0.9f }); +var audioSettings = await scopedStorage.ReadAsync("audio"); +``` + +## 组合使用示例 + +```csharp +// 创建基础序列化器和文件存储 +var serializer = new JsonSerializer(); +var baseStorage = new FileStorage(@"C:\MyGame\Data", serializer); + +// 创建不同作用域的存储 +var playerStorage = new ScopedStorage(baseStorage, "player"); +var gameStorage = new ScopedStorage(baseStorage, "game"); +var settingsStorage = new ScopedStorage(baseStorage, "settings"); + +// 在不同的作用域中使用相同键而不会冲突 +playerStorage.Write("level", 5); +gameStorage.Write("level", "forest_area_1"); +settingsStorage.Write("level", "high"); + +// 结果是三个不同的文件: +// - player/level.dat +// - game/level.dat +// - settings/level.dat +``` + +## 注意事项 + +1. **序列化器选择**:确保使用的 [ISerializer](../GFramework.Game.Abstractions/serializer/ISerializer.cs) + 实现能够正确处理你要存储的数据类型。 +2. **错误处理**:[FileStorage](./FileStorage.cs) 的 `Read(string key)` 方法会在键不存在时抛出异常,可以使用 + `Read(string key, T defaultValue)` 来避免异常。 +3. **线程安全**:当前实现不是线程安全的,如需在多线程环境中使用,请添加适当的同步机制。 +4. **文件权限**:确保应用程序对指定的存储目录有读写权限。 +5. **路径安全**:[FileStorage](./FileStorage.cs) 会自动防止路径遍历攻击,因此键不能包含 `..`,并且特殊字符会被替换为下划线。 \ No newline at end of file