dicom_web/
stow.rs

1//! Module for WADO-RS requests
2use 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
13/// A builder type for STOW-RS requests
14pub 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        // Basic authentication
54        if let Some(username) = &self.client.username {
55            request = request.basic_auth(username, self.client.password.as_ref());
56        }
57        // Bearer token
58        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        // Convert each instance to a multipart item
79        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        // Write the final boundary
92        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    /// Create a STOW-RS request to store instances
114    pub fn store_instances(&self) -> WadoStowRequest {
115        let url = format!("{}/studies", self.stow_url);
116        WadoStowRequest::new(self.clone(), url)
117    }
118
119    /// Create a WADO-RS request to retrieve the metadata of a specific study
120    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}