1use std::fmt::Display;
16use std::path;
17
18use common_error::root_source;
19use common_telemetry::{debug, error, info, warn};
20use opendal::layers::{
21 LoggingInterceptor, LoggingLayer, RetryEvent, RetryInterceptor, RetryLayer, TracingLayer,
22};
23use opendal::raw::{Operation, ServiceInfo};
24use opendal::services::FS_SCHEME;
25use opendal::{ErrorKind, HttpTransporter, OperationContext};
26use opendal_http_transport_reqwest::ReqwestTransport;
27use snafu::ResultExt;
28
29use crate::config::HttpClientConfig;
30use crate::{ObjectStore, error};
31
32pub fn is_object_storage(object_store: &ObjectStore) -> bool {
34 object_store.info().scheme() != FS_SCHEME
35}
36
37pub fn join_dir(parent: &str, child: &str) -> String {
45 let output = format!("{parent}/{child}/");
47 normalize_dir(&output)
48}
49
50pub fn normalize_dir(v: &str) -> String {
70 let has_root = v.starts_with('/');
71 let mut v = v
72 .split('/')
73 .filter(|v| !v.is_empty())
74 .collect::<Vec<&str>>()
75 .join("/");
76 if has_root {
77 v.insert(0, '/');
78 }
79 if !v.ends_with('/') {
80 v.push('/')
81 }
82 v
83}
84
85pub fn join_path(parent: &str, child: &str) -> String {
90 let output = format!("{parent}/{child}");
91 normalize_path(&output)
92}
93
94pub fn normalize_path(path: &str) -> String {
106 let path = path.trim();
108
109 if path.is_empty() {
111 return "/".to_string();
112 }
113
114 let has_leading = path.starts_with('/');
115 let has_trailing = path.ends_with('/');
116
117 let mut p = path
118 .split('/')
119 .filter(|v| !v.is_empty())
120 .collect::<Vec<_>>()
121 .join("/");
122
123 if !p.starts_with('/') && has_leading {
125 p.insert(0, '/');
126 }
127
128 if !p.ends_with('/') && has_trailing {
130 p.push('/');
131 }
132
133 p
134}
135
136pub fn with_instrument_layers(object_store: ObjectStore, path_label: bool) -> ObjectStore {
138 object_store
139 .layer(LoggingLayer::new(DefaultLoggingInterceptor))
140 .layer(TracingLayer::new())
141 .layer(crate::layers::build_prometheus_metrics_layer(path_label))
142}
143
144pub fn with_retry_layers(object_store: ObjectStore) -> ObjectStore {
146 object_store.layer(
147 RetryLayer::new()
148 .with_jitter()
149 .with_notify(PrintDetailedError),
150 )
151}
152
153static LOGGING_TARGET: &str = "opendal::services";
154
155struct LoggingContext<'a>(&'a [(&'a str, &'a str)]);
156
157impl Display for LoggingContext<'_> {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 for (i, (k, v)) in self.0.iter().enumerate() {
160 if i > 0 {
161 write!(f, " {}={}", k, v)?;
162 } else {
163 write!(f, "{}={}", k, v)?;
164 }
165 }
166 Ok(())
167 }
168}
169
170#[derive(Debug, Copy, Clone, Default)]
171pub struct DefaultLoggingInterceptor;
172
173impl LoggingInterceptor for DefaultLoggingInterceptor {
174 #[inline]
175 fn log(
176 &self,
177 info: &ServiceInfo,
178 operation: Operation,
179 context: &[(&str, &str)],
180 message: &str,
181 err: Option<&opendal::Error>,
182 ) {
183 if let Some(err) = err {
184 let root = root_source(err);
185 if err.kind() == ErrorKind::Unexpected {
187 error!(
188 target: LOGGING_TARGET,
189 "service={} name={} {}: {operation} {message} {err:#?}, root={root:#?}",
190 info.scheme(),
191 info.name(),
192 LoggingContext(context),
193 );
194 } else {
195 debug!(
196 target: LOGGING_TARGET,
197 "service={} name={} {}: {operation} {message} {err}, root={root:?}",
198 info.scheme(),
199 info.name(),
200 LoggingContext(context),
201 );
202 };
203 }
204
205 debug!(
206 target: LOGGING_TARGET,
207 "service={} name={} {}: {operation} {message}",
208 info.scheme(),
209 info.name(),
210 LoggingContext(context),
211 );
212 }
213}
214
215pub(crate) fn build_http_context(config: &HttpClientConfig) -> error::Result<OperationContext> {
217 if config.skip_ssl_validation {
218 common_telemetry::warn!(
219 "Skipping SSL validation for object storage HTTP client. Please ensure the environment is trusted."
220 );
221 }
222
223 let client = reqwest::ClientBuilder::new()
224 .pool_max_idle_per_host(config.pool_max_idle_per_host as usize)
225 .connect_timeout(config.connect_timeout)
226 .pool_idle_timeout(config.pool_idle_timeout)
227 .timeout(config.timeout)
228 .danger_accept_invalid_certs(config.skip_ssl_validation)
229 .build()
230 .context(error::BuildHttpClientSnafu)?;
231 let transport = HttpTransporter::new(ReqwestTransport::new(client));
232 Ok(OperationContext::new().with_http_transport(transport))
233}
234
235pub fn clean_temp_dir(dir: &str) -> error::Result<()> {
236 if path::Path::new(&dir).exists() {
237 info!("Begin to clean temp storage directory: {}", dir);
238 std::fs::remove_dir_all(dir).context(error::RemoveDirSnafu { dir })?;
239 info!("Cleaned temp storage directory: {}", dir);
240 }
241
242 Ok(())
243}
244
245pub struct PrintDetailedError;
247
248impl RetryInterceptor for PrintDetailedError {
250 fn intercept(&self, event: RetryEvent<'_>) {
251 warn!(
252 "Retry after {}s, error: {:#?}",
253 event.retry_after.as_secs_f64(),
254 event.err
255 );
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use opendal::services::Fs;
262
263 use super::*;
264 use crate::ObjectStore;
265 use crate::util::is_object_storage;
266
267 #[test]
268 fn test_normalize_dir() {
269 assert_eq!("/", normalize_dir("/"));
270 assert_eq!("/", normalize_dir(""));
271 assert_eq!("/test/", normalize_dir("/test"));
272 }
273
274 #[test]
275 fn test_join_dir() {
276 assert_eq!("/", join_dir("", ""));
277 assert_eq!("/", join_dir("/", ""));
278 assert_eq!("/", join_dir("", "/"));
279 assert_eq!("/", join_dir("/", "/"));
280 assert_eq!("/a/", join_dir("/a", ""));
281 assert_eq!("a/b/c/", join_dir("a/b", "c"));
282 assert_eq!("/a/b/c/", join_dir("/a/b", "c"));
283 assert_eq!("/a/b/c/", join_dir("/a/b", "c/"));
284 assert_eq!("/a/b/c/", join_dir("/a/b", "/c/"));
285 assert_eq!("/a/b/c/", join_dir("/a/b", "//c"));
286 }
287
288 #[test]
289 fn test_join_path() {
290 assert_eq!("/", join_path("", ""));
291 assert_eq!("/", join_path("/", ""));
292 assert_eq!("/", join_path("", "/"));
293 assert_eq!("/", join_path("/", "/"));
294 assert_eq!("a/", join_path("a", ""));
295 assert_eq!("/a", join_path("/", "a"));
296 assert_eq!("a/b/c.txt", join_path("a/b", "c.txt"));
297 assert_eq!("/a/b/c.txt", join_path("/a/b", "c.txt"));
298 assert_eq!("/a/b/c/", join_path("/a/b", "c/"));
299 assert_eq!("/a/b/c/", join_path("/a/b", "/c/"));
300 assert_eq!("/a/b/c.txt", join_path("/a/b", "//c.txt"));
301 assert_eq!("abc/def", join_path(" abc", "/def "));
302 assert_eq!("/abc", join_path("//", "/abc"));
303 assert_eq!("abc/def", join_path("abc/", "//def"));
304 }
305
306 #[test]
307 fn test_fs_is_not_object_storage() {
308 let object_store = ObjectStore::new(Fs::default().root("/tmp")).unwrap();
309
310 assert_eq!(FS_SCHEME, object_store.info().scheme());
311 assert!(!is_object_storage(&object_store));
312 }
313}