1use bytes::Bytes;
7use std::collections::HashMap;
8use std::time::Duration;
9
10#[cfg(feature = "http")]
11use reqwest::Client;
12
13use crate::auth::Credentials;
14use crate::error::{CloudError, HttpError, Result};
15use crate::retry::{RetryConfig, RetryExecutor};
16
17use super::CloudStorageBackend;
18
19#[derive(Debug, Clone)]
21pub enum HttpAuth {
22 None,
24 Basic {
26 username: String,
28 password: String,
30 },
31 Bearer {
33 token: String,
35 },
36 ApiKey {
38 header_name: String,
40 key: String,
42 },
43 Custom {
45 headers: HashMap<String, String>,
47 },
48}
49
50#[derive(Debug, Clone)]
52pub struct HttpBackend {
53 pub base_url: String,
55 pub auth: HttpAuth,
57 pub timeout: Duration,
59 pub retry_config: RetryConfig,
61 pub credentials: Option<Credentials>,
63 pub headers: HashMap<String, String>,
65 pub follow_redirects: bool,
67 pub max_redirects: usize,
69}
70
71impl HttpBackend {
72 #[must_use]
77 pub fn new(base_url: impl Into<String>) -> Self {
78 let mut url = base_url.into();
79 if url.ends_with('/') {
81 url.pop();
82 }
83
84 Self {
85 base_url: url,
86 auth: HttpAuth::None,
87 timeout: Duration::from_secs(300),
88 retry_config: RetryConfig::default(),
89 credentials: None,
90 headers: HashMap::new(),
91 follow_redirects: true,
92 max_redirects: 10,
93 }
94 }
95
96 #[must_use]
98 pub fn with_auth(mut self, auth: HttpAuth) -> Self {
99 self.auth = auth;
100 self
101 }
102
103 #[must_use]
105 pub fn with_timeout(mut self, timeout: Duration) -> Self {
106 self.timeout = timeout;
107 self
108 }
109
110 #[must_use]
112 pub fn with_retry_config(mut self, config: RetryConfig) -> Self {
113 self.retry_config = config;
114 self
115 }
116
117 #[must_use]
119 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
120 self.headers.insert(name.into(), value.into());
121 self
122 }
123
124 #[must_use]
126 pub fn with_follow_redirects(mut self, follow: bool) -> Self {
127 self.follow_redirects = follow;
128 self
129 }
130
131 fn full_url(&self, key: &str) -> String {
132 format!("{}/{}", self.base_url, key)
133 }
134
135 #[cfg(feature = "http")]
136 fn create_client(&self) -> Result<Client> {
137 let mut client_builder =
138 Client::builder()
139 .timeout(self.timeout)
140 .redirect(if self.follow_redirects {
141 reqwest::redirect::Policy::limited(self.max_redirects)
142 } else {
143 reqwest::redirect::Policy::none()
144 });
145
146 let mut headers = reqwest::header::HeaderMap::new();
148
149 match &self.auth {
151 HttpAuth::None => {}
152 HttpAuth::Basic { username, password } => {
153 let auth_value = format!("{}:{}", username, password);
154 let encoded = base64_encode(auth_value.as_bytes());
155 let header_value = format!("Basic {}", encoded);
156
157 headers.insert(
158 reqwest::header::AUTHORIZATION,
159 reqwest::header::HeaderValue::from_str(&header_value).map_err(|e| {
160 CloudError::Http(HttpError::InvalidHeader {
161 name: "Authorization".to_string(),
162 message: format!("{e}"),
163 })
164 })?,
165 );
166 }
167 HttpAuth::Bearer { token } => {
168 let header_value = format!("Bearer {}", token);
169
170 headers.insert(
171 reqwest::header::AUTHORIZATION,
172 reqwest::header::HeaderValue::from_str(&header_value).map_err(|e| {
173 CloudError::Http(HttpError::InvalidHeader {
174 name: "Authorization".to_string(),
175 message: format!("{e}"),
176 })
177 })?,
178 );
179 }
180 HttpAuth::ApiKey { header_name, key } => {
181 let header_name_parsed = reqwest::header::HeaderName::from_bytes(
182 header_name.as_bytes(),
183 )
184 .map_err(|e| {
185 CloudError::Http(HttpError::InvalidHeader {
186 name: header_name.clone(),
187 message: format!("{e}"),
188 })
189 })?;
190
191 headers.insert(
192 header_name_parsed,
193 reqwest::header::HeaderValue::from_str(key).map_err(|e| {
194 CloudError::Http(HttpError::InvalidHeader {
195 name: header_name.clone(),
196 message: format!("{e}"),
197 })
198 })?,
199 );
200 }
201 HttpAuth::Custom {
202 headers: custom_headers,
203 } => {
204 for (name, value) in custom_headers {
205 let header_name = reqwest::header::HeaderName::from_bytes(name.as_bytes())
206 .map_err(|e| {
207 CloudError::Http(HttpError::InvalidHeader {
208 name: name.clone(),
209 message: format!("{e}"),
210 })
211 })?;
212
213 headers.insert(
214 header_name,
215 reqwest::header::HeaderValue::from_str(value).map_err(|e| {
216 CloudError::Http(HttpError::InvalidHeader {
217 name: name.clone(),
218 message: format!("{e}"),
219 })
220 })?,
221 );
222 }
223 }
224 }
225
226 for (name, value) in &self.headers {
228 let header_name =
229 reqwest::header::HeaderName::from_bytes(name.as_bytes()).map_err(|e| {
230 CloudError::Http(HttpError::InvalidHeader {
231 name: name.clone(),
232 message: format!("{e}"),
233 })
234 })?;
235
236 headers.insert(
237 header_name,
238 reqwest::header::HeaderValue::from_str(value).map_err(|e| {
239 CloudError::Http(HttpError::InvalidHeader {
240 name: name.clone(),
241 message: format!("{e}"),
242 })
243 })?,
244 );
245 }
246
247 client_builder = client_builder.default_headers(headers);
248
249 client_builder.build().map_err(|e| {
250 CloudError::Http(HttpError::RequestBuild {
251 message: format!("{e}"),
252 })
253 })
254 }
255}
256
257fn base64_encode(input: &[u8]) -> String {
259 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
260 let mut result = String::new();
261
262 for chunk in input.chunks(3) {
263 let b1 = chunk[0];
264 let b2 = chunk.get(1).copied().unwrap_or(0);
265 let b3 = chunk.get(2).copied().unwrap_or(0);
266
267 let n = ((b1 as u32) << 16) | ((b2 as u32) << 8) | (b3 as u32);
268
269 result.push(CHARS[((n >> 18) & 63) as usize] as char);
270 result.push(CHARS[((n >> 12) & 63) as usize] as char);
271 result.push(if chunk.len() > 1 {
272 CHARS[((n >> 6) & 63) as usize] as char
273 } else {
274 '='
275 });
276 result.push(if chunk.len() > 2 {
277 CHARS[(n & 63) as usize] as char
278 } else {
279 '='
280 });
281 }
282
283 result
284}
285
286#[cfg(all(feature = "http", feature = "async"))]
287#[async_trait::async_trait]
288impl CloudStorageBackend for HttpBackend {
289 async fn get(&self, key: &str) -> Result<Bytes> {
290 let mut executor = RetryExecutor::new(self.retry_config.clone());
291
292 executor
293 .execute(|| async {
294 let client = self.create_client()?;
295 let url = self.full_url(key);
296
297 let response = client.get(&url).send().await.map_err(|e| {
298 CloudError::Http(HttpError::Network {
299 message: format!("HTTP GET failed for '{url}': {e}"),
300 })
301 })?;
302
303 let status = response.status();
304 if !status.is_success() {
305 return Err(CloudError::Http(HttpError::Status {
306 status: status.as_u16(),
307 message: format!("HTTP GET failed for '{url}'"),
308 }));
309 }
310
311 let bytes = response.bytes().await.map_err(|e| {
312 CloudError::Http(HttpError::ResponseParse {
313 message: format!("Failed to read response body: {e}"),
314 })
315 })?;
316
317 Ok(bytes)
318 })
319 .await
320 }
321
322 async fn put(&self, _key: &str, _data: &[u8]) -> Result<()> {
323 Err(CloudError::NotSupported {
325 operation: "HTTP backend is read-only".to_string(),
326 })
327 }
328
329 async fn delete(&self, _key: &str) -> Result<()> {
330 Err(CloudError::NotSupported {
332 operation: "HTTP backend is read-only".to_string(),
333 })
334 }
335
336 async fn exists(&self, key: &str) -> Result<bool> {
337 let client = self.create_client()?;
338 let url = self.full_url(key);
339
340 match client.head(&url).send().await {
341 Ok(response) => Ok(response.status().is_success()),
342 Err(_) => Ok(false),
343 }
344 }
345
346 async fn list_prefix(&self, _prefix: &str) -> Result<Vec<String>> {
347 Err(CloudError::NotSupported {
349 operation: "HTTP backend does not support listing".to_string(),
350 })
351 }
352
353 fn is_readonly(&self) -> bool {
354 true
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 #[test]
363 fn test_http_backend_new() {
364 let backend = HttpBackend::new("https://example.com/data");
365 assert_eq!(backend.base_url, "https://example.com/data");
366 }
367
368 #[test]
369 fn test_http_backend_builder() {
370 let backend = HttpBackend::new("https://example.com")
371 .with_auth(HttpAuth::Bearer {
372 token: "token123".to_string(),
373 })
374 .with_header("User-Agent", "OxiGDAL/1.0")
375 .with_timeout(Duration::from_secs(600))
376 .with_follow_redirects(false);
377
378 assert!(matches!(backend.auth, HttpAuth::Bearer { .. }));
379 assert_eq!(backend.headers.len(), 1);
380 assert_eq!(backend.timeout, Duration::from_secs(600));
381 assert!(!backend.follow_redirects);
382 }
383
384 #[test]
385 fn test_http_backend_full_url() {
386 let backend = HttpBackend::new("https://example.com/data");
387 assert_eq!(
388 backend.full_url("file.txt"),
389 "https://example.com/data/file.txt"
390 );
391 }
392
393 #[test]
394 fn test_base64_encode() {
395 assert_eq!(base64_encode(b"hello"), "aGVsbG8=");
396 assert_eq!(base64_encode(b"world"), "d29ybGQ=");
397 assert_eq!(base64_encode(b"user:pass"), "dXNlcjpwYXNz");
398 }
399}