oxirs_samm/
cloud_backends_http.rs1use crate::cloud_backends_common::url_encode;
6use crate::cloud_storage::CloudStorageBackend;
7use async_trait::async_trait;
8use reqwest::{Client, StatusCode};
9use tracing::debug;
10
11#[derive(Debug, Clone)]
25pub struct HttpConfig {
26 pub base_url: String,
28 pub auth_header: Option<String>,
30 pub path_prefix: String,
32}
33
34impl HttpConfig {
35 pub fn new(base_url: impl Into<String>) -> Self {
37 Self {
38 base_url: base_url.into(),
39 auth_header: None,
40 path_prefix: String::new(),
41 }
42 }
43
44 pub fn with_bearer(base_url: impl Into<String>, token: impl Into<String>) -> Self {
46 Self {
47 base_url: base_url.into(),
48 auth_header: Some(format!("Bearer {}", token.into())),
49 path_prefix: String::new(),
50 }
51 }
52
53 pub(crate) fn full_key(&self, key: &str) -> String {
54 if self.path_prefix.is_empty() {
55 key.to_string()
56 } else {
57 format!("{}{}", self.path_prefix, key)
58 }
59 }
60
61 fn object_url(&self, key: &str) -> String {
62 format!("{}/{}", self.base_url.trim_end_matches('/'), key)
63 }
64}
65
66pub struct HttpBackend {
68 config: HttpConfig,
69 client: Client,
70}
71
72impl HttpBackend {
73 pub fn new(config: HttpConfig) -> Result<Self, String> {
75 if config.base_url.is_empty() {
76 return Err("HttpConfig.base_url must not be empty".to_string());
77 }
78 let client = crate::cloud_backends_common::build_tls_client()?;
79 Ok(Self { config, client })
80 }
81
82 fn add_auth(&self, builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
83 if let Some(ref auth) = self.config.auth_header {
84 builder.header("Authorization", auth)
85 } else {
86 builder
87 }
88 }
89
90 async fn http_put(&self, key: &str, data: Vec<u8>) -> Result<(), String> {
91 let url = self.config.object_url(key);
92 debug!("HTTP PUT {}", url);
93
94 let request = self
95 .client
96 .put(&url)
97 .header("Content-Type", "application/octet-stream")
98 .body(data);
99 let request = self.add_auth(request);
100
101 let response = request
102 .send()
103 .await
104 .map_err(|e| format!("HTTP PUT failed: {e}"))?;
105
106 if response.status().is_success() {
107 Ok(())
108 } else {
109 let status = response.status();
110 let body = response.text().await.unwrap_or_default();
111 Err(format!("HTTP PUT returned {status}: {body}"))
112 }
113 }
114
115 async fn http_get(&self, key: &str) -> Result<Vec<u8>, String> {
116 let url = self.config.object_url(key);
117 debug!("HTTP GET {}", url);
118
119 let request = self.client.get(&url);
120 let request = self.add_auth(request);
121
122 let response = request
123 .send()
124 .await
125 .map_err(|e| format!("HTTP GET failed: {e}"))?;
126
127 if response.status().is_success() {
128 let bytes = response
129 .bytes()
130 .await
131 .map_err(|e| format!("Failed to read HTTP GET body: {e}"))?;
132 Ok(bytes.to_vec())
133 } else if response.status() == StatusCode::NOT_FOUND {
134 Err(format!("HTTP object not found: {key}"))
135 } else {
136 let status = response.status();
137 let body = response.text().await.unwrap_or_default();
138 Err(format!("HTTP GET returned {status}: {body}"))
139 }
140 }
141
142 async fn http_head(&self, key: &str) -> Result<bool, String> {
143 let url = self.config.object_url(key);
144 debug!("HTTP HEAD {}", url);
145
146 let request = self.client.head(&url);
147 let request = self.add_auth(request);
148
149 let response = request
150 .send()
151 .await
152 .map_err(|e| format!("HTTP HEAD failed: {e}"))?;
153
154 match response.status() {
155 StatusCode::OK => Ok(true),
156 StatusCode::NOT_FOUND => Ok(false),
157 other => Err(format!("HTTP HEAD returned unexpected status: {other}")),
158 }
159 }
160
161 async fn http_delete(&self, key: &str) -> Result<(), String> {
162 let url = self.config.object_url(key);
163 debug!("HTTP DELETE {}", url);
164
165 let request = self.client.delete(&url);
166 let request = self.add_auth(request);
167
168 let response = request
169 .send()
170 .await
171 .map_err(|e| format!("HTTP DELETE failed: {e}"))?;
172
173 if response.status().is_success() || response.status() == StatusCode::NOT_FOUND {
174 Ok(())
175 } else {
176 let status = response.status();
177 let body = response.text().await.unwrap_or_default();
178 Err(format!("HTTP DELETE returned {status}: {body}"))
179 }
180 }
181
182 async fn http_list(&self, prefix: &str) -> Result<Vec<String>, String> {
183 let url = if prefix.is_empty() {
184 self.config.base_url.clone()
185 } else {
186 format!("{}?prefix={}", self.config.base_url, url_encode(prefix))
187 };
188 debug!("HTTP LIST {}", url);
189
190 let request = self.client.get(&url);
191 let request = self.add_auth(request);
192
193 let response = request
194 .send()
195 .await
196 .map_err(|e| format!("HTTP LIST failed: {e}"))?;
197
198 if response.status().is_success() {
199 let keys: Vec<String> = response
200 .json()
201 .await
202 .map_err(|e| format!("Failed to parse HTTP LIST response as JSON: {e}"))?;
203 Ok(keys)
204 } else if response.status() == StatusCode::NOT_FOUND {
205 Ok(vec![])
206 } else {
207 let status = response.status();
208 let body = response.text().await.unwrap_or_default();
209 Err(format!("HTTP LIST returned {status}: {body}"))
210 }
211 }
212}
213
214#[async_trait]
215impl CloudStorageBackend for HttpBackend {
216 async fn upload(&self, key: &str, data: Vec<u8>) -> std::result::Result<(), String> {
217 let full_key = self.config.full_key(key);
218 self.http_put(&full_key, data).await
219 }
220
221 async fn download(&self, key: &str) -> std::result::Result<Vec<u8>, String> {
222 let full_key = self.config.full_key(key);
223 self.http_get(&full_key).await
224 }
225
226 async fn exists(&self, key: &str) -> std::result::Result<bool, String> {
227 let full_key = self.config.full_key(key);
228 self.http_head(&full_key).await
229 }
230
231 async fn delete(&self, key: &str) -> std::result::Result<(), String> {
232 let full_key = self.config.full_key(key);
233 self.http_delete(&full_key).await
234 }
235
236 async fn list(&self, prefix: &str) -> std::result::Result<Vec<String>, String> {
237 let full_prefix = self.config.full_key(prefix);
238 self.http_list(&full_prefix).await
239 }
240}