1use async_trait::async_trait;
10use hashtree_core::store::{Store, StoreError, StoreStats};
11use hashtree_core::types::Hash;
12use std::collections::HashMap;
13use std::fs;
14use std::path::{Path, PathBuf};
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::sync::RwLock;
17use std::time::SystemTime;
18
19pub struct FsBlobStore {
26 base_path: PathBuf,
27 max_bytes: AtomicU64,
28 pins: RwLock<HashMap<String, u32>>,
30}
31
32impl FsBlobStore {
33 pub fn new<P: AsRef<Path>>(path: P) -> Result<Self, StoreError> {
37 let base_path = path.as_ref().to_path_buf();
38 fs::create_dir_all(&base_path)?;
39
40 let pins = Self::load_pins(&base_path).unwrap_or_default();
42
43 Ok(Self {
44 base_path,
45 max_bytes: AtomicU64::new(0), pins: RwLock::new(pins),
47 })
48 }
49
50 pub fn with_max_bytes<P: AsRef<Path>>(path: P, max_bytes: u64) -> Result<Self, StoreError> {
52 let store = Self::new(path)?;
53 store.max_bytes.store(max_bytes, Ordering::Relaxed);
54 Ok(store)
55 }
56
57 fn pins_path(&self) -> PathBuf {
59 self.base_path.join("pins.json")
60 }
61
62 fn load_pins(base_path: &Path) -> Option<HashMap<String, u32>> {
64 let pins_path = base_path.join("pins.json");
65 let contents = fs::read_to_string(pins_path).ok()?;
66 serde_json::from_str(&contents).ok()
67 }
68
69 fn save_pins(&self) -> Result<(), StoreError> {
71 let pins = self.pins.read().unwrap();
72 let json = serde_json::to_string(&*pins)
73 .map_err(|e| StoreError::Other(format!("Failed to serialize pins: {}", e)))?;
74 fs::write(self.pins_path(), json)?;
75 Ok(())
76 }
77
78 fn blob_path_from_hex(&self, hash_hex: &str) -> PathBuf {
79 let (prefix, rest) = hash_hex.split_at(2);
80 let (subdir, filename) = rest.split_at(2);
81 self.base_path.join(prefix).join(subdir).join(filename)
82 }
83
84 fn legacy_blob_path(&self, hash: &Hash) -> PathBuf {
85 let hex = hex::encode(hash);
86 let (prefix, rest) = hex.split_at(2);
87 self.base_path.join(prefix).join(rest)
88 }
89
90 fn blob_path(&self, hash: &Hash) -> PathBuf {
94 self.blob_path_from_hex(&hex::encode(hash))
95 }
96
97 fn existing_blob_path(&self, hash: &Hash) -> Option<PathBuf> {
98 let primary = self.blob_path(hash);
99 if primary.exists() {
100 return Some(primary);
101 }
102
103 let legacy = self.legacy_blob_path(hash);
104 if legacy.exists() {
105 return Some(legacy);
106 }
107
108 None
109 }
110
111 fn hash_hex_for_blob_path(&self, path: &Path) -> Option<String> {
112 let relative = path.strip_prefix(&self.base_path).ok()?;
113 let mut hex = String::new();
114
115 for component in relative.iter() {
116 let part = component.to_str()?;
117 hex.push_str(part);
118 }
119
120 if hex.len() != 64 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
121 return None;
122 }
123
124 Some(hex.to_ascii_lowercase())
125 }
126
127 fn collect_blob_metadata_recursive(
128 &self,
129 dir: &Path,
130 blobs: &mut Vec<(PathBuf, String, fs::Metadata)>,
131 ) -> Result<(), StoreError> {
132 let entries = match fs::read_dir(dir) {
133 Ok(e) => e,
134 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
135 Err(e) => return Err(e.into()),
136 };
137
138 for entry in entries {
139 let entry = entry?;
140 let file_type = entry.file_type()?;
141 let path = entry.path();
142
143 if file_type.is_dir() {
144 self.collect_blob_metadata_recursive(&path, blobs)?;
145 continue;
146 }
147
148 if !file_type.is_file() {
149 continue;
150 }
151
152 if let Some(hex) = self.hash_hex_for_blob_path(&path) {
153 blobs.push((path, hex, entry.metadata()?));
154 }
155 }
156
157 Ok(())
158 }
159
160 fn collect_blob_metadata(&self) -> Result<Vec<(PathBuf, String, fs::Metadata)>, StoreError> {
161 let mut blobs = Vec::new();
162 self.collect_blob_metadata_recursive(&self.base_path, &mut blobs)?;
163 Ok(blobs)
164 }
165
166 pub fn put_sync(&self, hash: Hash, data: &[u8]) -> Result<bool, StoreError> {
168 let path = self.blob_path(&hash);
169
170 if self.existing_blob_path(&hash).is_some() {
172 return Ok(false);
173 }
174
175 if let Some(parent) = path.parent() {
177 fs::create_dir_all(parent)?;
178 }
179
180 let temp_path = path.with_extension("tmp");
182 fs::write(&temp_path, data)?;
183 fs::rename(&temp_path, &path)?;
184
185 Ok(true)
186 }
187
188 pub fn get_sync(&self, hash: &Hash) -> Result<Option<Vec<u8>>, StoreError> {
190 if let Some(path) = self.existing_blob_path(hash) {
191 Ok(Some(fs::read(&path)?))
192 } else {
193 Ok(None)
194 }
195 }
196
197 pub fn exists(&self, hash: &Hash) -> bool {
199 self.existing_blob_path(hash).is_some()
200 }
201
202 pub fn delete_sync(&self, hash: &Hash) -> Result<bool, StoreError> {
204 let primary = self.blob_path(hash);
205 let legacy = self.legacy_blob_path(hash);
206 let mut deleted = false;
207
208 for path in [primary, legacy] {
209 if path.exists() {
210 fs::remove_file(path)?;
211 deleted = true;
212 }
213 }
214
215 Ok(deleted)
216 }
217
218 pub fn list(&self) -> Result<Vec<Hash>, StoreError> {
220 let mut hashes = Vec::new();
221
222 for (_, full_hex, _) in self.collect_blob_metadata()? {
223 if let Ok(bytes) = hex::decode(&full_hex) {
224 if bytes.len() == 32 {
225 let mut hash = [0u8; 32];
226 hash.copy_from_slice(&bytes);
227 hashes.push(hash);
228 }
229 }
230 }
231
232 Ok(hashes)
233 }
234
235 pub fn stats(&self) -> Result<FsStats, StoreError> {
237 let pins = self.pins.read().unwrap();
238 let mut count = 0usize;
239 let mut total_bytes = 0u64;
240 let mut pinned_count = 0usize;
241 let mut pinned_bytes = 0u64;
242
243 for (_, hex, metadata) in self.collect_blob_metadata()? {
244 let size = metadata.len();
245 count += 1;
246 total_bytes += size;
247
248 if pins.get(&hex).copied().unwrap_or(0) > 0 {
249 pinned_count += 1;
250 pinned_bytes += size;
251 }
252 }
253
254 Ok(FsStats {
255 count,
256 total_bytes,
257 pinned_count,
258 pinned_bytes,
259 })
260 }
261
262 fn collect_blobs_for_eviction(&self) -> Vec<(PathBuf, String, SystemTime, u64)> {
264 self.collect_blob_metadata()
265 .map(|blobs| {
266 blobs
267 .into_iter()
268 .map(|(path, hex, metadata)| {
269 let mtime = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
270 let size = metadata.len();
271 (path, hex, mtime, size)
272 })
273 .collect()
274 })
275 .unwrap_or_default()
276 }
277
278 fn evict_to_target(&self, target_bytes: u64) -> u64 {
280 let pins = self.pins.read().unwrap();
281
282 let mut blobs = self.collect_blobs_for_eviction();
284
285 blobs.retain(|(_, hex, _, _)| pins.get(hex).copied().unwrap_or(0) == 0);
287
288 blobs.sort_by_key(|(_, _, mtime, _)| *mtime);
290
291 drop(pins); let current_bytes: u64 = self
295 .collect_blobs_for_eviction()
296 .iter()
297 .map(|(_, _, _, size)| *size)
298 .sum();
299
300 if current_bytes <= target_bytes {
301 return 0;
302 }
303
304 let to_free = current_bytes - target_bytes;
305 let mut freed = 0u64;
306
307 for (path, _, _, size) in blobs {
308 if freed >= to_free {
309 break;
310 }
311 if fs::remove_file(&path).is_ok() {
312 freed += size;
313 }
314 }
315
316 freed
317 }
318}
319
320#[derive(Debug, Clone)]
322pub struct FsStats {
323 pub count: usize,
324 pub total_bytes: u64,
325 pub pinned_count: usize,
326 pub pinned_bytes: u64,
327}
328
329#[async_trait]
330impl Store for FsBlobStore {
331 async fn put(&self, hash: Hash, data: Vec<u8>) -> Result<bool, StoreError> {
332 self.put_sync(hash, &data)
333 }
334
335 async fn get(&self, hash: &Hash) -> Result<Option<Vec<u8>>, StoreError> {
336 self.get_sync(hash)
337 }
338
339 async fn has(&self, hash: &Hash) -> Result<bool, StoreError> {
340 Ok(self.exists(hash))
341 }
342
343 async fn delete(&self, hash: &Hash) -> Result<bool, StoreError> {
344 let hex = hex::encode(hash);
345 {
347 let mut pins = self.pins.write().unwrap();
348 pins.remove(&hex);
349 }
350 let _ = self.save_pins(); self.delete_sync(hash)
352 }
353
354 fn set_max_bytes(&self, max: u64) {
355 self.max_bytes.store(max, Ordering::Relaxed);
356 }
357
358 fn max_bytes(&self) -> Option<u64> {
359 let max = self.max_bytes.load(Ordering::Relaxed);
360 if max > 0 {
361 Some(max)
362 } else {
363 None
364 }
365 }
366
367 async fn stats(&self) -> StoreStats {
368 match self.stats() {
369 Ok(fs_stats) => StoreStats {
370 count: fs_stats.count as u64,
371 bytes: fs_stats.total_bytes,
372 pinned_count: fs_stats.pinned_count as u64,
373 pinned_bytes: fs_stats.pinned_bytes,
374 },
375 Err(_) => StoreStats::default(),
376 }
377 }
378
379 async fn evict_if_needed(&self) -> Result<u64, StoreError> {
380 let max = self.max_bytes.load(Ordering::Relaxed);
381 if max == 0 {
382 return Ok(0); }
384
385 let current = match self.stats() {
386 Ok(s) => s.total_bytes,
387 Err(_) => return Ok(0),
388 };
389
390 if current <= max {
391 return Ok(0);
392 }
393
394 let target = max * 9 / 10;
396 Ok(self.evict_to_target(target))
397 }
398
399 async fn pin(&self, hash: &Hash) -> Result<(), StoreError> {
400 let hex = hex::encode(hash);
401 {
402 let mut pins = self.pins.write().unwrap();
403 *pins.entry(hex).or_insert(0) += 1;
404 }
405 self.save_pins()
406 }
407
408 async fn unpin(&self, hash: &Hash) -> Result<(), StoreError> {
409 let hex = hex::encode(hash);
410 {
411 let mut pins = self.pins.write().unwrap();
412 if let Some(count) = pins.get_mut(&hex) {
413 if *count > 0 {
414 *count -= 1;
415 }
416 if *count == 0 {
417 pins.remove(&hex);
418 }
419 }
420 }
421 self.save_pins()
422 }
423
424 fn pin_count(&self, hash: &Hash) -> u32 {
425 let hex = hex::encode(hash);
426 self.pins.read().unwrap().get(&hex).copied().unwrap_or(0)
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use hashtree_core::sha256;
434 use tempfile::TempDir;
435
436 #[tokio::test]
437 async fn test_put_get() {
438 let temp = TempDir::new().unwrap();
439 let store = FsBlobStore::new(temp.path().join("blobs")).unwrap();
440
441 let data = b"hello filesystem";
442 let hash = sha256(data);
443 store.put(hash, data.to_vec()).await.unwrap();
444
445 assert!(store.has(&hash).await.unwrap());
446 assert_eq!(store.get(&hash).await.unwrap(), Some(data.to_vec()));
447 }
448
449 #[tokio::test]
450 async fn test_get_missing() {
451 let temp = TempDir::new().unwrap();
452 let store = FsBlobStore::new(temp.path().join("blobs")).unwrap();
453
454 let hash = [0u8; 32];
455 assert!(!store.has(&hash).await.unwrap());
456 assert_eq!(store.get(&hash).await.unwrap(), None);
457 }
458
459 #[tokio::test]
460 async fn test_delete() {
461 let temp = TempDir::new().unwrap();
462 let store = FsBlobStore::new(temp.path().join("blobs")).unwrap();
463
464 let data = b"delete me";
465 let hash = sha256(data);
466 store.put(hash, data.to_vec()).await.unwrap();
467 assert!(store.has(&hash).await.unwrap());
468
469 assert!(store.delete(&hash).await.unwrap());
470 assert!(!store.has(&hash).await.unwrap());
471 assert!(!store.delete(&hash).await.unwrap());
472 }
473
474 #[tokio::test]
475 async fn test_deduplication() {
476 let temp = TempDir::new().unwrap();
477 let store = FsBlobStore::new(temp.path().join("blobs")).unwrap();
478
479 let data = b"same content";
480 let hash = sha256(data);
481
482 assert!(store.put(hash, data.to_vec()).await.unwrap());
484 assert!(!store.put(hash, data.to_vec()).await.unwrap());
486
487 assert_eq!(store.list().unwrap().len(), 1);
488 }
489
490 #[tokio::test]
491 async fn test_list() {
492 let temp = TempDir::new().unwrap();
493 let store = FsBlobStore::new(temp.path().join("blobs")).unwrap();
494
495 let d1 = b"one";
496 let d2 = b"two";
497 let d3 = b"three";
498 let h1 = sha256(d1);
499 let h2 = sha256(d2);
500 let h3 = sha256(d3);
501
502 store.put(h1, d1.to_vec()).await.unwrap();
503 store.put(h2, d2.to_vec()).await.unwrap();
504 store.put(h3, d3.to_vec()).await.unwrap();
505
506 let hashes = store.list().unwrap();
507 assert_eq!(hashes.len(), 3);
508 assert!(hashes.contains(&h1));
509 assert!(hashes.contains(&h2));
510 assert!(hashes.contains(&h3));
511 }
512
513 #[tokio::test]
514 async fn test_stats() {
515 let temp = TempDir::new().unwrap();
516 let store = FsBlobStore::new(temp.path().join("blobs")).unwrap();
517
518 let d1 = b"hello";
519 let d2 = b"world";
520 let h1 = sha256(d1);
521 store.put(h1, d1.to_vec()).await.unwrap();
522 store.put(sha256(d2), d2.to_vec()).await.unwrap();
523
524 let stats = store.stats().unwrap();
525 assert_eq!(stats.count, 2);
526 assert_eq!(stats.total_bytes, 10);
527 assert_eq!(stats.pinned_count, 0);
528 assert_eq!(stats.pinned_bytes, 0);
529
530 store.pin(&h1).await.unwrap();
532 let stats = store.stats().unwrap();
533 assert_eq!(stats.pinned_count, 1);
534 assert_eq!(stats.pinned_bytes, 5);
535 }
536
537 #[tokio::test]
538 async fn test_directory_structure() {
539 let temp = TempDir::new().unwrap();
540 let blobs_path = temp.path().join("blobs");
541 let store = FsBlobStore::new(&blobs_path).unwrap();
542
543 let data = b"test data";
544 let hash = sha256(data);
545 let hex = hex::encode(hash);
546
547 store.put(hash, data.to_vec()).await.unwrap();
548
549 let prefix = &hex[..2];
551 let subdir = &hex[2..4];
552 let rest = &hex[4..];
553 let expected_path = blobs_path.join(prefix).join(subdir).join(rest);
554
555 assert!(
556 expected_path.exists(),
557 "Blob should be at {:?}",
558 expected_path
559 );
560 assert_eq!(fs::read(&expected_path).unwrap(), data);
561 }
562
563 #[test]
564 fn test_blob_path_format() {
565 let temp = TempDir::new().unwrap();
566 let store = FsBlobStore::new(temp.path()).unwrap();
567
568 let mut hash = [0u8; 32];
570 hash[0] = 0x00;
571 hash[1] = 0x11;
572 hash[2] = 0x22;
573
574 let path = store.blob_path(&hash);
575 let path_str = path.to_string_lossy();
576
577 assert!(
579 path_str.contains("/00/"),
580 "Path should contain /00/ directory: {}",
581 path_str
582 );
583 assert!(
585 path_str.contains("/11/"),
586 "Path should contain /11/ directory: {}",
587 path_str
588 );
589 assert!(path.file_name().unwrap().len() == 60);
591 }
592
593 #[tokio::test]
594 async fn test_legacy_single_level_layout_remains_readable() {
595 let temp = TempDir::new().unwrap();
596 let blobs_path = temp.path().join("blobs");
597 let store = FsBlobStore::new(&blobs_path).unwrap();
598
599 let data = b"legacy blob";
600 let hash = sha256(data);
601 let hex = hex::encode(hash);
602 let legacy_path = blobs_path.join(&hex[..2]).join(&hex[2..]);
603 fs::create_dir_all(legacy_path.parent().unwrap()).unwrap();
604 fs::write(&legacy_path, data).unwrap();
605
606 assert!(store.has(&hash).await.unwrap());
607 assert_eq!(store.get(&hash).await.unwrap(), Some(data.to_vec()));
608
609 let listed = store.list().unwrap();
610 assert_eq!(listed, vec![hash]);
611
612 let stats = store.stats().unwrap();
613 assert_eq!(stats.count, 1);
614 assert_eq!(stats.total_bytes, data.len() as u64);
615
616 assert!(!store.put(hash, data.to_vec()).await.unwrap());
617 assert!(store.delete(&hash).await.unwrap());
618 assert!(!legacy_path.exists());
619 }
620
621 #[tokio::test]
622 async fn test_empty_store_stats() {
623 let temp = TempDir::new().unwrap();
624 let store = FsBlobStore::new(temp.path().join("blobs")).unwrap();
625
626 let stats = store.stats().unwrap();
627 assert_eq!(stats.count, 0);
628 assert_eq!(stats.total_bytes, 0);
629 }
630
631 #[tokio::test]
632 async fn test_empty_store_list() {
633 let temp = TempDir::new().unwrap();
634 let store = FsBlobStore::new(temp.path().join("blobs")).unwrap();
635
636 let hashes = store.list().unwrap();
637 assert!(hashes.is_empty());
638 }
639
640 #[tokio::test]
641 async fn test_pin_and_unpin() {
642 let temp = TempDir::new().unwrap();
643 let store = FsBlobStore::new(temp.path().join("blobs")).unwrap();
644
645 let data = b"pin me";
646 let hash = sha256(data);
647 store.put(hash, data.to_vec()).await.unwrap();
648
649 assert!(!store.is_pinned(&hash));
651 assert_eq!(store.pin_count(&hash), 0);
652
653 store.pin(&hash).await.unwrap();
655 assert!(store.is_pinned(&hash));
656 assert_eq!(store.pin_count(&hash), 1);
657
658 store.unpin(&hash).await.unwrap();
660 assert!(!store.is_pinned(&hash));
661 assert_eq!(store.pin_count(&hash), 0);
662 }
663
664 #[tokio::test]
665 async fn test_pin_ref_counting() {
666 let temp = TempDir::new().unwrap();
667 let store = FsBlobStore::new(temp.path().join("blobs")).unwrap();
668
669 let data = b"multi pin";
670 let hash = sha256(data);
671 store.put(hash, data.to_vec()).await.unwrap();
672
673 store.pin(&hash).await.unwrap();
675 store.pin(&hash).await.unwrap();
676 store.pin(&hash).await.unwrap();
677 assert_eq!(store.pin_count(&hash), 3);
678
679 store.unpin(&hash).await.unwrap();
681 assert_eq!(store.pin_count(&hash), 2);
682 assert!(store.is_pinned(&hash));
683
684 store.unpin(&hash).await.unwrap();
686 store.unpin(&hash).await.unwrap();
687 assert_eq!(store.pin_count(&hash), 0);
688 }
689
690 #[tokio::test]
691 async fn test_pins_persist_across_reload() {
692 let temp = TempDir::new().unwrap();
693 let blobs_path = temp.path().join("blobs");
694
695 let data = b"persist me";
696 let hash = sha256(data);
697
698 {
700 let store = FsBlobStore::new(&blobs_path).unwrap();
701 store.put(hash, data.to_vec()).await.unwrap();
702 store.pin(&hash).await.unwrap();
703 store.pin(&hash).await.unwrap();
704 assert_eq!(store.pin_count(&hash), 2);
705 }
706
707 {
709 let store = FsBlobStore::new(&blobs_path).unwrap();
710 assert_eq!(store.pin_count(&hash), 2);
711 assert!(store.is_pinned(&hash));
712 }
713 }
714
715 #[tokio::test]
716 async fn test_max_bytes() {
717 let temp = TempDir::new().unwrap();
718 let store = FsBlobStore::new(temp.path().join("blobs")).unwrap();
719
720 assert!(store.max_bytes().is_none());
721
722 store.set_max_bytes(1000);
723 assert_eq!(store.max_bytes(), Some(1000));
724
725 store.set_max_bytes(0);
726 assert!(store.max_bytes().is_none());
727 }
728
729 #[tokio::test]
730 async fn test_with_max_bytes() {
731 let temp = TempDir::new().unwrap();
732 let store = FsBlobStore::with_max_bytes(temp.path().join("blobs"), 500).unwrap();
733 assert_eq!(store.max_bytes(), Some(500));
734 }
735
736 #[tokio::test]
737 async fn test_eviction_respects_pins() {
738 let temp = TempDir::new().unwrap();
739 let store = FsBlobStore::with_max_bytes(temp.path().join("blobs"), 20).unwrap();
741
742 let d1 = b"aaaaa"; let d2 = b"bbbbb";
745 let d3 = b"ccccc";
746 let h1 = sha256(d1);
747 let h2 = sha256(d2);
748 let h3 = sha256(d3);
749
750 store.put(h1, d1.to_vec()).await.unwrap();
751 std::thread::sleep(std::time::Duration::from_millis(10)); store.put(h2, d2.to_vec()).await.unwrap();
753 std::thread::sleep(std::time::Duration::from_millis(10));
754 store.put(h3, d3.to_vec()).await.unwrap();
755
756 store.pin(&h1).await.unwrap();
758
759 let d4 = b"ddddd";
761 let h4 = sha256(d4);
762 std::thread::sleep(std::time::Duration::from_millis(10));
763 store.put(h4, d4.to_vec()).await.unwrap();
764
765 let d5 = b"eeeee";
767 let h5 = sha256(d5);
768 std::thread::sleep(std::time::Duration::from_millis(10));
769 store.put(h5, d5.to_vec()).await.unwrap();
770
771 let freed = store.evict_if_needed().await.unwrap();
773 assert!(freed > 0, "Should have freed some bytes");
774
775 assert!(store.has(&h1).await.unwrap(), "Pinned item should exist");
777 assert!(
779 !store.has(&h2).await.unwrap(),
780 "Oldest unpinned should be evicted"
781 );
782 assert!(store.has(&h5).await.unwrap(), "Newest should exist");
784 }
785
786 #[tokio::test]
787 async fn test_no_eviction_when_under_limit() {
788 let temp = TempDir::new().unwrap();
789 let store = FsBlobStore::with_max_bytes(temp.path().join("blobs"), 1000).unwrap();
790
791 let data = b"small";
792 let hash = sha256(data);
793 store.put(hash, data.to_vec()).await.unwrap();
794
795 let freed = store.evict_if_needed().await.unwrap();
796 assert_eq!(freed, 0);
797 assert!(store.has(&hash).await.unwrap());
798 }
799
800 #[tokio::test]
801 async fn test_no_eviction_without_limit() {
802 let temp = TempDir::new().unwrap();
803 let store = FsBlobStore::new(temp.path().join("blobs")).unwrap();
804
805 for i in 0..10u8 {
806 let data = vec![i; 100];
807 let hash = sha256(&data);
808 store.put(hash, data).await.unwrap();
809 }
810
811 let freed = store.evict_if_needed().await.unwrap();
812 assert_eq!(freed, 0);
813 assert_eq!(store.list().unwrap().len(), 10);
814 }
815
816 #[tokio::test]
817 async fn test_delete_removes_pin() {
818 let temp = TempDir::new().unwrap();
819 let store = FsBlobStore::new(temp.path().join("blobs")).unwrap();
820
821 let data = b"delete pinned";
822 let hash = sha256(data);
823 store.put(hash, data.to_vec()).await.unwrap();
824 store.pin(&hash).await.unwrap();
825 assert!(store.is_pinned(&hash));
826
827 store.delete(&hash).await.unwrap();
828 assert_eq!(store.pin_count(&hash), 0);
829 }
830}