mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 04:06:44 +08:00
Addressables catalog 里的 m_Crc(bundle IEEE CRC-32)此前从未解析;
声明的 size 也只作元数据、无校验能力。本次:
- ResourceEntry 新增 crc: Option<u32>(serde default 向后兼容),
compact 与 expanded 两种 catalog 形态均解析 m_Crc/m_Crc→crc
- core 新增 crc32_ieee(IEEE CRC-32,等价 zlib/Unity m_Crc)与
ResourceEntry::{declared_crc, verify_downloaded_bytes}:按声明的
size/crc 校验字节,0 视为「无 CRC」跳过
- SqliteResourceRepository 持久化 crc 列,旧库经幂等 ensure_column
迁移补列(pragma_table_info 判断后 ALTER)
- golden 投影与 fixture 补 crc 字段,验证真实形态 catalog 提取贯通
校验 API 暂不接入 import 覆盖路径(该路径按 CAS id 重写 hash/size 是
既定语义,且合成测试的声明值不匹配实际字节);接入下载/导入校验留
待 G-011。
验证:core crc32 标准向量 + verify 分支单测、expanded 形态非零 crc
提取单测、golden 端到端;core/adapters/infrastructure 全测试 + fmt +
clippy --all-targets -D warnings 全绿。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
671 lines
21 KiB
Rust
671 lines
21 KiB
Rust
//! 内存资源仓储实现。
|
|
|
|
use async_trait::async_trait;
|
|
use bat_core::domain::{Resource, ResourceEntry, ResourceType};
|
|
use bat_core::repositories::resource_repository::{ResourceQuery, ResourceRepository};
|
|
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions, SqliteQueryResult};
|
|
use sqlx::{QueryBuilder, Sqlite, SqlitePool};
|
|
use std::collections::HashMap;
|
|
use std::path::{Path, PathBuf};
|
|
use std::str::FromStr;
|
|
use tokio::sync::RwLock;
|
|
|
|
/// 以内存 `HashMap` 保存资源索引的仓储实现。
|
|
#[derive(Debug, Default)]
|
|
pub struct InMemoryResourceRepository {
|
|
resources: RwLock<HashMap<String, Resource>>,
|
|
}
|
|
|
|
impl InMemoryResourceRepository {
|
|
/// 创建空资源仓储。
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
fn sorted_resources(resources: &HashMap<String, Resource>) -> Vec<Resource> {
|
|
let mut resources = resources.values().cloned().collect::<Vec<_>>();
|
|
resources.sort_by(|left, right| left.id.cmp(&right.id));
|
|
resources
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ResourceRepository for InMemoryResourceRepository {
|
|
async fn add(&self, resource: Resource) -> bat_core::Result<String> {
|
|
let id = resource.id.clone();
|
|
self.resources.write().await.insert(id.clone(), resource);
|
|
Ok(id)
|
|
}
|
|
|
|
async fn find_by_id(&self, id: &str) -> bat_core::Result<Resource> {
|
|
self.resources
|
|
.read()
|
|
.await
|
|
.get(id)
|
|
.cloned()
|
|
.ok_or_else(|| bat_core::Error::NotFound(id.to_string()))
|
|
}
|
|
|
|
async fn find_by_hash(&self, hash: &str) -> bat_core::Result<Resource> {
|
|
let resources = self.resources.read().await;
|
|
Self::sorted_resources(&resources)
|
|
.into_iter()
|
|
.find(|resource| resource.entry.hash == hash)
|
|
.ok_or_else(|| bat_core::Error::NotFound(hash.to_string()))
|
|
}
|
|
|
|
async fn list(&self, query: ResourceQuery) -> bat_core::Result<Vec<Resource>> {
|
|
let resources = self.resources.read().await;
|
|
let resources = Self::sorted_resources(&resources)
|
|
.into_iter()
|
|
.filter(|resource| query_matches(&query, resource))
|
|
.collect();
|
|
Ok(resources)
|
|
}
|
|
|
|
async fn update(&self, resource: Resource) -> bat_core::Result<()> {
|
|
let mut resources = self.resources.write().await;
|
|
if !resources.contains_key(&resource.id) {
|
|
return Err(bat_core::Error::NotFound(resource.id));
|
|
}
|
|
|
|
resources.insert(resource.id.clone(), resource);
|
|
Ok(())
|
|
}
|
|
|
|
async fn delete(&self, id: &str) -> bat_core::Result<()> {
|
|
self.resources
|
|
.write()
|
|
.await
|
|
.remove(id)
|
|
.map(|_| ())
|
|
.ok_or_else(|| bat_core::Error::NotFound(id.to_string()))
|
|
}
|
|
|
|
async fn count(&self, query: ResourceQuery) -> bat_core::Result<u64> {
|
|
Ok(self.list(query).await?.len() as u64)
|
|
}
|
|
}
|
|
|
|
/// SQLite 资源仓储实现。
|
|
#[derive(Debug, Clone)]
|
|
pub struct SqliteResourceRepository {
|
|
pool: SqlitePool,
|
|
}
|
|
|
|
impl SqliteResourceRepository {
|
|
/// 打开或创建 SQLite 资源仓储。
|
|
pub async fn new(path: impl AsRef<Path>) -> bat_core::Result<Self> {
|
|
if let Some(parent) = path.as_ref().parent() {
|
|
tokio::fs::create_dir_all(parent).await?;
|
|
}
|
|
|
|
let options =
|
|
SqliteConnectOptions::from_str(&format!("sqlite://{}", path.as_ref().display()))
|
|
.map_err(|error| bat_core::Error::Other(error.into()))?
|
|
.create_if_missing(true);
|
|
|
|
let pool = SqlitePoolOptions::new()
|
|
.max_connections(1)
|
|
.connect_with(options)
|
|
.await
|
|
.map_err(|error| bat_core::Error::Other(error.into()))?;
|
|
|
|
let repository = Self { pool };
|
|
repository.init_schema().await?;
|
|
Ok(repository)
|
|
}
|
|
|
|
async fn init_schema(&self) -> bat_core::Result<()> {
|
|
Self::execute_query(
|
|
&self.pool,
|
|
sqlx::query(
|
|
r#"
|
|
CREATE TABLE IF NOT EXISTS resources (
|
|
id TEXT PRIMARY KEY NOT NULL,
|
|
path TEXT NOT NULL,
|
|
hash TEXT NOT NULL,
|
|
size INTEGER NOT NULL CHECK(size >= 0),
|
|
resource_type TEXT NOT NULL,
|
|
local_path TEXT NOT NULL,
|
|
address TEXT,
|
|
dependencies_json TEXT NOT NULL DEFAULT '[]',
|
|
crc INTEGER
|
|
)
|
|
"#,
|
|
),
|
|
)
|
|
.await?;
|
|
|
|
// 向后兼容:早于 crc 列的旧库缺少该列,按需补加(新建库已含该列,
|
|
// pragma 检查后不会重复 ALTER)。
|
|
Self::ensure_column(&self.pool, "resources", "crc", "INTEGER").await?;
|
|
|
|
Self::execute_query(
|
|
&self.pool,
|
|
sqlx::query(
|
|
r#"
|
|
CREATE INDEX IF NOT EXISTS idx_resources_hash
|
|
ON resources(hash)
|
|
"#,
|
|
),
|
|
)
|
|
.await?;
|
|
|
|
Self::execute_query(
|
|
&self.pool,
|
|
sqlx::query(
|
|
r#"
|
|
CREATE INDEX IF NOT EXISTS idx_resources_type
|
|
ON resources(resource_type)
|
|
"#,
|
|
),
|
|
)
|
|
.await?;
|
|
|
|
Self::execute_query(
|
|
&self.pool,
|
|
sqlx::query(
|
|
r#"
|
|
CREATE INDEX IF NOT EXISTS idx_resources_path
|
|
ON resources(path)
|
|
"#,
|
|
),
|
|
)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn execute_query<'q>(
|
|
pool: &SqlitePool,
|
|
query: sqlx::query::Query<'q, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'q>>,
|
|
) -> bat_core::Result<SqliteQueryResult> {
|
|
query
|
|
.execute(pool)
|
|
.await
|
|
.map_err(|error| bat_core::Error::Other(error.into()))
|
|
}
|
|
|
|
/// 幂等地为 `table` 补加 `column`(若尚不存在)。用于向后兼容的 schema 迁移。
|
|
async fn ensure_column(
|
|
pool: &SqlitePool,
|
|
table: &str,
|
|
column: &str,
|
|
column_type: &str,
|
|
) -> bat_core::Result<()> {
|
|
let exists: i64 =
|
|
sqlx::query_scalar("SELECT COUNT(*) FROM pragma_table_info(?1) WHERE name = ?2")
|
|
.bind(table)
|
|
.bind(column)
|
|
.fetch_one(pool)
|
|
.await
|
|
.map_err(|error| bat_core::Error::Other(error.into()))?;
|
|
if exists == 0 {
|
|
// 表名/列名/类型均为内部常量,非用户输入,可安全内插。
|
|
Self::execute_query(
|
|
pool,
|
|
sqlx::query(&format!(
|
|
"ALTER TABLE {table} ADD COLUMN {column} {column_type}"
|
|
)),
|
|
)
|
|
.await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn resource_type_to_str(resource_type: ResourceType) -> &'static str {
|
|
match resource_type {
|
|
ResourceType::AssetBundle => "AssetBundle",
|
|
ResourceType::Manifest => "Manifest",
|
|
ResourceType::TableBundle => "TableBundle",
|
|
ResourceType::TextAsset => "TextAsset",
|
|
ResourceType::Media => "Media",
|
|
ResourceType::Other => "Other",
|
|
}
|
|
}
|
|
|
|
fn resource_type_from_str(value: &str) -> bat_core::Result<ResourceType> {
|
|
match value {
|
|
"AssetBundle" => Ok(ResourceType::AssetBundle),
|
|
"Manifest" => Ok(ResourceType::Manifest),
|
|
"TableBundle" => Ok(ResourceType::TableBundle),
|
|
"TextAsset" => Ok(ResourceType::TextAsset),
|
|
"Media" => Ok(ResourceType::Media),
|
|
"Other" => Ok(ResourceType::Other),
|
|
other => Err(bat_core::Error::Serialization(format!(
|
|
"Unknown resource type: {}",
|
|
other
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn dependencies_to_json(dependencies: &[String]) -> bat_core::Result<String> {
|
|
serde_json::to_string(dependencies)
|
|
.map_err(|error| bat_core::Error::Serialization(error.to_string()))
|
|
}
|
|
|
|
fn dependencies_from_json(value: &str) -> bat_core::Result<Vec<String>> {
|
|
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;
|
|
Ok(Resource {
|
|
id,
|
|
local_path: PathBuf::from(local_path),
|
|
entry: ResourceEntry {
|
|
path,
|
|
hash,
|
|
size: size as u64,
|
|
resource_type: Self::resource_type_from_str(&resource_type)?,
|
|
address,
|
|
dependencies: Self::dependencies_from_json(&dependencies_json)?,
|
|
crc: crc.and_then(|value| u32::try_from(value).ok()),
|
|
},
|
|
})
|
|
}
|
|
|
|
fn apply_filters<'a>(
|
|
builder: &mut QueryBuilder<'a, Sqlite>,
|
|
query: &'a ResourceQuery,
|
|
) -> bat_core::Result<()> {
|
|
let mut has_where = false;
|
|
|
|
if let Some(resource_type) = query.resource_type {
|
|
push_condition_prefix(builder, &mut has_where);
|
|
builder.push("resource_type = ");
|
|
builder.push_bind(Self::resource_type_to_str(resource_type));
|
|
}
|
|
|
|
if let Some(hash) = &query.hash {
|
|
push_condition_prefix(builder, &mut has_where);
|
|
builder.push("hash = ");
|
|
builder.push_bind(hash);
|
|
}
|
|
|
|
if let Some(pattern) = &query.path_pattern {
|
|
push_condition_prefix(builder, &mut has_where);
|
|
builder.push("path LIKE ");
|
|
builder
|
|
.push_bind(glob_to_like(pattern))
|
|
.push(" ESCAPE '\\'");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn fetch_resources(
|
|
&self,
|
|
query: &ResourceQuery,
|
|
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",
|
|
);
|
|
Self::apply_filters(&mut builder, query)?;
|
|
builder.push(" ORDER BY id");
|
|
if let Some(limit) = limit {
|
|
builder.push(" LIMIT ").push_bind(limit as i64);
|
|
}
|
|
|
|
let rows: Vec<ResourceRow> = builder
|
|
.build_query_as()
|
|
.fetch_all(&self.pool)
|
|
.await
|
|
.map_err(|error| bat_core::Error::Other(error.into()))?;
|
|
|
|
rows.into_iter().map(Self::resource_from_row).collect()
|
|
}
|
|
|
|
async fn count_resources(&self, query: &ResourceQuery) -> bat_core::Result<u64> {
|
|
let mut builder = QueryBuilder::<Sqlite>::new("SELECT COUNT(*) FROM resources");
|
|
Self::apply_filters(&mut builder, query)?;
|
|
|
|
let count: i64 = builder
|
|
.build_query_scalar()
|
|
.fetch_one(&self.pool)
|
|
.await
|
|
.map_err(|error| bat_core::Error::Other(error.into()))?;
|
|
Ok(count as u64)
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ResourceRepository for SqliteResourceRepository {
|
|
async fn add(&self, resource: Resource) -> bat_core::Result<String> {
|
|
let dependencies = Self::dependencies_to_json(&resource.entry.dependencies)?;
|
|
Self::execute_query(
|
|
&self.pool,
|
|
sqlx::query(
|
|
r#"
|
|
INSERT INTO resources (
|
|
id, path, hash, size, resource_type, local_path, address, dependencies_json, crc
|
|
)
|
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
path = excluded.path,
|
|
hash = excluded.hash,
|
|
size = excluded.size,
|
|
resource_type = excluded.resource_type,
|
|
local_path = excluded.local_path,
|
|
address = excluded.address,
|
|
dependencies_json = excluded.dependencies_json,
|
|
crc = excluded.crc
|
|
"#,
|
|
)
|
|
.bind(resource.id.clone())
|
|
.bind(resource.entry.path.clone())
|
|
.bind(resource.entry.hash.clone())
|
|
.bind(resource.entry.size as i64)
|
|
.bind(Self::resource_type_to_str(resource.entry.resource_type))
|
|
.bind(resource.local_path.to_string_lossy().to_string())
|
|
.bind(resource.entry.address.clone())
|
|
.bind(dependencies)
|
|
.bind(resource.entry.crc.map(i64::from)),
|
|
)
|
|
.await?;
|
|
|
|
Ok(resource.id)
|
|
}
|
|
|
|
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
|
|
FROM resources
|
|
WHERE id = ?1
|
|
"#,
|
|
)
|
|
.bind(id)
|
|
.fetch_optional(&self.pool)
|
|
.await
|
|
.map_err(|error| bat_core::Error::Other(error.into()))?;
|
|
|
|
row.map(Self::resource_from_row)
|
|
.transpose()?
|
|
.ok_or_else(|| bat_core::Error::NotFound(id.to_string()))
|
|
}
|
|
|
|
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
|
|
FROM resources
|
|
WHERE hash = ?1
|
|
ORDER BY id
|
|
LIMIT 1
|
|
"#,
|
|
)
|
|
.bind(hash)
|
|
.fetch_optional(&self.pool)
|
|
.await
|
|
.map_err(|error| bat_core::Error::Other(error.into()))?;
|
|
|
|
row.map(Self::resource_from_row)
|
|
.transpose()?
|
|
.ok_or_else(|| bat_core::Error::NotFound(hash.to_string()))
|
|
}
|
|
|
|
async fn list(&self, query: ResourceQuery) -> bat_core::Result<Vec<Resource>> {
|
|
self.fetch_resources(&query, None).await
|
|
}
|
|
|
|
async fn update(&self, resource: Resource) -> bat_core::Result<()> {
|
|
let _existing = self.find_by_id(&resource.id).await?;
|
|
self.add(resource).await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn delete(&self, id: &str) -> bat_core::Result<()> {
|
|
let result = Self::execute_query(
|
|
&self.pool,
|
|
sqlx::query("DELETE FROM resources WHERE id = ?1").bind(id),
|
|
)
|
|
.await?;
|
|
if result.rows_affected() == 0 {
|
|
return Err(bat_core::Error::NotFound(id.to_string()));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn count(&self, query: ResourceQuery) -> bat_core::Result<u64> {
|
|
self.count_resources(&query).await
|
|
}
|
|
}
|
|
|
|
fn push_condition_prefix<'a>(builder: &mut QueryBuilder<'a, Sqlite>, has_where: &mut bool) {
|
|
if *has_where {
|
|
builder.push(" AND ");
|
|
} else {
|
|
builder.push(" WHERE ");
|
|
*has_where = true;
|
|
}
|
|
}
|
|
|
|
type ResourceRow = (
|
|
String,
|
|
String,
|
|
String,
|
|
i64,
|
|
String,
|
|
String,
|
|
Option<String>,
|
|
String,
|
|
Option<i64>,
|
|
);
|
|
|
|
fn glob_to_like(pattern: &str) -> String {
|
|
let mut escaped = String::new();
|
|
let mut chars = pattern.chars().peekable();
|
|
|
|
while let Some(ch) = chars.next() {
|
|
match ch {
|
|
'*' => {
|
|
if matches!(chars.peek(), Some('*')) {
|
|
chars.next();
|
|
}
|
|
escaped.push('%');
|
|
}
|
|
'?' => escaped.push('_'),
|
|
'%' | '_' | '\\' => {
|
|
escaped.push('\\');
|
|
escaped.push(ch);
|
|
}
|
|
other => escaped.push(other),
|
|
}
|
|
}
|
|
|
|
escaped
|
|
}
|
|
|
|
fn query_matches(query: &ResourceQuery, resource: &Resource) -> bool {
|
|
if let Some(resource_type) = query.resource_type {
|
|
if resource.entry.resource_type != resource_type {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if let Some(hash) = &query.hash {
|
|
if resource.entry.hash != *hash {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if let Some(pattern) = &query.path_pattern {
|
|
if !wildcard_matches(pattern, &resource.entry.path) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
fn wildcard_matches(pattern: &str, value: &str) -> bool {
|
|
wildcard_matches_bytes(pattern.as_bytes(), value.as_bytes())
|
|
}
|
|
|
|
fn wildcard_matches_bytes(pattern: &[u8], value: &[u8]) -> bool {
|
|
match (pattern.first(), value.first()) {
|
|
(None, None) => true,
|
|
(None, Some(_)) => false,
|
|
(Some(b'*'), _) => {
|
|
wildcard_matches_bytes(&pattern[1..], value)
|
|
|| value
|
|
.first()
|
|
.is_some_and(|_| wildcard_matches_bytes(pattern, &value[1..]))
|
|
}
|
|
(Some(b'?'), Some(_)) => wildcard_matches_bytes(&pattern[1..], &value[1..]),
|
|
(Some(pattern_byte), Some(value_byte)) if pattern_byte == value_byte => {
|
|
wildcard_matches_bytes(&pattern[1..], &value[1..])
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use bat_core::domain::{ResourceEntry, ResourceType};
|
|
use std::path::PathBuf;
|
|
|
|
fn resource(id: &str, path: &str, hash: &str, resource_type: ResourceType) -> Resource {
|
|
Resource {
|
|
id: id.to_string(),
|
|
local_path: PathBuf::from(path),
|
|
entry: ResourceEntry {
|
|
path: path.to_string(),
|
|
hash: hash.to_string(),
|
|
size: 7,
|
|
resource_type,
|
|
address: None,
|
|
dependencies: Vec::new(),
|
|
crc: None,
|
|
},
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn add_find_update_and_delete_resource() {
|
|
let repository = InMemoryResourceRepository::new();
|
|
let id = repository
|
|
.add(resource(
|
|
"resource/synthetic-minimal.bundle",
|
|
"synthetic-minimal.bundle",
|
|
"hash-a",
|
|
ResourceType::AssetBundle,
|
|
))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(id, "resource/synthetic-minimal.bundle");
|
|
assert_eq!(
|
|
repository.find_by_id(&id).await.unwrap().entry.path,
|
|
"synthetic-minimal.bundle"
|
|
);
|
|
|
|
let mut updated = repository.find_by_id(&id).await.unwrap();
|
|
updated.entry.size = 12;
|
|
repository.update(updated).await.unwrap();
|
|
assert_eq!(repository.find_by_id(&id).await.unwrap().entry.size, 12);
|
|
|
|
repository.delete(&id).await.unwrap();
|
|
assert!(matches!(
|
|
repository.find_by_id(&id).await,
|
|
Err(bat_core::Error::NotFound(_))
|
|
));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn list_filters_by_type_hash_and_path() {
|
|
let repository = InMemoryResourceRepository::new();
|
|
repository
|
|
.add(resource(
|
|
"resource/a",
|
|
"synthetic-minimal.bundle",
|
|
"hash-a",
|
|
ResourceType::AssetBundle,
|
|
))
|
|
.await
|
|
.unwrap();
|
|
repository
|
|
.add(resource(
|
|
"resource/b",
|
|
"catalog.json",
|
|
"hash-b",
|
|
ResourceType::Manifest,
|
|
))
|
|
.await
|
|
.unwrap();
|
|
|
|
let query = ResourceQuery {
|
|
resource_type: Some(ResourceType::AssetBundle),
|
|
hash: Some("hash-a".to_string()),
|
|
path_pattern: Some("synthetic-*.bundle".to_string()),
|
|
};
|
|
|
|
let results = repository.list(query).await.unwrap();
|
|
assert_eq!(results.len(), 1);
|
|
assert_eq!(results[0].id, "resource/a");
|
|
assert_eq!(
|
|
repository.find_by_hash("hash-a").await.unwrap().id,
|
|
"resource/a"
|
|
);
|
|
assert_eq!(repository.count(ResourceQuery::all()).await.unwrap(), 2);
|
|
}
|
|
|
|
async fn sqlite_repository() -> (tempfile::TempDir, SqliteResourceRepository) {
|
|
let temp_dir = tempfile::tempdir().unwrap();
|
|
let repository = SqliteResourceRepository::new(temp_dir.path().join("resources.sqlite"))
|
|
.await
|
|
.unwrap();
|
|
(temp_dir, repository)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn sqlite_repository_persists_and_filters_resources() {
|
|
let (_temp_dir, repository) = sqlite_repository().await;
|
|
let mut resource = resource(
|
|
"resource/sqlite-a",
|
|
"assets/model.bundle",
|
|
"hash-sqlite-a",
|
|
ResourceType::AssetBundle,
|
|
);
|
|
resource.entry.address = Some("Character_001".to_string());
|
|
resource
|
|
.entry
|
|
.dependencies
|
|
.push("assets/shared.bundle".to_string());
|
|
|
|
repository.add(resource.clone()).await.unwrap();
|
|
|
|
let by_id = repository.find_by_id(&resource.id).await.unwrap();
|
|
assert_eq!(by_id.entry.address.as_deref(), Some("Character_001"));
|
|
assert_eq!(
|
|
by_id.entry.dependencies,
|
|
vec!["assets/shared.bundle".to_string()]
|
|
);
|
|
assert_eq!(
|
|
repository.find_by_hash("hash-sqlite-a").await.unwrap().id,
|
|
resource.id
|
|
);
|
|
|
|
let bundles = repository
|
|
.list(ResourceQuery::by_type(ResourceType::AssetBundle))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(bundles.len(), 1);
|
|
|
|
let count = repository.count(ResourceQuery::all()).await.unwrap();
|
|
assert_eq!(count, 1);
|
|
|
|
repository.delete(&resource.id).await.unwrap();
|
|
assert!(matches!(
|
|
repository.find_by_id(&resource.id).await,
|
|
Err(bat_core::Error::NotFound(_))
|
|
));
|
|
}
|
|
}
|