dicom_web/
stow.rs

1//! Module for WADO-RS requests
2use 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
11/// A builder type for STOW-RS requests
12pub struct WadoStowRequest {
13    client: DicomWebClient,
14    url: String,
15    instances: BoxStream<'static, FileDicomObject<InMemDicomObject>>,
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_instances(
28        mut self,
29        instances: impl Stream<Item = FileDicomObject<InMemDicomObject>> + Send + 'static,
30    ) -> Self {
31        self.instances = instances.boxed();
32        self
33    }
34
35    pub async fn run(self) -> Result<(), DicomWebError> {
36        let mut request = self.client.client.post(&self.url);
37
38        // Basic authentication
39        if let Some(username) = &self.client.username {
40            request = request.basic_auth(username, self.client.password.as_ref());
41        }
42        // Bearer token
43        else if let Some(bearer_token) = &self.client.bearer_token {
44            request = request.bearer_auth(bearer_token);
45        }
46
47        let boundary: String = rand::rng()
48            .sample_iter(&Alphanumeric)
49            .take(8)
50            .map(char::from)
51            .collect();
52
53        let request = request.header(
54            "Content-Type",
55            format!(
56                "multipart/related; type=\"application/dicom\"; boundary={}",
57                boundary
58            ),
59        );
60
61        let boundary_clone = boundary.clone();
62
63        // Convert each instance to a multipart item
64        let multipart_stream = self.instances.map(move |instance| {
65            let mut multipart_item = Vec::new();
66            let mut buffer = Vec::new();
67            instance.clone().write_all(&mut buffer).unwrap();
68            multipart_item.extend_from_slice(b"--");
69            multipart_item.extend_from_slice(boundary.as_bytes());
70            multipart_item.extend_from_slice(b"\r\n");
71            multipart_item.extend_from_slice(b"Content-Type: application/dicom\r\n\r\n");
72            multipart_item.extend_from_slice(&buffer);
73            multipart_item.extend_from_slice(b"\r\n");
74            Ok::<_, std::io::Error>(multipart_item)
75        });
76
77        // Write the final boundary
78        let multipart_stream = multipart_stream.chain(futures_util::stream::once(async move {
79            Ok(format!("--{}--\r\n", boundary_clone).into_bytes())
80        }));
81
82        let response = request
83            .body(Body::wrap_stream(multipart_stream))
84            .send()
85            .await
86            .context(RequestFailedSnafu { url: &self.url })?;
87
88        if !response.status().is_success() {
89            return Err(DicomWebError::HttpStatusFailure {
90                status_code: response.status(),
91            });
92        }
93
94        Ok(())
95    }
96}
97
98impl DicomWebClient {
99    /// Create a STOW-RS request to store instances
100    pub fn store_instances(&self) -> WadoStowRequest {
101        let url = format!("{}/studies", self.stow_url);
102        WadoStowRequest::new(self.clone(), url)
103    }
104
105    /// Create a WADO-RS request to retrieve the metadata of a specific study
106    pub fn store_instances_in_study(&self, study_instance_uid: &str) -> WadoStowRequest {
107        let url = format!("{}/studies/{}", self.stow_url, study_instance_uid);
108        WadoStowRequest::new(self.clone(), url)
109    }
110}