mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 13:34:53 +08:00
feat(sync): 接入解析缓存与汉化发布前置
补齐官方 release 解析缓存、TextUnit 明细索引、资源变更集、Crowdin handoff 预留、ResourceRepository 导入元数据和 localized release patch 前置链路。 同时开放文件级 patch.apply 与 UnityFS TextAsset/string/semantic field patch CLI/RPC 入口,并保留官方原版资源与汉化产物双目录发布状态。 验证:cargo test -p bat-assetbundle --locked;cargo clippy -p bat-assetbundle --all-targets --locked -- -D warnings;cargo test -p bat-infrastructure --locked。
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
//! 内存资源仓储实现。
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bat_core::domain::{Resource, ResourceEntry, ResourceType};
|
||||
use bat_core::domain::{Resource, ResourceEntry, ResourceMetadata, ResourceType};
|
||||
use bat_core::repositories::resource_repository::{ResourceQuery, ResourceRepository};
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions, SqliteQueryResult};
|
||||
use sqlx::{QueryBuilder, Sqlite, SqlitePool};
|
||||
@@ -130,7 +130,8 @@ impl SqliteResourceRepository {
|
||||
local_path TEXT NOT NULL,
|
||||
address TEXT,
|
||||
dependencies_json TEXT NOT NULL DEFAULT '[]',
|
||||
crc INTEGER
|
||||
crc INTEGER,
|
||||
metadata_json TEXT NOT NULL DEFAULT '{}'
|
||||
)
|
||||
"#,
|
||||
),
|
||||
@@ -140,6 +141,13 @@ impl SqliteResourceRepository {
|
||||
// 向后兼容:早于 crc 列的旧库缺少该列,按需补加(新建库已含该列,
|
||||
// pragma 检查后不会重复 ALTER)。
|
||||
Self::ensure_column(&self.pool, "resources", "crc", "INTEGER").await?;
|
||||
Self::ensure_column(
|
||||
&self.pool,
|
||||
"resources",
|
||||
"metadata_json",
|
||||
"TEXT NOT NULL DEFAULT '{}'",
|
||||
)
|
||||
.await?;
|
||||
|
||||
Self::execute_query(
|
||||
&self.pool,
|
||||
@@ -250,9 +258,32 @@ impl SqliteResourceRepository {
|
||||
.map_err(|error| bat_core::Error::Serialization(error.to_string()))
|
||||
}
|
||||
|
||||
fn metadata_to_json(metadata: &ResourceMetadata) -> bat_core::Result<String> {
|
||||
serde_json::to_string(metadata)
|
||||
.map_err(|error| bat_core::Error::Serialization(error.to_string()))
|
||||
}
|
||||
|
||||
fn metadata_from_json(value: &str) -> bat_core::Result<ResourceMetadata> {
|
||||
if value.trim().is_empty() {
|
||||
return Ok(ResourceMetadata::default());
|
||||
}
|
||||
serde_json::from_str(value)
|
||||
.map_err(|error| bat_core::Error::Serialization(error.to_string()))
|
||||
}
|
||||
|
||||
fn resource_from_row(row: ResourceRow) -> bat_core::Result<Resource> {
|
||||
let (id, path, hash, size, resource_type, local_path, address, dependencies_json, crc) =
|
||||
row;
|
||||
let (
|
||||
id,
|
||||
path,
|
||||
hash,
|
||||
size,
|
||||
resource_type,
|
||||
local_path,
|
||||
address,
|
||||
dependencies_json,
|
||||
crc,
|
||||
metadata_json,
|
||||
) = row;
|
||||
Ok(Resource {
|
||||
id,
|
||||
local_path: PathBuf::from(local_path),
|
||||
@@ -265,6 +296,7 @@ impl SqliteResourceRepository {
|
||||
dependencies: Self::dependencies_from_json(&dependencies_json)?,
|
||||
crc: crc.and_then(|value| u32::try_from(value).ok()),
|
||||
},
|
||||
metadata: Self::metadata_from_json(&metadata_json)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -303,7 +335,7 @@ impl SqliteResourceRepository {
|
||||
limit: Option<usize>,
|
||||
) -> bat_core::Result<Vec<Resource>> {
|
||||
let mut builder = QueryBuilder::<Sqlite>::new(
|
||||
"SELECT id, path, hash, size, resource_type, local_path, address, dependencies_json, crc FROM resources",
|
||||
"SELECT id, path, hash, size, resource_type, local_path, address, dependencies_json, crc, metadata_json FROM resources",
|
||||
);
|
||||
Self::apply_filters(&mut builder, query)?;
|
||||
builder.push(" ORDER BY id");
|
||||
@@ -337,14 +369,15 @@ impl SqliteResourceRepository {
|
||||
impl ResourceRepository for SqliteResourceRepository {
|
||||
async fn add(&self, resource: Resource) -> bat_core::Result<String> {
|
||||
let dependencies = Self::dependencies_to_json(&resource.entry.dependencies)?;
|
||||
let metadata = Self::metadata_to_json(&resource.metadata)?;
|
||||
Self::execute_query(
|
||||
&self.pool,
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO resources (
|
||||
id, path, hash, size, resource_type, local_path, address, dependencies_json, crc
|
||||
id, path, hash, size, resource_type, local_path, address, dependencies_json, crc, metadata_json
|
||||
)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
path = excluded.path,
|
||||
hash = excluded.hash,
|
||||
@@ -353,7 +386,8 @@ impl ResourceRepository for SqliteResourceRepository {
|
||||
local_path = excluded.local_path,
|
||||
address = excluded.address,
|
||||
dependencies_json = excluded.dependencies_json,
|
||||
crc = excluded.crc
|
||||
crc = excluded.crc,
|
||||
metadata_json = excluded.metadata_json
|
||||
"#,
|
||||
)
|
||||
.bind(resource.id.clone())
|
||||
@@ -364,7 +398,8 @@ impl ResourceRepository for SqliteResourceRepository {
|
||||
.bind(resource.local_path.to_string_lossy().to_string())
|
||||
.bind(resource.entry.address.clone())
|
||||
.bind(dependencies)
|
||||
.bind(resource.entry.crc.map(i64::from)),
|
||||
.bind(resource.entry.crc.map(i64::from))
|
||||
.bind(metadata),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -374,7 +409,7 @@ impl ResourceRepository for SqliteResourceRepository {
|
||||
async fn find_by_id(&self, id: &str) -> bat_core::Result<Resource> {
|
||||
let row: Option<ResourceRow> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT id, path, hash, size, resource_type, local_path, address, dependencies_json, crc
|
||||
SELECT id, path, hash, size, resource_type, local_path, address, dependencies_json, crc, metadata_json
|
||||
FROM resources
|
||||
WHERE id = ?1
|
||||
"#,
|
||||
@@ -392,7 +427,7 @@ impl ResourceRepository for SqliteResourceRepository {
|
||||
async fn find_by_hash(&self, hash: &str) -> bat_core::Result<Resource> {
|
||||
let row: Option<ResourceRow> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT id, path, hash, size, resource_type, local_path, address, dependencies_json, crc
|
||||
SELECT id, path, hash, size, resource_type, local_path, address, dependencies_json, crc, metadata_json
|
||||
FROM resources
|
||||
WHERE hash = ?1
|
||||
ORDER BY id
|
||||
@@ -455,6 +490,7 @@ type ResourceRow = (
|
||||
Option<String>,
|
||||
String,
|
||||
Option<i64>,
|
||||
String,
|
||||
);
|
||||
|
||||
fn glob_to_like(pattern: &str) -> String {
|
||||
@@ -544,6 +580,7 @@ mod tests {
|
||||
dependencies: Vec::new(),
|
||||
crc: None,
|
||||
},
|
||||
metadata: ResourceMetadata::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -638,6 +675,10 @@ mod tests {
|
||||
.entry
|
||||
.dependencies
|
||||
.push("assets/shared.bundle".to_string());
|
||||
resource.metadata.official_release_id = Some("release-1".to_string());
|
||||
resource.metadata.platform = Some("windows".to_string());
|
||||
resource.metadata.text_assets = vec!["Scenario".to_string()];
|
||||
resource.metadata.text_unit_count = 3;
|
||||
|
||||
repository.add(resource.clone()).await.unwrap();
|
||||
|
||||
@@ -647,6 +688,13 @@ mod tests {
|
||||
by_id.entry.dependencies,
|
||||
vec!["assets/shared.bundle".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
by_id.metadata.official_release_id.as_deref(),
|
||||
Some("release-1")
|
||||
);
|
||||
assert_eq!(by_id.metadata.platform.as_deref(), Some("windows"));
|
||||
assert_eq!(by_id.metadata.text_assets, vec!["Scenario".to_string()]);
|
||||
assert_eq!(by_id.metadata.text_unit_count, 3);
|
||||
assert_eq!(
|
||||
repository.find_by_hash("hash-sqlite-a").await.unwrap().id,
|
||||
resource.id
|
||||
|
||||
Reference in New Issue
Block a user