Skip to content

Commit b126bfb

Browse files
authored
Support minio (#288)
S3 has two endpoints, a “path-based” endpoint and “virtual-hosted-style” endpoints. ([read more](https://medium.com/@e.osama85/the-difference-between-the-amazon-s3-path-style-urls-and-virtual-hosted-style-urls-4fafd5eca4db)) Virtual hosted style uses a different hostname for each bucket, while path-style uses one endpoint and puts the bucket name in the path. Path-style is easier to set up when using an S3-compatible API like minio, because you don't have to deal with DNS certs etc. Previously, we only used path-style if the URL was set to `localhost`, which was basically only useful for our tests that ran `minio` locally. This change allows the use of path-style URLs by setting the `AWS_S3_USE_PATH_STYLE` env variable. It also supports the old `localhost` behavior, but warns and suggests using `AWS_S3_USE_PATH_STYLE`. ## `bucket_prefix` bug fix This also fixes a bug where `Some("")` was being used as a `bucket_prefix`. When a bucket prefix is a `Some`, we prepend it to the path separated by "/", as in "{bucket_prefix}/{path}". With an empty `bucket_prefix`, the resulting path is an "absolute-path" (since it begins with a slash) instead of a "relative path" as the rest of the code expects. In particular, `Url::join` behaves very differently when passed an "absolute path" than a relative one: with an absolute path, the new path _replaces_ the whole other old one. So what was happening was that when we had a `bucket_prefix` of `/`, if the bucket name was in the path (as in path-style URLs), the bucket name was inadvertently removed from the path. The fix is as simple as ensuring that `Some("")` is converted to `None`, as in: ```rust let bucket_prefix = (!bucket_prefix.is_empty()).then(|| bucket_prefix); ```
1 parent e7c55a7 commit b126bfb

File tree

4 files changed

+42
-11
lines changed

4 files changed

+42
-11
lines changed

crates/y-sweet-core/src/store/s3.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ pub struct S3Config {
1818
pub bucket: String,
1919
pub region: String,
2020
pub bucket_prefix: Option<String>,
21+
22+
// Use old path-style URLs, needed to support some S3-compatible APIs (including some minio setups)
23+
pub path_style: bool,
2124
}
2225

2326
const PRESIGNED_URL_DURATION: Duration = Duration::from_secs(60 * 60);
@@ -38,14 +41,18 @@ impl S3Store {
3841
Credentials::new(config.key, config.secret)
3942
};
4043
let endpoint: Url = config.endpoint.parse().expect("endpoint is a valid url");
41-
let path_style =
42-
// if endpoint is localhost then bucket url must be of form http://localhost:<port>/<bucket>
43-
// instead of <method>:://<bucket>.<endpoint>
44-
if endpoint.host_str().expect("endpoint Url should have host") == "localhost" {
45-
rusty_s3::UrlStyle::Path
46-
} else {
47-
rusty_s3::UrlStyle::VirtualHost
48-
};
44+
45+
let path_style = if config.path_style {
46+
rusty_s3::UrlStyle::Path
47+
} else if endpoint.host_str() == Some("localhost") {
48+
// Since this was the old behavior before we added AWS_S3_USE_PATH_STYLE,
49+
// we continue to support it, but complain a bit.
50+
tracing::warn!("Inferring path-style URLs for localhost for backwards-compatibility. This behavior may change in the future. Set AWS_S3_USE_PATH_STYLE=true to ensure that path-style URLs are used.");
51+
rusty_s3::UrlStyle::Path
52+
} else {
53+
rusty_s3::UrlStyle::VirtualHost
54+
};
55+
4956
let bucket = Bucket::new(endpoint, path_style, config.bucket, config.region)
5057
.expect("Url has a valid scheme and host");
5158
let client = Client::new();

crates/y-sweet-worker/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/y-sweet-worker/src/config.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ fn parse_s3_config(env: &Env) -> anyhow::Result<S3Config> {
8888
.map_err(|_| anyhow::anyhow!("S3_BUCKET_NAME env var not supplied"))?
8989
.to_string(),
9090
bucket_prefix: env.var(S3_BUCKET_PREFIX).ok().map(|t| t.to_string()),
91+
path_style: false,
9192
})
9293
}
9394

crates/y-sweet/src/main.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,26 @@ const S3_SECRET_ACCESS_KEY: &str = "AWS_SECRET_ACCESS_KEY";
8989
const S3_SESSION_TOKEN: &str = "AWS_SESSION_TOKEN";
9090
const S3_REGION: &str = "AWS_REGION";
9191
const S3_ENDPOINT: &str = "AWS_ENDPOINT_URL_S3";
92-
fn parse_s3_config_from_env_and_args(bucket: String, prefix: String) -> anyhow::Result<S3Config> {
92+
const S3_USE_PATH_STYLE: &str = "AWS_S3_USE_PATH_STYLE";
93+
fn parse_s3_config_from_env_and_args(
94+
bucket: String,
95+
prefix: Option<String>,
96+
) -> anyhow::Result<S3Config> {
97+
let use_path_style = env::var(S3_USE_PATH_STYLE).ok();
98+
let path_style = if let Some(use_path_style) = use_path_style {
99+
if use_path_style.to_lowercase() == "true" {
100+
true
101+
} else if use_path_style.to_lowercase() == "false" || use_path_style.is_empty() {
102+
false
103+
} else {
104+
anyhow::bail!(
105+
"If AWS_S3_USE_PATH_STYLE is set, it must be either \"true\" or \"false\""
106+
)
107+
}
108+
} else {
109+
false
110+
};
111+
93112
Ok(S3Config {
94113
key: env::var(S3_ACCESS_KEY_ID)
95114
.map_err(|_| anyhow::anyhow!("{} env var not supplied", S3_ACCESS_KEY_ID))?,
@@ -104,7 +123,9 @@ fn parse_s3_config_from_env_and_args(bucket: String, prefix: String) -> anyhow::
104123
.map_err(|_| anyhow::anyhow!("{} env var not supplied", S3_SECRET_ACCESS_KEY))?,
105124
token: env::var(S3_SESSION_TOKEN).ok(),
106125
bucket,
107-
bucket_prefix: Some(prefix),
126+
bucket_prefix: prefix,
127+
// If the endpoint is overridden, we assume that the user wants path-style URLs.
128+
path_style,
108129
})
109130
}
110131

@@ -116,6 +137,7 @@ fn get_store_from_opts(store_path: &str) -> Result<Box<dyn Store>> {
116137
.ok_or_else(|| anyhow::anyhow!("Invalid S3 URL"))?
117138
.to_owned();
118139
let bucket_prefix = url.path().trim_start_matches('/').to_owned();
140+
let bucket_prefix = (!bucket_prefix.is_empty()).then(|| bucket_prefix); // "" => None
119141
let config = parse_s3_config_from_env_and_args(bucket, bucket_prefix)?;
120142
let store = S3Store::new(config);
121143
Ok(Box::new(store))
@@ -239,6 +261,7 @@ async fn main() -> Result<()> {
239261

240262
let store = match (bucket, prefix) {
241263
(Some(bucket), Some(prefix)) => {
264+
let prefix = (!prefix.is_empty()).then(|| prefix);
242265
let s3_config = parse_s3_config_from_env_and_args(bucket, prefix)
243266
.context("Failed to parse S3 configuration")?;
244267
let store = S3Store::new(s3_config);

0 commit comments

Comments
 (0)