use std::{
fs::{File, OpenOptions},
path::Path,
};
use api::v2::client::RpcTransport;
use heddle_object_model::object::{
EntryRedactions, ObjectSource, State, StateId, source_target::capture::ReferenceProof,
};
use heddle_pack::store::pack::{
StreamingPackBuilder, build_source_pack_with_references, build_visible_source_pack,
};
use super::{Error, PreparedPublication, PublicationOriginals};
use crate::{Thread, contract::*, transport};
pub struct SourceBudget {
pub max_objects: usize,
pub max_decoded_bytes: u64,
}
pub struct PublicationOptions {
pub client_operation_id: String,
pub source: EndpointRef,
pub sharing_policy_version: Vec<u8>,
pub checkpoint: Option<TransferCheckpoint>,
}
pub struct SourcePack {
directory: tempfile::TempDir,
revision: StateId,
artifacts: [PackExtent; 2],
}
pub struct VisibleSourcePack {
source: SourcePack,
complete: bool,
}
impl VisibleSourcePack {
pub fn prepare(
source: &impl ObjectSource,
selected: &State,
references: &[ReferenceProof],
redactions: &EntryRedactions,
scratch_root: &Path,
budget: SourceBudget,
) -> Result<Self, Error> {
let (source, complete) = SourcePack::prepare_disclosure(
source,
selected,
references,
Some(redactions),
scratch_root,
budget,
)?;
Ok(Self { source, complete })
}
pub fn is_complete(&self) -> bool {
self.complete
}
pub fn artifacts(&self) -> &[PackExtent; 2] {
self.source.artifacts()
}
pub async fn open_artifacts(&self) -> Result<[tokio::fs::File; 2], Error> {
self.source.open_artifacts().await
}
}
impl SourcePack {
pub fn prepare(
source: &impl ObjectSource,
selected: &State,
scratch_root: &Path,
budget: SourceBudget,
) -> Result<Self, Error> {
Self::prepare_with_references(source, selected, &[], scratch_root, budget)
}
pub fn prepare_with_references(
source: &impl ObjectSource,
selected: &State,
references: &[ReferenceProof],
scratch_root: &Path,
budget: SourceBudget,
) -> Result<Self, Error> {
Self::prepare_disclosure(source, selected, references, None, scratch_root, budget)
.map(|(source, _)| source)
}
fn prepare_disclosure(
source: &impl ObjectSource,
selected: &State,
references: &[ReferenceProof],
redactions: Option<&EntryRedactions>,
scratch_root: &Path,
budget: SourceBudget,
) -> Result<(Self, bool), Error> {
let directory = tempfile::Builder::new()
.prefix("thread-source-")
.tempdir_in(scratch_root)?;
let pack_path = directory.path().join("source.pack");
let index_path = directory.path().join("source.idx");
let pack = OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(&pack_path)?;
let builder = StreamingPackBuilder::new(
pack,
index_path.clone(),
Default::default(),
directory.path().join("buckets"),
)
.map_err(store_error)?;
let (pack, _, complete) = match redactions {
Some(redactions) => build_visible_source_pack(
builder,
source,
selected,
references,
redactions,
budget.max_objects,
budget.max_decoded_bytes,
),
None => build_source_pack_with_references(
builder,
source,
selected,
references,
budget.max_objects,
budget.max_decoded_bytes,
)
.map(|(output, stats)| (output, stats, true)),
}
.map_err(store_error)?;
drop(pack);
let artifacts = [
artifact(&pack_path, pack_extent::Kind::NativePack)?,
artifact(&index_path, pack_extent::Kind::NativeIndex)?,
];
Ok((
Self {
directory,
revision: selected.id(),
artifacts,
},
complete,
))
}
pub fn artifacts(&self) -> &[PackExtent; 2] {
&self.artifacts
}
pub async fn open_artifacts(&self) -> Result<[tokio::fs::File; 2], Error> {
Ok([
tokio::fs::File::open(self.directory.path().join("source.pack")).await?,
tokio::fs::File::open(self.directory.path().join("source.idx")).await?,
])
}
pub fn revision(&self) -> StateId {
self.revision
}
pub fn inventory_digest(&self) -> Result<[u8; 32], Error> {
super::inventory_digest(&self.artifacts)
}
}
impl<T: RpcTransport<Error = transport::Error>> Thread<'_, T> {
pub async fn publish_source(
&self,
source: &SourcePack,
originals: &PublicationOriginals,
options: PublicationOptions,
) -> Result<PublicationReceipt, Error> {
let opening = self.publication_opening(source, options)?;
let [pack, index] = source.open_artifacts().await?;
self.remote
.publish_content(&opening, originals, [pack, index])
.await
}
pub fn prepare_publication(
&self,
source: &SourcePack,
originals: PublicationOriginals,
options: PublicationOptions,
spool_genesis: heddle_object_model::object::ContentHash,
) -> Result<PreparedPublication, Error> {
Ok(PreparedPublication::new(
self.publication_opening(source, options)?,
originals,
spool_genesis,
)?)
}
pub async fn send_prepared(
&self,
source: &SourcePack,
prepared: &PreparedPublication,
) -> Result<PublicationReceipt, Error> {
let Some(publish_content_client_frame::Body::Open(open)) = &prepared.opening().body else {
return Err(Error::Invalid("prepared Open required"));
};
if open.thread.as_ref() != Some(&self.reference)
|| open.packs.as_slice() != source.artifacts()
|| prepared.plan().intent().revision != source.revision()
{
return Err(Error::Invalid(
"prepared publication differs from selected source",
));
}
let artifacts = source.open_artifacts().await?;
self.remote
.publish_content(prepared.opening(), prepared.originals(), artifacts)
.await
}
fn publication_opening(
&self,
source: &SourcePack,
options: PublicationOptions,
) -> Result<PublishContentClientFrame, Error> {
if self
.reference
.spool
.as_ref()
.is_none_or(|spool| spool.id.is_empty())
|| self
.reference
.id
.as_ref()
.is_none_or(|id| id.value.len() != 32)
|| options.source.public_key.len() != 32
|| (!options.sharing_policy_version.is_empty()
&& options.sharing_policy_version.len() != 32)
{
return Err(Error::Invalid(
"Thread, source endpoint and optional 32-byte policy version required",
));
}
let destination = self
.remote
.description
.endpoint
.clone()
.ok_or(Error::Invalid("remote endpoint identity missing"))?;
Ok(PublishContentClientFrame {
client_operation_id: options.client_operation_id,
body: Some(publish_content_client_frame::Body::Open(
PublishContentOpen {
thread: Some(self.reference.clone()),
revision: Some(RevisionRef {
spool: self.reference.spool.clone(),
revision: Some(revision_ref::Revision::State(
api::heddle::api::common::StateId {
value: source.revision.as_bytes().to_vec(),
},
)),
}),
sharing_policy_version: options.sharing_policy_version,
packs: source.artifacts.to_vec(),
checkpoint: options.checkpoint,
source: Some(options.source),
destination: Some(destination),
},
)),
})
}
}
fn artifact(path: &Path, kind: pack_extent::Kind) -> Result<PackExtent, Error> {
let mut file = File::open(path)?;
let length = file.metadata()?.len();
let mut hash = blake3::Hasher::new();
hash.update_reader(&mut file)?;
let address = ObjectAddress {
algorithm: "blake3".into(),
digest: hash.finalize().as_bytes().to_vec(),
};
Ok(PackExtent {
pack: Some(address.clone()),
kind: kind as i32,
offset: 0,
length,
extent_digest: Some(address),
})
}
fn store_error(error: impl std::fmt::Display) -> Error {
transport::Error::Io(error.to_string()).into()
}