123 lines
5.0 KiB
Rust
123 lines
5.0 KiB
Rust
use crate::config::Config;
|
|
use super::build_response;use crate::content_server;
|
|
use anyhow::Result;
|
|
use hyper::{Response, StatusCode};
|
|
|
|
use super::{ApiHandler, is_valid_app_id};
|
|
|
|
impl ApiHandler {
|
|
pub(super) async fn handle_content_catalog(config: &Config) -> Result<Response<hyper::Body>> {
|
|
match content_server::load_catalog(&config.data_dir).await {
|
|
Ok(catalog) => {
|
|
// Only expose public metadata for available items
|
|
let items: Vec<serde_json::Value> = catalog
|
|
.items
|
|
.iter()
|
|
.filter(|i| !matches!(i.availability, content_server::Availability::Nobody))
|
|
.map(|i| {
|
|
serde_json::json!({
|
|
"id": i.id,
|
|
"filename": i.filename,
|
|
"mime_type": i.mime_type,
|
|
"size_bytes": i.size_bytes,
|
|
"description": i.description,
|
|
"access": i.access,
|
|
})
|
|
})
|
|
.collect();
|
|
let body = serde_json::to_vec(&serde_json::json!({ "items": items }))
|
|
.unwrap_or_default();
|
|
Ok(build_response(StatusCode::OK, "application/json", hyper::Body::from(body)))
|
|
}
|
|
Err(e) => {
|
|
let body = serde_json::json!({ "error": e.to_string() });
|
|
let body_bytes = serde_json::to_vec(&body).unwrap_or_default();
|
|
Ok(build_response(StatusCode::INTERNAL_SERVER_ERROR, "application/json", hyper::Body::from(body_bytes)))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) async fn handle_content_request(
|
|
path: &str,
|
|
headers: &hyper::HeaderMap,
|
|
config: &Config,
|
|
) -> Result<Response<hyper::Body>> {
|
|
let content_id = path.strip_prefix("/content/").unwrap_or("");
|
|
if content_id.is_empty() || !is_valid_app_id(content_id) {
|
|
return Ok(build_response(StatusCode::BAD_REQUEST, "text/plain", hyper::Body::from("Invalid content ID")));
|
|
}
|
|
|
|
// Extract payment token from X-Payment-Token header
|
|
let payment_token = headers
|
|
.get("x-payment-token")
|
|
.and_then(|v| v.to_str().ok())
|
|
.map(|s| s.to_string());
|
|
|
|
// Extract federation peer DID from X-Federation-DID header
|
|
let peer_did = headers
|
|
.get("x-federation-did")
|
|
.and_then(|v| v.to_str().ok())
|
|
.map(|s| s.to_string());
|
|
|
|
// Parse Range header for streaming support
|
|
let range = headers
|
|
.get("range")
|
|
.and_then(|v| v.to_str().ok())
|
|
.and_then(content_server::parse_range_header);
|
|
|
|
match content_server::serve_content(
|
|
&config.data_dir,
|
|
content_id,
|
|
payment_token.as_deref(),
|
|
peer_did.as_deref(),
|
|
range,
|
|
)
|
|
.await
|
|
{
|
|
Ok(content_server::ServeResult::Ok(bytes, mime_type)) => {
|
|
let len = bytes.len();
|
|
Ok(Response::builder()
|
|
.status(StatusCode::OK)
|
|
.header("Content-Type", mime_type)
|
|
.header("Content-Length", len.to_string())
|
|
.header("Accept-Ranges", "bytes")
|
|
.body(hyper::Body::from(bytes))
|
|
.unwrap())
|
|
}
|
|
Ok(content_server::ServeResult::Partial {
|
|
bytes,
|
|
mime_type,
|
|
start,
|
|
end,
|
|
total,
|
|
}) => {
|
|
Ok(Response::builder()
|
|
.status(StatusCode::PARTIAL_CONTENT)
|
|
.header("Content-Type", mime_type)
|
|
.header("Content-Length", bytes.len().to_string())
|
|
.header("Content-Range", format!("bytes {}-{}/{}", start, end, total))
|
|
.header("Accept-Ranges", "bytes")
|
|
.body(hyper::Body::from(bytes))
|
|
.unwrap())
|
|
}
|
|
Ok(content_server::ServeResult::PaymentRequired(price_sats)) => {
|
|
let body = serde_json::json!({
|
|
"error": "Payment required",
|
|
"price_sats": price_sats,
|
|
"payment_header": "X-Payment-Token",
|
|
});
|
|
let body_bytes = serde_json::to_vec(&body).unwrap_or_default();
|
|
Ok(build_response(StatusCode::PAYMENT_REQUIRED, "application/json", hyper::Body::from(body_bytes)))
|
|
}
|
|
Ok(content_server::ServeResult::Forbidden) => {
|
|
Ok(build_response(StatusCode::FORBIDDEN, "application/json", hyper::Body::from(
|
|
r#"{"error":"Access denied — federation peer required"}"#,
|
|
)))
|
|
}
|
|
Ok(content_server::ServeResult::NotFound) | Err(_) => {
|
|
Ok(build_response(StatusCode::NOT_FOUND, "text/plain", hyper::Body::from("Content not found")))
|
|
}
|
|
}
|
|
}
|
|
}
|