wsi_streamer/slide/
s3_source.rs1use async_trait::async_trait;
7use aws_sdk_s3::Client;
8
9use crate::error::IoError;
10use crate::io::S3RangeReader;
11
12use super::{SlideListResult, SlideSource};
13
14const SLIDE_EXTENSIONS: &[&str] = &[".svs", ".tif", ".tiff"];
20
21fn is_slide_file(path: &str) -> bool {
23 let path_lower = path.to_lowercase();
24 SLIDE_EXTENSIONS.iter().any(|ext| path_lower.ends_with(ext))
25}
26
27#[derive(Clone)]
45pub struct S3SlideSource {
46 client: Client,
47 bucket: String,
48}
49
50impl S3SlideSource {
51 pub fn new(client: Client, bucket: String) -> Self {
57 Self { client, bucket }
58 }
59
60 pub fn bucket(&self) -> &str {
62 &self.bucket
63 }
64}
65
66#[async_trait]
67impl SlideSource for S3SlideSource {
68 type Reader = S3RangeReader;
69
70 async fn create_reader(&self, slide_id: &str) -> Result<Self::Reader, IoError> {
71 S3RangeReader::new(
72 self.client.clone(),
73 self.bucket.clone(),
74 slide_id.to_string(),
75 )
76 .await
77 }
78
79 async fn list_slides(
80 &self,
81 limit: u32,
82 cursor: Option<&str>,
83 prefix: Option<&str>,
84 ) -> Result<SlideListResult, IoError> {
85 let mut request = self
86 .client
87 .list_objects_v2()
88 .bucket(&self.bucket)
89 .max_keys(limit as i32);
90
91 if let Some(token) = cursor {
92 request = request.continuation_token(token);
93 }
94
95 if let Some(prefix) = prefix {
96 request = request.prefix(prefix);
97 }
98
99 let response = request
100 .send()
101 .await
102 .map_err(|e| IoError::S3(e.to_string()))?;
103
104 let slides: Vec<String> = response
105 .contents()
106 .iter()
107 .filter_map(|obj| obj.key())
108 .filter(|key| is_slide_file(key))
109 .map(|s| s.to_string())
110 .collect();
111
112 Ok(SlideListResult {
113 slides,
114 next_cursor: response.next_continuation_token().map(|s| s.to_string()),
115 })
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use aws_smithy_runtime::client::http::hyper_014::HyperClientBuilder;
123 use hyper_rustls::HttpsConnectorBuilder;
124
125 #[test]
126 fn test_s3_slide_source_bucket() {
127 let https_connector = HttpsConnectorBuilder::new()
130 .with_webpki_roots()
131 .https_only()
132 .enable_http1()
133 .enable_http2()
134 .build();
135 let http_client = HyperClientBuilder::new().build(https_connector);
136 let config = aws_sdk_s3::Config::builder()
137 .behavior_version_latest()
138 .http_client(http_client)
139 .build();
140 let client = aws_sdk_s3::Client::from_conf(config);
141 let source = S3SlideSource::new(client, "test-bucket".to_string());
142 assert_eq!(source.bucket(), "test-bucket");
143 }
144
145 #[test]
146 fn test_is_slide_file_svs() {
147 assert!(is_slide_file("slide.svs"));
148 assert!(is_slide_file("path/to/slide.svs"));
149 assert!(is_slide_file("SLIDE.SVS"));
150 assert!(is_slide_file("path/to/SLIDE.Svs"));
151 }
152
153 #[test]
154 fn test_is_slide_file_tif() {
155 assert!(is_slide_file("slide.tif"));
156 assert!(is_slide_file("path/to/slide.tif"));
157 assert!(is_slide_file("SLIDE.TIF"));
158 }
159
160 #[test]
161 fn test_is_slide_file_tiff() {
162 assert!(is_slide_file("slide.tiff"));
163 assert!(is_slide_file("path/to/slide.tiff"));
164 assert!(is_slide_file("SLIDE.TIFF"));
165 }
166
167 #[test]
168 fn test_is_slide_file_non_slide() {
169 assert!(!is_slide_file("image.jpg"));
170 assert!(!is_slide_file("document.pdf"));
171 assert!(!is_slide_file("slide.svs.backup"));
172 assert!(!is_slide_file("slide_svs"));
173 assert!(!is_slide_file(""));
174 assert!(!is_slide_file("no_extension"));
175 }
176}