opendal/services/http/
backend.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::fmt::Debug;
19use std::fmt::Formatter;
20use std::sync::Arc;
21
22use http::Response;
23use http::StatusCode;
24use log::debug;
25
26use super::core::HttpCore;
27use super::error::parse_error;
28use crate::raw::*;
29use crate::services::HttpConfig;
30use crate::*;
31
32impl Configurator for HttpConfig {
33    type Builder = HttpBuilder;
34
35    #[allow(deprecated)]
36    fn into_builder(self) -> Self::Builder {
37        HttpBuilder {
38            config: self,
39            http_client: None,
40        }
41    }
42}
43
44/// HTTP Read-only service support like [Nginx](https://www.nginx.com/) and [Caddy](https://caddyserver.com/).
45#[doc = include_str!("docs.md")]
46#[derive(Default)]
47pub struct HttpBuilder {
48    config: HttpConfig,
49
50    #[deprecated(since = "0.53.0", note = "Use `Operator::update_http_client` instead")]
51    http_client: Option<HttpClient>,
52}
53
54impl Debug for HttpBuilder {
55    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
56        let mut de = f.debug_struct("HttpBuilder");
57
58        de.field("config", &self.config).finish()
59    }
60}
61
62impl HttpBuilder {
63    /// Set endpoint for http backend.
64    ///
65    /// For example: `https://example.com`
66    pub fn endpoint(mut self, endpoint: &str) -> Self {
67        self.config.endpoint = if endpoint.is_empty() {
68            None
69        } else {
70            Some(endpoint.to_string())
71        };
72
73        self
74    }
75
76    /// set username for http backend
77    ///
78    /// default: no username
79    pub fn username(mut self, username: &str) -> Self {
80        if !username.is_empty() {
81            self.config.username = Some(username.to_owned());
82        }
83        self
84    }
85
86    /// set password for http backend
87    ///
88    /// default: no password
89    pub fn password(mut self, password: &str) -> Self {
90        if !password.is_empty() {
91            self.config.password = Some(password.to_owned());
92        }
93        self
94    }
95
96    /// set bearer token for http backend
97    ///
98    /// default: no access token
99    pub fn token(mut self, token: &str) -> Self {
100        if !token.is_empty() {
101            self.config.token = Some(token.to_string());
102        }
103        self
104    }
105
106    /// Set root path of http backend.
107    pub fn root(mut self, root: &str) -> Self {
108        self.config.root = if root.is_empty() {
109            None
110        } else {
111            Some(root.to_string())
112        };
113
114        self
115    }
116
117    /// Specify the http client that used by this service.
118    ///
119    /// # Notes
120    ///
121    /// This API is part of OpenDAL's Raw API. `HttpClient` could be changed
122    /// during minor updates.
123    #[deprecated(since = "0.53.0", note = "Use `Operator::update_http_client` instead")]
124    #[allow(deprecated)]
125    pub fn http_client(mut self, client: HttpClient) -> Self {
126        self.http_client = Some(client);
127        self
128    }
129}
130
131impl Builder for HttpBuilder {
132    const SCHEME: Scheme = Scheme::Http;
133    type Config = HttpConfig;
134
135    fn build(self) -> Result<impl Access> {
136        debug!("backend build started: {:?}", &self);
137
138        let endpoint = match &self.config.endpoint {
139            Some(v) => v,
140            None => {
141                return Err(Error::new(ErrorKind::ConfigInvalid, "endpoint is empty")
142                    .with_context("service", Scheme::Http))
143            }
144        };
145
146        let root = normalize_root(&self.config.root.unwrap_or_default());
147        debug!("backend use root {}", root);
148
149        let mut auth = None;
150        if let Some(username) = &self.config.username {
151            auth = Some(format_authorization_by_basic(
152                username,
153                self.config.password.as_deref().unwrap_or_default(),
154            )?);
155        }
156        if let Some(token) = &self.config.token {
157            auth = Some(format_authorization_by_bearer(token)?)
158        }
159
160        let info = AccessorInfo::default();
161        info.set_scheme(Scheme::Http)
162            .set_root(&root)
163            .set_native_capability(Capability {
164                stat: true,
165                stat_with_if_match: true,
166                stat_with_if_none_match: true,
167                stat_has_cache_control: true,
168                stat_has_content_length: true,
169                stat_has_content_type: true,
170                stat_has_content_encoding: true,
171                stat_has_content_range: true,
172                stat_has_etag: true,
173                stat_has_content_md5: true,
174                stat_has_last_modified: true,
175                stat_has_content_disposition: true,
176
177                read: true,
178
179                read_with_if_match: true,
180                read_with_if_none_match: true,
181
182                presign: auth.is_none(),
183                presign_read: auth.is_none(),
184                presign_stat: auth.is_none(),
185
186                shared: true,
187
188                ..Default::default()
189            });
190
191        // allow deprecated api here for compatibility
192        #[allow(deprecated)]
193        if let Some(client) = self.http_client {
194            info.update_http_client(|_| client);
195        }
196
197        let accessor_info = Arc::new(info);
198
199        let core = Arc::new(HttpCore {
200            info: accessor_info,
201            endpoint: endpoint.to_string(),
202            root,
203            authorization: auth,
204        });
205
206        Ok(HttpBackend { core })
207    }
208}
209
210/// Backend is used to serve `Accessor` support for http.
211#[derive(Clone)]
212pub struct HttpBackend {
213    core: Arc<HttpCore>,
214}
215
216impl Debug for HttpBackend {
217    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
218        f.debug_struct("HttpBackend")
219            .field("core", &self.core)
220            .finish()
221    }
222}
223
224impl Access for HttpBackend {
225    type Reader = HttpBody;
226    type Writer = ();
227    type Lister = ();
228    type Deleter = ();
229    type BlockingReader = ();
230    type BlockingWriter = ();
231    type BlockingLister = ();
232    type BlockingDeleter = ();
233
234    fn info(&self) -> Arc<AccessorInfo> {
235        self.core.info.clone()
236    }
237
238    async fn stat(&self, path: &str, args: OpStat) -> Result<RpStat> {
239        // Stat root always returns a DIR.
240        if path == "/" {
241            return Ok(RpStat::new(Metadata::new(EntryMode::DIR)));
242        }
243
244        let resp = self.core.http_head(path, &args).await?;
245
246        let status = resp.status();
247
248        match status {
249            StatusCode::OK => parse_into_metadata(path, resp.headers()).map(RpStat::new),
250            // HTTP Server like nginx could return FORBIDDEN if auto-index
251            // is not enabled, we should ignore them.
252            StatusCode::NOT_FOUND | StatusCode::FORBIDDEN if path.ends_with('/') => {
253                Ok(RpStat::new(Metadata::new(EntryMode::DIR)))
254            }
255            _ => Err(parse_error(resp)),
256        }
257    }
258
259    async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::Reader)> {
260        let resp = self.core.http_get(path, args.range(), &args).await?;
261
262        let status = resp.status();
263
264        match status {
265            StatusCode::OK | StatusCode::PARTIAL_CONTENT => {
266                Ok((RpRead::default(), resp.into_body()))
267            }
268            _ => {
269                let (part, mut body) = resp.into_parts();
270                let buf = body.to_buffer().await?;
271                Err(parse_error(Response::from_parts(part, buf)))
272            }
273        }
274    }
275
276    async fn presign(&self, path: &str, args: OpPresign) -> Result<RpPresign> {
277        if self.core.has_authorization() {
278            return Err(Error::new(
279                ErrorKind::Unsupported,
280                "Http doesn't support presigned request on backend with authorization",
281            ));
282        }
283
284        let req = match args.operation() {
285            PresignOperation::Stat(v) => self.core.http_head_request(path, v)?,
286            PresignOperation::Read(v) => {
287                self.core.http_get_request(path, BytesRange::default(), v)?
288            }
289            _ => {
290                return Err(Error::new(
291                    ErrorKind::Unsupported,
292                    "Http doesn't support presigned write",
293                ))
294            }
295        };
296
297        let (parts, _) = req.into_parts();
298
299        Ok(RpPresign::new(PresignedRequest::new(
300            parts.method,
301            parts.uri,
302            parts.headers,
303        )))
304    }
305}