Split server into crate
Some checks failed
CI / Typos (push) Failing after 17s
CI / Build and test (push) Successful in 1m41s
CI / Clippy (push) Has been cancelled
CI / Build and test (all features) (push) Has been cancelled
Docker / build-and-push (push) Has been cancelled

This commit is contained in:
2026-04-15 09:23:52 -07:00
parent 251d130987
commit 3bc66ddc48
18 changed files with 161 additions and 137 deletions

View File

@@ -0,0 +1,173 @@
use axum::{
Json,
body::Body,
extract::{Path, Query, State},
http::{StatusCode, header},
response::{IntoResponse, Response},
};
use pile_config::Label;
use pile_dataset::Datasets;
use pile_value::{
extract::traits::ExtractState,
value::{BinaryPileValue, PileValue},
};
use serde::Deserialize;
use std::{sync::Arc, time::Instant};
use tokio_util::io::ReaderStream;
use tracing::debug;
use utoipa::IntoParams;
#[derive(Deserialize, IntoParams)]
pub struct SchemaFieldQuery {
source: String,
key: String,
#[serde(default)]
download: bool,
name: Option<String>,
}
/// Extract a specific schema field from an item's metadata.
#[utoipa::path(
get,
path = "/schema/{field}",
params(
("field" = String, Path, description = "Schema field"),
("source" = String, Query, description = "Source label"),
("key" = String, Query, description = "Item key"),
("name" = Option<String>, Query, description = "Downloaded filename; defaults to the last segment of the key"),
),
responses(
(status = 200, description = "Field value as JSON"),
(status = 400, description = "Invalid source label or path"),
(status = 404, description = "Item or field not found"),
(status = 500, description = "Internal server error"),
)
)]
pub async fn schema_field(
State(state): State<Arc<Datasets>>,
Path(field): Path<String>,
Query(params): Query<SchemaFieldQuery>,
) -> Response {
let start = Instant::now();
let label = match Label::try_from(params.source.clone()) {
Ok(l) => l,
Err(e) => return (StatusCode::BAD_REQUEST, format!("{e:?}")).into_response(),
};
debug!(
message = "Serving /schema/{field}",
source = params.source,
key = params.key,
field = field,
);
let Some(item) = state.get(&label, &params.key).await else {
return StatusCode::NOT_FOUND.into_response();
};
let field_label = match Label::new(&field) {
Some(x) => x,
None => return StatusCode::NOT_FOUND.into_response(),
};
let paths = match state.config.schema.get(&field_label) {
Some(x) => &x.path,
None => return StatusCode::NOT_FOUND.into_response(),
};
let extract_state = ExtractState { ignore_mime: false };
let item = PileValue::Item(item);
let mut value = None;
for path in paths {
match item.query(&extract_state, path).await {
Ok(None) => continue,
Ok(Some(PileValue::Null)) => {
value = Some(PileValue::Null);
continue;
}
Ok(Some(v)) => {
value = Some(v);
break;
}
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")).into_response(),
}
}
let Some(value) = value else {
return (StatusCode::BAD_REQUEST, "no value").into_response();
};
debug!(
message = "Served /schema/{field}",
source = params.source,
key = params.key,
field = field,
time_ms = start.elapsed().as_millis()
);
let disposition_type = if params.download {
"attachment"
} else {
"inline"
};
let file_name = params.name.unwrap_or_else(|| {
params
.key
.rsplit('/')
.next()
.unwrap_or(&params.key)
.to_owned()
});
let disposition = format!("{disposition_type}; filename=\"{file_name}\"");
match value {
PileValue::String(s) => (
StatusCode::OK,
[
(header::CONTENT_TYPE, "text/plain".to_owned()),
(header::CONTENT_DISPOSITION, disposition),
],
s.to_string(),
)
.into_response(),
PileValue::Binary(binary) => {
let mime = binary.mime().to_string();
let body = match binary {
BinaryPileValue::Blob { bytes, .. } => Body::from(bytes.0.to_vec()),
BinaryPileValue::File { path, .. } => match tokio::fs::File::open(&path).await {
Ok(file) => Body::from_stream(ReaderStream::new(file)),
Err(e) => {
return (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}"))
.into_response();
}
},
};
(
StatusCode::OK,
[
(header::CONTENT_TYPE, mime),
(header::CONTENT_DISPOSITION, disposition),
],
body,
)
.into_response()
}
_ => match value.to_json(&extract_state).await {
Ok(json) => (
StatusCode::OK,
[(header::CONTENT_DISPOSITION, disposition)],
Json(json),
)
.into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")).into_response(),
},
}
}