sccache/cache/
s3.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5//     http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12
13use opendal::layers::LoggingLayer;
14use opendal::services::S3;
15use opendal::Operator;
16
17use crate::errors::*;
18
19use super::http_client::set_user_agent;
20
21pub struct S3Cache;
22
23impl S3Cache {
24    pub fn build(
25        bucket: &str,
26        region: Option<&str>,
27        key_prefix: &str,
28        no_credentials: bool,
29        endpoint: Option<&str>,
30        use_ssl: Option<bool>,
31        server_side_encryption: Option<bool>,
32    ) -> Result<Operator> {
33        let mut builder = S3::default()
34            .http_client(set_user_agent())
35            .bucket(bucket)
36            .root(key_prefix);
37
38        if let Some(region) = region {
39            builder = builder.region(region);
40        }
41
42        if no_credentials {
43            builder = builder
44                .disable_config_load()
45                // Disable EC2 metadata to avoid OpenDAL trying to load
46                // credentials from EC2 metadata.
47                //
48                // A.k.a, don't try to visit `http://169.254.169.254`
49                .disable_ec2_metadata()
50                // Allow anonymous access to S3 so that OpenDAL will not
51                // throw error when no credentials are provided.
52                .allow_anonymous();
53        }
54
55        if let Some(endpoint) = endpoint {
56            builder = builder.endpoint(&endpoint_resolver(endpoint, use_ssl)?);
57        }
58
59        if server_side_encryption.unwrap_or_default() {
60            builder = builder.server_side_encryption_with_s3_key();
61        }
62
63        let op = Operator::new(builder)?
64            .layer(LoggingLayer::default())
65            .finish();
66        Ok(op)
67    }
68}
69
70/// Resolve given endpoint along with use_ssl settings.
71fn endpoint_resolver(endpoint: &str, use_ssl: Option<bool>) -> Result<String> {
72    let endpoint_uri: http::Uri = endpoint
73        .try_into()
74        .map_err(|err| anyhow!("input endpoint {endpoint} is invalid: {:?}", err))?;
75    let mut parts = endpoint_uri.into_parts();
76    match use_ssl {
77        Some(true) => {
78            parts.scheme = Some(http::uri::Scheme::HTTPS);
79        }
80        Some(false) => {
81            parts.scheme = Some(http::uri::Scheme::HTTP);
82        }
83        None => {
84            if parts.scheme.is_none() {
85                parts.scheme = Some(http::uri::Scheme::HTTP);
86            }
87        }
88    }
89    // path_and_query is required when scheme is set
90    if parts.path_and_query.is_none() {
91        parts.path_and_query = Some(http::uri::PathAndQuery::from_static("/"));
92    }
93
94    Ok(http::Uri::from_parts(parts)?.to_string())
95}
96
97#[cfg(test)]
98mod test {
99    use super::*;
100
101    #[test]
102    fn test_endpoint_resolver() -> Result<()> {
103        let cases = vec![
104            (
105                "no scheme without use_ssl",
106                "s3-us-east-1.amazonaws.com",
107                None,
108                "http://s3-us-east-1.amazonaws.com/",
109            ),
110            (
111                "http without use_ssl",
112                "http://s3-us-east-1.amazonaws.com",
113                None,
114                "http://s3-us-east-1.amazonaws.com/",
115            ),
116            (
117                "https without use_ssl",
118                "https://s3-us-east-1.amazonaws.com",
119                None,
120                "https://s3-us-east-1.amazonaws.com/",
121            ),
122            (
123                "no scheme with use_ssl",
124                "s3-us-east-1.amazonaws.com",
125                Some(true),
126                "https://s3-us-east-1.amazonaws.com/",
127            ),
128            (
129                "http with use_ssl",
130                "http://s3-us-east-1.amazonaws.com",
131                Some(true),
132                "https://s3-us-east-1.amazonaws.com/",
133            ),
134            (
135                "https with use_ssl",
136                "https://s3-us-east-1.amazonaws.com",
137                Some(true),
138                "https://s3-us-east-1.amazonaws.com/",
139            ),
140            (
141                "no scheme with not use_ssl",
142                "s3-us-east-1.amazonaws.com",
143                Some(false),
144                "http://s3-us-east-1.amazonaws.com/",
145            ),
146            (
147                "http with not use_ssl",
148                "http://s3-us-east-1.amazonaws.com",
149                Some(false),
150                "http://s3-us-east-1.amazonaws.com/",
151            ),
152            (
153                "https with not use_ssl",
154                "https://s3-us-east-1.amazonaws.com",
155                Some(false),
156                "http://s3-us-east-1.amazonaws.com/",
157            ),
158        ];
159
160        for (name, endpoint, use_ssl, expected) in cases {
161            let actual = endpoint_resolver(endpoint, use_ssl)?;
162            assert_eq!(actual, expected, "{}", name);
163        }
164
165        Ok(())
166    }
167}