1use dicom_json::DicomJson;
4use dicom_object::{FileDicomObject, InMemDicomObject};
5
6use futures_util::{stream::BoxStream, Stream, StreamExt};
7use rand::{distr::Alphanumeric, RngExt};
8use reqwest::Body;
9use snafu::ResultExt;
10
11use crate::{
12 apply_auth_and_headers, validate_dicom_json_content_type, DeserializationFailedSnafu,
13 DicomWebClient, DicomWebError, RequestFailedSnafu,
14};
15
16pub struct StowRequest {
18 client: DicomWebClient,
19 url: String,
20 instances: BoxStream<'static, Result<Vec<u8>, std::io::Error>>,
21}
22
23impl StowRequest {
24 fn new(client: DicomWebClient, url: String) -> Self {
25 StowRequest {
26 client,
27 url,
28 instances: futures_util::stream::empty().boxed(),
29 }
30 }
31
32 pub fn with_data(mut self, data: impl Stream<Item = Vec<u8>> + Send + 'static) -> Self {
33 self.instances = data.map(Ok).boxed();
34 self
35 }
36
37 pub fn with_instances(
38 mut self,
39 instances: impl Stream<Item = FileDicomObject<InMemDicomObject>> + Send + 'static,
40 ) -> Self {
41 self.instances = instances
42 .map(|instance| {
43 let mut buffer = Vec::new();
44 instance.write_all(&mut buffer).map_err(|e| {
45 std::io::Error::other(format!("Failed to serialize DICOM instance: {}", e))
46 })?;
47 Ok(buffer)
48 })
49 .boxed();
50 self
51 }
52
53 pub async fn run(self) -> Result<InMemDicomObject, DicomWebError> {
54 let mut request = self.client.client.post(&self.url);
55 request = apply_auth_and_headers(request, &self.client);
56
57 let boundary: String = rand::rng()
58 .sample_iter(&Alphanumeric)
59 .take(8)
60 .map(char::from)
61 .collect();
62
63 let request = request.header(
64 "Content-Type",
65 format!(
66 "multipart/related; type=\"application/dicom\"; boundary={}",
67 boundary
68 ),
69 );
70
71 let boundary_clone = boundary.clone();
72
73 let multipart_stream = self.instances.map(move |data| {
75 let mut multipart_item = Vec::new();
76 let buffer = data?;
77 multipart_item.extend_from_slice(b"--");
78 multipart_item.extend_from_slice(boundary.as_bytes());
79 multipart_item.extend_from_slice(b"\r\n");
80 multipart_item.extend_from_slice(b"Content-Type: application/dicom\r\n\r\n");
81 multipart_item.extend_from_slice(&buffer);
82 multipart_item.extend_from_slice(b"\r\n");
83 Ok::<_, std::io::Error>(multipart_item)
84 });
85
86 let multipart_stream = multipart_stream.chain(futures_util::stream::once(async move {
88 Ok(format!("--{}--\r\n", boundary_clone).into_bytes())
89 }));
90
91 let response = request
92 .body(Body::wrap_stream(multipart_stream))
93 .send()
94 .await
95 .context(RequestFailedSnafu { url: &self.url })?;
96
97 if !response.status().is_success() {
98 return Err(DicomWebError::HttpStatusFailure {
99 status_code: response.status(),
100 });
101 }
102
103 let ct = response
105 .headers()
106 .get("Content-Type")
107 .ok_or(DicomWebError::MissingContentTypeHeader)?;
108 validate_dicom_json_content_type(ct.to_str().unwrap_or_default())?;
109
110 Ok(response
112 .json::<DicomJson<InMemDicomObject>>()
113 .await
114 .context(DeserializationFailedSnafu {})?
115 .into_inner())
116 }
117}
118
119impl DicomWebClient {
120 pub fn store_instances(&self) -> StowRequest {
122 let url = format!("{}/studies", self.stow_url);
123 StowRequest::new(self.clone(), url)
124 }
125
126 pub fn store_instances_in_study(&self, study_instance_uid: &str) -> StowRequest {
128 let url = format!("{}/studies/{}", self.stow_url, study_instance_uid);
129 StowRequest::new(self.clone(), url)
130 }
131}