From 2d4527d06647b459ce33017055fc5617d9a75dd2 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:19:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(storage):=20=E6=B7=BB=E5=8A=A0=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E6=93=8D=E4=BD=9C=E5=8A=9F=E8=83=BD=E5=92=8C=E5=AD=98?= =?UTF-8?q?=E6=A1=A3=E4=BB=93=E5=BA=93=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在IStorage接口中添加目录操作相关方法:ListDirectoriesAsync、 ListFilesAsync、DirectoryExistsAsync、CreateDirectoryAsync - 为FileStorage和GodotFileStorage实现目录操作功能 - 添加ScopedStorage的目录操作委托实现 - 新增ISaveRepository接口定义基于槽位的存档系统 - 实现SaveRepository类提供完整的存档管理功能 - 添加SaveConfiguration类用于存档系统配置 --- .../storage/IStorage.cs | 28 ++ .../data/ISaveRepository.cs | 57 ++++ GFramework.Game/data/SaveConfiguration.cs | 35 +++ GFramework.Game/data/SaveRepository.cs | 145 +++++++++ .../data/SaveRepository使用指南.md | 292 ++++++++++++++++++ GFramework.Game/storage/FileStorage.cs | 65 ++++ GFramework.Game/storage/ScopedStorage.cs | 40 +++ GFramework.Godot/storage/GodotFileStorage.cs | 88 ++++++ 8 files changed, 750 insertions(+) create mode 100644 GFramework.Game.Abstractions/data/ISaveRepository.cs create mode 100644 GFramework.Game/data/SaveConfiguration.cs create mode 100644 GFramework.Game/data/SaveRepository.cs create mode 100644 GFramework.Game/data/SaveRepository使用指南.md diff --git a/GFramework.Core.Abstractions/storage/IStorage.cs b/GFramework.Core.Abstractions/storage/IStorage.cs index 1d833e6..cc8ad40 100644 --- a/GFramework.Core.Abstractions/storage/IStorage.cs +++ b/GFramework.Core.Abstractions/storage/IStorage.cs @@ -75,4 +75,32 @@ public interface IStorage : IUtility /// 要删除的键 /// 表示异步操作的Task Task DeleteAsync(string key); + + /// + /// 列举指定路径下的所有子目录名称 + /// + /// 要列举的路径,空字符串表示根目录 + /// 子目录名称列表 + Task> ListDirectoriesAsync(string path = ""); + + /// + /// 列举指定路径下的所有文件名称 + /// + /// 要列举的路径,空字符串表示根目录 + /// 文件名称列表 + Task> ListFilesAsync(string path = ""); + + /// + /// 检查指定路径的目录是否存在 + /// + /// 要检查的目录路径 + /// 如果目录存在则返回true,否则返回false + Task DirectoryExistsAsync(string path); + + /// + /// 创建目录(递归创建父目录) + /// + /// 要创建的目录路径 + /// 表示异步操作的Task + Task CreateDirectoryAsync(string path); } \ No newline at end of file diff --git a/GFramework.Game.Abstractions/data/ISaveRepository.cs b/GFramework.Game.Abstractions/data/ISaveRepository.cs new file mode 100644 index 0000000..bcca839 --- /dev/null +++ b/GFramework.Game.Abstractions/data/ISaveRepository.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2026 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using GFramework.Core.Abstractions.utility; + +namespace GFramework.Game.Abstractions.data; + +/// +/// 存档仓库接口,管理基于槽位的存档系统 +/// +/// 存档数据类型,必须实现IData接口并具有无参构造函数 +public interface ISaveRepository : IUtility + where TSaveData : class, IData, new() +{ + /// + /// 检查指定槽位是否存在存档 + /// + /// 存档槽位编号 + /// 如果存档存在返回true,否则返回false + Task ExistsAsync(int slot); + + /// + /// 加载指定槽位的存档 + /// + /// 存档槽位编号 + /// 存档数据对象,如果不存在则返回新实例 + Task LoadAsync(int slot); + + /// + /// 保存存档到指定槽位 + /// + /// 存档槽位编号 + /// 要保存的存档数据 + Task SaveAsync(int slot, TSaveData data); + + /// + /// 删除指定槽位的存档 + /// + /// 存档槽位编号 + Task DeleteAsync(int slot); + + /// + /// 列出所有有效的存档槽位 + /// + /// 包含所有有效存档槽位编号的只读列表,按升序排列 + Task> ListSlotsAsync(); +} diff --git a/GFramework.Game/data/SaveConfiguration.cs b/GFramework.Game/data/SaveConfiguration.cs new file mode 100644 index 0000000..b7bdd3b --- /dev/null +++ b/GFramework.Game/data/SaveConfiguration.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2026 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace GFramework.Game.data; + +/// +/// 存档系统配置 +/// +public sealed class SaveConfiguration +{ + /// + /// 存档根目录 (如 "user://saves") + /// + public string SaveRoot { get; init; } = "user://saves"; + + /// + /// 存档槽位前缀 (如 "slot_") + /// + public string SaveSlotPrefix { get; init; } = "slot_"; + + /// + /// 存档文件名 (如 "save.json") + /// + public string SaveFileName { get; init; } = "save.json"; +} diff --git a/GFramework.Game/data/SaveRepository.cs b/GFramework.Game/data/SaveRepository.cs new file mode 100644 index 0000000..cc30082 --- /dev/null +++ b/GFramework.Game/data/SaveRepository.cs @@ -0,0 +1,145 @@ +// Copyright (c) 2026 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Globalization; +using GFramework.Core.Abstractions.storage; +using GFramework.Core.utility; +using GFramework.Game.Abstractions.data; +using GFramework.Game.storage; + +namespace GFramework.Game.data; + +/// +/// 基于槽位的存档仓库实现 +/// +/// 存档数据类型 +public class SaveRepository : AbstractContextUtility, ISaveRepository + where TSaveData : class, IData, new() +{ + private readonly SaveConfiguration _config; + private readonly IStorage _rootStorage; + + /// + /// 初始化存档仓库 + /// + /// 存储实例 + /// 存档配置 + public SaveRepository(IStorage storage, SaveConfiguration config) + { + ArgumentNullException.ThrowIfNull(storage); + ArgumentNullException.ThrowIfNull(config); + + _config = config; + _rootStorage = new ScopedStorage(storage, config.SaveRoot); + } + + /// + /// 检查指定槽位是否存在存档 + /// + /// 存档槽位编号 + /// 如果存档存在返回true,否则返回false + public async Task ExistsAsync(int slot) + { + var storage = GetSlotStorage(slot); + return await storage.ExistsAsync(_config.SaveFileName); + } + + /// + /// 加载指定槽位的存档 + /// + /// 存档槽位编号 + /// 存档数据对象,如果不存在则返回新实例 + public async Task LoadAsync(int slot) + { + var storage = GetSlotStorage(slot); + + if (await storage.ExistsAsync(_config.SaveFileName)) + return await storage.ReadAsync(_config.SaveFileName); + + return new TSaveData(); + } + + /// + /// 保存存档到指定槽位 + /// + /// 存档槽位编号 + /// 要保存的存档数据 + public async Task SaveAsync(int slot, TSaveData data) + { + var slotPath = $"{_config.SaveSlotPrefix}{slot}"; + + // 确保槽位目录存在 + if (!await _rootStorage.DirectoryExistsAsync(slotPath)) + await _rootStorage.CreateDirectoryAsync(slotPath); + + var storage = GetSlotStorage(slot); + await storage.WriteAsync(_config.SaveFileName, data); + } + + /// + /// 删除指定槽位的存档 + /// + /// 存档槽位编号 + public async Task DeleteAsync(int slot) + { + var storage = GetSlotStorage(slot); + await storage.DeleteAsync(_config.SaveFileName); + } + + /// + /// 列出所有有效的存档槽位 + /// + /// 包含所有有效存档槽位编号的只读列表,按升序排列 + public async Task> ListSlotsAsync() + { + // 列举所有槽位目录 + var directories = await _rootStorage.ListDirectoriesAsync(); + + var slots = new List(); + + foreach (var dirName in directories) + { + // 检查目录名是否符合槽位前缀 + if (!dirName.StartsWith(_config.SaveSlotPrefix, StringComparison.Ordinal)) + continue; + + // 提取槽位编号 + var slotStr = dirName[_config.SaveSlotPrefix.Length..]; + if (!int.TryParse(slotStr, CultureInfo.InvariantCulture, out var slot)) + continue; + + // 检查槽位中是否存在存档文件 + if (await ExistsAsync(slot)) + slots.Add(slot); + } + + return slots.OrderBy(x => x).ToList(); + } + + /// + /// 获取指定槽位的存储对象 + /// + /// 存档槽位编号 + /// 对应槽位的存储对象 + private IStorage GetSlotStorage(int slot) + { + return new ScopedStorage(_rootStorage, $"{_config.SaveSlotPrefix}{slot}"); + } + + /// + /// 初始化逻辑 + /// + protected override void OnInit() + { + } +} \ No newline at end of file diff --git a/GFramework.Game/data/SaveRepository使用指南.md b/GFramework.Game/data/SaveRepository使用指南.md new file mode 100644 index 0000000..ac75fa6 --- /dev/null +++ b/GFramework.Game/data/SaveRepository使用指南.md @@ -0,0 +1,292 @@ +# SaveRepository 使用指南 + +## 概述 + +`SaveRepository` 是 GFramework 提供的通用存档仓库实现,基于 `IStorage` 抽象层,支持基于槽位的存档管理。 + +## 核心特性 + +- ✅ **完全解耦引擎依赖** - 框架层无 Godot 代码 +- ✅ **配置对象带默认值** - 无需 ProjectSettings 也能工作 +- ✅ **泛型设计** - 支持任意实现 `IData` 的存档类型 +- ✅ **自动槽位管理** - 利用 `ScopedStorage` 实现路径隔离 +- ✅ **异步操作** - 所有方法均为异步,避免阻塞主线程 + +## 快速开始 + +### 1. 定义存档数据类型 + +```csharp +using GFramework.Game.Abstractions.data; + +public class GameSaveData : IData +{ + public string PlayerName { get; set; } = ""; + public int Level { get; set; } = 1; + public float PlayTime { get; set; } = 0f; + public DateTime SaveTime { get; set; } = DateTime.Now; +} +``` + +### 2. 在启动类中注册 + +```csharp +// 在 GameApp.cs 或类似的启动类中 +protected override void OnRegisterUtility() +{ + // 1. 注册序列化器 + var serializer = new JsonSerializer(); + this.RegisterUtility(serializer); + + // 2. 注册存储 + this.RegisterUtility(new GodotFileStorage(serializer)); + + // 3. 创建存档配置 + var saveConfig = new SaveConfiguration + { + SaveRoot = "user://saves", // 存档根目录 + SaveSlotPrefix = "slot_", // 槽位前缀 + SaveFileName = "save.json" // 存档文件名 + }; + + // 4. 注册存档仓库 + this.RegisterUtility>( + new SaveRepository( + this.GetUtility()!, + saveConfig + ) + ); +} +``` + +### 3. 使用存档仓库 + +```csharp +// 获取存档仓库 +var saveRepo = this.GetUtility>(); + +// 保存存档到槽位 1 +var saveData = new GameSaveData +{ + PlayerName = "玩家1", + Level = 10, + PlayTime = 3600f +}; +await saveRepo.SaveAsync(slot: 1, saveData); + +// 加载槽位 1 的存档 +var loadedData = await saveRepo.LoadAsync(slot: 1); +GD.Print($"玩家: {loadedData.PlayerName}, 等级: {loadedData.Level}"); + +// 检查槽位是否存在 +if (await saveRepo.ExistsAsync(slot: 1)) +{ + GD.Print("槽位 1 存在存档"); +} + +// 列出所有有效槽位 +var slots = await saveRepo.ListSlotsAsync(); +GD.Print($"找到 {slots.Count} 个存档槽位"); + +// 删除槽位 1 的存档 +await saveRepo.DeleteAsync(slot: 1); +``` + +## 高级用法 + +### 从 ProjectSettings 读取配置 + +```csharp +var saveConfig = new SaveConfiguration +{ + SaveRoot = ProjectSettings.HasSetting("application/config/save/save_path") + ? ProjectSettings.GetSetting("application/config/save/save_path").AsString() + : "user://saves", + SaveSlotPrefix = ProjectSettings.HasSetting("application/config/save/save_slot_prefix") + ? ProjectSettings.GetSetting("application/config/save/save_slot_prefix").AsString() + : "slot_", + SaveFileName = ProjectSettings.HasSetting("application/config/save/save_file_name") + ? ProjectSettings.GetSetting("application/config/save/save_file_name").AsString() + : "save.json" +}; +``` + +### 支持版本迁移 + +```csharp +using GFramework.Game.Abstractions.data; + +public class GameSaveData : IData, IVersionedData +{ + public int Version { get; set; } = 1; + public string PlayerName { get; set; } = ""; + public int Level { get; set; } = 1; + + // 版本 2 新增字段 + public int Experience { get; set; } = 0; +} + +// 实现迁移逻辑(未来可扩展) +public class GameSaveDataMigration_1_to_2 : IDataMigration +{ + public int FromVersion => 1; + public int ToVersion => 2; + + public GameSaveData Migrate(GameSaveData oldData) + { + // 迁移逻辑:为旧存档添加默认经验值 + oldData.Experience = oldData.Level * 100; + oldData.Version = 2; + return oldData; + } +} +``` + +### 多种存档类型 + +```csharp +// 游戏存档 +this.RegisterUtility>( + new SaveRepository(storage, new SaveConfiguration + { + SaveRoot = "user://saves/game", + SaveSlotPrefix = "slot_", + SaveFileName = "save.json" + }) +); + +// 用户配置存档 +this.RegisterUtility>( + new SaveRepository(storage, new SaveConfiguration + { + SaveRoot = "user://saves/profile", + SaveSlotPrefix = "profile_", + SaveFileName = "profile.json" + }) +); + +// 成就数据存档 +this.RegisterUtility>( + new SaveRepository(storage, new SaveConfiguration + { + SaveRoot = "user://saves/achievements", + SaveSlotPrefix = "achievement_", + SaveFileName = "data.json" + }) +); +``` + +## 文件结构 + +使用默认配置时,存档文件结构如下: + +``` +user://saves/ +├── slot_1/ +│ └── save.json +├── slot_2/ +│ └── save.json +└── slot_3/ + └── save.json +``` + +## API 参考 + +### ISaveRepository + +| 方法 | 说明 | 返回值 | +|------|------|--------| +| `ExistsAsync(int slot)` | 检查指定槽位是否存在存档 | `Task` | +| `LoadAsync(int slot)` | 加载指定槽位的存档 | `Task` | +| `SaveAsync(int slot, TSaveData data)` | 保存存档到指定槽位 | `Task` | +| `DeleteAsync(int slot)` | 删除指定槽位的存档 | `Task` | +| `ListSlotsAsync()` | 列出所有有效的存档槽位 | `Task>` | + +### SaveConfiguration + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `SaveRoot` | `string` | `"user://saves"` | 存档根目录 | +| `SaveSlotPrefix` | `string` | `"slot_"` | 槽位前缀 | +| `SaveFileName` | `string` | `"save.json"` | 存档文件名 | + +## 从旧版 SaveStorageUtility 迁移 + +### 主要变化 + +1. **所有方法改为异步** - 使用 `async/await` +2. **接口名称变更** - `ISaveStorageUtility` → `ISaveRepository` +3. **方法名称添加 Async 后缀** +4. **配置通过对象传递** - 支持默认值 + +### 迁移对照表 + +| 旧方法 | 新方法 | +|--------|--------| +| `Exists(int slot)` | `await ExistsAsync(int slot)` | +| `Load(int slot)` | `await LoadAsync(int slot)` | +| `Save(int slot, data)` | `await SaveAsync(int slot, data)` | +| `Delete(int slot)` | `await DeleteAsync(int slot)` | +| `ListSlots()` | `await ListSlotsAsync()` | + +### 迁移示例 + +**旧代码:** +```csharp +var saveUtil = this.GetUtility(); +saveUtil.Save(1, data); +var loadedData = saveUtil.Load(1); +``` + +**新代码:** +```csharp +var saveRepo = this.GetUtility>(); +await saveRepo.SaveAsync(1, data); +var loadedData = await saveRepo.LoadAsync(1); +``` + +## 常见问题 + +### Q: 如何切换到云存档? + +A: 只需替换 `IStorage` 实现即可: + +```csharp +// 本地存档 +this.RegisterUtility(new GodotFileStorage(serializer)); + +// 云存档(需要自行实现 CloudStorage) +this.RegisterUtility(new CloudStorage(apiKey, serializer)); +``` + +### Q: 如何加密存档? + +A: 使用装饰器模式包装 `IStorage`: + +```csharp +var baseStorage = new GodotFileStorage(serializer); +var encryptedStorage = new EncryptedStorage(baseStorage, encryptionKey); +this.RegisterUtility(encryptedStorage); +``` + +### Q: 如何实现自动备份? + +A: 使用 `DataRepository` 的 `AutoBackup` 选项(需要配合 `DataRepository` 使用)。 + +### Q: LoadAsync 返回的是新实例还是缓存实例? + +A: 每次调用都会从存储读取并返回新实例,不会缓存。如果需要缓存,请在业务层实现。 + +## 最佳实践 + +1. **使用异步方法** - 避免阻塞主线程 +2. **错误处理** - 使用 try-catch 捕获 IO 异常 +3. **定期保存** - 不要等到游戏退出才保存 +4. **槽位验证** - 加载前先检查槽位是否存在 +5. **版本管理** - 为存档数据实现 `IVersionedData` 接口 + +## 相关文档 + +- [IStorage 接口文档](./IStorage.md) +- [ScopedStorage 使用指南](./ScopedStorage.md) +- [数据仓库模式](./DataRepository.md) diff --git a/GFramework.Game/storage/FileStorage.cs b/GFramework.Game/storage/FileStorage.cs index 5b983fb..45dcc46 100644 --- a/GFramework.Game/storage/FileStorage.cs +++ b/GFramework.Game/storage/FileStorage.cs @@ -228,6 +228,71 @@ public sealed class FileStorage : IFileStorage #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>(Array.Empty()); + + var dirs = Directory.GetDirectories(fullPath) + .Select(Path.GetFileName) + .Where(name => !string.IsNullOrEmpty(name)) + .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) + .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 /// diff --git a/GFramework.Game/storage/ScopedStorage.cs b/GFramework.Game/storage/ScopedStorage.cs index 0a82ca7..0ce4785 100644 --- a/GFramework.Game/storage/ScopedStorage.cs +++ b/GFramework.Game/storage/ScopedStorage.cs @@ -105,6 +105,46 @@ public sealed class ScopedStorage(IStorage inner, string prefix) : IScopedStorag await inner.DeleteAsync(Key(key)); } + /// + /// 列举指定路径下的所有子目录名称 + /// + /// 要列举的路径,空字符串表示根目录 + /// 子目录名称列表 + public Task> ListDirectoriesAsync(string path = "") + { + return inner.ListDirectoriesAsync(Key(path)); + } + + /// + /// 列举指定路径下的所有文件名称 + /// + /// 要列举的路径,空字符串表示根目录 + /// 文件名称列表 + public Task> ListFilesAsync(string path = "") + { + return inner.ListFilesAsync(Key(path)); + } + + /// + /// 检查指定路径的目录是否存在 + /// + /// 要检查的目录路径 + /// 如果目录存在则返回true,否则返回false + public Task DirectoryExistsAsync(string path) + { + return inner.DirectoryExistsAsync(Key(path)); + } + + /// + /// 创建目录(递归创建父目录) + /// + /// 要创建的目录路径 + /// 表示异步操作的Task + public Task CreateDirectoryAsync(string path) + { + return inner.CreateDirectoryAsync(Key(path)); + } + /// /// 为给定的键添加前缀 /// diff --git a/GFramework.Godot/storage/GodotFileStorage.cs b/GFramework.Godot/storage/GodotFileStorage.cs index 5a27bf5..eebf571 100644 --- a/GFramework.Godot/storage/GodotFileStorage.cs +++ b/GFramework.Godot/storage/GodotFileStorage.cs @@ -263,6 +263,94 @@ public sealed class GodotFileStorage : IStorage #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 ///