use std::{
path::Path,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use async_trait::async_trait;
use tokio_stream::wrappers::ReceiverStream;
use turso::{Builder, Database};
use crate::{BlobId, BlobIdStream, BlobStore, RootStore, StoreError, digest};
const CREATE_BLOBS_TABLE: &str = "
CREATE TABLE IF NOT EXISTS corium_blobs (
id TEXT PRIMARY KEY NOT NULL,
data BLOB NOT NULL,
created_at_unix_seconds INTEGER NOT NULL DEFAULT (unixepoch())
)
";
const CREATE_ROOTS_TABLE: &str = "
CREATE TABLE IF NOT EXISTS corium_roots (
name TEXT PRIMARY KEY NOT NULL,
data BLOB NOT NULL
)
";
#[derive(Clone)]
pub struct TursoBlobStore {
database: Database,
}
impl TursoBlobStore {
pub async fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
let database = Self::open_database(path.as_ref()).await?;
Self::from_database(database).await
}
pub async fn open_existing(path: impl AsRef<Path>) -> Result<Self, StoreError> {
Ok(Self {
database: Self::open_database(path.as_ref()).await?,
})
}
async fn open_database(path: &Path) -> Result<Database, StoreError> {
let text = path
.to_str()
.ok_or_else(|| StoreError::InvalidTursoPath(path.to_path_buf()))?;
let database = Builder::new_local(text)
.experimental_multiprocess_wal(true)
.build()
.await?;
Ok(database)
}
pub async fn from_database(database: Database) -> Result<Self, StoreError> {
let connection = database.connect()?;
connection.execute(CREATE_BLOBS_TABLE, ()).await?;
connection.execute(CREATE_ROOTS_TABLE, ()).await?;
Ok(Self { database })
}
fn connect(&self) -> Result<turso::Connection, StoreError> {
Ok(self.database.connect()?)
}
}
#[async_trait]
impl BlobStore for TursoBlobStore {
async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
let id = digest(bytes);
self.connect()?
.execute(
"INSERT OR IGNORE INTO corium_blobs (id, data) VALUES (?1, ?2)",
(id.as_str(), bytes),
)
.await?;
Ok(id)
}
async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
let mut rows = self
.connect()?
.query("SELECT data FROM corium_blobs WHERE id = ?1", [id.as_str()])
.await?;
let Some(row) = rows.next().await? else {
return Ok(None);
};
let bytes: Vec<u8> = row.get(0)?;
if digest(&bytes) != *id {
return Err(StoreError::CorruptBlob(id.clone()));
}
Ok(Some(bytes))
}
async fn contains(&self, id: &BlobId) -> Result<bool, StoreError> {
let mut rows = self
.connect()?
.query("SELECT 1 FROM corium_blobs WHERE id = ?1", [id.as_str()])
.await?;
Ok(rows.next().await?.is_some())
}
async fn delete(&self, id: &BlobId) -> Result<(), StoreError> {
self.connect()?
.execute("DELETE FROM corium_blobs WHERE id = ?1", [id.as_str()])
.await?;
Ok(())
}
async fn list(&self) -> Result<BlobIdStream, StoreError> {
let mut rows = self
.connect()?
.query("SELECT id FROM corium_blobs ORDER BY id", ())
.await?;
let (tx, rx) = tokio::sync::mpsc::channel(64);
tokio::spawn(async move {
loop {
let row = match rows.next().await {
Ok(Some(row)) => row,
Ok(None) => return,
Err(error) => {
let _ = tx.send(Err(StoreError::Turso(error))).await;
return;
}
};
let text = match row.get::<String>(0) {
Ok(text) => text,
Err(error) => {
let _ = tx.send(Err(StoreError::Turso(error))).await;
return;
}
};
let Some(id) = BlobId::from_hex(&text) else {
let _ = tx
.send(Err(StoreError::InvalidTursoData(format!(
"invalid blob id {text:?}"
))))
.await;
return;
};
if tx.send(Ok(id)).await.is_err() {
return;
}
}
});
Ok(Box::pin(ReceiverStream::new(rx)))
}
async fn modified_at(&self, id: &BlobId) -> Result<Option<SystemTime>, StoreError> {
let mut rows = self
.connect()?
.query(
"SELECT created_at_unix_seconds FROM corium_blobs WHERE id = ?1",
[id.as_str()],
)
.await?;
let Some(row) = rows.next().await? else {
return Ok(None);
};
let seconds: i64 = row.get(0)?;
let seconds = u64::try_from(seconds).map_err(|_| {
StoreError::InvalidTursoData(format!("negative blob timestamp {seconds}"))
})?;
let timestamp = UNIX_EPOCH
.checked_add(Duration::from_secs(seconds))
.ok_or_else(|| {
StoreError::InvalidTursoData(format!("blob timestamp is out of range: {seconds}"))
})?;
Ok(Some(timestamp))
}
}
impl TursoBlobStore {
async fn read_root(
connection: &turso::Connection,
name: &str,
) -> Result<Option<Vec<u8>>, StoreError> {
let mut rows = connection
.query("SELECT data FROM corium_roots WHERE name = ?1", [name])
.await?;
match rows.next().await? {
Some(row) => Ok(Some(row.get::<Vec<u8>>(0)?)),
None => Ok(None),
}
}
}
#[async_trait]
impl RootStore for TursoBlobStore {
async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
Self::read_root(&self.connect()?, name).await
}
async fn cas_root(
&self,
name: &str,
expected: Option<&[u8]>,
new: &[u8],
) -> Result<(), StoreError> {
let connection = self.connect()?;
connection.execute("BEGIN IMMEDIATE", ()).await?;
let result = async {
let actual = Self::read_root(&connection, name).await?;
if actual.as_deref() != expected {
return Err(StoreError::CasFailed {
expected: expected.map(<[u8]>::to_vec),
actual,
});
}
connection
.execute(
"INSERT INTO corium_roots (name, data) VALUES (?1, ?2)
ON CONFLICT(name) DO UPDATE SET data = excluded.data",
(name, new),
)
.await?;
Ok(())
}
.await;
if result.is_ok() {
connection.execute("COMMIT", ()).await?;
} else {
let _ = connection.execute("ROLLBACK", ()).await;
}
result
}
async fn delete_root(&self, name: &str) -> Result<(), StoreError> {
self.connect()?
.execute("DELETE FROM corium_roots WHERE name = ?1", [name])
.await?;
Ok(())
}
async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError> {
let connection = self.connect()?;
let mut rows = connection
.query(
"SELECT name FROM corium_roots WHERE name >= ?1 ORDER BY name",
[prefix],
)
.await?;
let mut names = Vec::new();
while let Some(row) = rows.next().await? {
let name: String = row.get(0)?;
if name.starts_with(prefix) {
names.push(name);
} else {
break;
}
}
Ok(names)
}
}