1use std::{
7 collections::{BTreeMap, HashMap, HashSet},
8 fmt,
9 fs::{self, File, OpenOptions},
10 io,
11 path::{Path, PathBuf},
12 pin::Pin,
13 sync::{Arc, RwLock},
14 time::{Duration, SystemTime},
15};
16
17use async_trait::async_trait;
18use fs2::FileExt;
19use thiserror::Error;
20use tokio_stream::{Stream, StreamExt, wrappers::ReceiverStream};
21
22mod snapshot;
23pub use snapshot::{
24 INDEX_MANIFEST_MAGIC, chunk_segment_keys, decode_index_manifest, decode_segment_keys,
25 encode_index_manifest, index_blob_children, is_index_manifest,
26};
27
28#[cfg(feature = "postgres")]
29mod postgres_store;
30#[cfg(feature = "postgres")]
31pub use postgres_store::PostgresBlobStore;
32#[cfg(feature = "turso")]
33mod turso_store;
34#[cfg(feature = "turso")]
35pub use turso_store::TursoBlobStore;
36#[cfg(feature = "s3")]
37mod s3_store;
38#[cfg(feature = "s3")]
39pub use s3_store::S3BlobStore;
40
41#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
43pub struct BlobId(String);
44
45impl BlobId {
46 #[must_use]
48 pub fn as_str(&self) -> &str {
49 &self.0
50 }
51
52 #[must_use]
54 pub fn from_hex(text: &str) -> Option<Self> {
55 (text.len() == 64 && text.bytes().all(|byte| byte.is_ascii_hexdigit()))
56 .then(|| Self(text.to_owned()))
57 }
58}
59
60impl fmt::Display for BlobId {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 f.write_str(&self.0)
63 }
64}
65
66#[derive(Debug, Error)]
68pub enum StoreError {
69 #[error("store I/O failed: {0}")]
71 Io(#[from] io::Error),
72 #[error("root CAS failed: expected {expected:?}, actual {actual:?}")]
74 CasFailed {
75 expected: Option<Vec<u8>>,
77 actual: Option<Vec<u8>>,
79 },
80 #[error("blob content did not match digest {0}")]
82 CorruptBlob(BlobId),
83 #[error("reachable blob is missing: {0}")]
85 MissingBlob(BlobId),
86 #[error("invalid root name {0:?}")]
88 InvalidRootName(String),
89 #[error("store blocking task failed: {0}")]
91 BlockingTask(String),
92 #[cfg(feature = "postgres")]
94 #[error("PostgreSQL store failed: {0}")]
95 Postgres(#[from] deadpool_postgres::tokio_postgres::Error),
96 #[cfg(feature = "postgres")]
98 #[error("PostgreSQL connection pool failed: {0}")]
99 PostgresPool(#[from] deadpool_postgres::PoolError),
100 #[cfg(feature = "postgres")]
102 #[error("PostgreSQL connection pool configuration failed: {0}")]
103 PostgresPoolCreate(#[from] deadpool_postgres::CreatePoolError),
104 #[cfg(feature = "postgres")]
106 #[error("cannot load native certificate roots for PostgreSQL TLS: {0}")]
107 PostgresTlsRoots(String),
108 #[cfg(feature = "postgres")]
110 #[error("PostgreSQL store contains invalid data: {0}")]
111 InvalidPostgresData(String),
112 #[cfg(feature = "turso")]
114 #[error("Turso blob store failed: {0}")]
115 Turso(#[from] turso::Error),
116 #[cfg(feature = "turso")]
118 #[error("Turso database path is not valid UTF-8: {0:?}")]
119 InvalidTursoPath(PathBuf),
120 #[cfg(feature = "turso")]
122 #[error("Turso blob store contains invalid data: {0}")]
123 InvalidTursoData(String),
124 #[cfg(feature = "s3")]
126 #[error("S3 store failed: {0}")]
127 S3(String),
128 #[cfg(feature = "s3")]
130 #[error("S3 store contains invalid data: {0}")]
131 InvalidS3Data(String),
132}
133
134#[cfg(feature = "s3")]
143impl<E> From<aws_sdk_s3::error::SdkError<E>> for StoreError
144where
145 E: std::error::Error + Send + Sync + 'static,
146{
147 fn from(error: aws_sdk_s3::error::SdkError<E>) -> Self {
148 StoreError::S3(aws_sdk_s3::error::DisplayErrorContext(error).to_string())
149 }
150}
151
152pub type BlobIdStream = Pin<Box<dyn Stream<Item = Result<BlobId, StoreError>> + Send + 'static>>;
154
155async fn run_blocking<T>(
156 operation: impl FnOnce() -> Result<T, StoreError> + Send + 'static,
157) -> Result<T, StoreError>
158where
159 T: Send + 'static,
160{
161 tokio::task::spawn_blocking(operation)
162 .await
163 .map_err(|error| StoreError::BlockingTask(error.to_string()))?
164}
165
166#[async_trait]
168pub trait BlobStore: Send + Sync {
169 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError>;
175 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError>;
181 async fn contains(&self, id: &BlobId) -> Result<bool, StoreError> {
187 Ok(self.get(id).await?.is_some())
188 }
189 async fn put_if_absent(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
196 let id = digest(bytes);
197 if self.contains(&id).await? {
198 Ok(id)
199 } else {
200 self.put(bytes).await
201 }
202 }
203 async fn delete(&self, id: &BlobId) -> Result<(), StoreError>;
209 async fn list(&self) -> Result<BlobIdStream, StoreError>;
215 async fn modified_at(&self, _id: &BlobId) -> Result<Option<SystemTime>, StoreError> {
222 Ok(None)
223 }
224}
225
226#[async_trait]
228pub trait RootStore: Send + Sync {
229 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError>;
235 async fn cas_root(
241 &self,
242 name: &str,
243 expected: Option<&[u8]>,
244 new: &[u8],
245 ) -> Result<(), StoreError>;
246 async fn delete_root(&self, name: &str) -> Result<(), StoreError>;
252 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError>;
258}
259
260#[derive(Clone, Default)]
262pub struct MemoryStore {
263 inner: Arc<RwLock<MemoryInner>>,
264}
265#[derive(Default)]
266struct MemoryInner {
267 blobs: HashMap<BlobId, Vec<u8>>,
268 roots: BTreeMap<String, Vec<u8>>,
269}
270
271#[async_trait]
272impl BlobStore for MemoryStore {
273 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
274 let inner = Arc::clone(&self.inner);
275 let bytes = bytes.to_vec();
276 run_blocking(move || {
277 let id = digest(&bytes);
278 inner
279 .write()
280 .expect("poisoned store lock")
281 .blobs
282 .insert(id.clone(), bytes);
283 Ok(id)
284 })
285 .await
286 }
287 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
288 let inner = Arc::clone(&self.inner);
289 let id = id.clone();
290 run_blocking(move || {
291 Ok(inner
292 .read()
293 .expect("poisoned store lock")
294 .blobs
295 .get(&id)
296 .cloned())
297 })
298 .await
299 }
300 async fn delete(&self, id: &BlobId) -> Result<(), StoreError> {
301 let inner = Arc::clone(&self.inner);
302 let id = id.clone();
303 run_blocking(move || {
304 inner
305 .write()
306 .expect("poisoned store lock")
307 .blobs
308 .remove(&id);
309 Ok(())
310 })
311 .await
312 }
313 async fn list(&self) -> Result<BlobIdStream, StoreError> {
314 let inner = Arc::clone(&self.inner);
315 let ids = run_blocking(move || {
316 Ok(inner
317 .read()
318 .expect("poisoned store lock")
319 .blobs
320 .keys()
321 .cloned()
322 .collect::<Vec<_>>())
323 })
324 .await?;
325 Ok(Box::pin(tokio_stream::iter(ids.into_iter().map(Ok))))
326 }
327}
328#[async_trait]
329impl RootStore for MemoryStore {
330 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
331 let inner = Arc::clone(&self.inner);
332 let name = name.to_owned();
333 run_blocking(move || {
334 Ok(inner
335 .read()
336 .expect("poisoned store lock")
337 .roots
338 .get(&name)
339 .cloned())
340 })
341 .await
342 }
343 async fn cas_root(
344 &self,
345 name: &str,
346 expected: Option<&[u8]>,
347 new: &[u8],
348 ) -> Result<(), StoreError> {
349 let inner = Arc::clone(&self.inner);
350 let name = name.to_owned();
351 let expected = expected.map(<[u8]>::to_vec);
352 let new = new.to_vec();
353 run_blocking(move || {
354 let mut inner = inner.write().expect("poisoned store lock");
355 let actual = inner.roots.get(&name).cloned();
356 if actual != expected {
357 return Err(StoreError::CasFailed { expected, actual });
358 }
359 inner.roots.insert(name, new);
360 Ok(())
361 })
362 .await
363 }
364 async fn delete_root(&self, name: &str) -> Result<(), StoreError> {
365 let inner = Arc::clone(&self.inner);
366 let name = name.to_owned();
367 run_blocking(move || {
368 inner
369 .write()
370 .expect("poisoned store lock")
371 .roots
372 .remove(&name);
373 Ok(())
374 })
375 .await
376 }
377 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError> {
378 let inner = Arc::clone(&self.inner);
379 let prefix = prefix.to_owned();
380 run_blocking(move || {
381 Ok(inner
382 .read()
383 .expect("poisoned store lock")
384 .roots
385 .keys()
386 .filter(|name| name.starts_with(&prefix))
387 .cloned()
388 .collect())
389 })
390 .await
391 }
392}
393
394#[derive(Clone)]
396pub struct FsStore {
397 root: PathBuf,
398}
399impl FsStore {
400 pub fn open(root: impl AsRef<Path>) -> Result<Self, StoreError> {
406 let root = root.as_ref().to_path_buf();
407 fs::create_dir_all(root.join("blobs"))?;
408 fs::create_dir_all(root.join("roots"))?;
409 Ok(Self { root })
410 }
411 fn blob_path(&self, id: &BlobId) -> PathBuf {
412 self.root.join("blobs").join(id.as_str())
413 }
414 fn root_path(&self, name: &str) -> Result<PathBuf, StoreError> {
415 if name.is_empty()
416 || name == "."
417 || name == ".."
418 || name.contains('/')
419 || name.contains('\\')
420 {
421 return Err(StoreError::InvalidRootName(name.to_owned()));
422 }
423 Ok(self.root.join("roots").join(name))
424 }
425
426 fn root_lock(&self, name: &str) -> Result<RootLock, StoreError> {
427 let root_path = self.root_path(name)?;
428 RootLock::acquire(&root_path.with_extension("lock"))
429 }
430}
431#[async_trait]
432impl BlobStore for FsStore {
433 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
434 let store = self.clone();
435 let bytes = bytes.to_vec();
436 run_blocking(move || {
437 let id = digest(&bytes);
438 let path = store.blob_path(&id);
439 if !path.exists() {
440 let tmp = path.with_extension("tmp");
441 fs::write(&tmp, &bytes)?;
442 fs::rename(tmp, path)?;
443 }
444 Ok(id)
445 })
446 .await
447 }
448 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
449 let store = self.clone();
450 let id = id.clone();
451 run_blocking(move || {
452 let path = store.blob_path(&id);
453 if !path.exists() {
454 return Ok(None);
455 }
456 let bytes = fs::read(path)?;
457 if digest(&bytes) != id {
458 return Err(StoreError::CorruptBlob(id));
459 }
460 Ok(Some(bytes))
461 })
462 .await
463 }
464 async fn contains(&self, id: &BlobId) -> Result<bool, StoreError> {
465 let store = self.clone();
466 let id = id.clone();
467 run_blocking(move || Ok(store.blob_path(&id).is_file())).await
468 }
469 async fn delete(&self, id: &BlobId) -> Result<(), StoreError> {
470 let store = self.clone();
471 let id = id.clone();
472 run_blocking(move || match fs::remove_file(store.blob_path(&id)) {
473 Ok(()) => Ok(()),
474 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
475 Err(error) => Err(error.into()),
476 })
477 .await
478 }
479 async fn list(&self) -> Result<BlobIdStream, StoreError> {
480 let path = self.root.join("blobs");
481 let entries = run_blocking(move || Ok(fs::read_dir(path)?)).await?;
482 let (tx, rx) = tokio::sync::mpsc::channel(64);
483 let failure_tx = tx.clone();
484 tokio::spawn(async move {
485 let result = tokio::task::spawn_blocking(move || {
486 for entry in entries {
487 let id = (|| {
488 let entry = entry?;
489 if !entry.file_type()?.is_file() {
490 return Ok(None);
491 }
492 Ok(entry.file_name().to_str().and_then(BlobId::from_hex))
493 })();
494 match id {
495 Ok(Some(id)) => {
496 if tx.blocking_send(Ok(id)).is_err() {
497 return;
498 }
499 }
500 Ok(None) => {}
501 Err(error) => {
502 let _ = tx.blocking_send(Err(StoreError::Io(error)));
503 return;
504 }
505 }
506 }
507 })
508 .await;
509 if let Err(error) = result {
510 let _ = failure_tx
511 .send(Err(StoreError::BlockingTask(error.to_string())))
512 .await;
513 }
514 });
515 Ok(Box::pin(ReceiverStream::new(rx)))
516 }
517 async fn modified_at(&self, id: &BlobId) -> Result<Option<SystemTime>, StoreError> {
518 let store = self.clone();
519 let id = id.clone();
520 run_blocking(move || match fs::metadata(store.blob_path(&id)) {
521 Ok(metadata) => Ok(Some(metadata.modified()?)),
522 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
523 Err(error) => Err(error.into()),
524 })
525 .await
526 }
527}
528#[async_trait]
529impl RootStore for FsStore {
530 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
531 let store = self.clone();
532 let name = name.to_owned();
533 run_blocking(move || match fs::read(store.root_path(&name)?) {
534 Ok(value) => Ok(Some(value)),
535 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
536 Err(error) => Err(error.into()),
537 })
538 .await
539 }
540 async fn cas_root(
541 &self,
542 name: &str,
543 expected: Option<&[u8]>,
544 new: &[u8],
545 ) -> Result<(), StoreError> {
546 let store = self.clone();
547 let name = name.to_owned();
548 let expected = expected.map(<[u8]>::to_vec);
549 let new = new.to_vec();
550 run_blocking(move || {
551 let _lock = store.root_lock(&name)?;
552 let path = store.root_path(&name)?;
553 let actual = match fs::read(&path) {
554 Ok(value) => Some(value),
555 Err(error) if error.kind() == io::ErrorKind::NotFound => None,
556 Err(error) => return Err(error.into()),
557 };
558 if actual != expected {
559 return Err(StoreError::CasFailed { expected, actual });
560 }
561 let tmp = path.with_extension("tmp");
562 fs::write(&tmp, new)?;
563 fs::rename(tmp, path)?;
564 Ok(())
565 })
566 .await
567 }
568 async fn delete_root(&self, name: &str) -> Result<(), StoreError> {
569 let store = self.clone();
570 let name = name.to_owned();
571 run_blocking(move || {
572 let _lock = store.root_lock(&name)?;
573 match fs::remove_file(store.root_path(&name)?) {
574 Ok(()) => Ok(()),
575 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
576 Err(error) => Err(error.into()),
577 }
578 })
579 .await
580 }
581 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError> {
582 let root = self.root.clone();
583 let prefix = prefix.to_owned();
584 run_blocking(move || {
585 let mut names = Vec::new();
586 for entry in fs::read_dir(root.join("roots"))? {
587 let entry = entry?;
588 if !entry.file_type()?.is_file() {
589 continue;
590 }
591 if let Some(name) = entry.file_name().to_str() {
592 let auxiliary = Path::new(name).extension().is_some_and(|ext| {
593 ext.eq_ignore_ascii_case("lock") || ext.eq_ignore_ascii_case("tmp")
594 });
595 if name.starts_with(&prefix) && !auxiliary {
596 names.push(name.to_owned());
597 }
598 }
599 }
600 names.sort();
601 Ok(names)
602 })
603 .await
604 }
605}
606
607#[must_use]
609pub fn digest(bytes: &[u8]) -> BlobId {
610 BlobId(blake3::hash(bytes).to_hex().to_string())
611}
612
613#[must_use]
615pub fn db_root_name(db: &str) -> String {
616 format!("db:{db}")
617}
618
619#[must_use]
621pub fn meta_root_name(db: &str) -> String {
622 format!("meta:{db}")
623}
624
625pub const FORMAT_VERSION: u32 = 3;
637
638#[derive(Clone, Debug, Eq, PartialEq)]
647pub struct DbRoot {
648 pub format_version: u32,
650 pub lease_version: u64,
652 pub owner: String,
655 pub lease_expires_unix_ms: i64,
657 pub owner_endpoint: String,
660 pub index_basis_t: u64,
662 pub roots: Option<[BlobId; 4]>,
665 pub next_entity_id: u64,
675 pub last_tx_instant: i64,
682}
683
684fn field_line(out: &mut String, value: &str) {
686 if value.is_empty() {
687 out.push('-');
688 } else {
689 out.push_str(value);
690 }
691 out.push('\n');
692}
693
694fn parse_field(line: &str) -> String {
695 if line == "-" {
696 String::new()
697 } else {
698 line.to_owned()
699 }
700}
701
702impl DbRoot {
703 #[must_use]
705 pub fn encode(&self) -> Vec<u8> {
706 let mut out = format!(
707 "corium-root-v{}\n{}\n{}\n",
708 self.format_version, self.lease_version, self.index_basis_t
709 );
710 match &self.roots {
711 Some(roots) => {
712 for root in roots {
713 out.push_str(root.as_str());
714 out.push('\n');
715 }
716 }
717 None => out.push_str("-\n-\n-\n-\n"),
718 }
719 field_line(&mut out, &self.owner);
720 out.push_str(&self.lease_expires_unix_ms.to_string());
721 out.push('\n');
722 field_line(&mut out, &self.owner_endpoint);
723 out.push_str(&self.next_entity_id.to_string());
727 out.push('\n');
728 out.push_str(&self.last_tx_instant.to_string());
729 out.into_bytes()
730 }
731
732 #[must_use]
736 pub fn decode(bytes: &[u8]) -> Option<Self> {
737 let text = std::str::from_utf8(bytes).ok()?;
738 let mut lines = text.lines();
739 let first = lines.next()?;
740 let (format_version, lease_version) =
741 if let Some(version) = first.strip_prefix("corium-root-v") {
742 let format_version = version.parse().ok()?;
743 let lease_version = lines.next()?.parse().ok()?;
744 (format_version, lease_version)
745 } else {
746 (1, first.parse().ok()?)
749 };
750 let index_basis_t = lines.next()?.parse().ok()?;
751 let ids: Vec<&str> = lines.by_ref().take(4).collect();
752 if ids.len() != 4 {
753 return None;
754 }
755 let roots = if ids.iter().all(|id| *id == "-") {
756 None
757 } else {
758 Some([
759 BlobId::from_hex(ids[0])?,
760 BlobId::from_hex(ids[1])?,
761 BlobId::from_hex(ids[2])?,
762 BlobId::from_hex(ids[3])?,
763 ])
764 };
765 let owner = lines.next().map(parse_field).unwrap_or_default();
767 let lease_expires_unix_ms = lines.next().and_then(|l| l.parse().ok()).unwrap_or(0);
768 let owner_endpoint = lines.next().map(parse_field).unwrap_or_default();
769 let next_entity_id = lines.next().and_then(|l| l.parse().ok()).unwrap_or(0);
773 let last_tx_instant = lines
774 .next()
775 .and_then(|l| l.parse().ok())
776 .unwrap_or(i64::MIN);
777 Some(Self {
778 format_version,
779 lease_version,
780 owner,
781 lease_expires_unix_ms,
782 owner_endpoint,
783 index_basis_t,
784 roots,
785 next_entity_id,
786 last_tx_instant,
787 })
788 }
789}
790
791#[derive(Clone, Copy, Debug, Eq, PartialEq)]
793pub struct GcReport {
794 pub marked: usize,
796 pub swept: usize,
798 pub retained: usize,
800}
801
802pub async fn mark_and_sweep(
811 store: &dyn BlobStore,
812 live_roots: impl IntoIterator<Item = BlobId>,
813 mut children: impl FnMut(&BlobId, &[u8]) -> Result<Vec<BlobId>, StoreError>,
814) -> Result<GcReport, StoreError> {
815 mark_and_sweep_retained(
816 store,
817 live_roots,
818 &mut children,
819 Duration::ZERO,
820 SystemTime::now(),
821 )
822 .await
823}
824
825pub async fn mark_and_sweep_retained(
831 store: &dyn BlobStore,
832 live_roots: impl IntoIterator<Item = BlobId>,
833 mut children: impl FnMut(&BlobId, &[u8]) -> Result<Vec<BlobId>, StoreError>,
834 retention: Duration,
835 now: SystemTime,
836) -> Result<GcReport, StoreError> {
837 let mut marked = HashSet::new();
838 let mut pending = live_roots.into_iter().collect::<Vec<_>>();
839 while let Some(id) = pending.pop() {
840 if !marked.insert(id.clone()) {
841 continue;
842 }
843 let bytes = store
844 .get(&id)
845 .await?
846 .ok_or_else(|| StoreError::MissingBlob(id.clone()))?;
847 pending.extend(children(&id, &bytes)?);
848 }
849
850 let mut swept = 0;
851 let mut retained = 0;
852 let mut ids = store.list().await?;
853 while let Some(id) = ids.next().await {
854 let id = id?;
855 if !marked.contains(&id) {
856 let old_enough = retention.is_zero()
860 || store.modified_at(&id).await?.is_some_and(|modified| {
861 now.duration_since(modified).unwrap_or_default() >= retention
862 });
863 if old_enough {
864 store.delete(&id).await?;
865 swept += 1;
866 } else {
867 retained += 1;
868 }
869 }
870 }
871 Ok(GcReport {
872 marked: marked.len(),
873 swept,
874 retained,
875 })
876}
877
878struct RootLock {
879 file: File,
880}
881
882impl RootLock {
883 fn acquire(path: &Path) -> Result<Self, StoreError> {
884 let file = OpenOptions::new()
885 .read(true)
886 .write(true)
887 .create(true)
888 .truncate(false)
889 .open(path)?;
890 file.lock_exclusive()?;
891 Ok(Self { file })
892 }
893}
894
895impl Drop for RootLock {
896 fn drop(&mut self) {
897 let _ = FileExt::unlock(&self.file);
898 }
902}
903
904#[derive(Default)]
906pub struct SegmentCache {
907 entries: RwLock<HashMap<BlobId, Arc<[u8]>>>,
908}
909impl SegmentCache {
910 pub async fn get_or_load(
920 &self,
921 store: &dyn BlobStore,
922 id: &BlobId,
923 ) -> Result<Option<Arc<[u8]>>, StoreError> {
924 if let Some(v) = self.entries.read().expect("poisoned cache lock").get(id) {
925 return Ok(Some(v.clone()));
926 }
927 let Some(bytes) = store.get(id).await? else {
928 return Ok(None);
929 };
930 let bytes: Arc<[u8]> = bytes.into();
931 self.entries
932 .write()
933 .expect("poisoned cache lock")
934 .insert(id.clone(), bytes.clone());
935 Ok(Some(bytes))
936 }
937}