From f9a39d5ff9f806a667551dfc4a9ccbd97d8a4645 Mon Sep 17 00:00:00 2001 From: rm-dr <96270320+rm-dr@users.noreply.github.com> Date: Thu, 26 Mar 2026 09:57:02 -0700 Subject: [PATCH] Add `name` parameter --- crates/pile-dataset/src/serve/extract.rs | 19 +++++++++++++++---- crates/pile-dataset/src/serve/field.rs | 19 +++++++++++++++---- crates/pile-dataset/src/serve/item.rs | 14 +++++++++++++- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/crates/pile-dataset/src/serve/extract.rs b/crates/pile-dataset/src/serve/extract.rs index 718cfdd..a32a4e3 100644 --- a/crates/pile-dataset/src/serve/extract.rs +++ b/crates/pile-dataset/src/serve/extract.rs @@ -20,6 +20,7 @@ pub struct ExtractQuery { #[serde(default)] download: bool, + name: Option, } /// Extract a specific field from an item's metadata. @@ -31,6 +32,7 @@ pub struct ExtractQuery { ("source" = String, Query, description = "Source label"), ("key" = String, Query, description = "Item key"), ("path" = String, Query, description = "Object path (e.g. $.flac.title); repeat for fallbacks"), + ("name" = Option, Query, description = "Downloaded filename; defaults to the last segment of the key"), ), responses( (status = 200, description = "Field value as JSON"), @@ -112,18 +114,27 @@ pub async fn get_extract( time_ms = start.elapsed().as_millis() ); - let disposition = if params.download { + 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.to_owned()), + (header::CONTENT_DISPOSITION, disposition), ], s.to_string(), ) @@ -132,7 +143,7 @@ pub async fn get_extract( StatusCode::OK, [ (header::CONTENT_TYPE, mime.to_string()), - (header::CONTENT_DISPOSITION, disposition.to_owned()), + (header::CONTENT_DISPOSITION, disposition), ], bytes.as_ref().clone(), ) @@ -140,7 +151,7 @@ pub async fn get_extract( _ => match value.to_json(&extract_state).await { Ok(json) => ( StatusCode::OK, - [(header::CONTENT_DISPOSITION, disposition.to_owned())], + [(header::CONTENT_DISPOSITION, disposition)], Json(json), ) .into_response(), diff --git a/crates/pile-dataset/src/serve/field.rs b/crates/pile-dataset/src/serve/field.rs index cc8eb4a..47f9ba7 100644 --- a/crates/pile-dataset/src/serve/field.rs +++ b/crates/pile-dataset/src/serve/field.rs @@ -21,6 +21,7 @@ pub struct FieldQuery { #[serde(default)] download: bool, + name: Option, } /// Extract a specific field from an item's metadata. @@ -31,6 +32,7 @@ pub struct FieldQuery { ("source" = String, Query, description = "Source label"), ("key" = String, Query, description = "Item key"), ("field" = String, Query, description = "Schema field"), + ("name" = Option, Query, description = "Downloaded filename; defaults to the last segment of the key"), ), responses( (status = 200, description = "Field value as JSON"), @@ -98,18 +100,27 @@ pub async fn get_field( time_ms = start.elapsed().as_millis() ); - let disposition = if params.download { + 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.to_owned()), + (header::CONTENT_DISPOSITION, disposition), ], s.to_string(), ) @@ -118,7 +129,7 @@ pub async fn get_field( StatusCode::OK, [ (header::CONTENT_TYPE, mime.to_string()), - (header::CONTENT_DISPOSITION, disposition.to_owned()), + (header::CONTENT_DISPOSITION, disposition), ], bytes.as_ref().clone(), ) @@ -126,7 +137,7 @@ pub async fn get_field( _ => match value.to_json(&extract_state).await { Ok(json) => ( StatusCode::OK, - [(header::CONTENT_DISPOSITION, disposition.to_owned())], + [(header::CONTENT_DISPOSITION, disposition)], Json(json), ) .into_response(), diff --git a/crates/pile-dataset/src/serve/item.rs b/crates/pile-dataset/src/serve/item.rs index 6797245..eacec63 100644 --- a/crates/pile-dataset/src/serve/item.rs +++ b/crates/pile-dataset/src/serve/item.rs @@ -19,8 +19,10 @@ use crate::Datasets; pub struct ItemQuery { source: String, key: String, + #[serde(default)] download: bool, + name: Option, } /// Parse a `Range: bytes=...` header value. @@ -48,6 +50,7 @@ fn parse_byte_range(s: &str) -> Option<(Option, Option)> { params( ("source" = String, Query, description = "Source label"), ("key" = String, Query, description = "Item key"), + ("name" = Option, Query, description = "Downloaded filename; defaults to the last segment of the key"), ), responses( (status = 200, description = "Raw item bytes"), @@ -163,11 +166,20 @@ pub async fn item_get( StatusCode::OK }; - let disposition = if params.download { + 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}\""); let mut builder = axum::http::Response::builder() .status(status)