Skip to main content

dicom_web/
stow.rs

1//! Module for STOW-RS requests
2//! See https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.5
3use dicom_json::DicomJson;
4use dicom_object::{FileDicomObject, InMemDicomObject};
5
6use futures_util::{stream::BoxStream, Stream, StreamExt};
7use rand::{distr::Alphanumeric, RngExt};
8use reqwest::Body;
9use snafu::ResultExt;
10
11use crate::{
12    apply_auth_and_headers, validate_dicom_json_content_type, DeserializationFailedSnafu,
13    DicomWebClient, DicomWebError, RequestFailedSnafu,
14};
15
16/// A builder type for STOW-RS requests
17pub struct StowRequest {
18    client: DicomWebClient,
19    url: String,
20    instances: BoxStream<'static, Result<Vec<u8>, std::io::Error>>,
21}
22
23impl StowRequest {
24    fn new(client: DicomWebClient, url: String) -> Self {
25        StowRequest {
26            client,
27            url,
28            instances: futures_util::stream::empty().boxed(),
29        }
30    }
31
32    pub fn with_data(mut self, data: impl Stream<Item = Vec<u8>> + Send + 'static) -> Self {
33        self.instances = data.map(Ok).boxed();
34        self
35    }
36
37    pub fn with_instances(
38        mut self,
39        instances: impl Stream<Item = FileDicomObject<InMemDicomObject>> + Send + 'static,
40    ) -> Self {
41        self.instances = instances
42            .map(|instance| {
43                let mut buffer = Vec::new();
44                instance.write_all(&mut buffer).map_err(|e| {
45                    std::io::Error::other(format!("Failed to serialize DICOM instance: {}", e))
46                })?;
47                Ok(buffer)
48            })
49            .boxed();
50        self
51    }
52
53    pub async fn run(self) -> Result<InMemDicomObject, DicomWebError> {
54        let mut request = self.client.client.post(&self.url);
55        request = apply_auth_and_headers(request, &self.client);
56
57        let boundary: String = rand::rng()
58            .sample_iter(&Alphanumeric)
59            .take(8)
60            .map(char::from)
61            .collect();
62
63        let request = request.header(
64            "Content-Type",
65            format!(
66                "multipart/related; type=\"application/dicom\"; boundary={}",
67                boundary
68            ),
69        );
70
71        let boundary_clone = boundary.clone();
72
73        // Convert each instance to a multipart item
74        let multipart_stream = self.instances.map(move |data| {
75            let mut multipart_item = Vec::new();
76            let buffer = data?;
77            multipart_item.extend_from_slice(b"--");
78            multipart_item.extend_from_slice(boundary.as_bytes());
79            multipart_item.extend_from_slice(b"\r\n");
80            multipart_item.extend_from_slice(b"Content-Type: application/dicom\r\n\r\n");
81            multipart_item.extend_from_slice(&buffer);
82            multipart_item.extend_from_slice(b"\r\n");
83            Ok::<_, std::io::Error>(multipart_item)
84        });
85
86        // Write the final boundary
87        let multipart_stream = multipart_stream.chain(futures_util::stream::once(async move {
88            Ok(format!("--{}--\r\n", boundary_clone).into_bytes())
89        }));
90
91        let response = request
92            .body(Body::wrap_stream(multipart_stream))
93            .send()
94            .await
95            .context(RequestFailedSnafu { url: &self.url })?;
96
97        if !response.status().is_success() {
98            return Err(DicomWebError::HttpStatusFailure {
99                status_code: response.status(),
100            });
101        }
102
103        // Check if the response is a DICOM-JSON
104        let ct = response
105            .headers()
106            .get("Content-Type")
107            .ok_or(DicomWebError::MissingContentTypeHeader)?;
108        validate_dicom_json_content_type(ct.to_str().unwrap_or_default())?;
109
110        // STOW-RS response is a single DICOM JSON dataset (PS3.18 ยง10.5.1)
111        Ok(response
112            .json::<DicomJson<InMemDicomObject>>()
113            .await
114            .context(DeserializationFailedSnafu {})?
115            .into_inner())
116    }
117}
118
119impl DicomWebClient {
120    /// Create a STOW-RS request to store instances
121    pub fn store_instances(&self) -> StowRequest {
122        let url = format!("{}/studies", self.stow_url);
123        StowRequest::new(self.clone(), url)
124    }
125
126    /// Create a STOW-RS request to store instances in a specific study
127    pub fn store_instances_in_study(&self, study_instance_uid: &str) -> StowRequest {
128        let url = format!("{}/studies/{}", self.stow_url, study_instance_uid);
129        StowRequest::new(self.clone(), url)
130    }
131}