use std::path::PathBuf;
use crate::object::{
Action, ActionId, AnnotatedTag, Blob, ContentHash, OpenedTreeBody, PartialTree, State,
StateAttachment, StateAttachmentId, StateId, Tree, TreeEntry, TreeEntryReader,
TreeResumeCursor, is_redacted_tree, is_streamable_tree,
};
pub mod codec;
#[cfg(feature = "fs")]
mod delta_source;
#[cfg(feature = "fs")]
pub mod fs;
pub mod liveness;
#[cfg(any(test, feature = "memory-backend"))]
pub mod memory;
#[cfg(test)]
mod partial_tree_tests;
pub use heddle_pack::store::pack;
#[cfg(feature = "fs")]
pub mod shallow;
#[cfg(feature = "fs")]
mod snapshot_commit;
pub mod source;
pub mod store_compliance;
#[cfg(feature = "fs")]
pub mod writer_lease;
#[cfg(feature = "fs")]
pub use fs::{
DEFAULT_PACK_INSTALL_INTENT_TTL_SECS, FsRepackOperation, FsStore, PackInstallIntent,
PackInstallMetricsSnapshot, PackInstallPhase, PackInstallRecoverReport,
install_pack_bytes_journaled, pack_install_metrics_reset, pack_install_metrics_snapshot,
recover_pack_install_intents, recover_pack_install_intents_with_ttl,
};
pub use heddle_format::compression::{CompressionConfig, CompressionError, compress, decompress};
pub use liveness::{
AGENT_LEASE_DURATION, Liveness, current_boot_id, process_alive, reservation_liveness_at,
};
#[cfg(any(test, feature = "memory-backend"))]
pub use memory::InMemoryStore;
pub use pack::{
CancellationToken as RepackCancellationToken, LoadMonitor as RepackLoadMonitor, PackBuilder,
PackObjectId, PackReader, PackStats, RepackContext, RepackError, RepackHandle, RepackInventory,
RepackOperation, RepackOutcome, RepackPolicy, RepackReason, RepackReport, RepackResourceLimits,
RepackSchedule, RepackScheduler, StreamingPackBuilder, SyncData,
};
#[cfg(feature = "fs")]
pub use shallow::ShallowInfo;
#[cfg(feature = "fs")]
#[doc(hidden)]
pub use snapshot_commit::{
SNAPSHOT_COMMIT_ARTIFACT_SCHEMA, SnapshotCommitArtifact, SnapshotCommitDescriptor,
SnapshotPackManager,
};
#[cfg(feature = "async-source")]
pub use source::AsyncObjectSource;
pub use source::ObjectSource;
#[cfg(feature = "fs")]
pub use writer_lease::{
WriterLease, WriterLeaseAuthOutcome, WriterLeaseDraft, WriterLeaseGrant,
WriterLeaseReserveOutcome, WriterLeaseStatus, WriterLeaseStore, generate_writer_lease_id,
generate_writer_lease_token,
};
#[derive(Clone, Debug)]
pub struct TreeWrite {
pub tree: Tree,
pub parent: Option<ContentHash>,
}
impl TreeWrite {
pub fn anchor(tree: Tree) -> Self {
Self { tree, parent: None }
}
pub fn descendant(tree: Tree, parent: ContentHash) -> Self {
Self {
tree,
parent: Some(parent),
}
}
}
pub trait ExternalObjectSource: Send + Sync {
fn get_blob(&self, hash: &ContentHash) -> Result<Option<Blob>>;
fn get_tree(&self, hash: &ContentHash) -> Result<Option<Tree>>;
fn get_state(&self, id: &StateId) -> Result<Option<State>>;
fn list_states(&self) -> Result<Vec<StateId>>;
}
pub trait ObjectCacheControl: Send + Sync {
fn clear_recent_caches(&self);
}
pub use crate::error::{HeddleError as StoreError, HeddleError, Result};
pub trait SidecarStore: Send + Sync {
fn has_redactions_for_blob(&self, _blob: &ContentHash) -> Result<bool> {
Ok(false)
}
fn get_redactions_bytes_for_blob(&self, _blob: &ContentHash) -> Result<Option<Vec<u8>>> {
Ok(None)
}
fn put_redactions_bytes_for_blob(&self, _blob: &ContentHash, _bytes: &[u8]) -> Result<()> {
Err(HeddleError::InvalidObject(
"this object store does not support persisting redactions".to_string(),
))
}
fn list_blobs_with_redactions(&self) -> Result<Vec<ContentHash>> {
Ok(Vec::new())
}
fn has_state_visibility_for_state(&self, _state: &StateId) -> Result<bool> {
Ok(false)
}
fn get_state_visibility_bytes_for_state(&self, _state: &StateId) -> Result<Option<Vec<u8>>> {
Ok(None)
}
fn put_state_visibility_bytes_for_state(&self, _state: &StateId, _bytes: &[u8]) -> Result<()> {
Err(HeddleError::InvalidObject(
"this object store does not support persisting state visibility".to_string(),
))
}
fn list_states_with_visibility(&self) -> Result<Vec<StateId>> {
Ok(Vec::new())
}
}
#[derive(Clone, Debug)]
pub enum TreeRead {
Full(Tree),
Partial(PartialTree),
Absent,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PartialTreeWrite {
Stored {
redacted: usize,
visible: usize,
},
SupersededByFull,
}
pub trait ObjectStore: SidecarStore + Send + Sync {
fn get_annotated_tag(&self, _hash: &ContentHash) -> Result<Option<AnnotatedTag>> {
Ok(None)
}
fn put_annotated_tag(&self, _tag: &AnnotatedTag) -> Result<ContentHash> {
Err(HeddleError::InvalidObject(
"object store does not support annotated tags".to_string(),
))
}
fn list_annotated_tags(&self) -> Result<Vec<ContentHash>> {
Ok(Vec::new())
}
fn get_blob(&self, hash: &ContentHash) -> Result<Option<Blob>>;
fn put_blob(&self, blob: &Blob) -> Result<ContentHash>;
fn get_blob_bytes(&self, hash: &ContentHash) -> Result<Option<bytes::Bytes>> {
Ok(self
.get_blob(hash)?
.map(|blob| bytes::Bytes::from(blob.into_content())))
}
fn blob_size(&self, hash: &ContentHash) -> Result<Option<u64>> {
Ok(self.get_blob(hash)?.map(|blob| blob.content().len() as u64))
}
fn loose_blob_path(&self, _hash: &ContentHash) -> Option<PathBuf> {
None
}
fn promote_to_loose_uncompressed(&self, _hash: &ContentHash) -> Result<bool> {
Ok(false)
}
fn put_blob_with_hash(&self, blob: &Blob, hash: ContentHash) -> Result<ContentHash> {
if blob.hash() != hash {
return Err(HeddleError::InvalidObject("blob hash mismatch".to_string()));
}
self.put_blob(blob)
}
fn has_blob(&self, hash: &ContentHash) -> Result<bool>;
fn has_blob_locally(&self, hash: &ContentHash) -> Result<bool> {
self.has_blob(hash)
}
fn get_tree(&self, hash: &ContentHash) -> Result<Option<Tree>>;
fn get_tree_entry(&self, hash: &ContentHash, name: &str) -> Result<Option<TreeEntry>> {
Ok(self
.get_tree(hash)?
.and_then(|tree| tree.get(name).cloned()))
}
fn put_tree(&self, tree: &Tree) -> Result<ContentHash>;
fn has_tree(&self, hash: &ContentHash) -> Result<bool>;
fn has_tree_locally(&self, hash: &ContentHash) -> Result<bool> {
self.has_tree(hash)
}
fn has_partial_tree(&self, _hash: &ContentHash) -> Result<bool> {
Ok(false)
}
fn get_partial_tree_bytes(&self, _hash: &ContentHash) -> Result<Option<Vec<u8>>> {
Ok(None)
}
fn put_partial_tree_bytes(&self, _hash: &ContentHash, _bytes: &[u8]) -> Result<()> {
Err(HeddleError::InvalidObject(
"this object store does not support partial tree projections".to_string(),
))
}
fn list_partial_trees(&self) -> Result<Vec<ContentHash>> {
Ok(Vec::new())
}
fn remove_partial_tree(&self, _hash: &ContentHash) -> Result<()> {
Ok(())
}
fn put_partial_tree(&self, expected: &ContentHash, hrt1: &[u8]) -> Result<PartialTreeWrite> {
let partial = codec::decode_partial_tree(hrt1, *expected)?;
if self.has_tree(expected)? {
return Ok(PartialTreeWrite::SupersededByFull);
}
self.put_partial_tree_bytes(expected, hrt1)?;
let redacted = partial.redacted_count();
Ok(PartialTreeWrite::Stored {
redacted,
visible: partial.leaves().len() - redacted,
})
}
fn read_tree(&self, hash: &ContentHash) -> Result<TreeRead> {
if let Some(full) = self.get_tree(hash)? {
return Ok(TreeRead::Full(full));
}
match self.get_partial_tree_bytes(hash)? {
Some(bytes) => Ok(TreeRead::Partial(codec::decode_partial_tree(
&bytes, *hash,
)?)),
None => Ok(TreeRead::Absent),
}
}
fn open_tree(
&self,
tree_id: &ContentHash,
cursor: Option<&TreeResumeCursor>,
) -> Result<Option<TreeEntryReader<OpenedTreeBody>>> {
let Some(body) = self.get_tree_serialized(tree_id)? else {
return Ok(None);
};
let body = if is_streamable_tree(&body) {
body
} else {
let tree = self
.get_tree(tree_id)?
.ok_or_else(|| HeddleError::NotFound(format!("tree {tree_id}")))?;
tree.encode_lean()?
};
Ok(Some(TreeEntryReader::open(
OpenedTreeBody::Bytes(crate::object::BytesTreeSource::sequential_verify(body)),
*tree_id,
cursor,
)?))
}
fn get_state(&self, id: &StateId) -> Result<Option<State>>;
fn put_state(&self, state: &State) -> Result<()>;
fn has_state(&self, id: &StateId) -> Result<bool>;
fn list_states(&self) -> Result<Vec<StateId>>;
fn get_state_attachment(
&self,
_state: &StateId,
_id: &StateAttachmentId,
) -> Result<Option<StateAttachment>> {
Ok(None)
}
fn put_state_attachment(&self, _attachment: &StateAttachment) -> Result<StateAttachmentId> {
Err(HeddleError::InvalidObject(
"object store does not support state attachments".to_string(),
))
}
fn list_state_attachments(&self, _state: &StateId) -> Result<Vec<StateAttachment>> {
Ok(Vec::new())
}
fn get_action(&self, id: &ActionId) -> Result<Option<Action>>;
fn put_action(&self, action: &mut Action) -> Result<ActionId>;
fn list_actions(&self) -> Result<Vec<ActionId>>;
fn list_blobs(&self) -> Result<Vec<ContentHash>>;
fn list_trees(&self) -> Result<Vec<ContentHash>>;
fn put_blob_bytes_with_hash(&self, data: &[u8], hash: ContentHash) -> Result<ContentHash> {
self.put_blob_with_hash(&Blob::from_slice(data), hash)
}
fn get_tree_serialized(&self, hash: &ContentHash) -> Result<Option<Vec<u8>>> {
self.get_tree(hash)?
.map(|tree| tree.encode_canonical().map_err(HeddleError::from))
.transpose()
}
fn put_tree_serialized(&self, data: &[u8], hash: ContentHash) -> Result<ContentHash> {
if is_redacted_tree(data) {
self.put_partial_tree(&hash, data)?;
return Ok(hash);
}
let tree = codec::decode_tree_serialized_with_key(data, hash, None)?;
self.put_tree(&tree)
}
fn put_state_serialized(&self, data: &[u8], id: StateId) -> Result<()> {
let state = State::decode_current_msgpack(data)?;
if !state.accepts_stored_id(&id) {
return Err(HeddleError::InvalidObject(format!(
"state id mismatch: expected {id}, computed {}",
state.id()
)));
}
self.put_state(&state)
}
fn put_action_serialized(&self, data: &[u8], id: ActionId) -> Result<()> {
let mut action: Action = rmp_serde::from_slice(data)?;
let found_id = action.compute_id();
if found_id != id {
return Err(HeddleError::InvalidObject(format!(
"action id mismatch: expected {}, found {}",
id, found_id
)));
}
let stored_id = self.put_action(&mut action)?;
if stored_id != id {
return Err(HeddleError::InvalidObject(format!(
"action id mismatch after write: expected {}, found {}",
id, stored_id
)));
}
Ok(())
}
fn get_pack_object(
&self,
id: &pack::PackObjectId,
) -> Result<Option<(pack::ObjectType, Vec<u8>)>> {
match id {
pack::PackObjectId::AnnotatedTag(hash) => Ok(self
.get_annotated_tag(hash)?
.map(|tag| (pack::ObjectType::AnnotatedTag, tag.encode_current_msgpack()))),
pack::PackObjectId::Hash(hash) => {
if let Some(blob) = self.get_blob(hash)? {
return Ok(Some((pack::ObjectType::Blob, blob.content().to_vec())));
}
if let Some(tree) = self.get_tree(hash)? {
return Ok(Some((pack::ObjectType::Tree, tree.encode_canonical()?)));
}
if let Some(action) = self.get_action(&ActionId::from_hash(*hash))? {
return Ok(Some((
pack::ObjectType::Action,
rmp_serde::to_vec_named(&action)?,
)));
}
Ok(None)
}
pack::PackObjectId::StateId(change_id) => {
if let Some(state) = self.get_state(change_id)? {
Ok(Some((
pack::ObjectType::State,
state.encode_current_msgpack()?,
)))
} else {
Ok(None)
}
}
}
}
fn put_blobs_packed(&self, blobs: Vec<(ContentHash, Vec<u8>)>) -> Result<()> {
for (hash, data) in blobs {
if !self.has_blob(&hash)? {
self.put_blob_bytes_with_hash(&data, hash)?;
}
}
Ok(())
}
fn put_snapshot_objects_packed(
&self,
blobs: Vec<(ContentHash, Vec<u8>)>,
tree: &Tree,
state: &State,
) -> Result<()> {
self.put_blobs_packed(blobs)?;
self.put_tree(tree)?;
self.put_state(state)
}
fn put_snapshot_objects_and_attachments_packed(
&self,
blobs: Vec<(ContentHash, Vec<u8>)>,
tree: &Tree,
state: &State,
attachments: Vec<StateAttachment>,
) -> Result<()> {
self.put_snapshot_objects_packed(blobs, tree, state)?;
for attachment in attachments {
self.put_state_attachment(&attachment)?;
}
Ok(())
}
fn install_pack(&self, pack_data: &[u8], index_data: &[u8]) -> Result<Vec<pack::PackObjectId>> {
let reader = pack::PackReader::from_slice(pack_data, index_data)?;
let ids = reader.list_ids()?;
for id in &ids {
let Some((obj_type, data)) = reader.get_object(id)? else {
continue;
};
match (id, obj_type) {
(pack::PackObjectId::Hash(hash), pack::ObjectType::Blob) => {
self.put_blob_bytes_with_hash(&data, *hash)?;
}
(pack::PackObjectId::AnnotatedTag(hash), pack::ObjectType::AnnotatedTag) => {
let tag = AnnotatedTag::decode_current_msgpack(&data)
.map_err(|error| HeddleError::InvalidObject(error.to_string()))?;
if tag.hash() != *hash {
return Err(HeddleError::InvalidObject(
"annotated tag hash mismatch".to_string(),
));
}
self.put_annotated_tag(&tag)?;
}
(pack::PackObjectId::Hash(hash), pack::ObjectType::Tree) => {
self.put_tree_serialized(&data, *hash)?;
}
(pack::PackObjectId::Hash(hash), pack::ObjectType::Action) => {
self.put_action_serialized(&data, ActionId::from_hash(*hash))?;
}
(pack::PackObjectId::StateId(change_id), pack::ObjectType::State) => {
self.put_state_serialized(&data, *change_id)?;
}
(_, pack::ObjectType::TimelineOperation) => {
return Err(HeddleError::InvalidObject(
"timeline operations belong in the timeline pack store".to_string(),
));
}
_ => {
return Err(HeddleError::InvalidObject(format!(
"unsupported native pack object: {:?} {:?}",
id, obj_type
)));
}
}
}
Ok(ids)
}
fn install_pack_streaming(
&self,
pack_path: &std::path::Path,
index_path: &std::path::Path,
) -> Result<Vec<pack::PackObjectId>> {
let pack_data = std::fs::read(pack_path).map_err(StoreError::from)?;
let index_data = std::fs::read(index_path).map_err(StoreError::from)?;
let ids = self.install_pack(&pack_data, &index_data)?;
let _ = std::fs::remove_file(pack_path);
let _ = std::fs::remove_file(index_path);
Ok(ids)
}
fn begin_snapshot_write_batch(&self) -> Result<()> {
Ok(())
}
fn flush_snapshot_write_batch(&self) -> Result<()> {
Ok(())
}
fn abort_snapshot_write_batch(&self) {}
}