wip: the beginnings of a storage layer

This commit is contained in:
Jeremy Wall 2025-07-02 15:31:24 -05:00
parent 78f114254c
commit 2e673d5304
6 changed files with 2485 additions and 24 deletions

1580
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,3 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["exp1", "exp2", "offline-web-model"] members = ["exp1", "exp2", "offline-web-model", "offline-web-storage"]

View File

@ -61,9 +61,9 @@ impl Reference {
let mut cloned = self.clone(); let mut cloned = self.clone();
cloned.dependents.push(dep); cloned.dependents.push(dep);
// We ensure that our dependents are always sorted lexicographically by name. // We ensure that our dependents are always sorted lexicographically by name.
cloned.dependents.sort_by(|left, right| { cloned
left.name.cmp(&right.name) .dependents
}); .sort_by(|left, right| left.name.cmp(&right.name));
// Recalculate the ID based on dependents, content_address, and path // Recalculate the ID based on dependents, content_address, and path
let mut hasher = Self::initial_hash(&self.content_address, &self.name); let mut hasher = Self::initial_hash(&self.content_address, &self.name);
for dependent in &cloned.dependents { for dependent in &cloned.dependents {

View File

@ -0,0 +1,17 @@
[package]
name = "offline-web-storage"
version = "0.1.0"
edition = "2021"
[dependencies]
offline-web-model = { path = "../offline-web-model" }
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "uuid", "chrono"] }
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"
uuid = { version = "1.0", features = ["v4"] }
chrono = { version = "0.4", features = ["serde"] }
blake2 = "0.10"
[dev-dependencies]

View File

@ -0,0 +1,473 @@
use std::{collections::HashMap, sync::Arc};
use anyhow::Result;
use blake2::{Blake2b512, Digest};
use offline_web_model::Reference;
use sqlx::{Pool, Row, Sqlite, SqlitePool};
pub struct ReferenceStore {
pool: Pool<Sqlite>,
}
impl ReferenceStore {
pub async fn new(database_url: &str) -> Result<Self> {
let pool = SqlitePool::connect(database_url).await?;
let store = Self { pool };
store.initialize_schema().await?;
Ok(store)
}
async fn initialize_schema(&self) -> Result<()> {
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS refs (
id TEXT PRIMARY KEY,
content_address TEXT,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
"#,
)
.execute(&self.pool)
.await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS reference_dependencies (
parent_id TEXT NOT NULL,
dependent_id TEXT NOT NULL,
PRIMARY KEY (parent_id, dependent_id)
)
"#,
)
.execute(&self.pool)
.await?;
sqlx::query("CREATE INDEX IF NOT EXISTS idx_refs_name ON refs(name)")
.execute(&self.pool)
.await?;
sqlx::query("CREATE INDEX IF NOT EXISTS idx_refs_content_address ON refs(content_address)")
.execute(&self.pool)
.await?;
sqlx::query("CREATE INDEX IF NOT EXISTS idx_dependencies_parent ON reference_dependencies(parent_id)")
.execute(&self.pool)
.await?;
sqlx::query("CREATE INDEX IF NOT EXISTS idx_dependencies_dependent ON reference_dependencies(dependent_id)")
.execute(&self.pool)
.await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS content_objects (
content_address TEXT PRIMARY KEY,
content_data BLOB NOT NULL,
content_type TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
"#,
)
.execute(&self.pool)
.await?;
sqlx::query(
"CREATE INDEX IF NOT EXISTS idx_content_created ON content_objects(created_at)",
)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn store_reference(&self, reference: &Reference) -> Result<()> {
let mut tx = self.pool.begin().await?;
// Insert or update the reference
sqlx::query(
r#"
INSERT OR REPLACE INTO refs (id, content_address, name, updated_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
"#,
)
.bind(&reference.id)
.bind(&reference.content_address)
.bind(&reference.name)
.execute(&mut *tx)
.await?;
// Clear existing dependencies for this reference
sqlx::query("DELETE FROM reference_dependencies WHERE parent_id = ?")
.bind(&reference.id)
.execute(&mut *tx)
.await?;
// Insert new dependencies
for dependent in &reference.dependents {
sqlx::query(
"INSERT INTO reference_dependencies (parent_id, dependent_id) VALUES (?, ?)",
)
.bind(&reference.id)
.bind(&dependent.id)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(())
}
pub async fn get_reference(&self, id: &str) -> Result<Option<Reference>> {
// Get the reference record
let row = sqlx::query("SELECT id, content_address, name FROM refs WHERE id = ?")
.bind(id)
.fetch_optional(&self.pool)
.await?;
let Some(row) = row else {
return Ok(None);
};
let reference_id: String = row.get("id");
let content_address: Option<String> = row.get("content_address");
let name: String = row.get("name");
// Get dependencies
let dependents = self.get_dependents(&reference_id).await?;
Ok(Some(Reference {
id: reference_id,
content_address,
name,
dependents,
}))
}
fn get_dependents<'a>(
&'a self,
parent_id: &'a str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Vec<Arc<Reference>>>> + Send + 'a>>
{
Box::pin(async move {
let rows = sqlx::query(
r#"
SELECT r.id, r.content_address, r.name
FROM refs r
JOIN reference_dependencies rd ON r.id = rd.dependent_id
WHERE rd.parent_id = ?
ORDER BY r.name
"#,
)
.bind(parent_id)
.fetch_all(&self.pool)
.await?;
let mut dependents = Vec::new();
for row in rows {
let id: String = row.get("id");
let content_address: Option<String> = row.get("content_address");
let name: String = row.get("name");
// Recursively get dependents for each dependent
let nested_dependents = self.get_dependents(&id).await?;
dependents.push(Arc::new(Reference {
id,
content_address,
name,
dependents: nested_dependents,
}));
}
Ok(dependents)
})
}
pub async fn get_references_by_name(&self, name: &str) -> Result<Vec<Reference>> {
let rows = sqlx::query("SELECT id FROM refs WHERE name = ?")
.bind(name)
.fetch_all(&self.pool)
.await?;
let mut references = Vec::new();
for row in rows {
let id: String = row.get("id");
if let Some(reference) = self.get_reference(&id).await? {
references.push(reference);
}
}
Ok(references)
}
pub async fn get_references_by_content_address(
&self,
content_address: &str,
) -> Result<Vec<Reference>> {
let rows = sqlx::query("SELECT id FROM refs WHERE content_address = ?")
.bind(content_address)
.fetch_all(&self.pool)
.await?;
let mut references = Vec::new();
for row in rows {
let id: String = row.get("id");
if let Some(reference) = self.get_reference(&id).await? {
references.push(reference);
}
}
Ok(references)
}
pub async fn delete_reference(&self, id: &str) -> Result<bool> {
let result = sqlx::query("DELETE FROM refs WHERE id = ?")
.bind(id)
.execute(&self.pool)
.await?;
Ok(result.rows_affected() > 0)
}
pub async fn list_all_references(&self) -> Result<Vec<Reference>> {
let rows = sqlx::query("SELECT id FROM refs ORDER BY name")
.fetch_all(&self.pool)
.await?;
let mut references = Vec::new();
for row in rows {
let id: String = row.get("id");
if let Some(reference) = self.get_reference(&id).await? {
references.push(reference);
}
}
Ok(references)
}
pub async fn update_reference_graph(
&self,
updated_references: &HashMap<String, Arc<Reference>>,
) -> Result<()> {
let mut tx = self.pool.begin().await?;
for (_, reference) in updated_references {
// Update the reference
sqlx::query(
r#"
INSERT OR REPLACE INTO refs (id, content_address, name, updated_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
"#,
)
.bind(&reference.id)
.bind(&reference.content_address)
.bind(&reference.name)
.execute(&mut *tx)
.await?;
// Clear existing dependencies
sqlx::query("DELETE FROM reference_dependencies WHERE parent_id = ?")
.bind(&reference.id)
.execute(&mut *tx)
.await?;
// Insert new dependencies
for dependent in &reference.dependents {
sqlx::query(
"INSERT INTO reference_dependencies (parent_id, dependent_id) VALUES (?, ?)",
)
.bind(&reference.id)
.bind(&dependent.id)
.execute(&mut *tx)
.await?;
}
}
tx.commit().await?;
Ok(())
}
pub fn calculate_content_address(content: &[u8]) -> String {
let mut hasher = Blake2b512::new();
hasher.update(content);
format!("{:x}", hasher.finalize())
}
pub async fn store_content(
&self,
content: &[u8],
content_type: Option<String>,
) -> Result<String> {
let content_address = Self::calculate_content_address(content);
// Check if content already exists (deduplication)
let exists = sqlx::query("SELECT 1 FROM content_objects WHERE content_address = ?")
.bind(&content_address)
.fetch_optional(&self.pool)
.await?
.is_some();
if !exists {
sqlx::query(
r#"
INSERT INTO content_objects (content_address, content_data, content_type)
VALUES (?, ?, ?)
"#,
)
.bind(&content_address)
.bind(content)
.bind(&content_type)
.execute(&self.pool)
.await?;
}
Ok(content_address)
}
pub async fn get_content(&self, content_address: &str) -> Result<Option<Vec<u8>>> {
let row = sqlx::query("SELECT content_data FROM content_objects WHERE content_address = ?")
.bind(content_address)
.fetch_optional(&self.pool)
.await?;
Ok(row.map(|row| row.get::<Vec<u8>, _>("content_data")))
}
pub async fn get_content_info(&self, content_address: &str) -> Result<Option<ContentInfo>> {
let row = sqlx::query(
"SELECT content_type, created_at FROM content_objects WHERE content_address = ?",
)
.bind(content_address)
.fetch_optional(&self.pool)
.await?;
Ok(row.map(|row| ContentInfo {
content_address: content_address.to_string(),
content_type: row.get("content_type"),
size: 0, // Size not stored in database
created_at: row.get("created_at"),
}))
}
pub async fn content_exists(&self, content_address: &str) -> Result<bool> {
let exists = sqlx::query("SELECT 1 FROM content_objects WHERE content_address = ?")
.bind(content_address)
.fetch_optional(&self.pool)
.await?
.is_some();
Ok(exists)
}
pub async fn store_reference_with_content(
&self,
name: String,
content: &[u8],
content_type: Option<String>,
) -> Result<Reference> {
// Store the content and get its address
let content_address = self.store_content(content, content_type).await?;
// Create the reference
let reference = Reference::new(Some(content_address), name);
// Store the reference
self.store_reference(&reference).await?;
Ok(reference)
}
pub async fn get_reference_with_content(
&self,
id: &str,
) -> Result<Option<(Reference, Option<Vec<u8>>)>> {
let reference = self.get_reference(id).await?;
if let Some(ref reference) = reference {
if let Some(ref content_address) = reference.content_address {
let content = self.get_content(content_address).await?;
return Ok(Some((reference.clone(), content)));
}
}
Ok(reference.map(|r| (r, None)))
}
pub async fn delete_content(&self, content_address: &str) -> Result<bool> {
let result = sqlx::query("DELETE FROM content_objects WHERE content_address = ?")
.bind(content_address)
.execute(&self.pool)
.await?;
Ok(result.rows_affected() > 0)
}
pub async fn list_unreferenced_content(&self) -> Result<Vec<String>> {
let rows = sqlx::query(
r#"
SELECT co.content_address
FROM content_objects co
LEFT JOIN refs r ON co.content_address = r.content_address
WHERE r.content_address IS NULL
"#,
)
.fetch_all(&self.pool)
.await?;
Ok(rows
.into_iter()
.map(|row| row.get("content_address"))
.collect())
}
pub async fn cleanup_unreferenced_content(&self) -> Result<usize> {
let result = sqlx::query(
r#"
DELETE FROM content_objects
WHERE content_address IN (
SELECT co.content_address
FROM content_objects co
LEFT JOIN refs r ON co.content_address = r.content_address
WHERE r.content_address IS NULL
)
"#,
)
.execute(&self.pool)
.await?;
Ok(result.rows_affected() as usize)
}
pub async fn get_storage_stats(&self) -> Result<StorageStats> {
let content_count = sqlx::query("SELECT COUNT(*) as count FROM content_objects")
.fetch_one(&self.pool)
.await?;
let reference_count = sqlx::query("SELECT COUNT(*) as count FROM refs")
.fetch_one(&self.pool)
.await?;
Ok(StorageStats {
content_object_count: content_count.get::<i64, _>("count") as usize,
total_content_size: 0, // Size not tracked
reference_count: reference_count.get::<i64, _>("count") as usize,
})
}
}
#[derive(Debug, Clone)]
pub struct ContentInfo {
pub content_address: String,
pub content_type: Option<String>,
pub size: usize,
pub created_at: String,
}
#[derive(Debug, Clone)]
pub struct StorageStats {
pub content_object_count: usize,
pub total_content_size: usize,
pub reference_count: usize,
}

View File

@ -0,0 +1,431 @@
use std::{collections::HashMap, sync::Arc};
use offline_web_model::Reference;
use offline_web_storage::ReferenceStore;
async fn create_test_store() -> ReferenceStore {
let store = ReferenceStore::new("sqlite::memory:").await.unwrap();
store
}
#[tokio::test]
async fn test_store_and_retrieve_reference() {
let store = create_test_store().await;
let reference = Reference::new(Some("abc123".to_string()), "test.txt".to_string());
// Store the reference
store.store_reference(&reference).await.unwrap();
// Retrieve it
let retrieved = store.get_reference(&reference.id).await.unwrap();
assert!(retrieved.is_some());
let retrieved = retrieved.unwrap();
assert_eq!(retrieved.id, reference.id);
assert_eq!(retrieved.content_address, reference.content_address);
assert_eq!(retrieved.name, reference.name);
assert_eq!(retrieved.dependents.len(), 0);
}
#[tokio::test]
async fn test_store_reference_with_dependents() {
let store = create_test_store().await;
let dep1 = Arc::new(Reference::new(
Some("dep1".to_string()),
"dep1.txt".to_string(),
));
let dep2 = Arc::new(Reference::new(
Some("dep2".to_string()),
"dep2.txt".to_string(),
));
// Store dependencies first
store.store_reference(&dep1).await.unwrap();
store.store_reference(&dep2).await.unwrap();
let mut parent = Reference::new(Some("parent".to_string()), "parent.txt".to_string());
parent = parent.add_dep(dep1.clone());
parent = parent.add_dep(dep2.clone());
// Store parent with dependencies
store.store_reference(&parent).await.unwrap();
// Retrieve and verify
let retrieved = store.get_reference(&parent.id).await.unwrap().unwrap();
assert_eq!(retrieved.dependents.len(), 2);
let dep_names: Vec<_> = retrieved.dependents.iter().map(|d| &d.name).collect();
assert!(dep_names.contains(&&"dep1.txt".to_string()));
assert!(dep_names.contains(&&"dep2.txt".to_string()));
}
#[tokio::test]
async fn test_get_references_by_name() {
let store = create_test_store().await;
let ref1 = Reference::new(Some("abc1".to_string()), "test.txt".to_string());
let ref2 = Reference::new(Some("abc2".to_string()), "test.txt".to_string());
let ref3 = Reference::new(Some("abc3".to_string()), "other.txt".to_string());
store.store_reference(&ref1).await.unwrap();
store.store_reference(&ref2).await.unwrap();
store.store_reference(&ref3).await.unwrap();
let results = store.get_references_by_name("test.txt").await.unwrap();
assert_eq!(results.len(), 2);
let results = store.get_references_by_name("other.txt").await.unwrap();
assert_eq!(results.len(), 1);
let results = store
.get_references_by_name("nonexistent.txt")
.await
.unwrap();
assert_eq!(results.len(), 0);
}
#[tokio::test]
async fn test_get_references_by_content_address() {
let store = create_test_store().await;
let ref1 = Reference::new(Some("same_content".to_string()), "file1.txt".to_string());
let ref2 = Reference::new(Some("same_content".to_string()), "file2.txt".to_string());
let ref3 = Reference::new(
Some("different_content".to_string()),
"file3.txt".to_string(),
);
store.store_reference(&ref1).await.unwrap();
store.store_reference(&ref2).await.unwrap();
store.store_reference(&ref3).await.unwrap();
let results = store
.get_references_by_content_address("same_content")
.await
.unwrap();
assert_eq!(results.len(), 2);
let results = store
.get_references_by_content_address("different_content")
.await
.unwrap();
assert_eq!(results.len(), 1);
}
#[tokio::test]
async fn test_delete_reference() {
let store = create_test_store().await;
let reference = Reference::new(Some("abc123".to_string()), "test.txt".to_string());
store.store_reference(&reference).await.unwrap();
// Verify it exists
let retrieved = store.get_reference(&reference.id).await.unwrap();
assert!(retrieved.is_some());
// Delete it
let deleted = store.delete_reference(&reference.id).await.unwrap();
assert!(deleted);
// Verify it's gone
let retrieved = store.get_reference(&reference.id).await.unwrap();
assert!(retrieved.is_none());
// Try to delete again
let deleted = store.delete_reference(&reference.id).await.unwrap();
assert!(!deleted);
}
#[tokio::test]
async fn test_list_all_references() {
let store = create_test_store().await;
let ref1 = Reference::new(Some("abc1".to_string()), "a.txt".to_string());
let ref2 = Reference::new(Some("abc2".to_string()), "b.txt".to_string());
let ref3 = Reference::new(Some("abc3".to_string()), "c.txt".to_string());
store.store_reference(&ref1).await.unwrap();
store.store_reference(&ref2).await.unwrap();
store.store_reference(&ref3).await.unwrap();
let all_refs = store.list_all_references().await.unwrap();
assert_eq!(all_refs.len(), 3);
// Should be sorted by name
assert_eq!(all_refs[0].name, "a.txt");
assert_eq!(all_refs[1].name, "b.txt");
assert_eq!(all_refs[2].name, "c.txt");
}
#[tokio::test]
async fn test_update_reference_graph() {
let store = create_test_store().await;
let ref1 = Arc::new(Reference::new(
Some("abc1".to_string()),
"file1.txt".to_string(),
));
let ref2 = Arc::new(Reference::new(
Some("abc2".to_string()),
"file2.txt".to_string(),
));
let mut updated_refs = HashMap::new();
updated_refs.insert(ref1.id.clone(), ref1.clone());
updated_refs.insert(ref2.id.clone(), ref2.clone());
store.update_reference_graph(&updated_refs).await.unwrap();
// Verify both references were stored
let retrieved1 = store.get_reference(&ref1.id).await.unwrap();
let retrieved2 = store.get_reference(&ref2.id).await.unwrap();
assert!(retrieved1.is_some());
assert!(retrieved2.is_some());
assert_eq!(retrieved1.unwrap().name, "file1.txt");
assert_eq!(retrieved2.unwrap().name, "file2.txt");
}
#[tokio::test]
async fn test_store_and_retrieve_content() {
let store = create_test_store().await;
let content = b"Hello, world!";
let content_type = Some("text/plain".to_string());
// Store content
let content_address = store
.store_content(content, content_type.clone())
.await
.unwrap();
// Retrieve content
let retrieved_content = store.get_content(&content_address).await.unwrap();
assert!(retrieved_content.is_some());
assert_eq!(retrieved_content.unwrap(), content);
// Check content info
let content_info = store.get_content_info(&content_address).await.unwrap();
assert!(content_info.is_some());
let info = content_info.unwrap();
assert_eq!(info.content_address, content_address);
assert_eq!(info.content_type, content_type);
// Check content exists
assert!(store.content_exists(&content_address).await.unwrap());
}
#[tokio::test]
async fn test_content_deduplication() {
let store = create_test_store().await;
let content = b"Duplicate content";
// Store same content twice
let addr1 = store.store_content(content, None).await.unwrap();
let addr2 = store.store_content(content, None).await.unwrap();
// Should get same address
assert_eq!(addr1, addr2);
// Should only have one copy in storage
let stats = store.get_storage_stats().await.unwrap();
assert_eq!(stats.content_object_count, 1);
}
#[tokio::test]
async fn test_store_reference_with_content() {
let store = create_test_store().await;
let content = b"File content here";
let name = "test_file.txt".to_string();
let content_type = Some("text/plain".to_string());
// Store reference with content
let reference = store
.store_reference_with_content(name.clone(), content, content_type)
.await
.unwrap();
assert_eq!(reference.name, name);
assert!(reference.content_address.is_some());
// Retrieve reference with content
let (retrieved_ref, retrieved_content) = store
.get_reference_with_content(&reference.id)
.await
.unwrap()
.unwrap();
assert_eq!(retrieved_ref.id, reference.id);
assert_eq!(retrieved_ref.name, name);
assert!(retrieved_content.is_some());
assert_eq!(retrieved_content.unwrap(), content);
}
#[tokio::test]
async fn test_calculate_content_address() {
let content1 = b"Same content";
let content2 = b"Same content";
let content3 = b"Different content";
let addr1 = ReferenceStore::calculate_content_address(content1);
let addr2 = ReferenceStore::calculate_content_address(content2);
let addr3 = ReferenceStore::calculate_content_address(content3);
assert_eq!(addr1, addr2);
assert_ne!(addr1, addr3);
// Should be a valid hex string
assert!(addr1.chars().all(|c| c.is_ascii_hexdigit()));
assert_eq!(addr1.len(), 128); // Blake2b512 produces 64 bytes = 128 hex chars
}
#[tokio::test]
async fn test_delete_content() {
let store = create_test_store().await;
let content = b"Content to delete";
let content_address = store.store_content(content, None).await.unwrap();
// Verify content exists
assert!(store.content_exists(&content_address).await.unwrap());
// Delete content
let deleted = store.delete_content(&content_address).await.unwrap();
assert!(deleted);
// Verify content is gone
assert!(!store.content_exists(&content_address).await.unwrap());
let retrieved = store.get_content(&content_address).await.unwrap();
assert!(retrieved.is_none());
// Try to delete again
let deleted_again = store.delete_content(&content_address).await.unwrap();
assert!(!deleted_again);
}
#[tokio::test]
async fn test_cleanup_unreferenced_content() {
let store = create_test_store().await;
// Store some content with references
let content1 = b"Referenced content";
let ref1 = store
.store_reference_with_content("file1.txt".to_string(), content1, None)
.await
.unwrap();
// Store content without references
let content2 = b"Unreferenced content 1";
let content3 = b"Unreferenced content 2";
let _addr2 = store.store_content(content2, None).await.unwrap();
let _addr3 = store.store_content(content3, None).await.unwrap();
// Initial stats
let stats = store.get_storage_stats().await.unwrap();
assert_eq!(stats.content_object_count, 3);
assert_eq!(stats.reference_count, 1);
// List unreferenced content
let unreferenced = store.list_unreferenced_content().await.unwrap();
assert_eq!(unreferenced.len(), 2);
// Cleanup unreferenced content
let cleaned_up = store.cleanup_unreferenced_content().await.unwrap();
assert_eq!(cleaned_up, 2);
// Check final stats
let final_stats = store.get_storage_stats().await.unwrap();
assert_eq!(final_stats.content_object_count, 1);
assert_eq!(final_stats.reference_count, 1);
// Referenced content should still exist
let (retrieved_ref, retrieved_content) = store
.get_reference_with_content(&ref1.id)
.await
.unwrap()
.unwrap();
assert_eq!(retrieved_ref.id, ref1.id);
assert_eq!(retrieved_content.unwrap(), content1);
}
#[tokio::test]
async fn test_storage_stats() {
let store = create_test_store().await;
// Initial stats should be empty
let stats = store.get_storage_stats().await.unwrap();
assert_eq!(stats.content_object_count, 0);
assert_eq!(stats.total_content_size, 0);
assert_eq!(stats.reference_count, 0);
// Add some content and references
let content1 = b"First file";
let content2 = b"Second file content";
let _ref1 = store
.store_reference_with_content("file1.txt".to_string(), content1, None)
.await
.unwrap();
let _ref2 = store
.store_reference_with_content("file2.txt".to_string(), content2, None)
.await
.unwrap();
// Check updated stats
let final_stats = store.get_storage_stats().await.unwrap();
assert_eq!(final_stats.content_object_count, 2);
assert_eq!(final_stats.reference_count, 2);
}
#[tokio::test]
async fn test_reference_with_content_and_dependencies() {
let store = create_test_store().await;
// Create dependencies with content
let dep1_content = b"Dependency 1 content";
let dep2_content = b"Dependency 2 content";
let dep1 = store
.store_reference_with_content("dep1.txt".to_string(), dep1_content, None)
.await
.unwrap();
let dep2 = store
.store_reference_with_content("dep2.txt".to_string(), dep2_content, None)
.await
.unwrap();
// Create parent with content and dependencies
let parent_content = b"Parent content";
let parent = store
.store_reference_with_content("parent.txt".to_string(), parent_content, None)
.await
.unwrap();
// Add dependencies to parent
let parent_with_deps = parent.add_dep(Arc::new(dep1)).add_dep(Arc::new(dep2));
store.store_reference(&parent_with_deps).await.unwrap();
// Retrieve parent with content
let (retrieved_parent, retrieved_content) = store
.get_reference_with_content(&parent_with_deps.id)
.await
.unwrap()
.unwrap();
assert_eq!(retrieved_parent.dependents.len(), 2);
assert_eq!(retrieved_content.unwrap(), parent_content);
// Check that dependencies also have their content
for dep in &retrieved_parent.dependents {
let (_, dep_content) = store
.get_reference_with_content(&dep.id)
.await
.unwrap()
.unwrap();
assert!(dep_content.is_some());
}
}