1use std::{path::PathBuf, sync::Arc};
5
6use crate::object::{
7 Action, ActionId, AnnotatedTag, Blob, ContentHash, State, StateAttachment, StateAttachmentId,
8 StateId, Tree,
9};
10
11pub mod actor_presence;
12pub mod agent_task;
13pub mod codec;
14pub mod fs;
15pub mod liveness;
16#[cfg(any(test, feature = "memory-backend"))]
17pub mod memory;
18pub use heddle_pack::store::pack;
19pub mod shallow;
20mod snapshot_commit;
21pub mod source;
22pub mod store_compliance;
23pub mod writer_lease;
24
25pub use actor_presence::{
26 ActorChainNode, ActorPresence, ActorPresenceStatus, ActorPresenceStore, AgentUsageSummary,
27 ContextQueryEntry, generate_actor_session_id,
28};
29pub use agent_task::{
30 AGENT_TASK_SCHEMA_VERSION, AgentTaskRecord, AgentTaskStatus, AgentTaskStore,
31 generate_agent_task_id, validate_task_id,
32};
33pub use fs::{
34 DEFAULT_PACK_INSTALL_INTENT_TTL_SECS, FsRepackOperation, FsStore, PackInstallIntent,
35 PackInstallMetricsSnapshot, PackInstallPhase, PackInstallRecoverReport,
36 install_pack_bytes_journaled, pack_install_metrics_reset, pack_install_metrics_snapshot,
37 recover_pack_install_intents, recover_pack_install_intents_with_ttl,
38};
39pub use heddle_format::compression::{CompressionConfig, CompressionError, compress, decompress};
40pub use liveness::{
41 AGENT_LEASE_DURATION, Liveness, current_boot_id, process_alive, reservation_liveness_at,
42};
43#[cfg(any(test, feature = "memory-backend"))]
44pub use memory::InMemoryStore;
45pub use pack::{
46 CancellationToken as RepackCancellationToken, LoadMonitor as RepackLoadMonitor, PackBuilder,
47 PackObjectId, PackReader, PackStats, RepackContext, RepackError, RepackHandle, RepackInventory,
48 RepackOperation, RepackOutcome, RepackPolicy, RepackReason, RepackReport, RepackResourceLimits,
49 RepackSchedule, RepackScheduler, StreamingPackBuilder, SyncData,
50};
51pub use shallow::ShallowInfo;
52#[doc(hidden)]
53pub use snapshot_commit::{
54 SNAPSHOT_COMMIT_ARTIFACT_SCHEMA, SnapshotCommitArtifact, SnapshotCommitDescriptor,
55 SnapshotPackManager,
56};
57#[cfg(feature = "async-source")]
58pub use source::AsyncObjectSource;
59pub use source::ObjectSource;
60pub use writer_lease::{
61 WriterLease, WriterLeaseAuthOutcome, WriterLeaseDraft, WriterLeaseGrant,
62 WriterLeaseReserveOutcome, WriterLeaseStatus, WriterLeaseStore, generate_writer_lease_id,
63 generate_writer_lease_token,
64};
65
66pub trait ExternalObjectSource: Send + Sync {
70 fn get_blob(&self, hash: &ContentHash) -> Result<Option<Blob>>;
71 fn get_tree(&self, hash: &ContentHash) -> Result<Option<Tree>>;
72 fn get_state(&self, id: &StateId) -> Result<Option<State>>;
73 fn list_states(&self) -> Result<Vec<StateId>>;
74}
75
76pub use crate::error::{HeddleError as StoreError, HeddleError, Result};
77
78#[derive(Clone)]
90pub enum AnyStore {
91 Fs(FsStore),
92}
93
94macro_rules! any_store_dispatch {
100 ($self:ident, $method:ident ( $($arg:expr),* )) => {
101 match $self {
102 AnyStore::Fs(inner) => inner.$method($($arg),*),
103 }
104 };
105}
106
107impl ObjectStore for AnyStore {
108 fn get_annotated_tag(&self, hash: &ContentHash) -> Result<Option<AnnotatedTag>> {
109 any_store_dispatch!(self, get_annotated_tag(hash))
110 }
111 fn put_annotated_tag(&self, tag: &AnnotatedTag) -> Result<ContentHash> {
112 any_store_dispatch!(self, put_annotated_tag(tag))
113 }
114 fn list_annotated_tags(&self) -> Result<Vec<ContentHash>> {
115 any_store_dispatch!(self, list_annotated_tags())
116 }
117 fn get_blob(&self, hash: &ContentHash) -> Result<Option<Blob>> {
118 match self {
119 AnyStore::Fs(inner) => ObjectStore::get_blob(inner, hash),
120 }
121 }
122 fn put_blob(&self, blob: &Blob) -> Result<ContentHash> {
123 any_store_dispatch!(self, put_blob(blob))
124 }
125 fn get_blob_bytes(&self, hash: &ContentHash) -> Result<Option<bytes::Bytes>> {
126 match self {
127 AnyStore::Fs(inner) => ObjectStore::get_blob_bytes(inner, hash),
128 }
129 }
130 fn blob_size(&self, hash: &ContentHash) -> Result<Option<u64>> {
131 any_store_dispatch!(self, blob_size(hash))
132 }
133 fn loose_blob_path(&self, hash: &ContentHash) -> Option<PathBuf> {
134 any_store_dispatch!(self, loose_blob_path(hash))
135 }
136 fn promote_to_loose_uncompressed(&self, hash: &ContentHash) -> Result<bool> {
137 any_store_dispatch!(self, promote_to_loose_uncompressed(hash))
138 }
139 fn clear_recent_caches(&self) {
140 any_store_dispatch!(self, clear_recent_caches())
141 }
142 fn put_blob_with_hash(&self, blob: &Blob, hash: ContentHash) -> Result<ContentHash> {
143 any_store_dispatch!(self, put_blob_with_hash(blob, hash))
144 }
145 fn has_blob(&self, hash: &ContentHash) -> Result<bool> {
146 any_store_dispatch!(self, has_blob(hash))
147 }
148 fn has_blob_locally(&self, hash: &ContentHash) -> Result<bool> {
149 any_store_dispatch!(self, has_blob_locally(hash))
150 }
151 fn get_tree(&self, hash: &ContentHash) -> Result<Option<Tree>> {
152 match self {
153 AnyStore::Fs(inner) => ObjectStore::get_tree(inner, hash),
154 }
155 }
156 fn get_tree_serialized(&self, hash: &ContentHash) -> Result<Option<Vec<u8>>> {
157 match self {
158 AnyStore::Fs(inner) => ObjectStore::get_tree_serialized(inner, hash),
159 }
160 }
161 fn put_tree(&self, tree: &Tree) -> Result<ContentHash> {
162 any_store_dispatch!(self, put_tree(tree))
163 }
164 fn has_tree(&self, hash: &ContentHash) -> Result<bool> {
165 any_store_dispatch!(self, has_tree(hash))
166 }
167 fn has_tree_locally(&self, hash: &ContentHash) -> Result<bool> {
168 any_store_dispatch!(self, has_tree_locally(hash))
169 }
170 fn get_state(&self, id: &StateId) -> Result<Option<State>> {
171 match self {
172 AnyStore::Fs(inner) => ObjectStore::get_state(inner, id),
173 }
174 }
175 fn put_state(&self, state: &State) -> Result<()> {
176 any_store_dispatch!(self, put_state(state))
177 }
178 fn has_state(&self, id: &StateId) -> Result<bool> {
179 any_store_dispatch!(self, has_state(id))
180 }
181 fn list_states(&self) -> Result<Vec<StateId>> {
182 any_store_dispatch!(self, list_states())
183 }
184 fn get_state_attachment(
185 &self,
186 state: &StateId,
187 id: &StateAttachmentId,
188 ) -> Result<Option<StateAttachment>> {
189 any_store_dispatch!(self, get_state_attachment(state, id))
190 }
191 fn put_state_attachment(&self, attachment: &StateAttachment) -> Result<StateAttachmentId> {
192 any_store_dispatch!(self, put_state_attachment(attachment))
193 }
194 fn list_state_attachments(&self, state: &StateId) -> Result<Vec<StateAttachment>> {
195 any_store_dispatch!(self, list_state_attachments(state))
196 }
197 fn get_action(&self, id: &ActionId) -> Result<Option<Action>> {
198 any_store_dispatch!(self, get_action(id))
199 }
200 fn put_action(&self, action: &mut Action) -> Result<ActionId> {
201 any_store_dispatch!(self, put_action(action))
202 }
203 fn list_actions(&self) -> Result<Vec<ActionId>> {
204 any_store_dispatch!(self, list_actions())
205 }
206 fn list_blobs(&self) -> Result<Vec<ContentHash>> {
207 any_store_dispatch!(self, list_blobs())
208 }
209 fn list_trees(&self) -> Result<Vec<ContentHash>> {
210 any_store_dispatch!(self, list_trees())
211 }
212 fn put_blob_bytes_with_hash(&self, data: &[u8], hash: ContentHash) -> Result<ContentHash> {
213 any_store_dispatch!(self, put_blob_bytes_with_hash(data, hash))
214 }
215 fn put_tree_serialized(&self, data: &[u8], hash: ContentHash) -> Result<ContentHash> {
216 match self {
217 AnyStore::Fs(inner) => ObjectStore::put_tree_serialized(inner, data, hash),
218 }
219 }
220 fn put_state_serialized(&self, data: &[u8], id: StateId) -> Result<()> {
221 any_store_dispatch!(self, put_state_serialized(data, id))
222 }
223 fn put_action_serialized(&self, data: &[u8], id: ActionId) -> Result<()> {
224 any_store_dispatch!(self, put_action_serialized(data, id))
225 }
226 fn get_pack_object(
227 &self,
228 id: &pack::PackObjectId,
229 ) -> Result<Option<(pack::ObjectType, Vec<u8>)>> {
230 any_store_dispatch!(self, get_pack_object(id))
231 }
232 fn put_blobs_packed(&self, blobs: Vec<(ContentHash, Vec<u8>)>) -> Result<()> {
233 any_store_dispatch!(self, put_blobs_packed(blobs))
234 }
235 fn put_snapshot_objects_packed(
236 &self,
237 blobs: Vec<(ContentHash, Vec<u8>)>,
238 tree: &Tree,
239 state: &State,
240 ) -> Result<()> {
241 any_store_dispatch!(self, put_snapshot_objects_packed(blobs, tree, state))
242 }
243 fn put_snapshot_objects_and_attachments_packed(
244 &self,
245 blobs: Vec<(ContentHash, Vec<u8>)>,
246 tree: &Tree,
247 state: &State,
248 attachments: Vec<StateAttachment>,
249 ) -> Result<()> {
250 any_store_dispatch!(
251 self,
252 put_snapshot_objects_and_attachments_packed(blobs, tree, state, attachments)
253 )
254 }
255
256 fn install_pack(&self, pack_data: &[u8], index_data: &[u8]) -> Result<Vec<pack::PackObjectId>> {
257 any_store_dispatch!(self, install_pack(pack_data, index_data))
258 }
259 fn install_pack_streaming(
260 &self,
261 pack_path: &std::path::Path,
262 index_path: &std::path::Path,
263 ) -> Result<Vec<pack::PackObjectId>> {
264 any_store_dispatch!(self, install_pack_streaming(pack_path, index_path))
265 }
266 fn pack_objects(&self, delta_search: bool) -> Result<(u64, u64)> {
267 any_store_dispatch!(self, pack_objects(delta_search))
268 }
269 fn prune_loose_objects(&self) -> Result<(u64, u64)> {
270 any_store_dispatch!(self, prune_loose_objects())
271 }
272 fn discard_corrupt_clone_packs(&self) -> Result<usize> {
273 any_store_dispatch!(self, discard_corrupt_clone_packs())
274 }
275 fn begin_snapshot_write_batch(&self) -> Result<()> {
276 any_store_dispatch!(self, begin_snapshot_write_batch())
277 }
278 fn flush_snapshot_write_batch(&self) -> Result<()> {
279 any_store_dispatch!(self, flush_snapshot_write_batch())
280 }
281 fn abort_snapshot_write_batch(&self) {
282 any_store_dispatch!(self, abort_snapshot_write_batch())
283 }
284 fn has_redactions_for_blob(&self, blob: &ContentHash) -> Result<bool> {
285 any_store_dispatch!(self, has_redactions_for_blob(blob))
286 }
287 fn get_redactions_bytes_for_blob(&self, blob: &ContentHash) -> Result<Option<Vec<u8>>> {
288 any_store_dispatch!(self, get_redactions_bytes_for_blob(blob))
289 }
290 fn put_redactions_bytes_for_blob(&self, blob: &ContentHash, bytes: &[u8]) -> Result<()> {
291 any_store_dispatch!(self, put_redactions_bytes_for_blob(blob, bytes))
292 }
293 fn list_blobs_with_redactions(&self) -> Result<Vec<ContentHash>> {
294 any_store_dispatch!(self, list_blobs_with_redactions())
295 }
296 fn has_state_visibility_for_state(&self, state: &StateId) -> Result<bool> {
297 any_store_dispatch!(self, has_state_visibility_for_state(state))
298 }
299 fn get_state_visibility_bytes_for_state(&self, state: &StateId) -> Result<Option<Vec<u8>>> {
300 any_store_dispatch!(self, get_state_visibility_bytes_for_state(state))
301 }
302 fn put_state_visibility_bytes_for_state(&self, state: &StateId, bytes: &[u8]) -> Result<()> {
303 any_store_dispatch!(self, put_state_visibility_bytes_for_state(state, bytes))
304 }
305 fn list_states_with_visibility(&self) -> Result<Vec<StateId>> {
306 any_store_dispatch!(self, list_states_with_visibility())
307 }
308}
309
310impl AnyStore {
311 #[doc(hidden)]
313 pub fn snapshot_commit_recovery_descriptors(&self) -> Result<Vec<SnapshotCommitDescriptor>> {
314 match self {
315 Self::Fs(store) => store.snapshot_commit_recovery_descriptors_impl(),
316 }
317 }
318
319 pub fn set_external_source(&mut self, source: Arc<dyn ExternalObjectSource>) {
321 match self {
322 Self::Fs(store) => store.set_external_source(source),
323 }
324 }
325
326 #[doc(hidden)]
330 pub fn snapshot_commit_descriptors(&self) -> Result<Vec<SnapshotCommitDescriptor>> {
331 match self {
332 Self::Fs(store) => store.snapshot_commit_descriptors_impl(),
333 }
334 }
335
336 #[doc(hidden)]
340 pub fn snapshot_commit_descriptor_for_state(
341 &self,
342 state: &StateId,
343 ) -> Result<Option<SnapshotCommitDescriptor>> {
344 match self {
345 Self::Fs(store) => store.snapshot_commit_descriptor_for_state_impl(state),
346 }
347 }
348
349 #[doc(hidden)]
352 pub fn put_committed_snapshot_objects_packed(
353 &self,
354 blobs: Vec<(ContentHash, Vec<u8>)>,
355 trees: Vec<Tree>,
356 tree: &Tree,
357 state: &State,
358 attachments: Vec<StateAttachment>,
359 artifact: SnapshotCommitArtifact,
360 ) -> Result<SnapshotCommitDescriptor> {
361 match self {
362 Self::Fs(store) => store.put_committed_snapshot_objects_packed_impl(
363 blobs,
364 trees,
365 tree,
366 state,
367 attachments,
368 artifact,
369 ),
370 }
371 }
372}
373
374pub trait ObjectStore: Send + Sync {
376 fn get_annotated_tag(&self, _hash: &ContentHash) -> Result<Option<AnnotatedTag>> {
377 Ok(None)
378 }
379 fn put_annotated_tag(&self, _tag: &AnnotatedTag) -> Result<ContentHash> {
380 Err(HeddleError::InvalidObject(
381 "object store does not support annotated tags".to_string(),
382 ))
383 }
384 fn list_annotated_tags(&self) -> Result<Vec<ContentHash>> {
385 Ok(Vec::new())
386 }
387 fn get_blob(&self, hash: &ContentHash) -> Result<Option<Blob>>;
388 fn put_blob(&self, blob: &Blob) -> Result<ContentHash>;
389
390 fn get_blob_bytes(&self, hash: &ContentHash) -> Result<Option<bytes::Bytes>> {
401 Ok(self
402 .get_blob(hash)?
403 .map(|blob| bytes::Bytes::from(blob.into_content())))
404 }
405
406 fn blob_size(&self, hash: &ContentHash) -> Result<Option<u64>> {
420 Ok(self.get_blob(hash)?.map(|blob| blob.content().len() as u64))
421 }
422
423 fn loose_blob_path(&self, _hash: &ContentHash) -> Option<PathBuf> {
435 None
436 }
437
438 fn promote_to_loose_uncompressed(&self, _hash: &ContentHash) -> Result<bool> {
475 Ok(false)
476 }
477
478 fn clear_recent_caches(&self) {}
487
488 fn put_blob_with_hash(&self, blob: &Blob, hash: ContentHash) -> Result<ContentHash> {
489 if blob.hash() != hash {
490 return Err(HeddleError::InvalidObject("blob hash mismatch".to_string()));
491 }
492 self.put_blob(blob)
493 }
494
495 fn has_blob(&self, hash: &ContentHash) -> Result<bool>;
496 fn has_blob_locally(&self, hash: &ContentHash) -> Result<bool> {
500 self.has_blob(hash)
501 }
502 fn get_tree(&self, hash: &ContentHash) -> Result<Option<Tree>>;
503 fn put_tree(&self, tree: &Tree) -> Result<ContentHash>;
504 fn has_tree(&self, hash: &ContentHash) -> Result<bool>;
505 fn has_tree_locally(&self, hash: &ContentHash) -> Result<bool> {
508 self.has_tree(hash)
509 }
510 fn get_state(&self, id: &StateId) -> Result<Option<State>>;
511 fn put_state(&self, state: &State) -> Result<()>;
512 fn has_state(&self, id: &StateId) -> Result<bool>;
513 fn list_states(&self) -> Result<Vec<StateId>>;
514 fn get_state_attachment(
515 &self,
516 _state: &StateId,
517 _id: &StateAttachmentId,
518 ) -> Result<Option<StateAttachment>> {
519 Ok(None)
520 }
521 fn put_state_attachment(&self, _attachment: &StateAttachment) -> Result<StateAttachmentId> {
522 Err(HeddleError::InvalidObject(
523 "object store does not support state attachments".to_string(),
524 ))
525 }
526 fn list_state_attachments(&self, _state: &StateId) -> Result<Vec<StateAttachment>> {
527 Ok(Vec::new())
528 }
529 fn get_action(&self, id: &ActionId) -> Result<Option<Action>>;
530 fn put_action(&self, action: &mut Action) -> Result<ActionId>;
531 fn list_actions(&self) -> Result<Vec<ActionId>>;
532 fn list_blobs(&self) -> Result<Vec<ContentHash>>;
533 fn list_trees(&self) -> Result<Vec<ContentHash>>;
534
535 fn put_blob_bytes_with_hash(&self, data: &[u8], hash: ContentHash) -> Result<ContentHash> {
536 self.put_blob_with_hash(&Blob::from_slice(data), hash)
537 }
538
539 fn get_tree_serialized(&self, hash: &ContentHash) -> Result<Option<Vec<u8>>> {
547 Ok(self
548 .get_tree(hash)?
549 .map(|tree| rmp_serde::to_vec(&tree))
550 .transpose()?)
551 }
552
553 fn put_tree_serialized(&self, data: &[u8], hash: ContentHash) -> Result<ContentHash> {
554 let tree: Tree = rmp_serde::from_slice(data)?;
555 tree.validate()?;
556 if tree.hash() != hash {
557 return Err(HeddleError::Corruption {
558 expected: hash,
559 found: tree.hash(),
560 });
561 }
562 self.put_tree(&tree)
563 }
564
565 fn put_state_serialized(&self, data: &[u8], id: StateId) -> Result<()> {
566 let state: State = rmp_serde::from_slice(data)?;
567 let found = state.id();
568 if found != id {
569 return Err(HeddleError::InvalidObject(format!(
570 "state id mismatch: expected {id}, computed {found}"
571 )));
572 }
573 self.put_state(&state)
574 }
575
576 fn put_action_serialized(&self, data: &[u8], id: ActionId) -> Result<()> {
577 let mut action: Action = rmp_serde::from_slice(data)?;
578 let found_id = action.compute_id();
579 if found_id != id {
580 return Err(HeddleError::InvalidObject(format!(
581 "action id mismatch: expected {}, found {}",
582 id, found_id
583 )));
584 }
585 let stored_id = self.put_action(&mut action)?;
586 if stored_id != id {
587 return Err(HeddleError::InvalidObject(format!(
588 "action id mismatch after write: expected {}, found {}",
589 id, stored_id
590 )));
591 }
592 Ok(())
593 }
594
595 fn get_pack_object(
596 &self,
597 id: &pack::PackObjectId,
598 ) -> Result<Option<(pack::ObjectType, Vec<u8>)>> {
599 match id {
600 pack::PackObjectId::AnnotatedTag(hash) => Ok(self
601 .get_annotated_tag(hash)?
602 .map(|tag| (pack::ObjectType::AnnotatedTag, tag.encode_current_msgpack()))),
603 pack::PackObjectId::Hash(hash) => {
604 if let Some(blob) = self.get_blob(hash)? {
605 return Ok(Some((pack::ObjectType::Blob, blob.content().to_vec())));
606 }
607 if let Some(tree) = self.get_tree(hash)? {
608 return Ok(Some((
609 pack::ObjectType::Tree,
610 rmp_serde::to_vec_named(&tree)?,
611 )));
612 }
613 if let Some(action) = self.get_action(&ActionId::from_hash(*hash))? {
614 return Ok(Some((
615 pack::ObjectType::Action,
616 rmp_serde::to_vec_named(&action)?,
617 )));
618 }
619 Ok(None)
620 }
621 pack::PackObjectId::StateId(change_id) => {
622 if let Some(state) = self.get_state(change_id)? {
623 Ok(Some((
624 pack::ObjectType::State,
625 rmp_serde::to_vec_named(&state)?,
626 )))
627 } else {
628 Ok(None)
629 }
630 }
631 }
632 }
633
634 fn put_blobs_packed(&self, blobs: Vec<(ContentHash, Vec<u8>)>) -> Result<()> {
644 for (hash, data) in blobs {
645 if !self.has_blob(&hash)? {
646 self.put_blob_bytes_with_hash(&data, hash)?;
647 }
648 }
649 Ok(())
650 }
651
652 fn put_snapshot_objects_packed(
657 &self,
658 blobs: Vec<(ContentHash, Vec<u8>)>,
659 tree: &Tree,
660 state: &State,
661 ) -> Result<()> {
662 self.put_blobs_packed(blobs)?;
663 self.put_tree(tree)?;
664 self.put_state(state)
665 }
666
667 fn put_snapshot_objects_and_attachments_packed(
671 &self,
672 blobs: Vec<(ContentHash, Vec<u8>)>,
673 tree: &Tree,
674 state: &State,
675 attachments: Vec<StateAttachment>,
676 ) -> Result<()> {
677 self.put_snapshot_objects_packed(blobs, tree, state)?;
678 for attachment in attachments {
679 self.put_state_attachment(&attachment)?;
680 }
681 Ok(())
682 }
683 fn install_pack(&self, pack_data: &[u8], index_data: &[u8]) -> Result<Vec<pack::PackObjectId>> {
684 let reader = pack::PackReader::from_slice(pack_data, index_data)?;
685 let ids = reader.list_ids()?;
686 for id in &ids {
687 let Some((obj_type, data)) = reader.get_object(id)? else {
688 continue;
689 };
690 match (id, obj_type) {
691 (pack::PackObjectId::Hash(hash), pack::ObjectType::Blob) => {
692 self.put_blob_bytes_with_hash(&data, *hash)?;
693 }
694 (pack::PackObjectId::AnnotatedTag(hash), pack::ObjectType::AnnotatedTag) => {
695 let tag = AnnotatedTag::decode_current_msgpack(&data)
696 .map_err(|error| HeddleError::InvalidObject(error.to_string()))?;
697 if tag.hash() != *hash {
698 return Err(HeddleError::InvalidObject(
699 "annotated tag hash mismatch".to_string(),
700 ));
701 }
702 self.put_annotated_tag(&tag)?;
703 }
704 (pack::PackObjectId::Hash(hash), pack::ObjectType::Tree) => {
705 self.put_tree_serialized(&data, *hash)?;
706 }
707 (pack::PackObjectId::Hash(hash), pack::ObjectType::Action) => {
708 self.put_action_serialized(&data, ActionId::from_hash(*hash))?;
709 }
710 (pack::PackObjectId::StateId(change_id), pack::ObjectType::State) => {
711 self.put_state_serialized(&data, *change_id)?;
712 }
713 (_, pack::ObjectType::TimelineOperation) => {
714 return Err(HeddleError::InvalidObject(
715 "timeline operations belong in the timeline pack store".to_string(),
716 ));
717 }
718 _ => {
719 return Err(HeddleError::InvalidObject(format!(
720 "unsupported native pack object: {:?} {:?}",
721 id, obj_type
722 )));
723 }
724 }
725 }
726 Ok(ids)
727 }
728
729 fn install_pack_streaming(
746 &self,
747 pack_path: &std::path::Path,
748 index_path: &std::path::Path,
749 ) -> Result<Vec<pack::PackObjectId>> {
750 let pack_data = std::fs::read(pack_path).map_err(StoreError::from)?;
751 let index_data = std::fs::read(index_path).map_err(StoreError::from)?;
752 let ids = self.install_pack(&pack_data, &index_data)?;
753 let _ = std::fs::remove_file(pack_path);
757 let _ = std::fs::remove_file(index_path);
758 Ok(ids)
759 }
760
761 fn pack_objects(&self, delta_search: bool) -> Result<(u64, u64)> {
762 let _ = delta_search;
763 Ok((0, 0))
764 }
765
766 fn prune_loose_objects(&self) -> Result<(u64, u64)> {
767 Ok((0, 0))
768 }
769
770 fn discard_corrupt_clone_packs(&self) -> Result<usize> {
773 Ok(0)
774 }
775
776 fn begin_snapshot_write_batch(&self) -> Result<()> {
777 Ok(())
778 }
779
780 fn flush_snapshot_write_batch(&self) -> Result<()> {
781 Ok(())
782 }
783
784 fn abort_snapshot_write_batch(&self) {}
785
786 fn has_redactions_for_blob(&self, _blob: &ContentHash) -> Result<bool> {
798 Ok(false)
799 }
800
801 fn get_redactions_bytes_for_blob(&self, _blob: &ContentHash) -> Result<Option<Vec<u8>>> {
809 Ok(None)
810 }
811
812 fn put_redactions_bytes_for_blob(&self, _blob: &ContentHash, _bytes: &[u8]) -> Result<()> {
821 Err(HeddleError::InvalidObject(
822 "this object store does not support persisting redactions".to_string(),
823 ))
824 }
825
826 fn list_blobs_with_redactions(&self) -> Result<Vec<ContentHash>> {
833 Ok(Vec::new())
834 }
835
836 fn has_state_visibility_for_state(&self, _state: &StateId) -> Result<bool> {
846 Ok(false)
847 }
848
849 fn get_state_visibility_bytes_for_state(&self, _state: &StateId) -> Result<Option<Vec<u8>>> {
855 Ok(None)
856 }
857
858 fn put_state_visibility_bytes_for_state(&self, _state: &StateId, _bytes: &[u8]) -> Result<()> {
863 Err(HeddleError::InvalidObject(
864 "this object store does not support persisting state visibility".to_string(),
865 ))
866 }
867
868 fn list_states_with_visibility(&self) -> Result<Vec<StateId>> {
872 Ok(Vec::new())
873 }
874}
875
876#[cfg(test)]
877mod any_store_tests {
878 use sley::ObjectFormat as GitObjectFormat;
879 use tempfile::TempDir;
880
881 use super::*;
882 use crate::object::{Attribution, Operation, Principal};
883
884 fn fs_any_store() -> (TempDir, AnyStore) {
885 let temp = TempDir::new().unwrap();
886 let store = FsStore::new(temp.path().join(".heddle"));
887 store.init().unwrap();
888 (temp, AnyStore::Fs(store))
889 }
890
891 #[test]
897 fn fs_variant_dispatches_every_object_store_method() {
898 let (_temp, store) = fs_any_store();
899
900 let annotated_tag = AnnotatedTag::new(
902 GitObjectFormat::Sha1,
903 b"object 1111111111111111111111111111111111111111\ntype commit\ntag v1\ntagger Test <test@example.com> 1700000000 +0100\n\nrelease\n".to_vec(),
904 None,
905 None,
906 )
907 .unwrap();
908 let annotated_tag_hash = store.put_annotated_tag(&annotated_tag).unwrap();
909 assert_eq!(
910 store.get_annotated_tag(&annotated_tag_hash).unwrap(),
911 Some(annotated_tag)
912 );
913 assert!(
914 store
915 .list_annotated_tags()
916 .unwrap()
917 .contains(&annotated_tag_hash)
918 );
919
920 let blob = Blob::from("any-store dispatch blob");
922 let blob_hash = store.put_blob(&blob).unwrap();
923 assert_eq!(
924 ObjectStore::get_blob(&store, &blob_hash)
925 .unwrap()
926 .unwrap()
927 .content(),
928 blob.content()
929 );
930 assert!(store.has_blob(&blob_hash).unwrap());
931 assert_eq!(
932 ObjectStore::get_blob_bytes(&store, &blob_hash)
933 .unwrap()
934 .unwrap()
935 .as_ref(),
936 blob.content()
937 );
938 assert_eq!(
939 store.blob_size(&blob_hash).unwrap().unwrap(),
940 blob.content().len() as u64
941 );
942 assert!(store.loose_blob_path(&blob_hash).is_some());
943 store.promote_to_loose_uncompressed(&blob_hash).unwrap();
944 assert!(store.list_blobs().unwrap().contains(&blob_hash));
945
946 let bytes_blob = Blob::from("put-with-hash blob");
947 let bytes_hash = bytes_blob.hash();
948 assert_eq!(
949 store.put_blob_with_hash(&bytes_blob, bytes_hash).unwrap(),
950 bytes_hash
951 );
952 let raw_blob = Blob::from("raw bytes blob");
953 let raw_hash = raw_blob.hash();
954 assert_eq!(
955 store
956 .put_blob_bytes_with_hash(raw_blob.content(), raw_hash)
957 .unwrap(),
958 raw_hash
959 );
960
961 let tree = Tree::new();
963 let tree_hash = store.put_tree(&tree).unwrap();
964 assert!(ObjectStore::get_tree(&store, &tree_hash).unwrap().is_some());
965 assert!(store.has_tree(&tree_hash).unwrap());
966 assert!(store.list_trees().unwrap().contains(&tree_hash));
967 let tree2 = Tree::new();
968 let tree2_bytes = rmp_serde::to_vec_named(&tree2).unwrap();
969 assert_eq!(
970 store
971 .put_tree_serialized(&tree2_bytes, tree2.hash())
972 .unwrap(),
973 tree2.hash()
974 );
975
976 let attribution =
978 Attribution::human(Principal::new("AnyStore Test", "anystore@example.com"));
979 let state = State::new(tree_hash, vec![], attribution.clone());
980 let state_id = state.id();
981 store.put_state(&state).unwrap();
982 assert!(ObjectStore::get_state(&store, &state_id).unwrap().is_some());
983 assert!(store.has_state(&state_id).unwrap());
984 assert!(store.list_states().unwrap().contains(&state_id));
985 let state2 = State::new(tree2.hash(), vec![], attribution.clone());
986 let state2_bytes = rmp_serde::to_vec_named(&state2).unwrap();
987 store
988 .put_state_serialized(&state2_bytes, state2.id())
989 .unwrap();
990
991 let mut action = Action::new(
993 None,
994 StateId::from_bytes([3; 32]),
995 Operation::Snapshot,
996 "any-store action",
997 attribution,
998 );
999 let action_id = store.put_action(&mut action).unwrap();
1000 assert!(store.get_action(&action_id).unwrap().is_some());
1001 assert!(store.list_actions().unwrap().contains(&action_id));
1002 let action_bytes = rmp_serde::to_vec_named(&action).unwrap();
1003 store
1004 .put_action_serialized(&action_bytes, action_id)
1005 .unwrap();
1006
1007 let packed = Blob::from("packed-via-any-store");
1009 let packed_hash = packed.hash();
1010 store
1011 .put_blobs_packed(vec![(packed_hash, packed.into_content())])
1012 .unwrap();
1013 assert!(
1014 store
1015 .get_pack_object(&pack::PackObjectId::Hash(packed_hash))
1016 .unwrap()
1017 .is_some()
1018 );
1019 store.pack_objects(false).unwrap();
1020 store.prune_loose_objects().unwrap();
1021 let _ = store.install_pack(&[], &[]);
1025 let _ = store.install_pack_streaming(
1026 std::path::Path::new("/nonexistent/pack"),
1027 std::path::Path::new("/nonexistent/idx"),
1028 );
1029
1030 store.begin_snapshot_write_batch().unwrap();
1032 store.flush_snapshot_write_batch().unwrap();
1033 store.begin_snapshot_write_batch().unwrap();
1034 store.abort_snapshot_write_batch();
1035
1036 let redaction = b"any-store redaction bytes";
1038 store
1039 .put_redactions_bytes_for_blob(&blob_hash, redaction)
1040 .unwrap();
1041 assert!(store.has_redactions_for_blob(&blob_hash).unwrap());
1042 assert_eq!(
1043 store
1044 .get_redactions_bytes_for_blob(&blob_hash)
1045 .unwrap()
1046 .as_deref(),
1047 Some(redaction.as_slice())
1048 );
1049 assert!(
1050 store
1051 .list_blobs_with_redactions()
1052 .unwrap()
1053 .contains(&blob_hash)
1054 );
1055
1056 let state_visibility = b"any-store state visibility bytes";
1058 store
1059 .put_state_visibility_bytes_for_state(&state_id, state_visibility)
1060 .unwrap();
1061 assert!(store.has_state_visibility_for_state(&state_id).unwrap());
1062 assert_eq!(
1063 store
1064 .get_state_visibility_bytes_for_state(&state_id)
1065 .unwrap()
1066 .as_deref(),
1067 Some(state_visibility.as_slice())
1068 );
1069 assert!(
1070 store
1071 .list_states_with_visibility()
1072 .unwrap()
1073 .contains(&state_id)
1074 );
1075
1076 store.clear_recent_caches();
1078 }
1079}