object_store/aws/
resolve.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 crate::aws::STORE;
19use crate::{ClientOptions, Result};
20use snafu::{ensure, OptionExt, ResultExt, Snafu};
21
22/// A specialized `Error` for object store-related errors
23#[derive(Debug, Snafu)]
24#[allow(missing_docs)]
25enum Error {
26    #[snafu(display("Bucket '{}' not found", bucket))]
27    BucketNotFound { bucket: String },
28
29    #[snafu(display("Failed to resolve region for bucket '{}'", bucket))]
30    ResolveRegion {
31        bucket: String,
32        source: reqwest::Error,
33    },
34
35    #[snafu(display("Failed to parse the region for bucket '{}'", bucket))]
36    RegionParse { bucket: String },
37}
38
39impl From<Error> for crate::Error {
40    fn from(source: Error) -> Self {
41        Self::Generic {
42            store: STORE,
43            source: Box::new(source),
44        }
45    }
46}
47
48/// Get the bucket region using the [HeadBucket API]. This will fail if the bucket does not exist.
49///
50/// [HeadBucket API]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadBucket.html
51pub async fn resolve_bucket_region(bucket: &str, client_options: &ClientOptions) -> Result<String> {
52    use reqwest::StatusCode;
53
54    let endpoint = format!("https://{}.s3.amazonaws.com", bucket);
55
56    let client = client_options.client()?;
57
58    let response = client
59        .head(&endpoint)
60        .send()
61        .await
62        .context(ResolveRegionSnafu { bucket })?;
63
64    ensure!(
65        response.status() != StatusCode::NOT_FOUND,
66        BucketNotFoundSnafu { bucket }
67    );
68
69    let region = response
70        .headers()
71        .get("x-amz-bucket-region")
72        .and_then(|x| x.to_str().ok())
73        .context(RegionParseSnafu { bucket })?;
74
75    Ok(region.to_string())
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[tokio::test]
83    async fn test_private_bucket() {
84        let bucket = "bloxbender";
85
86        let region = resolve_bucket_region(bucket, &ClientOptions::new())
87            .await
88            .unwrap();
89
90        let expected = "us-west-2".to_string();
91
92        assert_eq!(region, expected);
93    }
94
95    #[tokio::test]
96    async fn test_bucket_does_not_exist() {
97        let bucket = "please-dont-exist";
98
99        let result = resolve_bucket_region(bucket, &ClientOptions::new()).await;
100
101        assert!(result.is_err());
102    }
103}