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