server/lib/src/grpc/estate.rs
//! `holger.estate.v1.EstateService` — the estate over the wire (owner plan 2b,
//! 2026-09-27). Read-only; admin-read gated exactly like the audit query plane
//! (a TWIN answers it too: reading a box changes nothing, and a twin is what a
//! recharge reads). The work is [`crate::estate_wire`]'s; this is the door.
use tonic::{Request, Response, Status};
use crate::estate_wire;
use crate::grpc::holger_estate_proto::estate_service_server::EstateService;
use crate::grpc::holger_estate_proto::{
EstateFile, FetchRequest, FetchResponse, ManifestRequest, ManifestResponse,
};
use crate::grpc::{coded, internal_err, HolgerGrpc};
use holger_errcode as ec;
impl HolgerGrpc {
fn estate_root(&self) -> Result<std::path::PathBuf, Status> {
self.data_root.clone().ok_or_else(|| {
coded(
&ec::SERVER_CAPABILITY_NOT_CONFIGURED,
tonic::Code::FailedPrecondition,
"this server has no data root, so it has no estate to serve",
)
})
}
}
#[tonic::async_trait]
impl EstateService for std::sync::Arc<HolgerGrpc> {
async fn manifest(
&self,
request: Request<ManifestRequest>,
) -> Result<Response<ManifestResponse>, Status> {
let _identity = self.authorize_admin_read(&request, "").await?;
let root = self.estate_root()?;
let m = tokio::task::spawn_blocking(move || estate_wire::manifest(&root))
.await
.map_err(|e| internal_err(format!("estate manifest task: {e}")))?
.map_err(|e| internal_err(format!("estate manifest: {e}")))?;
Ok(Response::new(ManifestResponse {
files: m
.files
.into_iter()
.map(|f| EstateFile {
path: f.path,
size: f.size,
blake3: f.blake3,
})
.collect(),
absent: m.absent,
}))
}
async fn fetch(
&self,
request: Request<FetchRequest>,
) -> Result<Response<FetchResponse>, Status> {
let _identity = self.authorize_admin_read(&request, "").await?;
let root = self.estate_root()?;
let req = request.into_inner();
estate_wire::safe_path(&req.path)
.map_err(|e| coded(&ec::SERVER_BAD_REQUEST, tonic::Code::InvalidArgument, e))?;
let path = req.path.clone();
let (data, size) = tokio::task::spawn_blocking(move || {
estate_wire::read_range(&root, &req.path, req.offset, req.max_bytes)
})
.await
.map_err(|e| internal_err(format!("estate fetch task: {e}")))?
.map_err(|e| {
// The IO detail (an errno, this box's own layout) stays in the log; the
// client is told only which of ITS paths was not served.
log::warn!("holger grpc: estate fetch {:?}: {e}", path);
coded(
&ec::TWIN_ESTATE_FILE_UNKNOWN,
tonic::Code::NotFound,
format!("estate file `{path}` is not held here"),
)
})?;
Ok(Response::new(FetchResponse {
data: data.into(),
size,
}))
}
}