dicom_web/
stow.rs

1//! Module for STOW-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, 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        // Basic authentication
52        if let Some(username) = &self.client.username {
53            request = request.basic_auth(username, self.client.password.as_ref());
54        }
55        // Bearer token
56        else if let Some(bearer_token) = &self.client.bearer_token {
57            request = request.bearer_auth(bearer_token);
58        }
59
60        // Extra headers
61        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        // Convert each instance to a multipart item
82        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        // Write the final boundary
95        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    /// Create a STOW-RS request to store instances
117    pub fn store_instances(&self) -> WadoStowRequest {
118        let url = format!("{}/studies", self.stow_url);
119        WadoStowRequest::new(self.clone(), url)
120    }
121
122    /// Create a WADO-RS request to retrieve the metadata of a specific study
123    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}