mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 11:25:15 +08:00
feat: implement production cas v1
This commit is contained in:
@@ -1,4 +1,276 @@
|
||||
//! 引用计数模块占位
|
||||
//! CAS 引用计数和对象元数据。
|
||||
|
||||
/// 引用计数管理(待实现)
|
||||
pub struct RefCounter;
|
||||
use crate::error::{CasError, Result};
|
||||
use crate::hash::Hash;
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
||||
use sqlx::SqlitePool;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// CAS 对象元数据。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ObjectMetadata {
|
||||
/// 对象 Hash。
|
||||
pub hash: Hash,
|
||||
/// 对象大小,单位字节。
|
||||
pub size: u64,
|
||||
/// 当前引用计数。
|
||||
pub ref_count: u64,
|
||||
/// 创建时间,Unix 秒。
|
||||
pub created_at: i64,
|
||||
/// 更新时间,Unix 秒。
|
||||
pub updated_at: i64,
|
||||
/// 引用计数首次归零时间,Unix 秒。
|
||||
pub zero_ref_at: Option<i64>,
|
||||
}
|
||||
|
||||
/// SQLite 引用计数存储。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SqliteRefCounter {
|
||||
pool: SqlitePool,
|
||||
}
|
||||
|
||||
impl SqliteRefCounter {
|
||||
/// 打开或创建 SQLite 元数据库。
|
||||
pub async fn new(path: impl AsRef<Path>) -> 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| CasError::Database(error.to_string()))?
|
||||
.create_if_missing(true);
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect_with(options)
|
||||
.await?;
|
||||
|
||||
let counter = Self { pool };
|
||||
counter.init_schema().await?;
|
||||
Ok(counter)
|
||||
}
|
||||
|
||||
async fn init_schema(&self) -> Result<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS cas_objects (
|
||||
hash TEXT PRIMARY KEY NOT NULL,
|
||||
size INTEGER NOT NULL CHECK(size >= 0),
|
||||
ref_count INTEGER NOT NULL CHECK(ref_count >= 0),
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
zero_ref_at INTEGER
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE INDEX IF NOT EXISTS idx_cas_objects_ref_count
|
||||
ON cas_objects(ref_count)
|
||||
"#,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn now() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_secs() as i64)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 注册对象元数据。如果对象已存在,仅更新大小和更新时间。
|
||||
pub async fn ensure_object(&self, hash: &Hash, size: u64) -> Result<()> {
|
||||
let now = Self::now();
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO cas_objects(hash, size, ref_count, created_at, updated_at, zero_ref_at)
|
||||
VALUES(?1, ?2, 0, ?3, ?3, ?3)
|
||||
ON CONFLICT(hash) DO UPDATE SET
|
||||
size = excluded.size,
|
||||
updated_at = excluded.updated_at
|
||||
"#,
|
||||
)
|
||||
.bind(hash.to_string())
|
||||
.bind(size as i64)
|
||||
.bind(now)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 增加对象引用计数。
|
||||
pub async fn add_reference(&self, hash: &Hash) -> Result<u64> {
|
||||
let now = Self::now();
|
||||
let updated: Option<i64> = sqlx::query_scalar(
|
||||
r#"
|
||||
UPDATE cas_objects
|
||||
SET ref_count = ref_count + 1,
|
||||
updated_at = ?1,
|
||||
zero_ref_at = NULL
|
||||
WHERE hash = ?2
|
||||
RETURNING ref_count
|
||||
"#,
|
||||
)
|
||||
.bind(now)
|
||||
.bind(hash.to_string())
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
updated
|
||||
.map(|count| count as u64)
|
||||
.ok_or_else(|| CasError::ObjectNotFound(hash.to_string()))
|
||||
}
|
||||
|
||||
/// 减少对象引用计数。
|
||||
pub async fn remove_reference(&self, hash: &Hash) -> Result<u64> {
|
||||
let now = Self::now();
|
||||
let updated: Option<i64> = sqlx::query_scalar(
|
||||
r#"
|
||||
UPDATE cas_objects
|
||||
SET ref_count = ref_count - 1,
|
||||
updated_at = ?1,
|
||||
zero_ref_at = CASE WHEN ref_count = 1 THEN ?1 ELSE zero_ref_at END
|
||||
WHERE hash = ?2 AND ref_count > 0
|
||||
RETURNING ref_count
|
||||
"#,
|
||||
)
|
||||
.bind(now)
|
||||
.bind(hash.to_string())
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
if let Some(count) = updated {
|
||||
return Ok(count as u64);
|
||||
}
|
||||
|
||||
if self.metadata(hash).await?.is_some() {
|
||||
Err(CasError::ReferenceUnderflow(hash.to_string()))
|
||||
} else {
|
||||
Err(CasError::ObjectNotFound(hash.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取对象引用计数。
|
||||
pub async fn get_reference_count(&self, hash: &Hash) -> Result<u64> {
|
||||
let count: Option<i64> =
|
||||
sqlx::query_scalar("SELECT ref_count FROM cas_objects WHERE hash = ?1")
|
||||
.bind(hash.to_string())
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
count
|
||||
.map(|count| count as u64)
|
||||
.ok_or_else(|| CasError::ObjectNotFound(hash.to_string()))
|
||||
}
|
||||
|
||||
/// 获取对象元数据。
|
||||
pub async fn metadata(&self, hash: &Hash) -> Result<Option<ObjectMetadata>> {
|
||||
let row: Option<(String, i64, i64, i64, i64, Option<i64>)> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT hash, size, ref_count, created_at, updated_at, zero_ref_at
|
||||
FROM cas_objects
|
||||
WHERE hash = ?1
|
||||
"#,
|
||||
)
|
||||
.bind(hash.to_string())
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
row.map(
|
||||
|(hash, size, ref_count, created_at, updated_at, zero_ref_at)| {
|
||||
Ok(ObjectMetadata {
|
||||
hash: hash.parse()?,
|
||||
size: size as u64,
|
||||
ref_count: ref_count as u64,
|
||||
created_at,
|
||||
updated_at,
|
||||
zero_ref_at,
|
||||
})
|
||||
},
|
||||
)
|
||||
.transpose()
|
||||
}
|
||||
|
||||
/// 列出引用计数为 0 的对象。
|
||||
pub async fn zero_ref_objects(&self) -> Result<Vec<Hash>> {
|
||||
let hashes: Vec<String> =
|
||||
sqlx::query_scalar("SELECT hash FROM cas_objects WHERE ref_count = 0")
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
hashes
|
||||
.into_iter()
|
||||
.map(|hash| hash.parse())
|
||||
.collect::<Result<Vec<_>>>()
|
||||
}
|
||||
|
||||
/// 删除对象元数据。
|
||||
pub async fn delete_metadata(&self, hash: &Hash) -> Result<bool> {
|
||||
let result = sqlx::query("DELETE FROM cas_objects WHERE hash = ?1")
|
||||
.bind(hash.to_string())
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::hash::compute_hash;
|
||||
|
||||
#[tokio::test]
|
||||
async fn reference_count_lifecycle() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let counter = SqliteRefCounter::new(temp_dir.path().join("metadata.sqlite"))
|
||||
.await
|
||||
.unwrap();
|
||||
let hash = compute_hash(b"tracked object");
|
||||
|
||||
counter.ensure_object(&hash, 14).await.unwrap();
|
||||
assert_eq!(counter.add_reference(&hash).await.unwrap(), 1);
|
||||
assert_eq!(counter.add_reference(&hash).await.unwrap(), 2);
|
||||
assert_eq!(counter.remove_reference(&hash).await.unwrap(), 1);
|
||||
assert_eq!(counter.remove_reference(&hash).await.unwrap(), 0);
|
||||
assert_eq!(counter.get_reference_count(&hash).await.unwrap(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn zero_reference_objects_are_listed() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let counter = SqliteRefCounter::new(temp_dir.path().join("metadata.sqlite"))
|
||||
.await
|
||||
.unwrap();
|
||||
let hash = compute_hash(b"candidate");
|
||||
|
||||
counter.ensure_object(&hash, 9).await.unwrap();
|
||||
counter.add_reference(&hash).await.unwrap();
|
||||
counter.remove_reference(&hash).await.unwrap();
|
||||
|
||||
assert_eq!(counter.zero_ref_objects().await.unwrap(), vec![hash]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn removing_zero_reference_returns_underflow() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let counter = SqliteRefCounter::new(temp_dir.path().join("metadata.sqlite"))
|
||||
.await
|
||||
.unwrap();
|
||||
let hash = compute_hash(b"underflow");
|
||||
|
||||
counter.ensure_object(&hash, 9).await.unwrap();
|
||||
|
||||
let error = counter.remove_reference(&hash).await.unwrap_err();
|
||||
assert!(matches!(error, CasError::ReferenceUnderflow(_)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user