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 discovery;
26pub use discovery::{DiscoveredStore, DiscoveredStoreSpec, StorageConnectionError};
27
28mod encrypted_store;
29pub use encrypted_store::EncryptedBlobStore;
30
31mod key_manifest;
32pub use key_manifest::{
33 KEY_MANIFEST_FORMAT_VERSION, KeyManifest, LOG_RECORDS_PER_EPOCH_LIMIT,
34 LOG_RECORDS_PER_EPOCH_WARN, ProtectionClassKey, StorageAlgorithm, StorageKey, StorageKeyState,
35 keys_root_name,
36};
37
38mod snapshot;
39pub use snapshot::{
40 INDEX_MANIFEST_MAGIC, chunk_segment_keys, decode_index_manifest, decode_segment_keys,
41 encode_index_manifest, encode_segment_chunk, index_blob_children, is_index_manifest,
42};
43
44#[cfg(feature = "postgres")]
45mod postgres_store;
46#[cfg(feature = "postgres")]
47pub use postgres_store::PostgresBlobStore;
48#[cfg(feature = "turso")]
49mod turso_store;
50#[cfg(feature = "turso")]
51pub use turso_store::TursoBlobStore;
52#[cfg(feature = "s3")]
53mod s3_store;
54#[cfg(feature = "s3")]
55pub use s3_store::{S3BlobStore, S3ClientConfig, normalize_s3_prefix};
56
57#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
59pub struct BlobId(String);
60
61impl BlobId {
62 #[must_use]
64 pub fn as_str(&self) -> &str {
65 &self.0
66 }
67
68 #[must_use]
70 pub fn from_hex(text: &str) -> Option<Self> {
71 (text.len() == 64 && text.bytes().all(|byte| byte.is_ascii_hexdigit()))
72 .then(|| Self(text.to_owned()))
73 }
74}
75
76impl fmt::Display for BlobId {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 f.write_str(&self.0)
79 }
80}
81
82#[derive(Debug, Error)]
84pub enum StoreError {
85 #[error("store I/O failed: {0}")]
87 Io(#[from] io::Error),
88 #[error("root CAS failed: expected {expected:?}, actual {actual:?}")]
90 CasFailed {
91 expected: Option<Vec<u8>>,
93 actual: Option<Vec<u8>>,
95 },
96 #[error("blob content did not match digest {0}")]
98 CorruptBlob(BlobId),
99 #[error("reachable blob is missing: {0}")]
101 MissingBlob(BlobId),
102 #[error("encrypted blob requires unavailable storage-key epoch {0}")]
104 MissingEncryptionKey(u32),
105 #[error("blob store returned id {actual}, expected {expected}")]
107 BlobIdMismatch {
108 expected: BlobId,
110 actual: BlobId,
112 },
113 #[error("encrypted blob failed: {0}")]
115 Encryption(#[from] corium_crypt::CryptError),
116 #[error("storage key unavailable: {0}")]
118 Keyring(corium_crypt::KeyError),
119 #[error("invalid key manifest: {0}")]
121 InvalidKeyManifest(String),
122 #[error("key manifest format {found} is newer than supported format {supported}")]
124 UnsupportedKeyManifest {
125 found: u32,
127 supported: u32,
129 },
130 #[error("unsupported storage encryption algorithm {0:?}")]
132 UnsupportedKeyAlgorithm(String),
133 #[error("storage key epochs are exhausted")]
135 StorageEpochExhausted,
136 #[error("invalid root name {0:?}")]
138 InvalidRootName(String),
139 #[error("store blocking task failed: {0}")]
141 BlockingTask(String),
142 #[error("the transactor's local storage at {0} is not reachable from this process")]
145 UnreachableLocalStorage(PathBuf),
146 #[cfg(feature = "postgres")]
148 #[error("PostgreSQL store failed: {0}")]
149 Postgres(#[from] deadpool_postgres::tokio_postgres::Error),
150 #[cfg(feature = "postgres")]
152 #[error("PostgreSQL connection pool failed: {0}")]
153 PostgresPool(#[from] deadpool_postgres::PoolError),
154 #[cfg(feature = "postgres")]
156 #[error("PostgreSQL connection pool configuration failed: {0}")]
157 PostgresPoolCreate(#[from] deadpool_postgres::CreatePoolError),
158 #[cfg(feature = "postgres")]
160 #[error("cannot load native certificate roots for PostgreSQL TLS: {0}")]
161 PostgresTlsRoots(String),
162 #[cfg(feature = "postgres")]
164 #[error("PostgreSQL store contains invalid data: {0}")]
165 InvalidPostgresData(String),
166 #[cfg(feature = "turso")]
168 #[error("Turso blob store failed: {0}")]
169 Turso(#[from] turso::Error),
170 #[cfg(feature = "turso")]
172 #[error("Turso database path is not valid UTF-8: {0:?}")]
173 InvalidTursoPath(PathBuf),
174 #[cfg(feature = "turso")]
176 #[error("Turso blob store contains invalid data: {0}")]
177 InvalidTursoData(String),
178 #[cfg(feature = "s3")]
180 #[error("S3 store failed: {0}")]
181 S3(String),
182 #[cfg(feature = "s3")]
184 #[error("S3 store contains invalid data: {0}")]
185 InvalidS3Data(String),
186}
187
188#[cfg(feature = "s3")]
197impl<E> From<aws_sdk_s3::error::SdkError<E>> for StoreError
198where
199 E: std::error::Error + Send + Sync + 'static,
200{
201 fn from(error: aws_sdk_s3::error::SdkError<E>) -> Self {
202 StoreError::S3(aws_sdk_s3::error::DisplayErrorContext(error).to_string())
203 }
204}
205
206pub type BlobIdStream = Pin<Box<dyn Stream<Item = Result<BlobId, StoreError>> + Send + 'static>>;
208
209async fn run_blocking<T>(
210 operation: impl FnOnce() -> Result<T, StoreError> + Send + 'static,
211) -> Result<T, StoreError>
212where
213 T: Send + 'static,
214{
215 tokio::task::spawn_blocking(operation)
216 .await
217 .map_err(|error| StoreError::BlockingTask(error.to_string()))?
218}
219
220#[async_trait]
222pub trait BlobStore: Send + Sync {
223 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError>;
229 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError>;
235 async fn contains(&self, id: &BlobId) -> Result<bool, StoreError> {
241 Ok(self.get(id).await?.is_some())
242 }
243 async fn put_if_absent(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
250 let id = digest(bytes);
251 if self.contains(&id).await? {
252 Ok(id)
253 } else {
254 self.put(bytes).await
255 }
256 }
257 async fn delete(&self, id: &BlobId) -> Result<(), StoreError>;
263 async fn list(&self) -> Result<BlobIdStream, StoreError>;
269 async fn modified_at(&self, _id: &BlobId) -> Result<Option<SystemTime>, StoreError> {
276 Ok(None)
277 }
278}
279
280#[async_trait]
282pub trait RootStore: Send + Sync {
283 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError>;
289 async fn cas_root(
295 &self,
296 name: &str,
297 expected: Option<&[u8]>,
298 new: &[u8],
299 ) -> Result<(), StoreError>;
300 async fn delete_root(&self, name: &str) -> Result<(), StoreError>;
306 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError>;
312}
313
314#[derive(Clone, Default)]
316pub struct MemoryStore {
317 inner: Arc<RwLock<MemoryInner>>,
318}
319#[derive(Default)]
320struct MemoryInner {
321 blobs: HashMap<BlobId, Vec<u8>>,
322 roots: BTreeMap<String, Vec<u8>>,
323}
324
325#[async_trait]
326impl BlobStore for MemoryStore {
327 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
328 let inner = Arc::clone(&self.inner);
329 let bytes = bytes.to_vec();
330 run_blocking(move || {
331 let id = digest(&bytes);
332 inner
333 .write()
334 .expect("poisoned store lock")
335 .blobs
336 .insert(id.clone(), bytes);
337 Ok(id)
338 })
339 .await
340 }
341 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
342 let inner = Arc::clone(&self.inner);
343 let id = id.clone();
344 run_blocking(move || {
345 Ok(inner
346 .read()
347 .expect("poisoned store lock")
348 .blobs
349 .get(&id)
350 .cloned())
351 })
352 .await
353 }
354 async fn delete(&self, id: &BlobId) -> Result<(), StoreError> {
355 let inner = Arc::clone(&self.inner);
356 let id = id.clone();
357 run_blocking(move || {
358 inner
359 .write()
360 .expect("poisoned store lock")
361 .blobs
362 .remove(&id);
363 Ok(())
364 })
365 .await
366 }
367 async fn list(&self) -> Result<BlobIdStream, StoreError> {
368 let inner = Arc::clone(&self.inner);
369 let ids = run_blocking(move || {
370 Ok(inner
371 .read()
372 .expect("poisoned store lock")
373 .blobs
374 .keys()
375 .cloned()
376 .collect::<Vec<_>>())
377 })
378 .await?;
379 Ok(Box::pin(tokio_stream::iter(ids.into_iter().map(Ok))))
380 }
381}
382#[async_trait]
383impl RootStore for MemoryStore {
384 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
385 let inner = Arc::clone(&self.inner);
386 let name = name.to_owned();
387 run_blocking(move || {
388 Ok(inner
389 .read()
390 .expect("poisoned store lock")
391 .roots
392 .get(&name)
393 .cloned())
394 })
395 .await
396 }
397 async fn cas_root(
398 &self,
399 name: &str,
400 expected: Option<&[u8]>,
401 new: &[u8],
402 ) -> Result<(), StoreError> {
403 let inner = Arc::clone(&self.inner);
404 let name = name.to_owned();
405 let expected = expected.map(<[u8]>::to_vec);
406 let new = new.to_vec();
407 run_blocking(move || {
408 let mut inner = inner.write().expect("poisoned store lock");
409 let actual = inner.roots.get(&name).cloned();
410 if actual != expected {
411 return Err(StoreError::CasFailed { expected, actual });
412 }
413 inner.roots.insert(name, new);
414 Ok(())
415 })
416 .await
417 }
418 async fn delete_root(&self, name: &str) -> Result<(), StoreError> {
419 let inner = Arc::clone(&self.inner);
420 let name = name.to_owned();
421 run_blocking(move || {
422 inner
423 .write()
424 .expect("poisoned store lock")
425 .roots
426 .remove(&name);
427 Ok(())
428 })
429 .await
430 }
431 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError> {
432 let inner = Arc::clone(&self.inner);
433 let prefix = prefix.to_owned();
434 run_blocking(move || {
435 Ok(inner
436 .read()
437 .expect("poisoned store lock")
438 .roots
439 .keys()
440 .filter(|name| name.starts_with(&prefix))
441 .cloned()
442 .collect())
443 })
444 .await
445 }
446}
447
448#[derive(Clone)]
450pub struct FsStore {
451 root: PathBuf,
452}
453impl FsStore {
454 pub fn open(root: impl AsRef<Path>) -> Result<Self, StoreError> {
460 let root = root.as_ref().to_path_buf();
461 fs::create_dir_all(root.join("blobs"))?;
462 fs::create_dir_all(root.join("roots"))?;
463 Ok(Self { root })
464 }
465 fn blob_path(&self, id: &BlobId) -> PathBuf {
466 self.root.join("blobs").join(id.as_str())
467 }
468 fn root_path(&self, name: &str) -> Result<PathBuf, StoreError> {
469 if name.is_empty()
470 || name == "."
471 || name == ".."
472 || name.contains('/')
473 || name.contains('\\')
474 {
475 return Err(StoreError::InvalidRootName(name.to_owned()));
476 }
477 Ok(self.root.join("roots").join(name))
478 }
479
480 fn root_lock(&self, name: &str) -> Result<RootLock, StoreError> {
481 let root_path = self.root_path(name)?;
482 RootLock::acquire(&root_path.with_extension("lock"))
483 }
484}
485#[async_trait]
486impl BlobStore for FsStore {
487 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
488 let store = self.clone();
489 let bytes = bytes.to_vec();
490 run_blocking(move || {
491 let id = digest(&bytes);
492 let path = store.blob_path(&id);
493 if !path.exists() {
494 let tmp = path.with_extension("tmp");
495 fs::write(&tmp, &bytes)?;
496 fs::rename(tmp, path)?;
497 }
498 Ok(id)
499 })
500 .await
501 }
502 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
503 let store = self.clone();
504 let id = id.clone();
505 run_blocking(move || {
506 let path = store.blob_path(&id);
507 if !path.exists() {
508 return Ok(None);
509 }
510 let bytes = fs::read(path)?;
511 if digest(&bytes) != id {
512 return Err(StoreError::CorruptBlob(id));
513 }
514 Ok(Some(bytes))
515 })
516 .await
517 }
518 async fn contains(&self, id: &BlobId) -> Result<bool, StoreError> {
519 let store = self.clone();
520 let id = id.clone();
521 run_blocking(move || Ok(store.blob_path(&id).is_file())).await
522 }
523 async fn delete(&self, id: &BlobId) -> Result<(), StoreError> {
524 let store = self.clone();
525 let id = id.clone();
526 run_blocking(move || match fs::remove_file(store.blob_path(&id)) {
527 Ok(()) => Ok(()),
528 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
529 Err(error) => Err(error.into()),
530 })
531 .await
532 }
533 async fn list(&self) -> Result<BlobIdStream, StoreError> {
534 let path = self.root.join("blobs");
535 let entries = run_blocking(move || Ok(fs::read_dir(path)?)).await?;
536 let (tx, rx) = tokio::sync::mpsc::channel(64);
537 let failure_tx = tx.clone();
538 tokio::spawn(async move {
539 let result = tokio::task::spawn_blocking(move || {
540 for entry in entries {
541 let id = (|| {
542 let entry = entry?;
543 if !entry.file_type()?.is_file() {
544 return Ok(None);
545 }
546 Ok(entry.file_name().to_str().and_then(BlobId::from_hex))
547 })();
548 match id {
549 Ok(Some(id)) => {
550 if tx.blocking_send(Ok(id)).is_err() {
551 return;
552 }
553 }
554 Ok(None) => {}
555 Err(error) => {
556 let _ = tx.blocking_send(Err(StoreError::Io(error)));
557 return;
558 }
559 }
560 }
561 })
562 .await;
563 if let Err(error) = result {
564 let _ = failure_tx
565 .send(Err(StoreError::BlockingTask(error.to_string())))
566 .await;
567 }
568 });
569 Ok(Box::pin(ReceiverStream::new(rx)))
570 }
571 async fn modified_at(&self, id: &BlobId) -> Result<Option<SystemTime>, StoreError> {
572 let store = self.clone();
573 let id = id.clone();
574 run_blocking(move || match fs::metadata(store.blob_path(&id)) {
575 Ok(metadata) => Ok(Some(metadata.modified()?)),
576 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
577 Err(error) => Err(error.into()),
578 })
579 .await
580 }
581}
582#[async_trait]
583impl RootStore for FsStore {
584 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
585 let store = self.clone();
586 let name = name.to_owned();
587 run_blocking(move || match fs::read(store.root_path(&name)?) {
588 Ok(value) => Ok(Some(value)),
589 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
590 Err(error) => Err(error.into()),
591 })
592 .await
593 }
594 async fn cas_root(
595 &self,
596 name: &str,
597 expected: Option<&[u8]>,
598 new: &[u8],
599 ) -> Result<(), StoreError> {
600 let store = self.clone();
601 let name = name.to_owned();
602 let expected = expected.map(<[u8]>::to_vec);
603 let new = new.to_vec();
604 run_blocking(move || {
605 let _lock = store.root_lock(&name)?;
606 let path = store.root_path(&name)?;
607 let actual = match fs::read(&path) {
608 Ok(value) => Some(value),
609 Err(error) if error.kind() == io::ErrorKind::NotFound => None,
610 Err(error) => return Err(error.into()),
611 };
612 if actual != expected {
613 return Err(StoreError::CasFailed { expected, actual });
614 }
615 let tmp = path.with_extension("tmp");
616 fs::write(&tmp, new)?;
617 fs::rename(tmp, path)?;
618 Ok(())
619 })
620 .await
621 }
622 async fn delete_root(&self, name: &str) -> Result<(), StoreError> {
623 let store = self.clone();
624 let name = name.to_owned();
625 run_blocking(move || {
626 let _lock = store.root_lock(&name)?;
627 match fs::remove_file(store.root_path(&name)?) {
628 Ok(()) => Ok(()),
629 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
630 Err(error) => Err(error.into()),
631 }
632 })
633 .await
634 }
635 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError> {
636 let root = self.root.clone();
637 let prefix = prefix.to_owned();
638 run_blocking(move || {
639 let mut names = Vec::new();
640 for entry in fs::read_dir(root.join("roots"))? {
641 let entry = entry?;
642 if !entry.file_type()?.is_file() {
643 continue;
644 }
645 if let Some(name) = entry.file_name().to_str() {
646 let auxiliary = Path::new(name).extension().is_some_and(|ext| {
647 ext.eq_ignore_ascii_case("lock") || ext.eq_ignore_ascii_case("tmp")
648 });
649 if name.starts_with(&prefix) && !auxiliary {
650 names.push(name.to_owned());
651 }
652 }
653 }
654 names.sort();
655 Ok(names)
656 })
657 .await
658 }
659}
660
661#[must_use]
663pub fn digest(bytes: &[u8]) -> BlobId {
664 BlobId(blake3::hash(bytes).to_hex().to_string())
665}
666
667#[must_use]
669pub fn db_root_name(db: &str) -> String {
670 format!("db:{db}")
671}
672
673#[must_use]
675pub fn meta_root_name(db: &str) -> String {
676 format!("meta:{db}")
677}
678
679pub const FORMAT_VERSION: u32 = 4;
697
698#[derive(Clone, Debug, Eq, PartialEq)]
707pub struct DbRoot {
708 pub format_version: u32,
710 pub lease_version: u64,
712 pub owner: String,
715 pub lease_expires_unix_ms: i64,
717 pub owner_endpoint: String,
720 pub index_basis_t: u64,
722 pub roots: Option<[BlobId; 4]>,
725 pub next_entity_id: u64,
735 pub last_tx_instant: i64,
742 pub key_manifest_version: u64,
752}
753
754fn field_line(out: &mut String, value: &str) {
756 if value.is_empty() {
757 out.push('-');
758 } else {
759 out.push_str(value);
760 }
761 out.push('\n');
762}
763
764fn parse_field(line: &str) -> String {
765 if line == "-" {
766 String::new()
767 } else {
768 line.to_owned()
769 }
770}
771
772impl DbRoot {
773 #[must_use]
775 pub fn encode(&self) -> Vec<u8> {
776 let mut out = format!(
777 "corium-root-v{}\n{}\n{}\n",
778 self.format_version, self.lease_version, self.index_basis_t
779 );
780 match &self.roots {
781 Some(roots) => {
782 for root in roots {
783 out.push_str(root.as_str());
784 out.push('\n');
785 }
786 }
787 None => out.push_str("-\n-\n-\n-\n"),
788 }
789 field_line(&mut out, &self.owner);
790 out.push_str(&self.lease_expires_unix_ms.to_string());
791 out.push('\n');
792 field_line(&mut out, &self.owner_endpoint);
793 out.push_str(&self.next_entity_id.to_string());
797 out.push('\n');
798 out.push_str(&self.last_tx_instant.to_string());
799 out.push('\n');
800 out.push_str(&self.key_manifest_version.to_string());
801 out.into_bytes()
802 }
803
804 #[must_use]
808 pub fn decode(bytes: &[u8]) -> Option<Self> {
809 let text = std::str::from_utf8(bytes).ok()?;
810 let mut lines = text.lines();
811 let first = lines.next()?;
812 let (format_version, lease_version) =
813 if let Some(version) = first.strip_prefix("corium-root-v") {
814 let format_version = version.parse().ok()?;
815 let lease_version = lines.next()?.parse().ok()?;
816 (format_version, lease_version)
817 } else {
818 (1, first.parse().ok()?)
821 };
822 let index_basis_t = lines.next()?.parse().ok()?;
823 let ids: Vec<&str> = lines.by_ref().take(4).collect();
824 if ids.len() != 4 {
825 return None;
826 }
827 let roots = if ids.iter().all(|id| *id == "-") {
828 None
829 } else {
830 Some([
831 BlobId::from_hex(ids[0])?,
832 BlobId::from_hex(ids[1])?,
833 BlobId::from_hex(ids[2])?,
834 BlobId::from_hex(ids[3])?,
835 ])
836 };
837 let owner = lines.next().map(parse_field).unwrap_or_default();
839 let lease_expires_unix_ms = lines.next().and_then(|l| l.parse().ok()).unwrap_or(0);
840 let owner_endpoint = lines.next().map(parse_field).unwrap_or_default();
841 let next_entity_id = lines.next().and_then(|l| l.parse().ok()).unwrap_or(0);
845 let last_tx_instant = lines
846 .next()
847 .and_then(|l| l.parse().ok())
848 .unwrap_or(i64::MIN);
849 let key_manifest_version = lines.next().and_then(|l| l.parse().ok()).unwrap_or(0);
852 Some(Self {
853 format_version,
854 lease_version,
855 owner,
856 lease_expires_unix_ms,
857 owner_endpoint,
858 index_basis_t,
859 roots,
860 next_entity_id,
861 last_tx_instant,
862 key_manifest_version,
863 })
864 }
865}
866
867#[derive(Clone, Copy, Debug, Eq, PartialEq)]
869pub struct GcReport {
870 pub marked: usize,
872 pub swept: usize,
874 pub retained: usize,
876}
877
878pub async fn mark_and_sweep(
887 store: &dyn BlobStore,
888 live_roots: impl IntoIterator<Item = BlobId>,
889 mut children: impl FnMut(&BlobId, &[u8]) -> Result<Vec<BlobId>, StoreError>,
890) -> Result<GcReport, StoreError> {
891 mark_and_sweep_retained(
892 store,
893 live_roots,
894 &mut children,
895 Duration::ZERO,
896 SystemTime::now(),
897 )
898 .await
899}
900
901pub async fn mark_and_sweep_retained(
907 store: &dyn BlobStore,
908 live_roots: impl IntoIterator<Item = BlobId>,
909 mut children: impl FnMut(&BlobId, &[u8]) -> Result<Vec<BlobId>, StoreError>,
910 retention: Duration,
911 now: SystemTime,
912) -> Result<GcReport, StoreError> {
913 let mut marked = HashSet::new();
914 let mut pending = live_roots.into_iter().collect::<Vec<_>>();
915 while let Some(id) = pending.pop() {
916 if !marked.insert(id.clone()) {
917 continue;
918 }
919 let bytes = store
920 .get(&id)
921 .await?
922 .ok_or_else(|| StoreError::MissingBlob(id.clone()))?;
923 pending.extend(children(&id, &bytes)?);
924 }
925
926 let mut swept = 0;
927 let mut retained = 0;
928 let mut ids = store.list().await?;
929 while let Some(id) = ids.next().await {
930 let id = id?;
931 if !marked.contains(&id) {
932 let old_enough = retention.is_zero()
936 || store.modified_at(&id).await?.is_some_and(|modified| {
937 now.duration_since(modified).unwrap_or_default() >= retention
938 });
939 if old_enough {
940 store.delete(&id).await?;
941 swept += 1;
942 } else {
943 retained += 1;
944 }
945 }
946 }
947 Ok(GcReport {
948 marked: marked.len(),
949 swept,
950 retained,
951 })
952}
953
954struct RootLock {
955 file: File,
956}
957
958impl RootLock {
959 fn acquire(path: &Path) -> Result<Self, StoreError> {
960 let file = OpenOptions::new()
961 .read(true)
962 .write(true)
963 .create(true)
964 .truncate(false)
965 .open(path)?;
966 file.lock_exclusive()?;
967 Ok(Self { file })
968 }
969}
970
971impl Drop for RootLock {
972 fn drop(&mut self) {
973 let _ = FileExt::unlock(&self.file);
974 }
978}