1use std::fs::File;
8use std::io::{Read, Seek, SeekFrom};
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12use crate::tiff_utils::AnyResult;
13
14pub trait RangeReader: Send + Sync {
21 fn read_range(&self, offset: u64, length: usize) -> AnyResult<Vec<u8>>;
23
24 fn size(&self) -> u64;
26
27 fn identifier(&self) -> &str;
29
30 fn is_local(&self) -> bool {
32 let id = self.identifier();
33 !id.starts_with("http://") && !id.starts_with("https://") && !id.starts_with("s3://")
34 }
35}
36
37pub struct LocalRangeReader {
39 path: PathBuf,
40 size: u64,
41}
42
43impl LocalRangeReader {
44 pub fn new(path: impl AsRef<Path>) -> AnyResult<Self> {
45 let path = path.as_ref().to_path_buf();
46 let metadata = std::fs::metadata(&path)?;
47 Ok(Self {
48 path,
49 size: metadata.len(),
50 })
51 }
52}
53
54impl RangeReader for LocalRangeReader {
55 fn read_range(&self, offset: u64, length: usize) -> AnyResult<Vec<u8>> {
56 let mut file = File::open(&self.path)?;
57 file.seek(SeekFrom::Start(offset))?;
58 let mut buffer = vec![0u8; length];
59 file.read_exact(&mut buffer)?;
60 Ok(buffer)
61 }
62
63 fn size(&self) -> u64 {
64 self.size
65 }
66
67 fn identifier(&self) -> &str {
68 self.path.to_str().unwrap_or("<invalid path>")
69 }
70}
71
72pub struct HttpRangeReader {
75 url: String,
76 size: u64,
77 client: reqwest::blocking::Client,
78}
79
80impl HttpRangeReader {
81 pub fn new(url: &str) -> AnyResult<Self> {
82 let client = reqwest::blocking::Client::builder()
83 .timeout(std::time::Duration::from_secs(30))
84 .build()?;
85
86 let response = client.head(url).send()?;
88 let size = response
89 .headers()
90 .get("content-length")
91 .and_then(|v| v.to_str().ok())
92 .and_then(|v| v.parse().ok())
93 .unwrap_or(0);
94
95 Ok(Self {
96 url: url.to_string(),
97 size,
98 client,
99 })
100 }
101}
102
103impl RangeReader for HttpRangeReader {
104 fn read_range(&self, offset: u64, length: usize) -> AnyResult<Vec<u8>> {
105 let range = format!("bytes={}-{}", offset, offset + length as u64 - 1);
106 let response = self.client
107 .get(&self.url)
108 .header("Range", range)
109 .send()?;
110
111 if !response.status().is_success() {
112 return Err(format!("HTTP request failed: {}", response.status()).into());
113 }
114
115 Ok(response.bytes()?.to_vec())
116 }
117
118 fn size(&self) -> u64 {
119 self.size
120 }
121
122 fn identifier(&self) -> &str {
123 &self.url
124 }
125}
126
127pub struct S3RangeReader {
129 #[allow(dead_code)]
130 bucket: String,
131 #[allow(dead_code)]
132 key: String,
133 size: u64,
134 url: String,
136}
137
138impl S3RangeReader {
139 pub fn new(url: &str) -> AnyResult<Self> {
141 let url_parsed = url::Url::parse(url)?;
143
144 if url_parsed.scheme() != "s3" {
145 return Err("URL must use s3:// scheme".into());
146 }
147
148 let bucket = url_parsed.host_str()
149 .ok_or("Missing bucket in S3 URL")?
150 .to_string();
151
152 let key = url_parsed.path().trim_start_matches('/').to_string();
153
154 if key.is_empty() {
155 return Err("Missing key in S3 URL".into());
156 }
157
158 Ok(Self {
161 bucket,
162 key,
163 size: 0, url: url.to_string(),
165 })
166 }
167
168 pub fn from_https(url: &str) -> AnyResult<Self> {
170 let http_reader = HttpRangeReader::new(url)?;
172
173 Ok(Self {
174 bucket: String::new(),
175 key: String::new(),
176 size: http_reader.size,
177 url: url.to_string(),
178 })
179 }
180}
181
182impl RangeReader for S3RangeReader {
183 fn read_range(&self, offset: u64, length: usize) -> AnyResult<Vec<u8>> {
184 let client = reqwest::blocking::Client::new();
187 let range = format!("bytes={}-{}", offset, offset + length as u64 - 1);
188
189 let response = client
190 .get(&self.url)
191 .header("Range", range)
192 .send()?;
193
194 if !response.status().is_success() {
195 return Err(format!("S3 request failed: {}", response.status()).into());
196 }
197
198 Ok(response.bytes()?.to_vec())
199 }
200
201 fn size(&self) -> u64 {
202 self.size
203 }
204
205 fn identifier(&self) -> &str {
206 &self.url
207 }
208}
209
210pub fn create_range_reader(source: &str) -> AnyResult<Arc<dyn RangeReader>> {
212 if source.starts_with("s3://") {
213 Ok(Arc::new(crate::s3::S3RangeReaderSync::new(source)?))
215 } else if source.starts_with("http://") || source.starts_with("https://") {
216 Ok(Arc::new(HttpRangeReader::new(source)?))
217 } else {
218 Ok(Arc::new(LocalRangeReader::new(source)?))
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use std::io::Write;
226 use tempfile::NamedTempFile;
227
228 #[test]
229 fn test_local_range_reader() {
230 let mut file = NamedTempFile::new().unwrap();
231 file.write_all(b"Hello, World!").unwrap();
232
233 let reader = LocalRangeReader::new(file.path()).unwrap();
234 assert_eq!(reader.size(), 13);
235
236 let data = reader.read_range(0, 5).unwrap();
237 assert_eq!(&data, b"Hello");
238
239 let data = reader.read_range(7, 5).unwrap();
240 assert_eq!(&data, b"World");
241 }
242}