1use std::{
6 collections::{BTreeMap, HashMap, HashSet},
7 fmt,
8 fs::{self, File, OpenOptions},
9 io,
10 path::{Path, PathBuf},
11 pin::Pin,
12 sync::{Arc, RwLock},
13 time::{Duration, SystemTime},
14};
15
16use async_trait::async_trait;
17use fs2::FileExt;
18use thiserror::Error;
19use tokio_stream::{Stream, StreamExt, wrappers::ReceiverStream};
20
21#[cfg(feature = "turso")]
22mod turso_store;
23#[cfg(feature = "turso")]
24pub use turso_store::TursoBlobStore;
25
26#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
28pub struct BlobId(String);
29
30impl BlobId {
31 #[must_use]
33 pub fn as_str(&self) -> &str {
34 &self.0
35 }
36
37 #[must_use]
39 pub fn from_hex(text: &str) -> Option<Self> {
40 (text.len() == 64 && text.bytes().all(|byte| byte.is_ascii_hexdigit()))
41 .then(|| Self(text.to_owned()))
42 }
43}
44
45impl fmt::Display for BlobId {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 f.write_str(&self.0)
48 }
49}
50
51#[derive(Debug, Error)]
53pub enum StoreError {
54 #[error("store I/O failed: {0}")]
56 Io(#[from] io::Error),
57 #[error("root CAS failed: expected {expected:?}, actual {actual:?}")]
59 CasFailed {
60 expected: Option<Vec<u8>>,
62 actual: Option<Vec<u8>>,
64 },
65 #[error("blob content did not match digest {0}")]
67 CorruptBlob(BlobId),
68 #[error("reachable blob is missing: {0}")]
70 MissingBlob(BlobId),
71 #[error("invalid root name {0:?}")]
73 InvalidRootName(String),
74 #[error("store blocking task failed: {0}")]
76 BlockingTask(String),
77 #[cfg(feature = "turso")]
79 #[error("Turso blob store failed: {0}")]
80 Turso(#[from] turso::Error),
81 #[cfg(feature = "turso")]
83 #[error("Turso database path is not valid UTF-8: {0:?}")]
84 InvalidTursoPath(PathBuf),
85 #[cfg(feature = "turso")]
87 #[error("Turso blob store contains invalid data: {0}")]
88 InvalidTursoData(String),
89}
90
91pub type BlobIdStream = Pin<Box<dyn Stream<Item = Result<BlobId, StoreError>> + Send + 'static>>;
93
94async fn run_blocking<T>(
95 operation: impl FnOnce() -> Result<T, StoreError> + Send + 'static,
96) -> Result<T, StoreError>
97where
98 T: Send + 'static,
99{
100 tokio::task::spawn_blocking(operation)
101 .await
102 .map_err(|error| StoreError::BlockingTask(error.to_string()))?
103}
104
105#[async_trait]
107pub trait BlobStore: Send + Sync {
108 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError>;
114 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError>;
120 async fn contains(&self, id: &BlobId) -> Result<bool, StoreError> {
126 Ok(self.get(id).await?.is_some())
127 }
128 async fn delete(&self, id: &BlobId) -> Result<(), StoreError>;
134 async fn list(&self) -> Result<BlobIdStream, StoreError>;
140 async fn modified_at(&self, _id: &BlobId) -> Result<Option<SystemTime>, StoreError> {
147 Ok(None)
148 }
149}
150
151#[async_trait]
153pub trait RootStore: Send + Sync {
154 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError>;
160 async fn cas_root(
166 &self,
167 name: &str,
168 expected: Option<&[u8]>,
169 new: &[u8],
170 ) -> Result<(), StoreError>;
171 async fn delete_root(&self, name: &str) -> Result<(), StoreError>;
177 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError>;
183}
184
185#[derive(Clone, Default)]
187pub struct MemoryStore {
188 inner: Arc<RwLock<MemoryInner>>,
189}
190#[derive(Default)]
191struct MemoryInner {
192 blobs: HashMap<BlobId, Vec<u8>>,
193 roots: BTreeMap<String, Vec<u8>>,
194}
195
196#[async_trait]
197impl BlobStore for MemoryStore {
198 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
199 let inner = Arc::clone(&self.inner);
200 let bytes = bytes.to_vec();
201 run_blocking(move || {
202 let id = digest(&bytes);
203 inner
204 .write()
205 .expect("poisoned store lock")
206 .blobs
207 .insert(id.clone(), bytes);
208 Ok(id)
209 })
210 .await
211 }
212 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
213 let inner = Arc::clone(&self.inner);
214 let id = id.clone();
215 run_blocking(move || {
216 Ok(inner
217 .read()
218 .expect("poisoned store lock")
219 .blobs
220 .get(&id)
221 .cloned())
222 })
223 .await
224 }
225 async fn delete(&self, id: &BlobId) -> Result<(), StoreError> {
226 let inner = Arc::clone(&self.inner);
227 let id = id.clone();
228 run_blocking(move || {
229 inner
230 .write()
231 .expect("poisoned store lock")
232 .blobs
233 .remove(&id);
234 Ok(())
235 })
236 .await
237 }
238 async fn list(&self) -> Result<BlobIdStream, StoreError> {
239 let inner = Arc::clone(&self.inner);
240 let ids = run_blocking(move || {
241 Ok(inner
242 .read()
243 .expect("poisoned store lock")
244 .blobs
245 .keys()
246 .cloned()
247 .collect::<Vec<_>>())
248 })
249 .await?;
250 Ok(Box::pin(tokio_stream::iter(ids.into_iter().map(Ok))))
251 }
252}
253#[async_trait]
254impl RootStore for MemoryStore {
255 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
256 let inner = Arc::clone(&self.inner);
257 let name = name.to_owned();
258 run_blocking(move || {
259 Ok(inner
260 .read()
261 .expect("poisoned store lock")
262 .roots
263 .get(&name)
264 .cloned())
265 })
266 .await
267 }
268 async fn cas_root(
269 &self,
270 name: &str,
271 expected: Option<&[u8]>,
272 new: &[u8],
273 ) -> Result<(), StoreError> {
274 let inner = Arc::clone(&self.inner);
275 let name = name.to_owned();
276 let expected = expected.map(<[u8]>::to_vec);
277 let new = new.to_vec();
278 run_blocking(move || {
279 let mut inner = inner.write().expect("poisoned store lock");
280 let actual = inner.roots.get(&name).cloned();
281 if actual != expected {
282 return Err(StoreError::CasFailed { expected, actual });
283 }
284 inner.roots.insert(name, new);
285 Ok(())
286 })
287 .await
288 }
289 async fn delete_root(&self, name: &str) -> Result<(), StoreError> {
290 let inner = Arc::clone(&self.inner);
291 let name = name.to_owned();
292 run_blocking(move || {
293 inner
294 .write()
295 .expect("poisoned store lock")
296 .roots
297 .remove(&name);
298 Ok(())
299 })
300 .await
301 }
302 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError> {
303 let inner = Arc::clone(&self.inner);
304 let prefix = prefix.to_owned();
305 run_blocking(move || {
306 Ok(inner
307 .read()
308 .expect("poisoned store lock")
309 .roots
310 .keys()
311 .filter(|name| name.starts_with(&prefix))
312 .cloned()
313 .collect())
314 })
315 .await
316 }
317}
318
319#[derive(Clone)]
321pub struct FsStore {
322 root: PathBuf,
323}
324impl FsStore {
325 pub fn open(root: impl AsRef<Path>) -> Result<Self, StoreError> {
331 let root = root.as_ref().to_path_buf();
332 fs::create_dir_all(root.join("blobs"))?;
333 fs::create_dir_all(root.join("roots"))?;
334 Ok(Self { root })
335 }
336 fn blob_path(&self, id: &BlobId) -> PathBuf {
337 self.root.join("blobs").join(id.as_str())
338 }
339 fn root_path(&self, name: &str) -> Result<PathBuf, StoreError> {
340 if name.is_empty()
341 || name == "."
342 || name == ".."
343 || name.contains('/')
344 || name.contains('\\')
345 {
346 return Err(StoreError::InvalidRootName(name.to_owned()));
347 }
348 Ok(self.root.join("roots").join(name))
349 }
350
351 fn root_lock(&self, name: &str) -> Result<RootLock, StoreError> {
352 let root_path = self.root_path(name)?;
353 RootLock::acquire(&root_path.with_extension("lock"))
354 }
355}
356#[async_trait]
357impl BlobStore for FsStore {
358 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
359 let store = self.clone();
360 let bytes = bytes.to_vec();
361 run_blocking(move || {
362 let id = digest(&bytes);
363 let path = store.blob_path(&id);
364 if !path.exists() {
365 let tmp = path.with_extension("tmp");
366 fs::write(&tmp, &bytes)?;
367 fs::rename(tmp, path)?;
368 }
369 Ok(id)
370 })
371 .await
372 }
373 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
374 let store = self.clone();
375 let id = id.clone();
376 run_blocking(move || {
377 let path = store.blob_path(&id);
378 if !path.exists() {
379 return Ok(None);
380 }
381 let bytes = fs::read(path)?;
382 if digest(&bytes) != id {
383 return Err(StoreError::CorruptBlob(id));
384 }
385 Ok(Some(bytes))
386 })
387 .await
388 }
389 async fn contains(&self, id: &BlobId) -> Result<bool, StoreError> {
390 let store = self.clone();
391 let id = id.clone();
392 run_blocking(move || Ok(store.blob_path(&id).is_file())).await
393 }
394 async fn delete(&self, id: &BlobId) -> Result<(), StoreError> {
395 let store = self.clone();
396 let id = id.clone();
397 run_blocking(move || match fs::remove_file(store.blob_path(&id)) {
398 Ok(()) => Ok(()),
399 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
400 Err(error) => Err(error.into()),
401 })
402 .await
403 }
404 async fn list(&self) -> Result<BlobIdStream, StoreError> {
405 let path = self.root.join("blobs");
406 let entries = run_blocking(move || Ok(fs::read_dir(path)?)).await?;
407 let (tx, rx) = tokio::sync::mpsc::channel(64);
408 let failure_tx = tx.clone();
409 tokio::spawn(async move {
410 let result = tokio::task::spawn_blocking(move || {
411 for entry in entries {
412 let id = (|| {
413 let entry = entry?;
414 if !entry.file_type()?.is_file() {
415 return Ok(None);
416 }
417 Ok(entry.file_name().to_str().and_then(BlobId::from_hex))
418 })();
419 match id {
420 Ok(Some(id)) => {
421 if tx.blocking_send(Ok(id)).is_err() {
422 return;
423 }
424 }
425 Ok(None) => {}
426 Err(error) => {
427 let _ = tx.blocking_send(Err(StoreError::Io(error)));
428 return;
429 }
430 }
431 }
432 })
433 .await;
434 if let Err(error) = result {
435 let _ = failure_tx
436 .send(Err(StoreError::BlockingTask(error.to_string())))
437 .await;
438 }
439 });
440 Ok(Box::pin(ReceiverStream::new(rx)))
441 }
442 async fn modified_at(&self, id: &BlobId) -> Result<Option<SystemTime>, StoreError> {
443 let store = self.clone();
444 let id = id.clone();
445 run_blocking(move || match fs::metadata(store.blob_path(&id)) {
446 Ok(metadata) => Ok(Some(metadata.modified()?)),
447 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
448 Err(error) => Err(error.into()),
449 })
450 .await
451 }
452}
453#[async_trait]
454impl RootStore for FsStore {
455 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
456 let store = self.clone();
457 let name = name.to_owned();
458 run_blocking(move || match fs::read(store.root_path(&name)?) {
459 Ok(value) => Ok(Some(value)),
460 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
461 Err(error) => Err(error.into()),
462 })
463 .await
464 }
465 async fn cas_root(
466 &self,
467 name: &str,
468 expected: Option<&[u8]>,
469 new: &[u8],
470 ) -> Result<(), StoreError> {
471 let store = self.clone();
472 let name = name.to_owned();
473 let expected = expected.map(<[u8]>::to_vec);
474 let new = new.to_vec();
475 run_blocking(move || {
476 let _lock = store.root_lock(&name)?;
477 let path = store.root_path(&name)?;
478 let actual = match fs::read(&path) {
479 Ok(value) => Some(value),
480 Err(error) if error.kind() == io::ErrorKind::NotFound => None,
481 Err(error) => return Err(error.into()),
482 };
483 if actual != expected {
484 return Err(StoreError::CasFailed { expected, actual });
485 }
486 let tmp = path.with_extension("tmp");
487 fs::write(&tmp, new)?;
488 fs::rename(tmp, path)?;
489 Ok(())
490 })
491 .await
492 }
493 async fn delete_root(&self, name: &str) -> Result<(), StoreError> {
494 let store = self.clone();
495 let name = name.to_owned();
496 run_blocking(move || {
497 let _lock = store.root_lock(&name)?;
498 match fs::remove_file(store.root_path(&name)?) {
499 Ok(()) => Ok(()),
500 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
501 Err(error) => Err(error.into()),
502 }
503 })
504 .await
505 }
506 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError> {
507 let root = self.root.clone();
508 let prefix = prefix.to_owned();
509 run_blocking(move || {
510 let mut names = Vec::new();
511 for entry in fs::read_dir(root.join("roots"))? {
512 let entry = entry?;
513 if !entry.file_type()?.is_file() {
514 continue;
515 }
516 if let Some(name) = entry.file_name().to_str() {
517 let auxiliary = Path::new(name).extension().is_some_and(|ext| {
518 ext.eq_ignore_ascii_case("lock") || ext.eq_ignore_ascii_case("tmp")
519 });
520 if name.starts_with(&prefix) && !auxiliary {
521 names.push(name.to_owned());
522 }
523 }
524 }
525 names.sort();
526 Ok(names)
527 })
528 .await
529 }
530}
531
532fn digest(bytes: &[u8]) -> BlobId {
533 BlobId(blake3::hash(bytes).to_hex().to_string())
534}
535
536#[must_use]
538pub fn db_root_name(db: &str) -> String {
539 format!("db:{db}")
540}
541
542pub const FORMAT_VERSION: u32 = 2;
548
549#[derive(Clone, Debug, Eq, PartialEq)]
558pub struct DbRoot {
559 pub format_version: u32,
561 pub lease_version: u64,
563 pub owner: String,
566 pub lease_expires_unix_ms: i64,
568 pub owner_endpoint: String,
571 pub index_basis_t: u64,
573 pub roots: Option<[BlobId; 4]>,
576}
577
578fn field_line(out: &mut String, value: &str) {
580 if value.is_empty() {
581 out.push('-');
582 } else {
583 out.push_str(value);
584 }
585 out.push('\n');
586}
587
588fn parse_field(line: &str) -> String {
589 if line == "-" {
590 String::new()
591 } else {
592 line.to_owned()
593 }
594}
595
596impl DbRoot {
597 #[must_use]
599 pub fn encode(&self) -> Vec<u8> {
600 let mut out = format!(
601 "corium-root-v{}\n{}\n{}\n",
602 self.format_version, self.lease_version, self.index_basis_t
603 );
604 match &self.roots {
605 Some(roots) => {
606 for root in roots {
607 out.push_str(root.as_str());
608 out.push('\n');
609 }
610 }
611 None => out.push_str("-\n-\n-\n-\n"),
612 }
613 field_line(&mut out, &self.owner);
614 out.push_str(&self.lease_expires_unix_ms.to_string());
615 out.push('\n');
616 field_line(&mut out, &self.owner_endpoint);
617 out.into_bytes()
618 }
619
620 #[must_use]
624 pub fn decode(bytes: &[u8]) -> Option<Self> {
625 let text = std::str::from_utf8(bytes).ok()?;
626 let mut lines = text.lines();
627 let first = lines.next()?;
628 let (format_version, lease_version) =
629 if let Some(version) = first.strip_prefix("corium-root-v") {
630 let format_version = version.parse().ok()?;
631 let lease_version = lines.next()?.parse().ok()?;
632 (format_version, lease_version)
633 } else {
634 (1, first.parse().ok()?)
637 };
638 let index_basis_t = lines.next()?.parse().ok()?;
639 let ids: Vec<&str> = lines.by_ref().take(4).collect();
640 if ids.len() != 4 {
641 return None;
642 }
643 let roots = if ids.iter().all(|id| *id == "-") {
644 None
645 } else {
646 Some([
647 BlobId::from_hex(ids[0])?,
648 BlobId::from_hex(ids[1])?,
649 BlobId::from_hex(ids[2])?,
650 BlobId::from_hex(ids[3])?,
651 ])
652 };
653 let owner = lines.next().map(parse_field).unwrap_or_default();
655 let lease_expires_unix_ms = lines.next().and_then(|l| l.parse().ok()).unwrap_or(0);
656 let owner_endpoint = lines.next().map(parse_field).unwrap_or_default();
657 Some(Self {
658 format_version,
659 lease_version,
660 owner,
661 lease_expires_unix_ms,
662 owner_endpoint,
663 index_basis_t,
664 roots,
665 })
666 }
667}
668
669#[derive(Clone, Copy, Debug, Eq, PartialEq)]
671pub struct GcReport {
672 pub marked: usize,
674 pub swept: usize,
676 pub retained: usize,
678}
679
680pub async fn mark_and_sweep(
689 store: &dyn BlobStore,
690 live_roots: impl IntoIterator<Item = BlobId>,
691 mut children: impl FnMut(&BlobId, &[u8]) -> Result<Vec<BlobId>, StoreError>,
692) -> Result<GcReport, StoreError> {
693 mark_and_sweep_retained(
694 store,
695 live_roots,
696 &mut children,
697 Duration::ZERO,
698 SystemTime::now(),
699 )
700 .await
701}
702
703pub async fn mark_and_sweep_retained(
709 store: &dyn BlobStore,
710 live_roots: impl IntoIterator<Item = BlobId>,
711 mut children: impl FnMut(&BlobId, &[u8]) -> Result<Vec<BlobId>, StoreError>,
712 retention: Duration,
713 now: SystemTime,
714) -> Result<GcReport, StoreError> {
715 let mut marked = HashSet::new();
716 let mut pending = live_roots.into_iter().collect::<Vec<_>>();
717 while let Some(id) = pending.pop() {
718 if !marked.insert(id.clone()) {
719 continue;
720 }
721 let bytes = store
722 .get(&id)
723 .await?
724 .ok_or_else(|| StoreError::MissingBlob(id.clone()))?;
725 pending.extend(children(&id, &bytes)?);
726 }
727
728 let mut swept = 0;
729 let mut retained = 0;
730 let mut ids = store.list().await?;
731 while let Some(id) = ids.next().await {
732 let id = id?;
733 if !marked.contains(&id) {
734 let old_enough = retention.is_zero()
738 || store.modified_at(&id).await?.is_some_and(|modified| {
739 now.duration_since(modified).unwrap_or_default() >= retention
740 });
741 if old_enough {
742 store.delete(&id).await?;
743 swept += 1;
744 } else {
745 retained += 1;
746 }
747 }
748 }
749 Ok(GcReport {
750 marked: marked.len(),
751 swept,
752 retained,
753 })
754}
755
756struct RootLock {
757 file: File,
758}
759
760impl RootLock {
761 fn acquire(path: &Path) -> Result<Self, StoreError> {
762 let file = OpenOptions::new()
763 .read(true)
764 .write(true)
765 .create(true)
766 .truncate(false)
767 .open(path)?;
768 file.lock_exclusive()?;
769 Ok(Self { file })
770 }
771}
772
773impl Drop for RootLock {
774 fn drop(&mut self) {
775 let _ = FileExt::unlock(&self.file);
776 }
780}
781
782#[derive(Default)]
784pub struct SegmentCache {
785 entries: RwLock<HashMap<BlobId, Arc<[u8]>>>,
786}
787impl SegmentCache {
788 pub async fn get_or_load(
798 &self,
799 store: &dyn BlobStore,
800 id: &BlobId,
801 ) -> Result<Option<Arc<[u8]>>, StoreError> {
802 if let Some(v) = self.entries.read().expect("poisoned cache lock").get(id) {
803 return Ok(Some(v.clone()));
804 }
805 let Some(bytes) = store.get(id).await? else {
806 return Ok(None);
807 };
808 let bytes: Arc<[u8]> = bytes.into();
809 self.entries
810 .write()
811 .expect("poisoned cache lock")
812 .insert(id.clone(), bytes.clone());
813 Ok(Some(bytes))
814 }
815}