core/src/services/http/backend.rs (227 lines of code) (raw):

// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. use std::fmt::Debug; use std::fmt::Formatter; use std::sync::Arc; use http::Response; use http::StatusCode; use log::debug; use super::core::HttpCore; use super::error::parse_error; use crate::raw::*; use crate::services::HttpConfig; use crate::*; impl Configurator for HttpConfig { type Builder = HttpBuilder; #[allow(deprecated)] fn into_builder(self) -> Self::Builder { HttpBuilder { config: self, http_client: None, } } } /// HTTP Read-only service support like [Nginx](https://www.nginx.com/) and [Caddy](https://caddyserver.com/). #[doc = include_str!("docs.md")] #[derive(Default)] pub struct HttpBuilder { config: HttpConfig, #[deprecated(since = "0.53.0", note = "Use `Operator::update_http_client` instead")] http_client: Option<HttpClient>, } impl Debug for HttpBuilder { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let mut de = f.debug_struct("HttpBuilder"); de.field("config", &self.config).finish() } } impl HttpBuilder { /// Set endpoint for http backend. /// /// For example: `https://example.com` pub fn endpoint(mut self, endpoint: &str) -> Self { self.config.endpoint = if endpoint.is_empty() { None } else { Some(endpoint.to_string()) }; self } /// set username for http backend /// /// default: no username pub fn username(mut self, username: &str) -> Self { if !username.is_empty() { self.config.username = Some(username.to_owned()); } self } /// set password for http backend /// /// default: no password pub fn password(mut self, password: &str) -> Self { if !password.is_empty() { self.config.password = Some(password.to_owned()); } self } /// set bearer token for http backend /// /// default: no access token pub fn token(mut self, token: &str) -> Self { if !token.is_empty() { self.config.token = Some(token.to_string()); } self } /// Set root path of http backend. pub fn root(mut self, root: &str) -> Self { self.config.root = if root.is_empty() { None } else { Some(root.to_string()) }; self } /// Specify the http client that used by this service. /// /// # Notes /// /// This API is part of OpenDAL's Raw API. `HttpClient` could be changed /// during minor updates. #[deprecated(since = "0.53.0", note = "Use `Operator::update_http_client` instead")] #[allow(deprecated)] pub fn http_client(mut self, client: HttpClient) -> Self { self.http_client = Some(client); self } } impl Builder for HttpBuilder { const SCHEME: Scheme = Scheme::Http; type Config = HttpConfig; fn build(self) -> Result<impl Access> { debug!("backend build started: {:?}", &self); let endpoint = match &self.config.endpoint { Some(v) => v, None => { return Err(Error::new(ErrorKind::ConfigInvalid, "endpoint is empty") .with_context("service", Scheme::Http)) } }; let root = normalize_root(&self.config.root.unwrap_or_default()); debug!("backend use root {}", root); let mut auth = None; if let Some(username) = &self.config.username { auth = Some(format_authorization_by_basic( username, self.config.password.as_deref().unwrap_or_default(), )?); } if let Some(token) = &self.config.token { auth = Some(format_authorization_by_bearer(token)?) } let info = AccessorInfo::default(); info.set_scheme(Scheme::Http) .set_root(&root) .set_native_capability(Capability { stat: true, stat_with_if_match: true, stat_with_if_none_match: true, stat_has_cache_control: true, stat_has_content_length: true, stat_has_content_type: true, stat_has_content_encoding: true, stat_has_content_range: true, stat_has_etag: true, stat_has_content_md5: true, stat_has_last_modified: true, stat_has_content_disposition: true, read: true, read_with_if_match: true, read_with_if_none_match: true, presign: auth.is_none(), presign_read: auth.is_none(), presign_stat: auth.is_none(), shared: true, ..Default::default() }); // allow deprecated api here for compatibility #[allow(deprecated)] if let Some(client) = self.http_client { info.update_http_client(|_| client); } let accessor_info = Arc::new(info); let core = Arc::new(HttpCore { info: accessor_info, endpoint: endpoint.to_string(), root, authorization: auth, }); Ok(HttpBackend { core }) } } /// Backend is used to serve `Accessor` support for http. #[derive(Clone)] pub struct HttpBackend { core: Arc<HttpCore>, } impl Debug for HttpBackend { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("HttpBackend") .field("core", &self.core) .finish() } } impl Access for HttpBackend { type Reader = HttpBody; type Writer = (); type Lister = (); type Deleter = (); type BlockingReader = (); type BlockingWriter = (); type BlockingLister = (); type BlockingDeleter = (); fn info(&self) -> Arc<AccessorInfo> { self.core.info.clone() } async fn stat(&self, path: &str, args: OpStat) -> Result<RpStat> { // Stat root always returns a DIR. if path == "/" { return Ok(RpStat::new(Metadata::new(EntryMode::DIR))); } let resp = self.core.http_head(path, &args).await?; let status = resp.status(); match status { StatusCode::OK => parse_into_metadata(path, resp.headers()).map(RpStat::new), // HTTP Server like nginx could return FORBIDDEN if auto-index // is not enabled, we should ignore them. StatusCode::NOT_FOUND | StatusCode::FORBIDDEN if path.ends_with('/') => { Ok(RpStat::new(Metadata::new(EntryMode::DIR))) } _ => Err(parse_error(resp)), } } async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::Reader)> { let resp = self.core.http_get(path, args.range(), &args).await?; let status = resp.status(); match status { StatusCode::OK | StatusCode::PARTIAL_CONTENT => { Ok((RpRead::default(), resp.into_body())) } _ => { let (part, mut body) = resp.into_parts(); let buf = body.to_buffer().await?; Err(parse_error(Response::from_parts(part, buf))) } } } async fn presign(&self, path: &str, args: OpPresign) -> Result<RpPresign> { if self.core.has_authorization() { return Err(Error::new( ErrorKind::Unsupported, "Http doesn't support presigned request on backend with authorization", )); } let req = match args.operation() { PresignOperation::Stat(v) => self.core.http_head_request(path, v)?, PresignOperation::Read(v) => { self.core.http_get_request(path, BytesRange::default(), v)? } _ => { return Err(Error::new( ErrorKind::Unsupported, "Http doesn't support presigned write", )) } }; let (parts, _) = req.into_parts(); Ok(RpPresign::new(PresignedRequest::new( parts.method, parts.uri, parts.headers, ))) } }