ahri_tre_protocol/
upload.rs1use crate::{
3 ProtocolRequestEnvelope,
4 ingest::{IngestDatasetFileSource, IngestFileSource},
5 request::ProtocolRequest,
6};
7use ahri_tre_types::DisclosureBudgetOverrides;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(deny_unknown_fields)]
12pub struct UploadRequest {
13 pub request: ProtocolRequestEnvelope,
14 pub metadata: UploadMetadata,
15 #[serde(default)]
16 pub budgets: DisclosureBudgetOverrides,
17}
18#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(deny_unknown_fields)]
21pub struct LocalUploadRequest {
22 pub request: ProtocolRequestEnvelope,
23 #[serde(default)]
24 pub budgets: DisclosureBudgetOverrides,
25}
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(deny_unknown_fields)]
28pub struct UploadMetadata {
29 pub filename: String,
30 pub media_type: String,
31 pub content_length: u64,
32 pub digest: String,
33}
34impl UploadRequest {
35 pub fn is_stream_request(&self) -> bool {
36 matches!(&self.request.request,
37 ProtocolRequest::IngestFile(request) if matches!(request.source, IngestFileSource::Stream))
38 || matches!(&self.request.request,
39 ProtocolRequest::IngestDatasetFile(request) if matches!(request.source, IngestDatasetFileSource::Stream))
40 || matches!(&self.request.request,
41 ProtocolRequest::IngestRedcap(request) if matches!(request.source, crate::ingest::RedcapIngestSource::Stream { .. }))
42 }
43}
44pub fn upload_format(filename: &str, declared: &str, media_type: &str) -> Option<&'static str> {
46 if filename.is_empty()
47 || filename.len() > 255
48 || filename.starts_with('.')
49 || filename
50 .chars()
51 .any(|c| c.is_control() || matches!(c, '/' | '\\' | ':'))
52 {
53 return None;
54 }
55 let extension = filename.rsplit_once('.')?.1.to_ascii_lowercase();
56 let format = normalize_format(declared)?;
57 let extension_format = normalize_format(&extension)?;
58 (format == extension_format && media_type == upload_media_type(format)?).then_some(format)
59}
60pub fn normalize_format(format: &str) -> Option<&'static str> {
61 match format.to_ascii_lowercase().as_str() {
62 "csv" | "text/csv" | "edam:format_3752" => Some("csv"),
63 "json" | "ndjson" | "application/json" | "edam:format_3464" => Some("json"),
64 "xlsx"
65 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
66 | "edam:format_3620" => Some("xlsx"),
67 "arrow"
68 | "ipc"
69 | "arrow_ipc"
70 | "feather"
71 | "application/vnd.apache.arrow.stream"
72 | "application/vnd.apache.arrow.file" => Some("arrow"),
73 "parquet" | "application/vnd.apache.parquet" => Some("parquet"),
74 _ => None,
75 }
76}
77pub fn upload_media_type(format: &str) -> Option<&'static str> {
78 match normalize_format(format)? {
79 "csv" => Some("text/csv"),
80 "json" => Some("application/json"),
81 "xlsx" => Some("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
82 "arrow" => Some("application/vnd.apache.arrow.stream"),
83 "parquet" | "application/vnd.apache.parquet" => Some("application/vnd.apache.parquet"),
84 _ => None,
85 }
86}