Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tinycloud-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ async-std = ["sea-orm/runtime-async-std-rustls"]

[dependencies]
dashmap = "5.5"
async-trait.workspace = true
sea-orm = { version = "1.1", default-features = false, features = ["macros", "with-time", "with-json"] }
sea-orm-migration = { version = "1.1", default-features = false }
futures.workspace = true
Expand Down
147 changes: 147 additions & 0 deletions tinycloud-core/src/database_artifacts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
use async_trait::async_trait;
use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, DbErr, EntityTrait};
use time::{format_description::well_known::Rfc3339, OffsetDateTime};

use crate::{hash::hash, models::database_artifact};

#[derive(Debug, Clone)]
pub struct DatabaseArtifact {
pub payload: Vec<u8>,
pub content_hash: String,
pub revision: i64,
pub size_bytes: i64,
pub updated_at: String,
pub backend: String,
pub storage_mode: String,
}

#[derive(Debug, thiserror::Error)]
pub enum DatabaseArtifactError {
#[error("database artifact storage error: {0}")]
Db(#[from] DbErr),
#[error("database artifact payload too large: {0} bytes")]
PayloadTooLarge(u64),
#[error("database artifact backend error: {0}")]
Backend(String),
}

#[async_trait]
pub trait DatabaseArtifactRepository: Send + Sync {
async fn load(
&self,
service: &str,
space: &str,
name: &str,
) -> Result<Option<DatabaseArtifact>, DatabaseArtifactError>;

async fn save(
&self,
service: &str,
space: &str,
name: &str,
payload: Vec<u8>,
) -> Result<DatabaseArtifact, DatabaseArtifactError>;
}

#[derive(Clone)]
pub struct SeaOrmDatabaseArtifactRepository {
conn: DatabaseConnection,
}

impl SeaOrmDatabaseArtifactRepository {
pub fn new(conn: DatabaseConnection) -> Self {
Self { conn }
}
}

#[async_trait]
impl DatabaseArtifactRepository for SeaOrmDatabaseArtifactRepository {
async fn load(
&self,
service: &str,
space: &str,
name: &str,
) -> Result<Option<DatabaseArtifact>, DatabaseArtifactError> {
database_artifact::Entity::find_by_id((
service.to_string(),
space.to_string(),
name.to_string(),
))
.one(&self.conn)
.await
.map(|row| {
row.map(|model| DatabaseArtifact {
payload: model.payload,
content_hash: model.content_hash,
revision: model.revision,
size_bytes: model.size_bytes,
updated_at: model.updated_at,
backend: model.backend,
storage_mode: model.storage_mode,
})
})
.map_err(DatabaseArtifactError::Db)
}

async fn save(
&self,
service: &str,
space: &str,
name: &str,
payload: Vec<u8>,
) -> Result<DatabaseArtifact, DatabaseArtifactError> {
let size_bytes = i64::try_from(payload.len())
.map_err(|_| DatabaseArtifactError::PayloadTooLarge(payload.len() as u64))?;
let content_hash = hash(&payload).to_cid(0x55).to_string();
let now = OffsetDateTime::now_utc()
.format(&Rfc3339)
.expect("current timestamps should format as RFC3339");

let existing = database_artifact::Entity::find_by_id((
service.to_string(),
space.to_string(),
name.to_string(),
))
.one(&self.conn)
.await?;

let revision = existing
.as_ref()
.map(|model| model.revision + 1)
.unwrap_or(1);
let created_at = existing
.as_ref()
.map(|model| model.created_at.clone())
.unwrap_or_else(|| now.clone());

let active = database_artifact::ActiveModel {
service: Set(service.to_string()),
space: Set(space.to_string()),
name: Set(name.to_string()),
revision: Set(revision),
content_hash: Set(content_hash.clone()),
payload: Set(payload.clone()),
size_bytes: Set(size_bytes),
backend: Set("storage.database".to_string()),
storage_mode: Set("database-blob".to_string()),
created_at: Set(created_at),
updated_at: Set(now.clone()),
};

let model = if existing.is_some() {
active.update(&self.conn).await?
} else {
active.insert(&self.conn).await?
};

Ok(DatabaseArtifact {
payload,
content_hash,
revision: model.revision,
size_bytes: model.size_bytes,
updated_at: model.updated_at,
backend: model.backend,
storage_mode: model.storage_mode,
})
}
}
3 changes: 2 additions & 1 deletion tinycloud-core/src/duckdb/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ fn handle_export(
) -> Result<Vec<u8>, DuckDbError> {
match mode {
StorageMode::File(_) => {
// File-backed: read the file directly
conn.execute_batch("CHECKPOINT;")
.map_err(|e| DuckDbError::Internal(e.to_string()))?;
std::fs::read(file_path).map_err(|e| DuckDbError::Internal(e.to_string()))
}
StorageMode::InMemory => {
Expand Down
Loading
Loading