mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 07:24:55 +08:00
fix(repository): 扩展资源索引查询面
This commit is contained in:
+1
-1
@@ -27,7 +27,7 @@ Rust 侧官方日服资源链路已经从实验验证推进到正式入口:
|
|||||||
10. 默认官方原版资源目录是 `./bat-resources`,默认汉化产物目录是 `./bat-localized`,默认后台状态目录是 `/tmp/bat-pid`;官方资源目录是发布根目录,包含 `current` symlink、`versions/<id>` 和 `.staging/<id>`,非 dry-run 会先写 staging,校验完成后发布 versioned 目录并原子切换 `current`;启用 `--auto-discover` 的 release 会包含 `official-launcher-bootstrap.json`,up-to-date 轮询会为旧 release 补写该产物;如果上一轮同一 app version、bundle version 和 Addressables root 的 staging 失败但目录仍安全存在,下一轮会复用该 staging 并按 manifest 逐文件校验/补下载;后台状态目录包含 `bat.sock`、`bat.pid`、`bat-status.json`、`bat-daemon.log`、`bat-events.jsonl`、任务历史 `bat-tasks.json` 和短生命周期 `bat-control.lock`;非 dry-run 使用 `--output/.official-sync.lock` 防止并发写同一资源目录,live daemon 会阻止前台写命令直接修改它正在管理的同一目录。
|
10. 默认官方原版资源目录是 `./bat-resources`,默认汉化产物目录是 `./bat-localized`,默认后台状态目录是 `/tmp/bat-pid`;官方资源目录是发布根目录,包含 `current` symlink、`versions/<id>` 和 `.staging/<id>`,非 dry-run 会先写 staging,校验完成后发布 versioned 目录并原子切换 `current`;启用 `--auto-discover` 的 release 会包含 `official-launcher-bootstrap.json`,up-to-date 轮询会为旧 release 补写该产物;如果上一轮同一 app version、bundle version 和 Addressables root 的 staging 失败但目录仍安全存在,下一轮会复用该 staging 并按 manifest 逐文件校验/补下载;后台状态目录包含 `bat.sock`、`bat.pid`、`bat-status.json`、`bat-daemon.log`、`bat-events.jsonl`、任务历史 `bat-tasks.json` 和短生命周期 `bat-control.lock`;非 dry-run 使用 `--output/.official-sync.lock` 防止并发写同一资源目录,live daemon 会阻止前台写命令直接修改它正在管理的同一目录。
|
||||||
11. 官方同步会拒绝危险输出目录、路径逃逸和现有 symlink 路径组件;下载目标、`.part`、manifest、snapshot、PID、status、log 和控制锁文件不会跟随 symlink,daemon 状态类文件默认以 `0600` 权限创建。
|
11. 官方同步会拒绝危险输出目录、路径逃逸和现有 symlink 路径组件;下载目标、`.part`、manifest、snapshot、PID、status、log 和控制锁文件不会跟随 symlink,daemon 状态类文件默认以 `0600` 权限创建。
|
||||||
12. `<output>/official-version-state.json` 会明确保存当前已完成版本、正在拉取版本、上一个可用版本和失败版本;同一 app version、bundle version 和 Addressables root 的失败只保留最新一条,同一版本开始重新拉取或后续发布成功时会清理对应失败记录;`bat status` 会显示最后成功时间、下次检查时间、最后错误摘要、当前阶段、当前下载 URL 进度、版本状态摘要、最近历史失败版本和原因、结构化日志路径和轮转日志路径,人类可读输出不会把完整版本状态 JSON 内联打印。
|
12. `<output>/official-version-state.json` 会明确保存当前已完成版本、正在拉取版本、上一个可用版本和失败版本;同一 app version、bundle version 和 Addressables root 的失败只保留最新一条,同一版本开始重新拉取或后续发布成功时会清理对应失败记录;`bat status` 会显示最后成功时间、下次检查时间、最后错误摘要、当前阶段、当前下载 URL 进度、版本状态摘要、最近历史失败版本和原因、结构化日志路径和轮转日志路径,人类可读输出不会把完整版本状态 JSON 内联打印。
|
||||||
13. 资源导入链路已支持 CAS + `ResourceRepository` 索引写入,官方同步可用 `--import-repository` / `BAT_IMPORT_REPOSITORY=1` 在已校验 release 发布后触发导入,默认 CAS 路径为 `<output>/.cas`、SQLite 索引为 `<output>/resources.sqlite`,也可通过 `--import-cas-root`、`--import-resource-db`、`BAT_IMPORT_CAS_ROOT`、`BAT_IMPORT_RESOURCE_DB` 覆盖;`resource.index` RPC 可按类型、hash、路径模式分页查询现有索引,数据库不存在时返回 `available=false` 且不会创建空库。`Resource` metadata 已通过 `metadata_json` 兼容迁移保存 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 数量/格式;当前/上一个/结构变化 catalog、失败 staging 复用、403/404、hash mismatch、CRC、metadata 迁移与 UnityFS 边界校验均有离线回归 fixture 或单测覆盖。
|
13. 资源导入链路已支持 CAS + `ResourceRepository` 索引写入,官方同步可用 `--import-repository` / `BAT_IMPORT_REPOSITORY=1` 在已校验 release 发布后触发导入,默认 CAS 路径为 `<output>/.cas`、SQLite 索引为 `<output>/resources.sqlite`,也可通过 `--import-cas-root`、`--import-resource-db`、`BAT_IMPORT_CAS_ROOT`、`BAT_IMPORT_RESOURCE_DB` 覆盖;`resource.index` RPC/CLI 可按类型、hash、路径模式、官方 release ID、平台、destination、bundle path、archive entry、parse status 和 TextUnit format 分页查询现有索引,数据库不存在时返回 `available=false` 且不会创建空库。`Resource` metadata 已通过 `metadata_json` 兼容迁移保存 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 数量/格式;当前/上一个/结构变化 catalog、失败 staging 复用、403/404、hash mismatch、CRC、metadata 迁移与 UnityFS 边界校验均有离线回归 fixture 或单测覆盖。
|
||||||
14. 非 dry-run 官方同步在校验完成并发布后,会先对比上一完整 release 与当前 release 的 `official-download-manifest.json`,在当前 release 下写入 `official-resource-changes.json` 和 `crowdin-translation-handoff.json`;同一 destination 只有 size 或 BLAKE3 改变才算 modified,仅 URL/CDN 根变化但内容相同不会触发解析/翻译候选。随后刷新 `official-parse-cache.json` 和 `official-textunit-index.json`,并从 Added/Modified 资源、parse cache 与 TextUnit 明细索引派生 `official-textunit-tasks.json` 和 `crowdin-textunit-queue.json`;删除资源只进入差异记录,不进入 TextUnit/Crowdin 队列。`parse.text_units` 和 `parse.errors` RPC/CLI 可按 destination、archive entry、path id、class id、field path 和 format 查询当前 release 的 TextUnit 明细与解析错误;TextUnit 已包含 class id、field path、字段 offset/byte size 等可追溯定位。Crowdin 当前仅预留本地离线队列,不发网络请求;up-to-date 轮询在已有有效解析缓存、TextUnit 明细索引和 TextUnit 队列时只读取摘要,不重复解析。官方同步报告默认 `localized_release_status=not_localized`,含义是原版资源已经发布、汉化资源未发布;UnityFS TextAsset patch 发布成功后会写 `localized-patch-manifest.json`,校验 hash/size/diff/rollback 后才允许 `localized.status` 返回 `status=published`、`status_code=localized.published` 和 `localized_release_status=localized`。`bat` 首次启动会在二进制所在目录释放 `.env` 配置模板(`0600`),之后每次启动自动加载(不覆盖已存在的环境变量),支持 `BAT_OUTPUT`/`BAT_LOCALIZED_OUTPUT`/`BAT_IMPORT_REPOSITORY`/`BAT_IMPORT_CAS_ROOT`/`BAT_IMPORT_RESOURCE_DB`/`BAT_STATE_DIR`/`BAT_AUTO_DISCOVER`/`BAT_WATCH`/`BAT_DAEMON`/`BAT_PROXY` 等键,实现编辑 `.env` 后无参启动;优先级为命令行参数 > 进程环境变量 > `.env` > 内置默认值,`BAT_SKIP_ENV_FILE=1` 可整体禁用;Redis 键为预留。daemon 任务历史持久化在 `<state-dir>/bat-tasks.json`(版本化、`0600` 原子写),重启后任务经 `task.*` 仍可查,中断任务标记 `task_interrupted`(`BAT-ERR-700005`)。
|
14. 非 dry-run 官方同步在校验完成并发布后,会先对比上一完整 release 与当前 release 的 `official-download-manifest.json`,在当前 release 下写入 `official-resource-changes.json` 和 `crowdin-translation-handoff.json`;同一 destination 只有 size 或 BLAKE3 改变才算 modified,仅 URL/CDN 根变化但内容相同不会触发解析/翻译候选。随后刷新 `official-parse-cache.json` 和 `official-textunit-index.json`,并从 Added/Modified 资源、parse cache 与 TextUnit 明细索引派生 `official-textunit-tasks.json` 和 `crowdin-textunit-queue.json`;删除资源只进入差异记录,不进入 TextUnit/Crowdin 队列。`parse.text_units` 和 `parse.errors` RPC/CLI 可按 destination、archive entry、path id、class id、field path 和 format 查询当前 release 的 TextUnit 明细与解析错误;TextUnit 已包含 class id、field path、字段 offset/byte size 等可追溯定位。Crowdin 当前仅预留本地离线队列,不发网络请求;up-to-date 轮询在已有有效解析缓存、TextUnit 明细索引和 TextUnit 队列时只读取摘要,不重复解析。官方同步报告默认 `localized_release_status=not_localized`,含义是原版资源已经发布、汉化资源未发布;UnityFS TextAsset patch 发布成功后会写 `localized-patch-manifest.json`,校验 hash/size/diff/rollback 后才允许 `localized.status` 返回 `status=published`、`status_code=localized.published` 和 `localized_release_status=localized`。`bat` 首次启动会在二进制所在目录释放 `.env` 配置模板(`0600`),之后每次启动自动加载(不覆盖已存在的环境变量),支持 `BAT_OUTPUT`/`BAT_LOCALIZED_OUTPUT`/`BAT_IMPORT_REPOSITORY`/`BAT_IMPORT_CAS_ROOT`/`BAT_IMPORT_RESOURCE_DB`/`BAT_STATE_DIR`/`BAT_AUTO_DISCOVER`/`BAT_WATCH`/`BAT_DAEMON`/`BAT_PROXY` 等键,实现编辑 `.env` 后无参启动;优先级为命令行参数 > 进程环境变量 > `.env` > 内置默认值,`BAT_SKIP_ENV_FILE=1` 可整体禁用;Redis 键为预留。daemon 任务历史持久化在 `<state-dir>/bat-tasks.json`(版本化、`0600` 原子写),重启后任务经 `task.*` 仍可查,中断任务标记 `task_interrupted`(`BAT-ERR-700005`)。
|
||||||
|
|
||||||
仍需明确:这不是完整产品完成。完整 AssetBundle 重打包、翻译、Web、以及 `bat-api` 的服务器联调/可选业务扩展仍是后续工作;G-008(产品级 Go 同步 CLI)已决策关闭。真实官方网络全量拉取 smoke 已固化(G-018 已关闭);真实大文件与运行报告默认在 `/tmp` 隔离目录,不纳入 Git。Go 细节见 `docs/reports/GO_STATUS.md`。
|
仍需明确:这不是完整产品完成。完整 AssetBundle 重打包、翻译、Web、以及 `bat-api` 的服务器联调/可选业务扩展仍是后续工作;G-008(产品级 Go 同步 CLI)已决策关闭。真实官方网络全量拉取 smoke 已固化(G-018 已关闭);真实大文件与运行报告默认在 `/tmp` 隔离目录,不纳入 Git。Go 细节见 `docs/reports/GO_STATUS.md`。
|
||||||
|
|||||||
+2
-2
@@ -44,7 +44,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
|||||||
2. `bat-patch` 已具备确定性 Binary hunk diff/apply、RFC 6902 JSON Patch apply、UTF-8 Text Patch、通用 Patch manifest、BLAKE3/size 完整性校验和 rollback 元数据;文件级 `patch.apply` RPC / `patch-apply` CLI 与 UnityFS TextAsset / TypeTree string / TypeTree 语义字段写入入口已开放,当前发布级可用的是 `bat-assetbundle` + `LocalizedPatchService` 的 UnityFS TextAsset patch 前置链路。
|
2. `bat-patch` 已具备确定性 Binary hunk diff/apply、RFC 6902 JSON Patch apply、UTF-8 Text Patch、通用 Patch manifest、BLAKE3/size 完整性校验和 rollback 元数据;文件级 `patch.apply` RPC / `patch-apply` CLI 与 UnityFS TextAsset / TypeTree string / TypeTree 语义字段写入入口已开放,当前发布级可用的是 `bat-assetbundle` + `LocalizedPatchService` 的 UnityFS TextAsset patch 前置链路。
|
||||||
3. Go 侧边界已冻结(见 `docs/reports/GO_STATUS.md`):同步/运维命令行 = Rust `bat`;资源分发 = `cmd/bat-api` MVP;`internal/backendrpc` 完成;`cmd/bat` 仅为试验(`bin/bat-go`)。完整游戏业务 API / Web / SDK 仍未完成。
|
3. Go 侧边界已冻结(见 `docs/reports/GO_STATUS.md`):同步/运维命令行 = Rust `bat`;资源分发 = `cmd/bat-api` MVP;`internal/backendrpc` 完成;`cmd/bat` 仅为试验(`bin/bat-go`)。完整游戏业务 API / Web / SDK 仍未完成。
|
||||||
4. Addressables parser 已覆盖当前真实形态 fixture/golden,但还不是完整 Unity Addressables/SBP catalog 兼容层。
|
4. Addressables parser 已覆盖当前真实形态 fixture/golden,但还不是完整 Unity Addressables/SBP catalog 兼容层。
|
||||||
5. 官方同步结果可配置为发布后自动导入 CAS + ResourceRepository,并通过 `resource.index` RPC 查询;Resource metadata 已保存 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 摘要;单条 TextUnit 明细和解析错误已持久化到 `official-textunit-index.json`,可通过 `parse.text_units` / `parse.errors` 查询,翻译任务状态仍需继续推进。
|
5. 官方同步结果可配置为发布后自动导入 CAS + ResourceRepository,并通过 `resource.index` RPC/CLI 查询;Resource metadata 已保存 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 摘要,资源级查询已覆盖 release、平台、destination、archive entry、parse status 和 TextUnit format;单条 TextUnit 明细和解析错误已持久化到 `official-textunit-index.json`,可通过 `parse.text_units` / `parse.errors` 查询,翻译任务状态仍需继续推进。
|
||||||
6. 汉化 Patch 发布前置已具备 UnityFS TextAsset manifest/apply/diff/rollback/完整性校验和 `localized.status` 严格校验;真实 Crowdin worker、翻译记忆到完整汉化文件集合的构建仍未完成。
|
6. 汉化 Patch 发布前置已具备 UnityFS TextAsset manifest/apply/diff/rollback/完整性校验和 `localized.status` 严格校验;真实 Crowdin worker、翻译记忆到完整汉化文件集合的构建仍未完成。
|
||||||
7. 真实官方网络全量下载 smoke 已固化为可重复脚本和 runbook(G-018 已关闭);真实运行记录处于长期运行测试阶段,报告待后续提供。
|
7. 真实官方网络全量下载 smoke 已固化为可重复脚本和 runbook(G-018 已关闭);真实运行记录处于长期运行测试阶段,报告待后续提供。
|
||||||
8. Web、数据库迁移、OpenAPI、插件加载机制尚未实现。
|
8. Web、数据库迁移、OpenAPI、插件加载机制尚未实现。
|
||||||
@@ -178,7 +178,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
|||||||
4. Rust 自动更新入口:**已完成当前生产入口**。`bat` 支持 snapshot、marker diff、bootstrap cache、one-shot、`--watch`、`--daemon`、默认 1 小时间隔、北京时间固定强制刷新,以及 Unix socket JSON-RPC 后台运维命令返回。
|
4. Rust 自动更新入口:**已完成当前生产入口**。`bat` 支持 snapshot、marker diff、bootstrap cache、one-shot、`--watch`、`--daemon`、默认 1 小时间隔、北京时间固定强制刷新,以及 Unix socket JSON-RPC 后台运维命令返回。
|
||||||
5. Go 入口边界:**已冻结**。同步命令行 = Rust `bat`(G-008 关闭);资源分发 = `bat-api` MVP(G-009 部分完成)。详见 `docs/reports/GO_STATUS.md`。
|
5. Go 入口边界:**已冻结**。同步命令行 = Rust `bat`(G-008 关闭);资源分发 = `bat-api` MVP(G-009 部分完成)。详见 `docs/reports/GO_STATUS.md`。
|
||||||
6. 用户级 `sync`、`manifest inspect`、`cache status`:**未完成**。Rust `bat --json` 是当前稳定进程边界;`bat-ffi` 只提供可选兼容用的 Manifest inspect 和 sync plan JSON helper。
|
6. 用户级 `sync`、`manifest inspect`、`cache status`:**未完成**。Rust `bat --json` 是当前稳定进程边界;`bat-ffi` 只提供可选兼容用的 Manifest inspect 和 sync plan JSON helper。
|
||||||
7. 下载结果写入 CAS + ResourceRepository:**部分完成**。CAS 和 SQLite ResourceRepository 已存在,官方同步入口可用 `--import-repository` / `BAT_IMPORT_REPOSITORY=1` 在已校验 release 发布后导入;`resource.index` 可查询现有索引和资源 metadata;`parse.text_units` / `parse.errors` 可查询当前 release 的 TextUnit 明细与解析错误。剩余工作是翻译任务状态、CAS 诊断入口和更丰富查询。
|
7. 下载结果写入 CAS + ResourceRepository:**部分完成**。CAS 和 SQLite ResourceRepository 已存在,官方同步入口可用 `--import-repository` / `BAT_IMPORT_REPOSITORY=1` 在已校验 release 发布后导入;`resource.index` 可按资源级 release、平台、destination、archive entry、parse status 和 TextUnit format 查询现有索引和资源 metadata;`parse.text_units` / `parse.errors` 可查询当前 release 的 TextUnit 明细与解析错误。剩余工作是翻译任务状态、CAS 诊断入口和面向大索引的查询优化。
|
||||||
8. Linux 生产同步不依赖已安装官方启动器:**已完成当前 Rust 入口**。`--auto-discover` 只使用官方 HTTP metadata 和临时目录解析 `GameMainConfig`。
|
8. Linux 生产同步不依赖已安装官方启动器:**已完成当前 Rust 入口**。`--auto-discover` 只使用官方 HTTP metadata 和临时目录解析 `GameMainConfig`。
|
||||||
9. 真实官方网络全量下载 smoke test:**命令已固化(G-018 已关闭)**。`scripts/official-full-pull-smoke.sh` / `make official-smoke` 已固化 dry-run、首次下载、二次 up-to-date 和本地损坏 repair 的可重复流程;真实运行处于长期运行测试阶段,报告待后续提供。
|
9. 真实官方网络全量下载 smoke test:**命令已固化(G-018 已关闭)**。`scripts/official-full-pull-smoke.sh` / `make official-smoke` 已固化 dry-run、首次下载、二次 up-to-date 和本地损坏 repair 的可重复流程;真实运行处于长期运行测试阶段,报告待后续提供。
|
||||||
10. 官方发布后的增量 handoff 与解析缓存:**已完成基础入口**。新 release 发布后先生成 `official-resource-changes.json` 和 `crowdin-translation-handoff.json`,新增+变更资源进入解析/翻译候选;`official-parse-cache.json` 基于下载 manifest 覆盖直接 UnityFS bundle、zip 内 UnityFS 条目和非候选资源记录;随后生成 `official-textunit-index.json`、`official-textunit-tasks.json` 与 `crowdin-textunit-queue.json`,本地文件未变化且缓存/索引有效时跳过重复解析。
|
10. 官方发布后的增量 handoff 与解析缓存:**已完成基础入口**。新 release 发布后先生成 `official-resource-changes.json` 和 `crowdin-translation-handoff.json`,新增+变更资源进入解析/翻译候选;`official-parse-cache.json` 基于下载 manifest 覆盖直接 UnityFS bundle、zip 内 UnityFS 条目和非候选资源记录;随后生成 `official-textunit-index.json`、`official-textunit-tasks.json` 与 `crowdin-textunit-queue.json`,本地文件未变化且缓存/索引有效时跳过重复解析。
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
- `cmd/bat-api`:资源 bootstrap + 分发 HTTP MVP(issue #19 / G-009);`/v1/bootstrap` 和 `/v1/launcher/bootstrap` 组织 `bat` 已发布 release 的启动前资源入口,launcher 形状兼容端点仅输出资源 metadata / GameMainConfig 引导,`/healthz` 暴露 RPC refresh 诊断,`/readyz` 做 release readiness,CDN path 支持 `GET`/`HEAD`/`Range`、ETag、Last-Modified 和缓存头;玩家-facing 控制面已具备 token 鉴权、限流、访问日志、反代 IP 适配、动态 JSON no-store、OpenAPI 和管理控制白名单;`.env` 配置端口/RPC socket/刷新周期;生产资源根来自 RPC,不负责自动拉取。
|
- `cmd/bat-api`:资源 bootstrap + 分发 HTTP MVP(issue #19 / G-009);`/v1/bootstrap` 和 `/v1/launcher/bootstrap` 组织 `bat` 已发布 release 的启动前资源入口,launcher 形状兼容端点仅输出资源 metadata / GameMainConfig 引导,`/healthz` 暴露 RPC refresh 诊断,`/readyz` 做 release readiness,CDN path 支持 `GET`/`HEAD`/`Range`、ETag、Last-Modified 和缓存头;玩家-facing 控制面已具备 token 鉴权、限流、访问日志、反代 IP 适配、动态 JSON no-store、OpenAPI 和管理控制白名单;`.env` 配置端口/RPC socket/刷新周期;生产资源根来自 RPC,不负责自动拉取。
|
||||||
- Go 边界权威说明:[`docs/reports/GO_STATUS.md`](docs/reports/GO_STATUS.md)(G-008 已关闭:同步 CLI = Rust `bat`)。
|
- Go 边界权威说明:[`docs/reports/GO_STATUS.md`](docs/reports/GO_STATUS.md)(G-008 已关闭:同步 CLI = Rust `bat`)。
|
||||||
- 官方同步会维护 `<output>/official-version-state.json`,明确记录当前已完成版本、正在拉取版本、上一个可用版本和失败版本。
|
- 官方同步会维护 `<output>/official-version-state.json`,明确记录当前已完成版本、正在拉取版本、上一个可用版本和失败版本。
|
||||||
- 资源导入链路可配置为在官方 release 发布后写入 CAS + `ResourceRepository`,资源 metadata 会记录 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 数量/格式,TextAsset/Table/Media 会按类型分类索引;`resource.index` RPC 可按类型、hash、路径模式分页查询索引。
|
- 资源导入链路可配置为在官方 release 发布后写入 CAS + `ResourceRepository`,资源 metadata 会记录 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 数量/格式,TextAsset/Table/Media 会按类型分类索引;`resource.index` RPC/CLI 可按类型、hash、路径模式、官方 release ID、平台、destination、archive entry、parse status 和 TextUnit format 分页查询索引。
|
||||||
- 新 release 发布后会生成 `official-resource-changes.json`、`official-parse-cache.json`、`official-textunit-index.json`、`official-textunit-tasks.json`、`crowdin-translation-handoff.json` 和 `crowdin-textunit-queue.json`;其中 TextUnit/Crowdin 队列只使用 Added/Modified 资源,不调用 Crowdin 网络 API。
|
- 新 release 发布后会生成 `official-resource-changes.json`、`official-parse-cache.json`、`official-textunit-index.json`、`official-textunit-tasks.json`、`crowdin-translation-handoff.json` 和 `crowdin-textunit-queue.json`;其中 TextUnit/Crowdin 队列只使用 Added/Modified 资源,不调用 Crowdin 网络 API。
|
||||||
- `LocalizedPatchService` 已具备 UnityFS TextAsset patch 发布前置能力:在 `--localized-output` / `BAT_LOCALIZED_OUTPUT` 配置的独立汉化目录 staging 中复制官方 release、应用 TextAsset patch、写 `localized-patch-manifest.json`(hash、size、diff、rollback)、校验后发布到 `versions/<id>` 并切换 `current`。
|
- `LocalizedPatchService` 已具备 UnityFS TextAsset patch 发布前置能力:在 `--localized-output` / `BAT_LOCALIZED_OUTPUT` 配置的独立汉化目录 staging 中复制官方 release、应用 TextAsset patch、写 `localized-patch-manifest.json`(hash、size、diff、rollback)、校验后发布到 `versions/<id>` 并切换 `current`。
|
||||||
- `bat-patch` 已具备通用 Patch 基础:确定性 Binary hunk diff/apply、RFC 6902 JSON Patch apply、UTF-8 Text Patch、Patch manifest、BLAKE3/size 完整性校验和 rollback 元数据;文件级 `patch.apply` RPC / `patch-apply` CLI 与 UnityFS TextAsset / TypeTree string / TypeTree 语义字段写入入口已开放,TypeTree 语义字段支持基础标量、固定 Unity float/int/hash 值类型的 leaf/direct-child 形态、PPtr、managed-reference registry payload 字符串、object 字段组合、unknown fixed-size raw bytes 同长度替换和 TypeTree schema 支撑的 array/vector/map 整体替换;TextUnit 提取会把 managed-reference 类型信息保留为上下文而非翻译文本,汉化发布当前仍走 UnityFS TextAsset 前置链路。
|
- `bat-patch` 已具备通用 Patch 基础:确定性 Binary hunk diff/apply、RFC 6902 JSON Patch apply、UTF-8 Text Patch、Patch manifest、BLAKE3/size 完整性校验和 rollback 元数据;文件级 `patch.apply` RPC / `patch-apply` CLI 与 UnityFS TextAsset / TypeTree string / TypeTree 语义字段写入入口已开放,TypeTree 语义字段支持基础标量、固定 Unity float/int/hash 值类型的 leaf/direct-child 形态、PPtr、managed-reference registry payload 字符串、object 字段组合、unknown fixed-size raw bytes 同长度替换和 TypeTree schema 支撑的 array/vector/map 整体替换;TextUnit 提取会把 managed-reference 类型信息保留为上下文而非翻译文本,汉化发布当前仍走 UnityFS TextAsset 前置链路。
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ use async_trait::async_trait;
|
|||||||
|
|
||||||
/// 资源查询条件
|
/// 资源查询条件
|
||||||
///
|
///
|
||||||
/// 用于构建灵活的资源查询。支持按类型、Hash、路径模式过滤。
|
/// 用于构建灵活的资源查询。支持按类型、Hash、路径、官方 release 和解析摘要过滤。
|
||||||
///
|
///
|
||||||
/// # 示例
|
/// # 示例
|
||||||
///
|
///
|
||||||
@@ -56,9 +56,10 @@ use async_trait::async_trait;
|
|||||||
/// resource_type: Some(ResourceType::AssetBundle),
|
/// resource_type: Some(ResourceType::AssetBundle),
|
||||||
/// hash: Some("abc123".to_string()),
|
/// hash: Some("abc123".to_string()),
|
||||||
/// path_pattern: Some("academy-*.bundle".to_string()),
|
/// path_pattern: Some("academy-*.bundle".to_string()),
|
||||||
|
/// ..ResourceQuery::all()
|
||||||
/// };
|
/// };
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct ResourceQuery {
|
pub struct ResourceQuery {
|
||||||
/// 按资源类型过滤
|
/// 按资源类型过滤
|
||||||
///
|
///
|
||||||
@@ -90,6 +91,41 @@ pub struct ResourceQuery {
|
|||||||
/// - `"**/*.json"` - 匹配所有 JSON 文件
|
/// - `"**/*.json"` - 匹配所有 JSON 文件
|
||||||
/// - `"assets/???.png"` - 匹配三个字符的 PNG 文件
|
/// - `"assets/???.png"` - 匹配三个字符的 PNG 文件
|
||||||
pub path_pattern: Option<String>,
|
pub path_pattern: Option<String>,
|
||||||
|
|
||||||
|
/// 按官方 release ID 过滤。
|
||||||
|
///
|
||||||
|
/// 匹配 `ResourceMetadata::official_release_id`。
|
||||||
|
pub official_release_id: Option<String>,
|
||||||
|
|
||||||
|
/// 按资源平台过滤。
|
||||||
|
///
|
||||||
|
/// 匹配 `ResourceMetadata::platform`,例如 `windows` 或 `android`。
|
||||||
|
pub platform: Option<String>,
|
||||||
|
|
||||||
|
/// 按官方 destination 过滤。
|
||||||
|
///
|
||||||
|
/// 匹配 `ResourceEntry::path`,用于从 release manifest destination 反查资源。
|
||||||
|
pub destination: Option<String>,
|
||||||
|
|
||||||
|
/// 按资源或所在 bundle 的官方相对路径过滤。
|
||||||
|
///
|
||||||
|
/// 匹配 `ResourceMetadata::bundle_path`。
|
||||||
|
pub bundle_path: Option<String>,
|
||||||
|
|
||||||
|
/// 按 ZIP/archive entry 过滤。
|
||||||
|
///
|
||||||
|
/// 匹配 `ResourceMetadata::archive_entries` 中的任意一项。
|
||||||
|
pub archive_entry: Option<String>,
|
||||||
|
|
||||||
|
/// 按解析状态过滤。
|
||||||
|
///
|
||||||
|
/// 匹配 `ResourceMetadata::parse_statuses` 中的任意一项。
|
||||||
|
pub parse_status: Option<String>,
|
||||||
|
|
||||||
|
/// 按 TextUnit payload format 过滤。
|
||||||
|
///
|
||||||
|
/// 匹配 `ResourceMetadata::text_unit_formats` 中的任意一项。
|
||||||
|
pub text_unit_format: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceQuery {
|
impl ResourceQuery {
|
||||||
@@ -105,11 +141,7 @@ impl ResourceQuery {
|
|||||||
/// let all_resources = repo.list(ResourceQuery::all()).await?;
|
/// let all_resources = repo.list(ResourceQuery::all()).await?;
|
||||||
/// ```
|
/// ```
|
||||||
pub fn all() -> Self {
|
pub fn all() -> Self {
|
||||||
Self {
|
Self::default()
|
||||||
resource_type: None,
|
|
||||||
hash: None,
|
|
||||||
path_pattern: None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 按类型查询
|
/// 按类型查询
|
||||||
@@ -132,8 +164,7 @@ impl ResourceQuery {
|
|||||||
pub fn by_type(resource_type: ResourceType) -> Self {
|
pub fn by_type(resource_type: ResourceType) -> Self {
|
||||||
Self {
|
Self {
|
||||||
resource_type: Some(resource_type),
|
resource_type: Some(resource_type),
|
||||||
hash: None,
|
..Self::default()
|
||||||
path_pattern: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,11 +187,23 @@ impl ResourceQuery {
|
|||||||
/// ```
|
/// ```
|
||||||
pub fn by_hash(hash: String) -> Self {
|
pub fn by_hash(hash: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
resource_type: None,
|
|
||||||
hash: Some(hash),
|
hash: Some(hash),
|
||||||
path_pattern: None,
|
..Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 是否包含需要读取完整 `Resource` 后才能判断的条件。
|
||||||
|
///
|
||||||
|
/// 基础索引可先用类型、hash、路径模式和 destination 缩小范围;这些条件
|
||||||
|
/// 需要再按 metadata 过滤,确保 `list()` 与 `count()` 的语义一致。
|
||||||
|
pub fn requires_resource_scan(&self) -> bool {
|
||||||
|
self.official_release_id.is_some()
|
||||||
|
|| self.platform.is_some()
|
||||||
|
|| self.bundle_path.is_some()
|
||||||
|
|| self.archive_entry.is_some()
|
||||||
|
|| self.parse_status.is_some()
|
||||||
|
|| self.text_unit_format.is_some()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 资源仓储接口
|
/// 资源仓储接口
|
||||||
@@ -305,6 +348,7 @@ pub trait ResourceRepository: Send + Sync {
|
|||||||
/// resource_type: Some(ResourceType::AssetBundle),
|
/// resource_type: Some(ResourceType::AssetBundle),
|
||||||
/// path_pattern: Some("academy-*.bundle".to_string()),
|
/// path_pattern: Some("academy-*.bundle".to_string()),
|
||||||
/// hash: None,
|
/// hash: None,
|
||||||
|
/// ..ResourceQuery::all()
|
||||||
/// };
|
/// };
|
||||||
/// let filtered = repo.list(query).await?;
|
/// let filtered = repo.list(query).await?;
|
||||||
/// ```
|
/// ```
|
||||||
@@ -416,6 +460,13 @@ mod tests {
|
|||||||
assert!(query.resource_type.is_none());
|
assert!(query.resource_type.is_none());
|
||||||
assert!(query.hash.is_none());
|
assert!(query.hash.is_none());
|
||||||
assert!(query.path_pattern.is_none());
|
assert!(query.path_pattern.is_none());
|
||||||
|
assert!(query.official_release_id.is_none());
|
||||||
|
assert!(query.platform.is_none());
|
||||||
|
assert!(query.destination.is_none());
|
||||||
|
assert!(query.bundle_path.is_none());
|
||||||
|
assert!(query.archive_entry.is_none());
|
||||||
|
assert!(query.parse_status.is_none());
|
||||||
|
assert!(query.text_unit_format.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 测试按类型查询
|
/// 测试按类型查询
|
||||||
@@ -425,6 +476,7 @@ mod tests {
|
|||||||
assert_eq!(query.resource_type, Some(ResourceType::AssetBundle));
|
assert_eq!(query.resource_type, Some(ResourceType::AssetBundle));
|
||||||
assert!(query.hash.is_none());
|
assert!(query.hash.is_none());
|
||||||
assert!(query.path_pattern.is_none());
|
assert!(query.path_pattern.is_none());
|
||||||
|
assert!(!query.requires_resource_scan());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 测试按 Hash 查询
|
/// 测试按 Hash 查询
|
||||||
@@ -434,6 +486,7 @@ mod tests {
|
|||||||
assert_eq!(query.hash, Some("abc123".to_string()));
|
assert_eq!(query.hash, Some("abc123".to_string()));
|
||||||
assert!(query.resource_type.is_none());
|
assert!(query.resource_type.is_none());
|
||||||
assert!(query.path_pattern.is_none());
|
assert!(query.path_pattern.is_none());
|
||||||
|
assert!(!query.requires_resource_scan());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 测试组合查询
|
/// 测试组合查询
|
||||||
@@ -443,11 +496,32 @@ mod tests {
|
|||||||
resource_type: Some(ResourceType::AssetBundle),
|
resource_type: Some(ResourceType::AssetBundle),
|
||||||
hash: Some("hash123".to_string()),
|
hash: Some("hash123".to_string()),
|
||||||
path_pattern: Some("*.bundle".to_string()),
|
path_pattern: Some("*.bundle".to_string()),
|
||||||
|
official_release_id: Some("v-current".to_string()),
|
||||||
|
platform: Some("windows".to_string()),
|
||||||
|
destination: Some("Bundles/academy.bundle".to_string()),
|
||||||
|
bundle_path: Some("Bundles/academy.bundle".to_string()),
|
||||||
|
archive_entry: Some("academy".to_string()),
|
||||||
|
parse_status: Some("parsed".to_string()),
|
||||||
|
text_unit_format: Some("json".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(query.resource_type, Some(ResourceType::AssetBundle));
|
assert_eq!(query.resource_type, Some(ResourceType::AssetBundle));
|
||||||
assert_eq!(query.hash, Some("hash123".to_string()));
|
assert_eq!(query.hash, Some("hash123".to_string()));
|
||||||
assert_eq!(query.path_pattern, Some("*.bundle".to_string()));
|
assert_eq!(query.path_pattern, Some("*.bundle".to_string()));
|
||||||
|
assert_eq!(query.official_release_id, Some("v-current".to_string()));
|
||||||
|
assert_eq!(query.platform, Some("windows".to_string()));
|
||||||
|
assert_eq!(
|
||||||
|
query.destination,
|
||||||
|
Some("Bundles/academy.bundle".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
query.bundle_path,
|
||||||
|
Some("Bundles/academy.bundle".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(query.archive_entry, Some("academy".to_string()));
|
||||||
|
assert_eq!(query.parse_status, Some("parsed".to_string()));
|
||||||
|
assert_eq!(query.text_unit_format, Some("json".to_string()));
|
||||||
|
assert!(query.requires_resource_scan());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 测试 ResourceQuery 可以被克隆
|
/// 测试 ResourceQuery 可以被克隆
|
||||||
|
|||||||
@@ -244,12 +244,14 @@ cargo run -p bat-infrastructure --bin bat -- parse-text-units --limit 50
|
|||||||
cargo run -p bat-infrastructure --bin bat -- parse-errors --limit 50
|
cargo run -p bat-infrastructure --bin bat -- parse-errors --limit 50
|
||||||
cargo run -p bat-infrastructure --bin bat -- localized-status
|
cargo run -p bat-infrastructure --bin bat -- localized-status
|
||||||
cargo run -p bat-infrastructure --bin bat -- resource-index --limit 50
|
cargo run -p bat-infrastructure --bin bat -- resource-index --limit 50
|
||||||
|
cargo run -p bat-infrastructure --bin bat -- resource-index --release-id <ID> --platform windows --archive-entry <PATH> --format json --limit 50
|
||||||
```
|
```
|
||||||
|
|
||||||
`parse-status` 会额外显示 TextUnit 明细索引和队列摘要;`parse-text-units` /
|
`parse-status` 会额外显示 TextUnit 明细索引和队列摘要;`parse-text-units` /
|
||||||
`parse-errors` 可按 destination、archive entry、path id、class id、field path
|
`parse-errors` 可按 destination、archive entry、path id、class id、field path
|
||||||
和 format 分页查询当前官方 release 的 TextUnit 明细与解析错误;
|
和 format 分页查询当前官方 release 的 TextUnit 明细与解析错误;
|
||||||
`resource-index` 返回的资源 JSON 包含 release、平台、bundle path、TextAsset 和 TextUnit metadata;
|
`resource-index` 返回的资源 JSON 包含 release、平台、bundle path、TextAsset 和 TextUnit metadata,
|
||||||
|
并可按 release、平台、destination、bundle path、archive entry、parse status 和 TextUnit format 做资源级过滤;
|
||||||
`localized-status` 只有在 `localized-version-state.json`、`current` symlink 和
|
`localized-status` 只有在 `localized-version-state.json`、`current` symlink 和
|
||||||
`localized-patch-manifest.json` 都匹配当前官方 release 时才返回 `localized`。
|
`localized-patch-manifest.json` 都匹配当前官方 release 时才返回 `localized`。
|
||||||
|
|
||||||
|
|||||||
@@ -106,14 +106,18 @@ contract 为准,不应绕过 daemon 状态文件或扩展 `bat-ffi` 作为主
|
|||||||
| `resource.repair` | 已实现 | `null` | `{ "task_id": "...", "kind": "resource.repair" }`。 |
|
| `resource.repair` | 已实现 | `null` | `{ "task_id": "...", "kind": "resource.repair" }`。 |
|
||||||
| `resource.manifest` | 已实现 | `{ "offset": 0, "limit": 100 }` | 当前 download manifest 分页。 |
|
| `resource.manifest` | 已实现 | `{ "offset": 0, "limit": 100 }` | 当前 download manifest 分页。 |
|
||||||
| `resource.list` | 已实现 | `{ "offset": 0, "limit": 100 }` | `resource.manifest` 的兼容别名。 |
|
| `resource.list` | 已实现 | `{ "offset": 0, "limit": 100 }` | `resource.manifest` 的兼容别名。 |
|
||||||
| `resource.index` | 已实现 | `{ "offset": 0, "limit": 100, "type": "asset_bundle", "hash": "...", "path_pattern": "*" }` | 当前 `ResourceRepository` 分页/过滤查询。 |
|
| `resource.index` | 已实现 | `{ "offset": 0, "limit": 100, "type": "asset_bundle", "hash": "...", "path_pattern": "*", "release_id": "...", "platform": "windows", "destination": "...", "archive_entry": "...", "parse_status": "parsed", "format": "json" }` | 当前 `ResourceRepository` 分页/过滤查询。 |
|
||||||
|
|
||||||
`resource.repair` 会开启本地 manifest audit + repair,不继承 `force`。
|
`resource.repair` 会开启本地 manifest audit + repair,不继承 `force`。
|
||||||
`resource.manifest` / `resource.list` 查询当前已发布 release 的
|
`resource.manifest` / `resource.list` 查询当前已发布 release 的
|
||||||
`official-download-manifest.json`;`resource.index` 查询可选导入产生的
|
`official-download-manifest.json`;`resource.index` 查询可选导入产生的
|
||||||
SQLite `ResourceRepository`,索引不存在时返回 `ok=true` 且
|
SQLite `ResourceRepository`,索引不存在时返回 `ok=true` 且
|
||||||
`data.available=false`,不会隐式创建数据库。`limit` 范围是 `1..=1000`,
|
`data.available=false`,不会隐式创建数据库。`resource.index` 可按
|
||||||
非法参数返回 `BAT-ERR-700002`。
|
`resource_type`/`type`、`hash`、`path_pattern`、`official_release_id`/`release_id`、
|
||||||
|
`platform`、`destination`、`bundle_path`、`archive_entry`、`parse_status` 和
|
||||||
|
`text_unit_format`/`format` 过滤;`path_id`、`class_id` 和 `field_path`
|
||||||
|
属于 `parse.text_units` / `parse.errors` 的对象级查询。`limit` 范围是
|
||||||
|
`1..=1000`,非法参数返回 `BAT-ERR-700002`。
|
||||||
|
|
||||||
`resource.index` 的 `entries[]` 是 `Resource` JSON,除 `id`、`local_path`、
|
`resource.index` 的 `entries[]` 是 `Resource` JSON,除 `id`、`local_path`、
|
||||||
`entry` 外会包含 `metadata`:`official_release_id`、`platform`、
|
`entry` 外会包含 `metadata`:`official_release_id`、`platform`、
|
||||||
|
|||||||
@@ -282,7 +282,7 @@
|
|||||||
- `SqliteResourceRepository` 已存在,可按领域 repository 接口保存资源元数据。
|
- `SqliteResourceRepository` 已存在,可按领域 repository 接口保存资源元数据。
|
||||||
- `ResourceImportService` 已能把 manifest 中有数据的资源写入 CAS + `ResourceRepository`,AssetBundle 会记录 UnityFS 摘要,TextAsset/Table/Media 会分类索引。
|
- `ResourceImportService` 已能把 manifest 中有数据的资源写入 CAS + `ResourceRepository`,AssetBundle 会记录 UnityFS 摘要,TextAsset/Table/Media 会分类索引。
|
||||||
- 官方同步下载结果可用 `--import-repository` / `BAT_IMPORT_REPOSITORY=1` 在已校验 release 发布后自动导入 CAS + ResourceRepository;默认 CAS 为 `<output>/.cas`,默认 SQLite 索引为 `<output>/resources.sqlite`,也可通过 `--import-cas-root`、`--import-resource-db`、`BAT_IMPORT_CAS_ROOT`、`BAT_IMPORT_RESOURCE_DB` 覆盖。
|
- 官方同步下载结果可用 `--import-repository` / `BAT_IMPORT_REPOSITORY=1` 在已校验 release 发布后自动导入 CAS + ResourceRepository;默认 CAS 为 `<output>/.cas`,默认 SQLite 索引为 `<output>/resources.sqlite`,也可通过 `--import-cas-root`、`--import-resource-db`、`BAT_IMPORT_CAS_ROOT`、`BAT_IMPORT_RESOURCE_DB` 覆盖。
|
||||||
- `resource.index` RPC 已能按资源类型、hash、路径模式分页查询现有 SQLite 索引;数据库不存在时返回 `available=false`,不会因查询创建空库。
|
- `resource.index` RPC/CLI 已能按资源类型、hash、路径模式、官方 release ID、平台、destination、bundle path、archive entry、parse status 和 TextUnit format 分页查询现有 SQLite 索引;数据库不存在时返回 `available=false`,不会因查询创建空库。
|
||||||
- 官方 release 发布后会写出 `official-resource-changes.json` 和 `crowdin-translation-handoff.json`,新增+变更资源进入解析/翻译 handoff,删除资源只进入差异记录。
|
- 官方 release 发布后会写出 `official-resource-changes.json` 和 `crowdin-translation-handoff.json`,新增+变更资源进入解析/翻译 handoff,删除资源只进入差异记录。
|
||||||
- `Resource` metadata 已通过 SQLite `metadata_json` 兼容迁移保存 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 数量/格式;`resource.index` 会返回这些 metadata。
|
- `Resource` metadata 已通过 SQLite `metadata_json` 兼容迁移保存 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 数量/格式;`resource.index` 会返回这些 metadata。
|
||||||
- 官方 release 发布后会持久化 `official-textunit-index.json`,记录单条 TextUnit 和解析错误;`parse.text_units` / `parse.errors` RPC 和 `parse-text-units` / `parse-errors` CLI 可按 destination、archive entry、path id、class id、field path 和 format 分页过滤。
|
- 官方 release 发布后会持久化 `official-textunit-index.json`,记录单条 TextUnit 和解析错误;`parse.text_units` / `parse.errors` RPC 和 `parse-text-units` / `parse-errors` CLI 可按 destination、archive entry、path id、class id、field path 和 format 分页过滤。
|
||||||
@@ -292,7 +292,7 @@
|
|||||||
验收:
|
验收:
|
||||||
|
|
||||||
- schema 和迁移可重复执行。
|
- schema 和迁移可重复执行。
|
||||||
- 可按版本、类型、hash、路径查询资源,且能明确区分索引缺失、版本缺失和空结果。
|
- 可按版本、平台、类型、hash、路径、destination、archive entry、parse status 和 TextUnit format 查询资源,且能明确区分索引缺失、版本缺失和空结果。
|
||||||
- 官方同步后的资源可通过 CLI/RPC 查询并能追溯到 CAS 对象。
|
- 官方同步后的资源可通过 CLI/RPC 查询并能追溯到 CAS 对象。
|
||||||
- `official-parse-cache.json` 的 bundle、zip entry、TextAsset 和 TextUnit 摘要能进入 ResourceRepository 查询面。
|
- `official-parse-cache.json` 的 bundle、zip entry、TextAsset 和 TextUnit 摘要能进入 ResourceRepository 查询面。
|
||||||
- `parse.status` 能报告 TextUnit 索引、TextUnit 队列路径与摘要。
|
- `parse.status` 能报告 TextUnit 索引、TextUnit 队列路径与摘要。
|
||||||
|
|||||||
@@ -234,8 +234,12 @@ struct CliOptions {
|
|||||||
query_resource_type: Option<ResourceType>,
|
query_resource_type: Option<ResourceType>,
|
||||||
query_hash: Option<String>,
|
query_hash: Option<String>,
|
||||||
query_path_pattern: Option<String>,
|
query_path_pattern: Option<String>,
|
||||||
|
query_official_release_id: Option<String>,
|
||||||
|
query_platform: Option<String>,
|
||||||
query_destination: Option<String>,
|
query_destination: Option<String>,
|
||||||
|
query_bundle_path: Option<String>,
|
||||||
query_archive_entry: Option<String>,
|
query_archive_entry: Option<String>,
|
||||||
|
query_parse_status: Option<String>,
|
||||||
query_path_id: Option<i64>,
|
query_path_id: Option<i64>,
|
||||||
query_class_id: Option<i32>,
|
query_class_id: Option<i32>,
|
||||||
query_field_path: Option<String>,
|
query_field_path: Option<String>,
|
||||||
@@ -286,8 +290,12 @@ impl Default for CliOptions {
|
|||||||
query_resource_type: None,
|
query_resource_type: None,
|
||||||
query_hash: None,
|
query_hash: None,
|
||||||
query_path_pattern: None,
|
query_path_pattern: None,
|
||||||
|
query_official_release_id: None,
|
||||||
|
query_platform: None,
|
||||||
query_destination: None,
|
query_destination: None,
|
||||||
|
query_bundle_path: None,
|
||||||
query_archive_entry: None,
|
query_archive_entry: None,
|
||||||
|
query_parse_status: None,
|
||||||
query_path_id: None,
|
query_path_id: None,
|
||||||
query_class_id: None,
|
query_class_id: None,
|
||||||
query_field_path: None,
|
query_field_path: None,
|
||||||
@@ -2782,6 +2790,13 @@ fn build_resource_index_report(
|
|||||||
"resource_type": query.resource_type,
|
"resource_type": query.resource_type,
|
||||||
"hash": query.hash,
|
"hash": query.hash,
|
||||||
"path_pattern": query.path_pattern,
|
"path_pattern": query.path_pattern,
|
||||||
|
"official_release_id": query.official_release_id,
|
||||||
|
"platform": query.platform,
|
||||||
|
"destination": query.destination,
|
||||||
|
"bundle_path": query.bundle_path,
|
||||||
|
"archive_entry": query.archive_entry,
|
||||||
|
"parse_status": query.parse_status,
|
||||||
|
"text_unit_format": query.text_unit_format,
|
||||||
},
|
},
|
||||||
"entries": entries,
|
"entries": entries,
|
||||||
}))
|
}))
|
||||||
@@ -3243,6 +3258,17 @@ fn rpc_resource_index_params(
|
|||||||
resource_type,
|
resource_type,
|
||||||
hash: rpc_string_param(params, "hash").map(str::to_string),
|
hash: rpc_string_param(params, "hash").map(str::to_string),
|
||||||
path_pattern: rpc_string_param(params, "path_pattern").map(str::to_string),
|
path_pattern: rpc_string_param(params, "path_pattern").map(str::to_string),
|
||||||
|
official_release_id: rpc_string_param(params, "official_release_id")
|
||||||
|
.or_else(|| rpc_string_param(params, "release_id"))
|
||||||
|
.map(str::to_string),
|
||||||
|
platform: rpc_string_param(params, "platform").map(str::to_string),
|
||||||
|
destination: rpc_string_param(params, "destination").map(str::to_string),
|
||||||
|
bundle_path: rpc_string_param(params, "bundle_path").map(str::to_string),
|
||||||
|
archive_entry: rpc_string_param(params, "archive_entry").map(str::to_string),
|
||||||
|
parse_status: rpc_string_param(params, "parse_status").map(str::to_string),
|
||||||
|
text_unit_format: rpc_string_param(params, "text_unit_format")
|
||||||
|
.or_else(|| rpc_string_param(params, "format"))
|
||||||
|
.map(str::to_string),
|
||||||
};
|
};
|
||||||
Ok((query, offset, limit))
|
Ok((query, offset, limit))
|
||||||
}
|
}
|
||||||
@@ -4244,6 +4270,33 @@ fn readonly_query_rpc_params(options: &CliOptions) -> Option<serde_json::Value>
|
|||||||
if let Some(path_pattern) = options.query_path_pattern.as_ref() {
|
if let Some(path_pattern) = options.query_path_pattern.as_ref() {
|
||||||
params.insert("path_pattern".to_string(), serde_json::json!(path_pattern));
|
params.insert("path_pattern".to_string(), serde_json::json!(path_pattern));
|
||||||
}
|
}
|
||||||
|
if let Some(release_id) = options.query_official_release_id.as_ref() {
|
||||||
|
params.insert(
|
||||||
|
"official_release_id".to_string(),
|
||||||
|
serde_json::json!(release_id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(platform) = options.query_platform.as_ref() {
|
||||||
|
params.insert("platform".to_string(), serde_json::json!(platform));
|
||||||
|
}
|
||||||
|
if let Some(destination) = options.query_destination.as_ref() {
|
||||||
|
params.insert("destination".to_string(), serde_json::json!(destination));
|
||||||
|
}
|
||||||
|
if let Some(bundle_path) = options.query_bundle_path.as_ref() {
|
||||||
|
params.insert("bundle_path".to_string(), serde_json::json!(bundle_path));
|
||||||
|
}
|
||||||
|
if let Some(archive_entry) = options.query_archive_entry.as_ref() {
|
||||||
|
params.insert(
|
||||||
|
"archive_entry".to_string(),
|
||||||
|
serde_json::json!(archive_entry),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(parse_status) = options.query_parse_status.as_ref() {
|
||||||
|
params.insert("parse_status".to_string(), serde_json::json!(parse_status));
|
||||||
|
}
|
||||||
|
if let Some(format) = options.query_format.as_ref() {
|
||||||
|
params.insert("text_unit_format".to_string(), serde_json::json!(format));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
CliCommand::ParseTextUnits | CliCommand::ParseErrors => {
|
CliCommand::ParseTextUnits | CliCommand::ParseErrors => {
|
||||||
if let Some(destination) = options.query_destination.as_ref() {
|
if let Some(destination) = options.query_destination.as_ref() {
|
||||||
@@ -4314,27 +4367,28 @@ fn build_readonly_query_report(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn validate_readonly_query_options(options: &CliOptions) -> anyhow::Result<()> {
|
fn validate_readonly_query_options(options: &CliOptions) -> anyhow::Result<()> {
|
||||||
let has_resource_index_filter =
|
let has_resource_index_filter = options.query_resource_type.is_some()
|
||||||
options.query_resource_type.is_some() || options.query_hash.is_some();
|
|| options.query_hash.is_some()
|
||||||
let has_textunit_filter = options.query_destination.is_some()
|
|| options.query_official_release_id.is_some()
|
||||||
|| options.query_archive_entry.is_some()
|
|| options.query_platform.is_some()
|
||||||
|| options.query_path_id.is_some()
|
|| options.query_bundle_path.is_some()
|
||||||
|
|| options.query_parse_status.is_some();
|
||||||
|
let has_parse_object_filter = options.query_path_id.is_some()
|
||||||
|| options.query_class_id.is_some()
|
|| options.query_class_id.is_some()
|
||||||
|| options.query_field_path.is_some()
|
|| options.query_field_path.is_some();
|
||||||
|| options.query_format.is_some();
|
|
||||||
|
|
||||||
match options.command {
|
match options.command {
|
||||||
CliCommand::ResourceIndex => {
|
CliCommand::ResourceIndex => {
|
||||||
if has_textunit_filter {
|
if has_parse_object_filter {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"--destination/--archive-entry/--path-id/--class-id/--field-path/--format 只适用于 parse-text-units 或 parse-errors"
|
"--path-id/--class-id/--field-path 只适用于 parse-text-units 或 parse-errors"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CliCommand::ParseTextUnits | CliCommand::ParseErrors => {
|
CliCommand::ParseTextUnits | CliCommand::ParseErrors => {
|
||||||
if has_resource_index_filter {
|
if has_resource_index_filter {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"--resource-type/--hash 只适用于 resource-index"
|
"--resource-type/--hash/--release-id/--platform/--bundle-path/--parse-status 只适用于 resource-index"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4526,6 +4580,13 @@ fn resource_index_query_from_options(options: &CliOptions) -> ResourceQuery {
|
|||||||
resource_type: options.query_resource_type,
|
resource_type: options.query_resource_type,
|
||||||
hash: options.query_hash.clone(),
|
hash: options.query_hash.clone(),
|
||||||
path_pattern: options.query_path_pattern.clone(),
|
path_pattern: options.query_path_pattern.clone(),
|
||||||
|
official_release_id: options.query_official_release_id.clone(),
|
||||||
|
platform: options.query_platform.clone(),
|
||||||
|
destination: options.query_destination.clone(),
|
||||||
|
bundle_path: options.query_bundle_path.clone(),
|
||||||
|
archive_entry: options.query_archive_entry.clone(),
|
||||||
|
parse_status: options.query_parse_status.clone(),
|
||||||
|
text_unit_format: options.query_format.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7261,14 +7322,30 @@ fn parse_args_with_env(
|
|||||||
options.query_path_pattern = Some(next_option_value(&mut args, &flag)?);
|
options.query_path_pattern = Some(next_option_value(&mut args, &flag)?);
|
||||||
options.query_option_explicit = true;
|
options.query_option_explicit = true;
|
||||||
}
|
}
|
||||||
|
"--release-id" | "--official-release-id" => {
|
||||||
|
options.query_official_release_id = Some(next_option_value(&mut args, &flag)?);
|
||||||
|
options.query_option_explicit = true;
|
||||||
|
}
|
||||||
|
"--platform" => {
|
||||||
|
options.query_platform = Some(next_option_value(&mut args, &flag)?);
|
||||||
|
options.query_option_explicit = true;
|
||||||
|
}
|
||||||
"--destination" => {
|
"--destination" => {
|
||||||
options.query_destination = Some(next_option_value(&mut args, &flag)?);
|
options.query_destination = Some(next_option_value(&mut args, &flag)?);
|
||||||
options.query_option_explicit = true;
|
options.query_option_explicit = true;
|
||||||
}
|
}
|
||||||
|
"--bundle-path" => {
|
||||||
|
options.query_bundle_path = Some(next_option_value(&mut args, &flag)?);
|
||||||
|
options.query_option_explicit = true;
|
||||||
|
}
|
||||||
"--archive-entry" => {
|
"--archive-entry" => {
|
||||||
options.query_archive_entry = Some(next_option_value(&mut args, &flag)?);
|
options.query_archive_entry = Some(next_option_value(&mut args, &flag)?);
|
||||||
options.query_option_explicit = true;
|
options.query_option_explicit = true;
|
||||||
}
|
}
|
||||||
|
"--parse-status" => {
|
||||||
|
options.query_parse_status = Some(next_option_value(&mut args, &flag)?);
|
||||||
|
options.query_option_explicit = true;
|
||||||
|
}
|
||||||
"--path-id" => {
|
"--path-id" => {
|
||||||
options.query_path_id = Some(
|
options.query_path_id = Some(
|
||||||
next_option_value(&mut args, &flag)?
|
next_option_value(&mut args, &flag)?
|
||||||
@@ -7675,12 +7752,16 @@ fn print_usage(binary: &str) {
|
|||||||
eprintln!(
|
eprintln!(
|
||||||
" --path-pattern <GLOB> Filter resource-index or parse detail by path pattern"
|
" --path-pattern <GLOB> Filter resource-index or parse detail by path pattern"
|
||||||
);
|
);
|
||||||
eprintln!(" --destination <PATH> Filter parse detail by official destination");
|
eprintln!(" --release-id <ID> Filter resource-index by official release ID");
|
||||||
eprintln!(" --archive-entry <PATH> Filter parse detail by ZIP/archive entry");
|
eprintln!(" --platform <NAME> Filter resource-index by metadata platform");
|
||||||
|
eprintln!(" --destination <PATH> Filter resource-index or parse detail by official destination");
|
||||||
|
eprintln!(" --bundle-path <PATH> Filter resource-index by metadata bundle path");
|
||||||
|
eprintln!(" --archive-entry <PATH> Filter resource-index or parse detail by ZIP/archive entry");
|
||||||
|
eprintln!(" --parse-status <STATUS> Filter resource-index by parse status");
|
||||||
eprintln!(" --path-id <ID> Filter parse detail by Unity object path ID");
|
eprintln!(" --path-id <ID> Filter parse detail by Unity object path ID");
|
||||||
eprintln!(" --class-id <ID> Filter parse detail by Unity class ID");
|
eprintln!(" --class-id <ID> Filter parse detail by Unity class ID");
|
||||||
eprintln!(" --field-path <PATH> Filter parse detail, or TypeTree field path after UnityFS field patch commands");
|
eprintln!(" --field-path <PATH> Filter parse detail, or TypeTree field path after UnityFS field patch commands");
|
||||||
eprintln!(" --format <NAME> Filter parse text units by payload format");
|
eprintln!(" --format <NAME> Filter resource-index or parse text units by payload format");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!("Write patch:");
|
eprintln!("Write patch:");
|
||||||
eprintln!(" --patch-kind <binary|json|text> Patch type for patch-apply");
|
eprintln!(" --patch-kind <binary|json|text> Patch type for patch-apply");
|
||||||
@@ -8332,6 +8413,20 @@ mod tests {
|
|||||||
"abc",
|
"abc",
|
||||||
"--path-pattern",
|
"--path-pattern",
|
||||||
"TextAssets/**",
|
"TextAssets/**",
|
||||||
|
"--release-id",
|
||||||
|
"v-current",
|
||||||
|
"--platform",
|
||||||
|
"windows",
|
||||||
|
"--destination",
|
||||||
|
"TextAssets/Scenario.json",
|
||||||
|
"--bundle-path",
|
||||||
|
"Bundles/story.bundle",
|
||||||
|
"--archive-entry",
|
||||||
|
"story/Scenario.json",
|
||||||
|
"--parse-status",
|
||||||
|
"parsed",
|
||||||
|
"--format",
|
||||||
|
"json",
|
||||||
"--offset",
|
"--offset",
|
||||||
"5",
|
"5",
|
||||||
"--limit",
|
"--limit",
|
||||||
@@ -8342,6 +8437,25 @@ mod tests {
|
|||||||
assert_eq!(index.query_resource_type, Some(ResourceType::TextAsset));
|
assert_eq!(index.query_resource_type, Some(ResourceType::TextAsset));
|
||||||
assert_eq!(index.query_hash.as_deref(), Some("abc"));
|
assert_eq!(index.query_hash.as_deref(), Some("abc"));
|
||||||
assert_eq!(index.query_path_pattern.as_deref(), Some("TextAssets/**"));
|
assert_eq!(index.query_path_pattern.as_deref(), Some("TextAssets/**"));
|
||||||
|
assert_eq!(
|
||||||
|
index.query_official_release_id.as_deref(),
|
||||||
|
Some("v-current")
|
||||||
|
);
|
||||||
|
assert_eq!(index.query_platform.as_deref(), Some("windows"));
|
||||||
|
assert_eq!(
|
||||||
|
index.query_destination.as_deref(),
|
||||||
|
Some("TextAssets/Scenario.json")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
index.query_bundle_path.as_deref(),
|
||||||
|
Some("Bundles/story.bundle")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
index.query_archive_entry.as_deref(),
|
||||||
|
Some("story/Scenario.json")
|
||||||
|
);
|
||||||
|
assert_eq!(index.query_parse_status.as_deref(), Some("parsed"));
|
||||||
|
assert_eq!(index.query_format.as_deref(), Some("json"));
|
||||||
assert_eq!(index.query_offset, 5);
|
assert_eq!(index.query_offset, 5);
|
||||||
assert_eq!(index.query_limit, 25);
|
assert_eq!(index.query_limit, 25);
|
||||||
|
|
||||||
@@ -8388,6 +8502,11 @@ mod tests {
|
|||||||
|
|
||||||
let error = parse(&["bat", "parse-text-units", "--hash", "abc"]).unwrap_err();
|
let error = parse(&["bat", "parse-text-units", "--hash", "abc"]).unwrap_err();
|
||||||
assert!(error.to_string().contains("--resource-type/--hash"));
|
assert!(error.to_string().contains("--resource-type/--hash"));
|
||||||
|
|
||||||
|
let error = parse(&["bat", "resource-index", "--path-id", "42"]).unwrap_err();
|
||||||
|
assert!(error
|
||||||
|
.to_string()
|
||||||
|
.contains("--path-id/--class-id/--field-path"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -9871,6 +9990,20 @@ mod tests {
|
|||||||
state_dir.to_str().unwrap(),
|
state_dir.to_str().unwrap(),
|
||||||
"--resource-type",
|
"--resource-type",
|
||||||
"asset_bundle",
|
"asset_bundle",
|
||||||
|
"--release-id",
|
||||||
|
"v-current",
|
||||||
|
"--platform",
|
||||||
|
"windows",
|
||||||
|
"--destination",
|
||||||
|
"Bundles/story.bundle",
|
||||||
|
"--bundle-path",
|
||||||
|
"Bundles/story.bundle",
|
||||||
|
"--archive-entry",
|
||||||
|
"story/TextAsset",
|
||||||
|
"--parse-status",
|
||||||
|
"parsed",
|
||||||
|
"--format",
|
||||||
|
"json",
|
||||||
"--offset",
|
"--offset",
|
||||||
"2",
|
"2",
|
||||||
"--limit",
|
"--limit",
|
||||||
@@ -9896,6 +10029,13 @@ mod tests {
|
|||||||
assert_eq!(seen[0].0, RPC_METHOD_RESOURCE_INDEX);
|
assert_eq!(seen[0].0, RPC_METHOD_RESOURCE_INDEX);
|
||||||
let params = seen[0].1.as_ref().unwrap();
|
let params = seen[0].1.as_ref().unwrap();
|
||||||
assert_eq!(params["resource_type"], "asset_bundle");
|
assert_eq!(params["resource_type"], "asset_bundle");
|
||||||
|
assert_eq!(params["official_release_id"], "v-current");
|
||||||
|
assert_eq!(params["platform"], "windows");
|
||||||
|
assert_eq!(params["destination"], "Bundles/story.bundle");
|
||||||
|
assert_eq!(params["bundle_path"], "Bundles/story.bundle");
|
||||||
|
assert_eq!(params["archive_entry"], "story/TextAsset");
|
||||||
|
assert_eq!(params["parse_status"], "parsed");
|
||||||
|
assert_eq!(params["text_unit_format"], "json");
|
||||||
assert_eq!(params["offset"], 2);
|
assert_eq!(params["offset"], 2);
|
||||||
assert_eq!(params["limit"], 3);
|
assert_eq!(params["limit"], 3);
|
||||||
}
|
}
|
||||||
@@ -10500,6 +10640,8 @@ mod tests {
|
|||||||
official_release_id: Some("v-current".to_string()),
|
official_release_id: Some("v-current".to_string()),
|
||||||
platform: Some("windows".to_string()),
|
platform: Some("windows".to_string()),
|
||||||
bundle_path: Some(path.to_string()),
|
bundle_path: Some(path.to_string()),
|
||||||
|
archive_entries: vec!["story/Scenario.json".to_string()],
|
||||||
|
parse_statuses: vec!["parsed".to_string()],
|
||||||
text_assets: vec!["Scenario".to_string()],
|
text_assets: vec!["Scenario".to_string()],
|
||||||
text_unit_count: 4,
|
text_unit_count: 4,
|
||||||
text_unit_formats: vec!["json".to_string()],
|
text_unit_formats: vec!["json".to_string()],
|
||||||
@@ -10573,6 +10715,13 @@ mod tests {
|
|||||||
"resource.index",
|
"resource.index",
|
||||||
Some(serde_json::json!({
|
Some(serde_json::json!({
|
||||||
"resource_type": "text_asset",
|
"resource_type": "text_asset",
|
||||||
|
"release_id": "v-current",
|
||||||
|
"platform": "windows",
|
||||||
|
"destination": "TextAssets/c.json",
|
||||||
|
"bundle_path": "TextAssets/c.json",
|
||||||
|
"archive_entry": "story/Scenario.json",
|
||||||
|
"parse_status": "parsed",
|
||||||
|
"format": "json",
|
||||||
"offset": 0,
|
"offset": 0,
|
||||||
"limit": 10
|
"limit": 10
|
||||||
})),
|
})),
|
||||||
@@ -10597,8 +10746,23 @@ mod tests {
|
|||||||
assert_eq!(entries[0]["entry"]["resource_type"], "TextAsset");
|
assert_eq!(entries[0]["entry"]["resource_type"], "TextAsset");
|
||||||
assert_eq!(entries[0]["metadata"]["official_release_id"], "v-current");
|
assert_eq!(entries[0]["metadata"]["official_release_id"], "v-current");
|
||||||
assert_eq!(entries[0]["metadata"]["platform"], "windows");
|
assert_eq!(entries[0]["metadata"]["platform"], "windows");
|
||||||
|
assert_eq!(
|
||||||
|
entries[0]["metadata"]["archive_entries"][0],
|
||||||
|
"story/Scenario.json"
|
||||||
|
);
|
||||||
|
assert_eq!(entries[0]["metadata"]["parse_statuses"][0], "parsed");
|
||||||
assert_eq!(entries[0]["metadata"]["text_assets"][0], "Scenario");
|
assert_eq!(entries[0]["metadata"]["text_assets"][0], "Scenario");
|
||||||
assert_eq!(entries[0]["metadata"]["text_unit_count"], 4);
|
assert_eq!(entries[0]["metadata"]["text_unit_count"], 4);
|
||||||
|
assert_eq!(value["data"]["query"]["official_release_id"], "v-current");
|
||||||
|
assert_eq!(value["data"]["query"]["platform"], "windows");
|
||||||
|
assert_eq!(value["data"]["query"]["destination"], "TextAssets/c.json");
|
||||||
|
assert_eq!(value["data"]["query"]["bundle_path"], "TextAssets/c.json");
|
||||||
|
assert_eq!(
|
||||||
|
value["data"]["query"]["archive_entry"],
|
||||||
|
"story/Scenario.json"
|
||||||
|
);
|
||||||
|
assert_eq!(value["data"]["query"]["parse_status"], "parsed");
|
||||||
|
assert_eq!(value["data"]["query"]["text_unit_format"], "json");
|
||||||
|
|
||||||
let envelope = dispatch_rpc_method(
|
let envelope = dispatch_rpc_method(
|
||||||
&rpc_request(
|
&rpc_request(
|
||||||
|
|||||||
@@ -326,6 +326,12 @@ impl SqliteResourceRepository {
|
|||||||
.push(" ESCAPE '\\'");
|
.push(" ESCAPE '\\'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(destination) = &query.destination {
|
||||||
|
push_condition_prefix(builder, &mut has_where);
|
||||||
|
builder.push("path = ");
|
||||||
|
builder.push_bind(destination);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,10 +355,21 @@ impl SqliteResourceRepository {
|
|||||||
.await
|
.await
|
||||||
.map_err(|error| bat_core::Error::Other(error.into()))?;
|
.map_err(|error| bat_core::Error::Other(error.into()))?;
|
||||||
|
|
||||||
rows.into_iter().map(Self::resource_from_row).collect()
|
let resources = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(Self::resource_from_row)
|
||||||
|
.collect::<bat_core::Result<Vec<_>>>()?;
|
||||||
|
Ok(resources
|
||||||
|
.into_iter()
|
||||||
|
.filter(|resource| query_matches(query, resource))
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn count_resources(&self, query: &ResourceQuery) -> bat_core::Result<u64> {
|
async fn count_resources(&self, query: &ResourceQuery) -> bat_core::Result<u64> {
|
||||||
|
if query.requires_resource_scan() {
|
||||||
|
return Ok(self.fetch_resources(query, None).await?.len() as u64);
|
||||||
|
}
|
||||||
|
|
||||||
let mut builder = QueryBuilder::<Sqlite>::new("SELECT COUNT(*) FROM resources");
|
let mut builder = QueryBuilder::<Sqlite>::new("SELECT COUNT(*) FROM resources");
|
||||||
Self::apply_filters(&mut builder, query)?;
|
Self::apply_filters(&mut builder, query)?;
|
||||||
|
|
||||||
@@ -536,6 +553,63 @@ fn query_matches(query: &ResourceQuery, resource: &Resource) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(release_id) = &query.official_release_id {
|
||||||
|
if resource.metadata.official_release_id.as_deref() != Some(release_id.as_str()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(platform) = &query.platform {
|
||||||
|
if resource.metadata.platform.as_deref() != Some(platform.as_str()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(destination) = &query.destination {
|
||||||
|
if resource.entry.path != *destination {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(bundle_path) = &query.bundle_path {
|
||||||
|
if resource.metadata.bundle_path.as_deref() != Some(bundle_path.as_str()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(archive_entry) = &query.archive_entry {
|
||||||
|
if !resource
|
||||||
|
.metadata
|
||||||
|
.archive_entries
|
||||||
|
.iter()
|
||||||
|
.any(|entry| entry == archive_entry)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(parse_status) = &query.parse_status {
|
||||||
|
if !resource
|
||||||
|
.metadata
|
||||||
|
.parse_statuses
|
||||||
|
.iter()
|
||||||
|
.any(|status| status == parse_status)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(text_unit_format) = &query.text_unit_format {
|
||||||
|
if !resource
|
||||||
|
.metadata
|
||||||
|
.text_unit_formats
|
||||||
|
.iter()
|
||||||
|
.any(|format| format == text_unit_format)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,6 +715,7 @@ mod tests {
|
|||||||
resource_type: Some(ResourceType::AssetBundle),
|
resource_type: Some(ResourceType::AssetBundle),
|
||||||
hash: Some("hash-a".to_string()),
|
hash: Some("hash-a".to_string()),
|
||||||
path_pattern: Some("synthetic-*.bundle".to_string()),
|
path_pattern: Some("synthetic-*.bundle".to_string()),
|
||||||
|
..ResourceQuery::all()
|
||||||
};
|
};
|
||||||
|
|
||||||
let results = repository.list(query).await.unwrap();
|
let results = repository.list(query).await.unwrap();
|
||||||
@@ -653,6 +728,52 @@ mod tests {
|
|||||||
assert_eq!(repository.count(ResourceQuery::all()).await.unwrap(), 2);
|
assert_eq!(repository.count(ResourceQuery::all()).await.unwrap(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_filters_by_release_parse_and_textunit_metadata() {
|
||||||
|
let repository = InMemoryResourceRepository::new();
|
||||||
|
let mut matching = resource(
|
||||||
|
"resource/text-a",
|
||||||
|
"TextAssets/a.json",
|
||||||
|
"hash-a",
|
||||||
|
ResourceType::TextAsset,
|
||||||
|
);
|
||||||
|
matching.metadata.official_release_id = Some("v-current".to_string());
|
||||||
|
matching.metadata.platform = Some("windows".to_string());
|
||||||
|
matching.metadata.bundle_path = Some("Bundles/story.bundle".to_string());
|
||||||
|
matching.metadata.archive_entries = vec!["story/Scenario.json".to_string()];
|
||||||
|
matching.metadata.parse_statuses = vec!["parsed".to_string()];
|
||||||
|
matching.metadata.text_unit_formats = vec!["json".to_string()];
|
||||||
|
repository.add(matching).await.unwrap();
|
||||||
|
|
||||||
|
let mut stale = resource(
|
||||||
|
"resource/text-b",
|
||||||
|
"TextAssets/b.json",
|
||||||
|
"hash-b",
|
||||||
|
ResourceType::TextAsset,
|
||||||
|
);
|
||||||
|
stale.metadata.official_release_id = Some("v-old".to_string());
|
||||||
|
stale.metadata.platform = Some("android".to_string());
|
||||||
|
stale.metadata.parse_statuses = vec!["failed".to_string()];
|
||||||
|
stale.metadata.text_unit_formats = vec!["plain".to_string()];
|
||||||
|
repository.add(stale).await.unwrap();
|
||||||
|
|
||||||
|
let query = ResourceQuery {
|
||||||
|
official_release_id: Some("v-current".to_string()),
|
||||||
|
platform: Some("windows".to_string()),
|
||||||
|
destination: Some("TextAssets/a.json".to_string()),
|
||||||
|
bundle_path: Some("Bundles/story.bundle".to_string()),
|
||||||
|
archive_entry: Some("story/Scenario.json".to_string()),
|
||||||
|
parse_status: Some("parsed".to_string()),
|
||||||
|
text_unit_format: Some("json".to_string()),
|
||||||
|
..ResourceQuery::all()
|
||||||
|
};
|
||||||
|
|
||||||
|
let results = repository.list(query.clone()).await.unwrap();
|
||||||
|
assert_eq!(results.len(), 1);
|
||||||
|
assert_eq!(results[0].id, "resource/text-a");
|
||||||
|
assert_eq!(repository.count(query).await.unwrap(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
async fn sqlite_repository() -> (tempfile::TempDir, SqliteResourceRepository) {
|
async fn sqlite_repository() -> (tempfile::TempDir, SqliteResourceRepository) {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let repository = SqliteResourceRepository::new(temp_dir.path().join("resources.sqlite"))
|
let repository = SqliteResourceRepository::new(temp_dir.path().join("resources.sqlite"))
|
||||||
@@ -677,8 +798,12 @@ mod tests {
|
|||||||
.push("assets/shared.bundle".to_string());
|
.push("assets/shared.bundle".to_string());
|
||||||
resource.metadata.official_release_id = Some("release-1".to_string());
|
resource.metadata.official_release_id = Some("release-1".to_string());
|
||||||
resource.metadata.platform = Some("windows".to_string());
|
resource.metadata.platform = Some("windows".to_string());
|
||||||
|
resource.metadata.bundle_path = Some("assets/model.bundle".to_string());
|
||||||
|
resource.metadata.archive_entries = vec!["serialized/Scenario".to_string()];
|
||||||
|
resource.metadata.parse_statuses = vec!["parsed".to_string()];
|
||||||
resource.metadata.text_assets = vec!["Scenario".to_string()];
|
resource.metadata.text_assets = vec!["Scenario".to_string()];
|
||||||
resource.metadata.text_unit_count = 3;
|
resource.metadata.text_unit_count = 3;
|
||||||
|
resource.metadata.text_unit_formats = vec!["json".to_string()];
|
||||||
|
|
||||||
repository.add(resource.clone()).await.unwrap();
|
repository.add(resource.clone()).await.unwrap();
|
||||||
|
|
||||||
@@ -693,8 +818,18 @@ mod tests {
|
|||||||
Some("release-1")
|
Some("release-1")
|
||||||
);
|
);
|
||||||
assert_eq!(by_id.metadata.platform.as_deref(), Some("windows"));
|
assert_eq!(by_id.metadata.platform.as_deref(), Some("windows"));
|
||||||
|
assert_eq!(
|
||||||
|
by_id.metadata.bundle_path.as_deref(),
|
||||||
|
Some("assets/model.bundle")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
by_id.metadata.archive_entries,
|
||||||
|
vec!["serialized/Scenario".to_string()]
|
||||||
|
);
|
||||||
|
assert_eq!(by_id.metadata.parse_statuses, vec!["parsed".to_string()]);
|
||||||
assert_eq!(by_id.metadata.text_assets, vec!["Scenario".to_string()]);
|
assert_eq!(by_id.metadata.text_assets, vec!["Scenario".to_string()]);
|
||||||
assert_eq!(by_id.metadata.text_unit_count, 3);
|
assert_eq!(by_id.metadata.text_unit_count, 3);
|
||||||
|
assert_eq!(by_id.metadata.text_unit_formats, vec!["json".to_string()]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
repository.find_by_hash("hash-sqlite-a").await.unwrap().id,
|
repository.find_by_hash("hash-sqlite-a").await.unwrap().id,
|
||||||
resource.id
|
resource.id
|
||||||
@@ -709,6 +844,28 @@ mod tests {
|
|||||||
let count = repository.count(ResourceQuery::all()).await.unwrap();
|
let count = repository.count(ResourceQuery::all()).await.unwrap();
|
||||||
assert_eq!(count, 1);
|
assert_eq!(count, 1);
|
||||||
|
|
||||||
|
let query = ResourceQuery {
|
||||||
|
official_release_id: Some("release-1".to_string()),
|
||||||
|
platform: Some("windows".to_string()),
|
||||||
|
destination: Some("assets/model.bundle".to_string()),
|
||||||
|
bundle_path: Some("assets/model.bundle".to_string()),
|
||||||
|
archive_entry: Some("serialized/Scenario".to_string()),
|
||||||
|
parse_status: Some("parsed".to_string()),
|
||||||
|
text_unit_format: Some("json".to_string()),
|
||||||
|
..ResourceQuery::all()
|
||||||
|
};
|
||||||
|
let filtered = repository.list(query.clone()).await.unwrap();
|
||||||
|
assert_eq!(filtered.len(), 1);
|
||||||
|
assert_eq!(filtered[0].id, resource.id);
|
||||||
|
assert_eq!(repository.count(query).await.unwrap(), 1);
|
||||||
|
|
||||||
|
let missing = ResourceQuery {
|
||||||
|
official_release_id: Some("release-missing".to_string()),
|
||||||
|
..ResourceQuery::all()
|
||||||
|
};
|
||||||
|
assert!(repository.list(missing.clone()).await.unwrap().is_empty());
|
||||||
|
assert_eq!(repository.count(missing).await.unwrap(), 0);
|
||||||
|
|
||||||
repository.delete(&resource.id).await.unwrap();
|
repository.delete(&resource.id).await.unwrap();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
repository.find_by_id(&resource.id).await,
|
repository.find_by_id(&resource.id).await,
|
||||||
|
|||||||
Reference in New Issue
Block a user