Skip to main content

opendal_service_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::sync::Arc;
20
21use http::StatusCode;
22use log::debug;
23
24use super::HTTP_SCHEME;
25use super::config::HttpConfig;
26use super::core::{ErrorContext, HttpCore, parse_error};
27use super::reader::*;
28use opendal_core::raw::*;
29use opendal_core::*;
30
31/// HTTP Read-only service support like [Nginx](https://www.nginx.com/) and [Caddy](https://caddyserver.com/).
32#[doc = include_str!("docs.md")]
33#[derive(Default)]
34pub struct HttpBuilder {
35    pub(super) config: HttpConfig,
36}
37
38impl Debug for HttpBuilder {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.debug_struct("HttpBuilder")
41            .field("config", &self.config)
42            .finish_non_exhaustive()
43    }
44}
45
46impl HttpBuilder {
47    /// Set endpoint for http backend.
48    ///
49    /// For example: `https://example.com`
50    pub fn endpoint(mut self, endpoint: &str) -> Self {
51        self.config.endpoint = if endpoint.is_empty() {
52            None
53        } else {
54            Some(endpoint.to_string())
55        };
56
57        self
58    }
59
60    /// set username for http backend
61    ///
62    /// default: no username
63    pub fn username(mut self, username: &str) -> Self {
64        if !username.is_empty() {
65            self.config.username = Some(username.to_owned());
66        }
67        self
68    }
69
70    /// set password for http backend
71    ///
72    /// default: no password
73    pub fn password(mut self, password: &str) -> Self {
74        if !password.is_empty() {
75            self.config.password = Some(password.to_owned());
76        }
77        self
78    }
79
80    /// set bearer token for http backend
81    ///
82    /// default: no access token
83    pub fn token(mut self, token: &str) -> Self {
84        if !token.is_empty() {
85            self.config.token = Some(token.to_string());
86        }
87        self
88    }
89
90    /// Set root path of http backend.
91    pub fn root(mut self, root: &str) -> Self {
92        self.config.root = if root.is_empty() {
93            None
94        } else {
95            Some(root.to_string())
96        };
97
98        self
99    }
100}
101
102impl Builder for HttpBuilder {
103    type Config = HttpConfig;
104
105    fn build(self) -> Result<impl Service> {
106        debug!("backend build started: {:?}", self);
107
108        let endpoint = match &self.config.endpoint {
109            Some(v) => v,
110            None => {
111                return Err(Error::new(ErrorKind::ConfigInvalid, "endpoint is empty")
112                    .with_context("service", HTTP_SCHEME));
113            }
114        };
115
116        let root = normalize_root(&self.config.root.unwrap_or_default());
117        debug!("backend use root {root}");
118
119        let mut auth = None;
120        if let Some(username) = &self.config.username {
121            auth = Some(format_authorization_by_basic(
122                username,
123                self.config.password.as_deref().unwrap_or_default(),
124            )?);
125        }
126        if let Some(token) = &self.config.token {
127            auth = Some(format_authorization_by_bearer(token)?)
128        }
129
130        let info = ServiceInfo::new(HTTP_SCHEME, &root, "");
131        let capability = Capability {
132            stat: true,
133            stat_with_if_match: true,
134            stat_with_if_none_match: true,
135            stat_with_if_modified_since: true,
136            stat_with_if_unmodified_since: true,
137
138            read: true,
139            read_with_suffix: true,
140
141            read_with_if_match: true,
142            read_with_if_none_match: true,
143            read_with_if_modified_since: true,
144            read_with_if_unmodified_since: true,
145
146            presign: auth.is_none(),
147            presign_read: auth.is_none(),
148            presign_stat: auth.is_none(),
149
150            shared: true,
151
152            ..Default::default()
153        };
154
155        let accessor_info = info;
156
157        let core = Arc::new(HttpCore {
158            info: accessor_info,
159            capability,
160            endpoint: endpoint.to_string(),
161            root,
162            authorization: auth,
163        });
164
165        Ok(HttpBackend { core })
166    }
167}
168
169/// HttpBackend implements [`Service`] for read-only HTTP directory listings and files.
170#[derive(Clone, Debug)]
171pub struct HttpBackend {
172    pub(crate) core: Arc<HttpCore>,
173}
174
175impl Service for HttpBackend {
176    type Reader = oio::StreamReader<HttpReader>;
177    type Writer = ();
178    type Lister = ();
179    type Deleter = ();
180    type Copier = ();
181    type Composer = ();
182
183    fn info(&self) -> ServiceInfo {
184        self.core.info.clone()
185    }
186
187    fn capability(&self) -> Capability {
188        self.core.capability
189    }
190
191    async fn create_dir(
192        &self,
193        _ctx: &OperationContext,
194        _path: &str,
195        _args: OpCreateDir,
196    ) -> Result<RpCreateDir> {
197        Err(Error::new(
198            ErrorKind::Unsupported,
199            "operation is not supported",
200        ))
201    }
202
203    async fn stat(&self, ctx: &OperationContext, path: &str, args: OpStat) -> Result<RpStat> {
204        // Stat root always returns a DIR.
205        if path == "/" {
206            return Ok(RpStat::new(MetadataBuilder::dir().build()));
207        }
208
209        let resp = self.core.http_head(ctx, path, &args).await?;
210
211        let status = resp.status();
212
213        match status {
214            StatusCode::OK => parse_into_metadata(path, resp.headers()).map(RpStat::new),
215            // HTTP Server like nginx could return FORBIDDEN if auto-index
216            // is not enabled, we should ignore them.
217            StatusCode::NOT_FOUND | StatusCode::FORBIDDEN if path.ends_with('/') => {
218                Ok(RpStat::new(MetadataBuilder::dir().build()))
219            }
220            _ => Err(parse_error(
221                ErrorContext::new(ServiceOperation("Head")),
222                resp,
223            )),
224        }
225    }
226    fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
227        let output: oio::StreamReader<HttpReader> = {
228            Ok(oio::StreamReader::new(HttpReader::new(
229                self.clone(),
230                ctx.clone(),
231                path,
232                args,
233            )))
234        }?;
235
236        Ok(output)
237    }
238
239    fn write(&self, _ctx: &OperationContext, _path: &str, _args: OpWrite) -> Result<Self::Writer> {
240        Err(Error::new(
241            ErrorKind::Unsupported,
242            "operation is not supported",
243        ))
244    }
245
246    fn delete(&self, _ctx: &OperationContext) -> Result<Self::Deleter> {
247        Err(Error::new(
248            ErrorKind::Unsupported,
249            "operation is not supported",
250        ))
251    }
252
253    fn list(&self, _ctx: &OperationContext, _path: &str, _args: OpList) -> Result<Self::Lister> {
254        Err(Error::new(
255            ErrorKind::Unsupported,
256            "operation is not supported",
257        ))
258    }
259
260    fn copy(
261        &self,
262        _ctx: &OperationContext,
263        _from: &str,
264        _to: &str,
265        _args: OpCopy,
266    ) -> Result<Self::Copier> {
267        Err(Error::new(
268            ErrorKind::Unsupported,
269            "operation is not supported",
270        ))
271    }
272
273    async fn rename(
274        &self,
275        _ctx: &OperationContext,
276        _from: &str,
277        _to: &str,
278        _args: OpRename,
279    ) -> Result<RpRename> {
280        Err(Error::new(
281            ErrorKind::Unsupported,
282            "operation is not supported",
283        ))
284    }
285
286    async fn presign(
287        &self,
288        _ctx: &OperationContext,
289        path: &str,
290        args: OpPresign,
291    ) -> Result<RpPresign> {
292        if self.core.has_authorization() {
293            return Err(Error::new(
294                ErrorKind::Unsupported,
295                "Http doesn't support presigned request on backend with authorization",
296            ));
297        }
298
299        let req = match args.operation() {
300            PresignOperation::Stat(v) => self.core.http_head_request(path, v)?,
301            PresignOperation::Read(range, v) => self.core.http_get_request(path, *range, v)?,
302            _ => {
303                return Err(Error::new(
304                    ErrorKind::Unsupported,
305                    "Http doesn't support presigned write",
306                ));
307            }
308        };
309
310        let (parts, _) = req.into_parts();
311
312        Ok(RpPresign::new(PresignedRequest::new(
313            parts.method,
314            parts.uri,
315            parts.headers,
316        )))
317    }
318}