use crate::content_type;
use crate::error::StorageError;
use doido_core::Result;
use doido_model::sea_orm::{
ConnectionTrait, DatabaseConnection, DbBackend, Statement, Value as DbValue,
};
use serde_json::Value;
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct Blob {
pub key: String,
pub filename: String,
pub content_type: Option<String>,
pub metadata: Option<Value>,
pub service_name: String,
pub byte_size: i64,
pub checksum: Option<String>,
pub created_at: String,
}
impl Blob {
pub fn image(&self) -> bool {
self.content_type
.as_deref()
.is_some_and(content_type::is_image)
}
pub fn video(&self) -> bool {
self.content_type
.as_deref()
.is_some_and(content_type::is_video)
}
pub fn audio(&self) -> bool {
self.content_type
.as_deref()
.is_some_and(content_type::is_audio)
}
pub fn text(&self) -> bool {
self.content_type
.as_deref()
.is_some_and(content_type::is_text)
}
}
pub fn new_key() -> String {
Uuid::new_v4().simple().to_string()
}
pub fn build(filename: &str, data: &[u8], service_name: &str, metadata: Option<Value>) -> Blob {
Blob {
key: new_key(),
filename: filename.to_string(),
content_type: Some(content_type::detect(filename, data)),
metadata,
service_name: service_name.to_string(),
byte_size: crate::checksum::byte_size(data),
checksum: Some(crate::checksum::md5_base64(data)),
created_at: chrono::Utc::now().to_rfc3339(),
}
}
fn db_err(e: impl std::fmt::Display) -> StorageError {
StorageError::Db(e.to_string())
}
pub async fn insert(conn: &DatabaseConnection, blob: &Blob) -> Result<()> {
let metadata = blob
.metadata
.as_ref()
.map(|m| m.to_string())
.map(DbValue::from)
.unwrap_or(DbValue::from(None::<String>));
let stmt = Statement::from_sql_and_values(
DbBackend::Sqlite,
"INSERT INTO storage_blobs \
(key, filename, content_type, metadata, service_name, byte_size, checksum, created_at) \
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
[
DbValue::from(blob.key.clone()),
DbValue::from(blob.filename.clone()),
DbValue::from(blob.content_type.clone()),
metadata,
DbValue::from(blob.service_name.clone()),
DbValue::from(blob.byte_size),
DbValue::from(blob.checksum.clone()),
DbValue::from(blob.created_at.clone()),
],
);
conn.execute_raw(stmt).await.map_err(db_err)?;
Ok(())
}
pub async fn find(conn: &DatabaseConnection, key: &str) -> Result<Option<Blob>> {
let stmt = Statement::from_sql_and_values(
DbBackend::Sqlite,
"SELECT key, filename, content_type, metadata, service_name, byte_size, checksum, created_at \
FROM storage_blobs WHERE key = ?",
[DbValue::from(key)],
);
let row = conn.query_one_raw(stmt).await.map_err(db_err)?;
let Some(row) = row else { return Ok(None) };
let metadata: Option<String> = row.try_get("", "metadata").ok().flatten();
Ok(Some(Blob {
key: row.try_get("", "key").map_err(db_err)?,
filename: row.try_get("", "filename").map_err(db_err)?,
content_type: row.try_get("", "content_type").ok().flatten(),
metadata: metadata.and_then(|m| serde_json::from_str(&m).ok()),
service_name: row.try_get("", "service_name").map_err(db_err)?,
byte_size: row.try_get("", "byte_size").map_err(db_err)?,
checksum: row.try_get("", "checksum").ok().flatten(),
created_at: row.try_get("", "created_at").map_err(db_err)?,
}))
}
pub async fn delete(conn: &DatabaseConnection, key: &str) -> Result<()> {
let stmt = Statement::from_sql_and_values(
DbBackend::Sqlite,
"DELETE FROM storage_blobs WHERE key = ?",
[DbValue::from(key)],
);
conn.execute_raw(stmt).await.map_err(db_err)?;
Ok(())
}