Introduce src/server/ as a streaming-first git/lfs server surface. All
request and response bodies cross the API as
Box<dyn AsyncRead + Unpin + Send + Sync> (alias BoxedAsyncRead) — no
Vec<u8> bodies, no read_to_end convenience, no http crate.
What's in the box:
- src/server/mod.rs: re-exports + pipe re-export of
command::cmd::DEFAULT_OUTPUT_CAP_BYTES as server::DEFAULT_OUTPUT_CAP_BYTES
- src/server/backend.rs: ServerBackend async-trait (authorize,
list_refs_stream, produce_pack_stream, ingest_pack_stream,
lfs_get_stream, lfs_put_stream). Every method currently returns
ServerError::Unimplemented("...") placeholders — the wiring is
real, the bodies are intentionally empty.
- src/server/request.rs: ServerRequest (not Clone, body is a boxed
reader), HeaderMap = HashMap<String, String>, RefUpdateBatch
(oid strings, no gix::ObjectId), BoxedAsyncRead alias
- src/server/stream.rs: StreamGuard (Clone + Send + Sync) carrying
cancel() / handle() so spawned workers can race the request
- src/server/endpoint.rs, error.rs, limits.rs: EndpointKind enum,
ServerError, DEFAULT_LFS_OBJECT_CAP_BYTES (5 GiB cap, separate from
the 64 MiB output cap)
- tests/server.rs: PlaceholderBackend skeleton + ENV_LOCK +
200 ms cancel convention reused from tests/env.rs and tests/pipe.rs
Public API impact on GitBaby:
- Drop pipe from GitBaby::new — the server owns its own stream
lifecycle, so the Pipe parameter is no longer required at
construction time. Existing callers must be updated:
- before: GitBaby::new(facade, pipe)
- after: GitBaby::new(facade)
Documentation:
- AGENTS.md gains a "Planned but unimplemented (streaming-first
server)" section documenting the deliberately-empty contracts so
future contributors don't "fill them in" without a concrete
git/lfs protocol task.
CI: cargo build + cargo test (61 total, 23 new from server suite)
green.
79 lines
2.1 KiB
Rust
79 lines
2.1 KiB
Rust
use std::fmt;
|
|
use std::path::PathBuf;
|
|
|
|
use crate::error::BabyError;
|
|
|
|
#[derive(Debug)]
|
|
pub enum ServerError {
|
|
Unimplemented(&'static str),
|
|
Unauthorised {
|
|
scheme: &'static str,
|
|
reason: String,
|
|
},
|
|
InvalidRequest(String),
|
|
PayloadTooLarge {
|
|
cap_bytes: u64,
|
|
actual_bytes: u64,
|
|
},
|
|
Io {
|
|
path: PathBuf,
|
|
source: std::io::Error,
|
|
},
|
|
Pipe(String),
|
|
Backend(BabyError),
|
|
Cancelled,
|
|
}
|
|
|
|
impl fmt::Display for ServerError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Unimplemented(what) => write!(f, "not yet implemented: {}", what),
|
|
Self::Unauthorised { scheme, reason } => {
|
|
write!(f, "unauthorised ({scheme}): {}", reason)
|
|
}
|
|
Self::InvalidRequest(msg) => write!(f, "invalid server request: {}", msg),
|
|
Self::PayloadTooLarge {
|
|
cap_bytes,
|
|
actual_bytes,
|
|
} => write!(
|
|
f,
|
|
"output exceeds cap: {} bytes > {} bytes cap",
|
|
actual_bytes, cap_bytes
|
|
),
|
|
Self::Io { path, source } => {
|
|
write!(f, "io error at `{}`: {}", path.display(), source)
|
|
}
|
|
Self::Pipe(msg) => write!(f, "server pipe error: {}", msg),
|
|
Self::Backend(err) => write!(f, "backend error: {}", err),
|
|
Self::Cancelled => write!(f, "server stream was cancelled"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for ServerError {
|
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
|
match self {
|
|
Self::Io { source, .. } => Some(source),
|
|
Self::Backend(err) => Some(err),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<BabyError> for ServerError {
|
|
fn from(err: BabyError) -> Self {
|
|
Self::Backend(err)
|
|
}
|
|
}
|
|
|
|
impl From<std::io::Error> for ServerError {
|
|
fn from(source: std::io::Error) -> Self {
|
|
Self::Io {
|
|
path: PathBuf::new(),
|
|
source,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub type ServerResult<T> = std::result::Result<T, ServerError>;
|