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 segment_cache;
23pub use segment_cache::{SegmentCache, SegmentCacheConfig, SegmentCacheMetrics, SegmentReader};
24
25mod encrypted_store;
26pub use encrypted_store::EncryptedBlobStore;
27
28mod snapshot;
29pub use snapshot::{
30 INDEX_MANIFEST_MAGIC, chunk_segment_keys, decode_index_manifest, decode_segment_keys,
31 encode_index_manifest, encode_segment_chunk, index_blob_children, is_index_manifest,
32};
33
34#[cfg(feature = "postgres")]
35mod postgres_store;
36#[cfg(feature = "postgres")]
37pub use postgres_store::PostgresBlobStore;
38#[cfg(feature = "turso")]
39mod turso_store;
40#[cfg(feature = "turso")]
41pub use turso_store::TursoBlobStore;
42#[cfg(feature = "s3")]
43mod s3_store;
44#[cfg(feature = "s3")]
45pub use s3_store::{S3BlobStore, S3ClientConfig, normalize_s3_prefix};
46
47#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
49pub struct BlobId(String);
50
51impl BlobId {
52 #[must_use]
54 pub fn as_str(&self) -> &str {
55 &self.0
56 }
57
58 #[must_use]
60 pub fn from_hex(text: &str) -> Option<Self> {
61 (text.len() == 64 && text.bytes().all(|byte| byte.is_ascii_hexdigit()))
62 .then(|| Self(text.to_owned()))
63 }
64}
65
66impl fmt::Display for BlobId {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 f.write_str(&self.0)
69 }
70}
71
72#[derive(Debug, Error)]
74pub enum StoreError {
75 #[error("store I/O failed: {0}")]
77 Io(#[from] io::Error),
78 #[error("root CAS failed: expected {expected:?}, actual {actual:?}")]
80 CasFailed {
81 expected: Option<Vec<u8>>,
83 actual: Option<Vec<u8>>,
85 },
86 #[error("blob content did not match digest {0}")]
88 CorruptBlob(BlobId),
89 #[error("reachable blob is missing: {0}")]
91 MissingBlob(BlobId),
92 #[error("encrypted blob requires unavailable storage-key epoch {0}")]
94 MissingEncryptionKey(u32),
95 #[error("blob store returned id {actual}, expected {expected}")]
97 BlobIdMismatch {
98 expected: BlobId,
100 actual: BlobId,
102 },
103 #[error("encrypted blob failed: {0}")]
105 Encryption(#[from] corium_crypt::CryptError),
106 #[error("invalid root name {0:?}")]
108 InvalidRootName(String),
109 #[error("store blocking task failed: {0}")]
111 BlockingTask(String),
112 #[cfg(feature = "postgres")]
114 #[error("PostgreSQL store failed: {0}")]
115 Postgres(#[from] deadpool_postgres::tokio_postgres::Error),
116 #[cfg(feature = "postgres")]
118 #[error("PostgreSQL connection pool failed: {0}")]
119 PostgresPool(#[from] deadpool_postgres::PoolError),
120 #[cfg(feature = "postgres")]
122 #[error("PostgreSQL connection pool configuration failed: {0}")]
123 PostgresPoolCreate(#[from] deadpool_postgres::CreatePoolError),
124 #[cfg(feature = "postgres")]
126 #[error("cannot load native certificate roots for PostgreSQL TLS: {0}")]
127 PostgresTlsRoots(String),
128 #[cfg(feature = "postgres")]
130 #[error("PostgreSQL store contains invalid data: {0}")]
131 InvalidPostgresData(String),
132 #[cfg(feature = "turso")]
134 #[error("Turso blob store failed: {0}")]
135 Turso(#[from] turso::Error),
136 #[cfg(feature = "turso")]
138 #[error("Turso database path is not valid UTF-8: {0:?}")]
139 InvalidTursoPath(PathBuf),
140 #[cfg(feature = "turso")]
142 #[error("Turso blob store contains invalid data: {0}")]
143 InvalidTursoData(String),
144 #[cfg(feature = "s3")]
146 #[error("S3 store failed: {0}")]
147 S3(String),
148 #[cfg(feature = "s3")]
150 #[error("S3 store contains invalid data: {0}")]
151 InvalidS3Data(String),
152}
153
154#[cfg(feature = "s3")]
163impl<E> From<aws_sdk_s3::error::SdkError<E>> for StoreError
164where
165 E: std::error::Error + Send + Sync + 'static,
166{
167 fn from(error: aws_sdk_s3::error::SdkError<E>) -> Self {
168 StoreError::S3(aws_sdk_s3::error::DisplayErrorContext(error).to_string())
169 }
170}
171
172pub type BlobIdStream = Pin<Box<dyn Stream<Item = Result<BlobId, StoreError>> + Send + 'static>>;
174
175async fn run_blocking<T>(
176 operation: impl FnOnce() -> Result<T, StoreError> + Send + 'static,
177) -> Result<T, StoreError>
178where
179 T: Send + 'static,
180{
181 tokio::task::spawn_blocking(operation)
182 .await
183 .map_err(|error| StoreError::BlockingTask(error.to_string()))?
184}
185
186#[async_trait]
188pub trait BlobStore: Send + Sync {
189 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError>;
195 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError>;
201 async fn contains(&self, id: &BlobId) -> Result<bool, StoreError> {
207 Ok(self.get(id).await?.is_some())
208 }
209 async fn put_if_absent(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
216 let id = digest(bytes);
217 if self.contains(&id).await? {
218 Ok(id)
219 } else {
220 self.put(bytes).await
221 }
222 }
223 async fn delete(&self, id: &BlobId) -> Result<(), StoreError>;
229 async fn list(&self) -> Result<BlobIdStream, StoreError>;
235 async fn modified_at(&self, _id: &BlobId) -> Result<Option<SystemTime>, StoreError> {
242 Ok(None)
243 }
244}
245
246#[async_trait]
248pub trait RootStore: Send + Sync {
249 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError>;
255 async fn cas_root(
261 &self,
262 name: &str,
263 expected: Option<&[u8]>,
264 new: &[u8],
265 ) -> Result<(), StoreError>;
266 async fn delete_root(&self, name: &str) -> Result<(), StoreError>;
272 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError>;
278}
279
280#[derive(Clone, Default)]
282pub struct MemoryStore {
283 inner: Arc<RwLock<MemoryInner>>,
284}
285#[derive(Default)]
286struct MemoryInner {
287 blobs: HashMap<BlobId, Vec<u8>>,
288 roots: BTreeMap<String, Vec<u8>>,
289}
290
291#[async_trait]
292impl BlobStore for MemoryStore {
293 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
294 let inner = Arc::clone(&self.inner);
295 let bytes = bytes.to_vec();
296 run_blocking(move || {
297 let id = digest(&bytes);
298 inner
299 .write()
300 .expect("poisoned store lock")
301 .blobs
302 .insert(id.clone(), bytes);
303 Ok(id)
304 })
305 .await
306 }
307 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
308 let inner = Arc::clone(&self.inner);
309 let id = id.clone();
310 run_blocking(move || {
311 Ok(inner
312 .read()
313 .expect("poisoned store lock")
314 .blobs
315 .get(&id)
316 .cloned())
317 })
318 .await
319 }
320 async fn delete(&self, id: &BlobId) -> Result<(), StoreError> {
321 let inner = Arc::clone(&self.inner);
322 let id = id.clone();
323 run_blocking(move || {
324 inner
325 .write()
326 .expect("poisoned store lock")
327 .blobs
328 .remove(&id);
329 Ok(())
330 })
331 .await
332 }
333 async fn list(&self) -> Result<BlobIdStream, StoreError> {
334 let inner = Arc::clone(&self.inner);
335 let ids = run_blocking(move || {
336 Ok(inner
337 .read()
338 .expect("poisoned store lock")
339 .blobs
340 .keys()
341 .cloned()
342 .collect::<Vec<_>>())
343 })
344 .await?;
345 Ok(Box::pin(tokio_stream::iter(ids.into_iter().map(Ok))))
346 }
347}
348#[async_trait]
349impl RootStore for MemoryStore {
350 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
351 let inner = Arc::clone(&self.inner);
352 let name = name.to_owned();
353 run_blocking(move || {
354 Ok(inner
355 .read()
356 .expect("poisoned store lock")
357 .roots
358 .get(&name)
359 .cloned())
360 })
361 .await
362 }
363 async fn cas_root(
364 &self,
365 name: &str,
366 expected: Option<&[u8]>,
367 new: &[u8],
368 ) -> Result<(), StoreError> {
369 let inner = Arc::clone(&self.inner);
370 let name = name.to_owned();
371 let expected = expected.map(<[u8]>::to_vec);
372 let new = new.to_vec();
373 run_blocking(move || {
374 let mut inner = inner.write().expect("poisoned store lock");
375 let actual = inner.roots.get(&name).cloned();
376 if actual != expected {
377 return Err(StoreError::CasFailed { expected, actual });
378 }
379 inner.roots.insert(name, new);
380 Ok(())
381 })
382 .await
383 }
384 async fn delete_root(&self, name: &str) -> Result<(), StoreError> {
385 let inner = Arc::clone(&self.inner);
386 let name = name.to_owned();
387 run_blocking(move || {
388 inner
389 .write()
390 .expect("poisoned store lock")
391 .roots
392 .remove(&name);
393 Ok(())
394 })
395 .await
396 }
397 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError> {
398 let inner = Arc::clone(&self.inner);
399 let prefix = prefix.to_owned();
400 run_blocking(move || {
401 Ok(inner
402 .read()
403 .expect("poisoned store lock")
404 .roots
405 .keys()
406 .filter(|name| name.starts_with(&prefix))
407 .cloned()
408 .collect())
409 })
410 .await
411 }
412}
413
414#[derive(Clone)]
416pub struct FsStore {
417 root: PathBuf,
418}
419impl FsStore {
420 pub fn open(root: impl AsRef<Path>) -> Result<Self, StoreError> {
426 let root = root.as_ref().to_path_buf();
427 fs::create_dir_all(root.join("blobs"))?;
428 fs::create_dir_all(root.join("roots"))?;
429 Ok(Self { root })
430 }
431 fn blob_path(&self, id: &BlobId) -> PathBuf {
432 self.root.join("blobs").join(id.as_str())
433 }
434 fn root_path(&self, name: &str) -> Result<PathBuf, StoreError> {
435 if name.is_empty()
436 || name == "."
437 || name == ".."
438 || name.contains('/')
439 || name.contains('\\')
440 {
441 return Err(StoreError::InvalidRootName(name.to_owned()));
442 }
443 Ok(self.root.join("roots").join(name))
444 }
445
446 fn root_lock(&self, name: &str) -> Result<RootLock, StoreError> {
447 let root_path = self.root_path(name)?;
448 RootLock::acquire(&root_path.with_extension("lock"))
449 }
450}
451#[async_trait]
452impl BlobStore for FsStore {
453 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
454 let store = self.clone();
455 let bytes = bytes.to_vec();
456 run_blocking(move || {
457 let id = digest(&bytes);
458 let path = store.blob_path(&id);
459 if !path.exists() {
460 let tmp = path.with_extension("tmp");
461 fs::write(&tmp, &bytes)?;
462 fs::rename(tmp, path)?;
463 }
464 Ok(id)
465 })
466 .await
467 }
468 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
469 let store = self.clone();
470 let id = id.clone();
471 run_blocking(move || {
472 let path = store.blob_path(&id);
473 if !path.exists() {
474 return Ok(None);
475 }
476 let bytes = fs::read(path)?;
477 if digest(&bytes) != id {
478 return Err(StoreError::CorruptBlob(id));
479 }
480 Ok(Some(bytes))
481 })
482 .await
483 }
484 async fn contains(&self, id: &BlobId) -> Result<bool, StoreError> {
485 let store = self.clone();
486 let id = id.clone();
487 run_blocking(move || Ok(store.blob_path(&id).is_file())).await
488 }
489 async fn delete(&self, id: &BlobId) -> Result<(), StoreError> {
490 let store = self.clone();
491 let id = id.clone();
492 run_blocking(move || match fs::remove_file(store.blob_path(&id)) {
493 Ok(()) => Ok(()),
494 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
495 Err(error) => Err(error.into()),
496 })
497 .await
498 }
499 async fn list(&self) -> Result<BlobIdStream, StoreError> {
500 let path = self.root.join("blobs");
501 let entries = run_blocking(move || Ok(fs::read_dir(path)?)).await?;
502 let (tx, rx) = tokio::sync::mpsc::channel(64);
503 let failure_tx = tx.clone();
504 tokio::spawn(async move {
505 let result = tokio::task::spawn_blocking(move || {
506 for entry in entries {
507 let id = (|| {
508 let entry = entry?;
509 if !entry.file_type()?.is_file() {
510 return Ok(None);
511 }
512 Ok(entry.file_name().to_str().and_then(BlobId::from_hex))
513 })();
514 match id {
515 Ok(Some(id)) => {
516 if tx.blocking_send(Ok(id)).is_err() {
517 return;
518 }
519 }
520 Ok(None) => {}
521 Err(error) => {
522 let _ = tx.blocking_send(Err(StoreError::Io(error)));
523 return;
524 }
525 }
526 }
527 })
528 .await;
529 if let Err(error) = result {
530 let _ = failure_tx
531 .send(Err(StoreError::BlockingTask(error.to_string())))
532 .await;
533 }
534 });
535 Ok(Box::pin(ReceiverStream::new(rx)))
536 }
537 async fn modified_at(&self, id: &BlobId) -> Result<Option<SystemTime>, StoreError> {
538 let store = self.clone();
539 let id = id.clone();
540 run_blocking(move || match fs::metadata(store.blob_path(&id)) {
541 Ok(metadata) => Ok(Some(metadata.modified()?)),
542 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
543 Err(error) => Err(error.into()),
544 })
545 .await
546 }
547}
548#[async_trait]
549impl RootStore for FsStore {
550 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
551 let store = self.clone();
552 let name = name.to_owned();
553 run_blocking(move || match fs::read(store.root_path(&name)?) {
554 Ok(value) => Ok(Some(value)),
555 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
556 Err(error) => Err(error.into()),
557 })
558 .await
559 }
560 async fn cas_root(
561 &self,
562 name: &str,
563 expected: Option<&[u8]>,
564 new: &[u8],
565 ) -> Result<(), StoreError> {
566 let store = self.clone();
567 let name = name.to_owned();
568 let expected = expected.map(<[u8]>::to_vec);
569 let new = new.to_vec();
570 run_blocking(move || {
571 let _lock = store.root_lock(&name)?;
572 let path = store.root_path(&name)?;
573 let actual = match fs::read(&path) {
574 Ok(value) => Some(value),
575 Err(error) if error.kind() == io::ErrorKind::NotFound => None,
576 Err(error) => return Err(error.into()),
577 };
578 if actual != expected {
579 return Err(StoreError::CasFailed { expected, actual });
580 }
581 let tmp = path.with_extension("tmp");
582 fs::write(&tmp, new)?;
583 fs::rename(tmp, path)?;
584 Ok(())
585 })
586 .await
587 }
588 async fn delete_root(&self, name: &str) -> Result<(), StoreError> {
589 let store = self.clone();
590 let name = name.to_owned();
591 run_blocking(move || {
592 let _lock = store.root_lock(&name)?;
593 match fs::remove_file(store.root_path(&name)?) {
594 Ok(()) => Ok(()),
595 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
596 Err(error) => Err(error.into()),
597 }
598 })
599 .await
600 }
601 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError> {
602 let root = self.root.clone();
603 let prefix = prefix.to_owned();
604 run_blocking(move || {
605 let mut names = Vec::new();
606 for entry in fs::read_dir(root.join("roots"))? {
607 let entry = entry?;
608 if !entry.file_type()?.is_file() {
609 continue;
610 }
611 if let Some(name) = entry.file_name().to_str() {
612 let auxiliary = Path::new(name).extension().is_some_and(|ext| {
613 ext.eq_ignore_ascii_case("lock") || ext.eq_ignore_ascii_case("tmp")
614 });
615 if name.starts_with(&prefix) && !auxiliary {
616 names.push(name.to_owned());
617 }
618 }
619 }
620 names.sort();
621 Ok(names)
622 })
623 .await
624 }
625}
626
627#[must_use]
629pub fn digest(bytes: &[u8]) -> BlobId {
630 BlobId(blake3::hash(bytes).to_hex().to_string())
631}
632
633#[must_use]
635pub fn db_root_name(db: &str) -> String {
636 format!("db:{db}")
637}
638
639#[must_use]
641pub fn meta_root_name(db: &str) -> String {
642 format!("meta:{db}")
643}
644
645pub const FORMAT_VERSION: u32 = 3;
657
658#[derive(Clone, Debug, Eq, PartialEq)]
667pub struct DbRoot {
668 pub format_version: u32,
670 pub lease_version: u64,
672 pub owner: String,
675 pub lease_expires_unix_ms: i64,
677 pub owner_endpoint: String,
680 pub index_basis_t: u64,
682 pub roots: Option<[BlobId; 4]>,
685 pub next_entity_id: u64,
695 pub last_tx_instant: i64,
702}
703
704fn field_line(out: &mut String, value: &str) {
706 if value.is_empty() {
707 out.push('-');
708 } else {
709 out.push_str(value);
710 }
711 out.push('\n');
712}
713
714fn parse_field(line: &str) -> String {
715 if line == "-" {
716 String::new()
717 } else {
718 line.to_owned()
719 }
720}
721
722impl DbRoot {
723 #[must_use]
725 pub fn encode(&self) -> Vec<u8> {
726 let mut out = format!(
727 "corium-root-v{}\n{}\n{}\n",
728 self.format_version, self.lease_version, self.index_basis_t
729 );
730 match &self.roots {
731 Some(roots) => {
732 for root in roots {
733 out.push_str(root.as_str());
734 out.push('\n');
735 }
736 }
737 None => out.push_str("-\n-\n-\n-\n"),
738 }
739 field_line(&mut out, &self.owner);
740 out.push_str(&self.lease_expires_unix_ms.to_string());
741 out.push('\n');
742 field_line(&mut out, &self.owner_endpoint);
743 out.push_str(&self.next_entity_id.to_string());
747 out.push('\n');
748 out.push_str(&self.last_tx_instant.to_string());
749 out.into_bytes()
750 }
751
752 #[must_use]
756 pub fn decode(bytes: &[u8]) -> Option<Self> {
757 let text = std::str::from_utf8(bytes).ok()?;
758 let mut lines = text.lines();
759 let first = lines.next()?;
760 let (format_version, lease_version) =
761 if let Some(version) = first.strip_prefix("corium-root-v") {
762 let format_version = version.parse().ok()?;
763 let lease_version = lines.next()?.parse().ok()?;
764 (format_version, lease_version)
765 } else {
766 (1, first.parse().ok()?)
769 };
770 let index_basis_t = lines.next()?.parse().ok()?;
771 let ids: Vec<&str> = lines.by_ref().take(4).collect();
772 if ids.len() != 4 {
773 return None;
774 }
775 let roots = if ids.iter().all(|id| *id == "-") {
776 None
777 } else {
778 Some([
779 BlobId::from_hex(ids[0])?,
780 BlobId::from_hex(ids[1])?,
781 BlobId::from_hex(ids[2])?,
782 BlobId::from_hex(ids[3])?,
783 ])
784 };
785 let owner = lines.next().map(parse_field).unwrap_or_default();
787 let lease_expires_unix_ms = lines.next().and_then(|l| l.parse().ok()).unwrap_or(0);
788 let owner_endpoint = lines.next().map(parse_field).unwrap_or_default();
789 let next_entity_id = lines.next().and_then(|l| l.parse().ok()).unwrap_or(0);
793 let last_tx_instant = lines
794 .next()
795 .and_then(|l| l.parse().ok())
796 .unwrap_or(i64::MIN);
797 Some(Self {
798 format_version,
799 lease_version,
800 owner,
801 lease_expires_unix_ms,
802 owner_endpoint,
803 index_basis_t,
804 roots,
805 next_entity_id,
806 last_tx_instant,
807 })
808 }
809}
810
811#[derive(Clone, Copy, Debug, Eq, PartialEq)]
813pub struct GcReport {
814 pub marked: usize,
816 pub swept: usize,
818 pub retained: usize,
820}
821
822pub async fn mark_and_sweep(
831 store: &dyn BlobStore,
832 live_roots: impl IntoIterator<Item = BlobId>,
833 mut children: impl FnMut(&BlobId, &[u8]) -> Result<Vec<BlobId>, StoreError>,
834) -> Result<GcReport, StoreError> {
835 mark_and_sweep_retained(
836 store,
837 live_roots,
838 &mut children,
839 Duration::ZERO,
840 SystemTime::now(),
841 )
842 .await
843}
844
845pub async fn mark_and_sweep_retained(
851 store: &dyn BlobStore,
852 live_roots: impl IntoIterator<Item = BlobId>,
853 mut children: impl FnMut(&BlobId, &[u8]) -> Result<Vec<BlobId>, StoreError>,
854 retention: Duration,
855 now: SystemTime,
856) -> Result<GcReport, StoreError> {
857 let mut marked = HashSet::new();
858 let mut pending = live_roots.into_iter().collect::<Vec<_>>();
859 while let Some(id) = pending.pop() {
860 if !marked.insert(id.clone()) {
861 continue;
862 }
863 let bytes = store
864 .get(&id)
865 .await?
866 .ok_or_else(|| StoreError::MissingBlob(id.clone()))?;
867 pending.extend(children(&id, &bytes)?);
868 }
869
870 let mut swept = 0;
871 let mut retained = 0;
872 let mut ids = store.list().await?;
873 while let Some(id) = ids.next().await {
874 let id = id?;
875 if !marked.contains(&id) {
876 let old_enough = retention.is_zero()
880 || store.modified_at(&id).await?.is_some_and(|modified| {
881 now.duration_since(modified).unwrap_or_default() >= retention
882 });
883 if old_enough {
884 store.delete(&id).await?;
885 swept += 1;
886 } else {
887 retained += 1;
888 }
889 }
890 }
891 Ok(GcReport {
892 marked: marked.len(),
893 swept,
894 retained,
895 })
896}
897
898struct RootLock {
899 file: File,
900}
901
902impl RootLock {
903 fn acquire(path: &Path) -> Result<Self, StoreError> {
904 let file = OpenOptions::new()
905 .read(true)
906 .write(true)
907 .create(true)
908 .truncate(false)
909 .open(path)?;
910 file.lock_exclusive()?;
911 Ok(Self { file })
912 }
913}
914
915impl Drop for RootLock {
916 fn drop(&mut self) {
917 let _ = FileExt::unlock(&self.file);
918 }
922}