Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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,
        }))
    }
}