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