126 lines
4.5 KiB
Rust
126 lines
4.5 KiB
Rust
|
|
//! HTTP handlers for the content-addressed blob store.
|
||
|
|
//!
|
||
|
|
//! - `POST /api/blob` — session-authenticated. Raw body is the blob;
|
||
|
|
//! headers set mime/filename. Returns `{cid, size, mime}`.
|
||
|
|
//! - `GET /blob/<cid>?cap=<hex>&exp=<epoch>&peer=<pubkey>` — peer-facing.
|
||
|
|
//! Capability verified against the stored HMAC key; bytes streamed back.
|
||
|
|
|
||
|
|
use super::{build_response, ApiHandler};
|
||
|
|
use crate::blobs::BlobStore;
|
||
|
|
use anyhow::Result;
|
||
|
|
use hyper::{Body, HeaderMap, Response, StatusCode};
|
||
|
|
use std::sync::Arc;
|
||
|
|
|
||
|
|
impl ApiHandler {
|
||
|
|
pub(super) async fn handle_blob_upload(
|
||
|
|
store: &Arc<BlobStore>,
|
||
|
|
self_pubkey_hex: &str,
|
||
|
|
headers: &HeaderMap,
|
||
|
|
body: hyper::body::Bytes,
|
||
|
|
) -> Result<Response<Body>> {
|
||
|
|
let mime = headers
|
||
|
|
.get("x-blob-mime")
|
||
|
|
.and_then(|v| v.to_str().ok())
|
||
|
|
.unwrap_or("application/octet-stream")
|
||
|
|
.to_string();
|
||
|
|
let filename = headers
|
||
|
|
.get("x-blob-filename")
|
||
|
|
.and_then(|v| v.to_str().ok())
|
||
|
|
.map(|s| s.to_string());
|
||
|
|
|
||
|
|
let bytes = body.to_vec();
|
||
|
|
match store.put(&bytes, &mime, filename, None).await {
|
||
|
|
Ok(meta) => {
|
||
|
|
// Include a self-signed capability URL so the UI can round-trip
|
||
|
|
// the upload end-to-end without any peer. 7-day expiry.
|
||
|
|
let exp = (chrono::Utc::now().timestamp() as u64)
|
||
|
|
+ crate::blobs::DEFAULT_CAP_TTL_SECS;
|
||
|
|
let cap = store.issue_capability(&meta.cid, self_pubkey_hex, exp);
|
||
|
|
let self_test_url = format!(
|
||
|
|
"/blob/{}?cap={}&exp={}&peer={}",
|
||
|
|
meta.cid, cap, exp, self_pubkey_hex
|
||
|
|
);
|
||
|
|
let resp = serde_json::json!({
|
||
|
|
"cid": meta.cid,
|
||
|
|
"size": meta.size,
|
||
|
|
"mime": meta.mime,
|
||
|
|
"filename": meta.filename,
|
||
|
|
"self_test_url": self_test_url,
|
||
|
|
});
|
||
|
|
Ok(build_response(
|
||
|
|
StatusCode::OK,
|
||
|
|
"application/json",
|
||
|
|
Body::from(serde_json::to_vec(&resp).unwrap_or_default()),
|
||
|
|
))
|
||
|
|
}
|
||
|
|
Err(e) => Ok(build_response(
|
||
|
|
StatusCode::BAD_REQUEST,
|
||
|
|
"text/plain",
|
||
|
|
Body::from(format!("blob upload failed: {}", e)),
|
||
|
|
)),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub(super) async fn handle_blob_download(
|
||
|
|
store: &Arc<BlobStore>,
|
||
|
|
path: &str,
|
||
|
|
query: &str,
|
||
|
|
) -> Result<Response<Body>> {
|
||
|
|
let cid = path.strip_prefix("/blob/").unwrap_or("");
|
||
|
|
if cid.is_empty() || !cid.chars().all(|c| c.is_ascii_hexdigit()) || cid.len() != 64 {
|
||
|
|
return Ok(build_response(
|
||
|
|
StatusCode::BAD_REQUEST,
|
||
|
|
"text/plain",
|
||
|
|
Body::from("invalid cid"),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parse query params: cap, exp, peer (all required)
|
||
|
|
let mut cap = None;
|
||
|
|
let mut exp: Option<u64> = None;
|
||
|
|
let mut peer = None;
|
||
|
|
for pair in query.split('&') {
|
||
|
|
let mut it = pair.splitn(2, '=');
|
||
|
|
match (it.next(), it.next()) {
|
||
|
|
(Some("cap"), Some(v)) => cap = Some(v.to_string()),
|
||
|
|
(Some("exp"), Some(v)) => exp = v.parse().ok(),
|
||
|
|
(Some("peer"), Some(v)) => peer = Some(v.to_string()),
|
||
|
|
_ => {}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let (Some(cap), Some(exp), Some(peer)) = (cap, exp, peer) else {
|
||
|
|
return Ok(build_response(
|
||
|
|
StatusCode::UNAUTHORIZED,
|
||
|
|
"text/plain",
|
||
|
|
Body::from("missing cap/exp/peer"),
|
||
|
|
));
|
||
|
|
};
|
||
|
|
|
||
|
|
if let Err(e) = store.verify_capability(cid, &peer, exp, &cap) {
|
||
|
|
tracing::warn!("blob cap rejected: cid={} peer={} reason={}", cid, peer, e);
|
||
|
|
return Ok(build_response(
|
||
|
|
StatusCode::FORBIDDEN,
|
||
|
|
"text/plain",
|
||
|
|
Body::from(format!("capability rejected: {}", e)),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
let bytes = match store.get(cid).await {
|
||
|
|
Ok(b) => b,
|
||
|
|
Err(_) => {
|
||
|
|
return Ok(build_response(
|
||
|
|
StatusCode::NOT_FOUND,
|
||
|
|
"text/plain",
|
||
|
|
Body::from("blob not found"),
|
||
|
|
))
|
||
|
|
}
|
||
|
|
};
|
||
|
|
let mime = store
|
||
|
|
.meta(cid)
|
||
|
|
.await
|
||
|
|
.map(|m| m.mime)
|
||
|
|
.unwrap_or_else(|_| "application/octet-stream".to_string());
|
||
|
|
Ok(build_response(StatusCode::OK, &mime, Body::from(bytes)))
|
||
|
|
}
|
||
|
|
}
|