148 lines
3.6 KiB
Rust
148 lines
3.6 KiB
Rust
use axum::{
|
|
Json,
|
|
extract::{Path, Query, State},
|
|
http::{StatusCode, header},
|
|
response::{IntoResponse, Response},
|
|
};
|
|
use pile_config::Label;
|
|
use pile_value::{extract::traits::ExtractState, value::PileValue};
|
|
use serde::Deserialize;
|
|
use std::{sync::Arc, time::Instant};
|
|
use tracing::debug;
|
|
use utoipa::IntoParams;
|
|
|
|
use crate::Datasets;
|
|
|
|
#[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, ¶ms.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(Some(PileValue::Null)) | Ok(None) => 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::NOT_FOUND.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(¶ms.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::Blob { mime, bytes } => (
|
|
StatusCode::OK,
|
|
[
|
|
(header::CONTENT_TYPE, mime.to_string()),
|
|
(header::CONTENT_DISPOSITION, disposition),
|
|
],
|
|
bytes.as_ref().clone(),
|
|
)
|
|
.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(),
|
|
},
|
|
}
|
|
}
|