diff --git a/s3/client_test.go b/s3/client_test.go index d15e75ab..de754cf7 100644 --- a/s3/client_test.go +++ b/s3/client_test.go @@ -22,6 +22,7 @@ func TestParseURL_Success(t *testing.T) { require.Equal(t, "us-east-1", cfg.Region) require.Equal(t, "AKID", cfg.AccessKey) require.Equal(t, "SECRET", cfg.SecretKey) + require.Equal(t, "https", cfg.Scheme) require.Equal(t, "s3.us-east-1.amazonaws.com", cfg.Endpoint) require.False(t, cfg.PathStyle) } @@ -38,6 +39,7 @@ func TestParseURL_WithEndpoint(t *testing.T) { require.Nil(t, err) require.Equal(t, "my-bucket", cfg.Bucket) require.Equal(t, "prefix", cfg.Prefix) + require.Equal(t, "https", cfg.Scheme) require.Equal(t, "s3.example.com", cfg.Endpoint) require.True(t, cfg.PathStyle) } @@ -45,6 +47,7 @@ func TestParseURL_WithEndpoint(t *testing.T) { func TestParseURL_EndpointHTTP(t *testing.T) { cfg, err := ParseURL("s3://AKID:SECRET@my-bucket?region=us-east-1&endpoint=http://localhost:9000") require.Nil(t, err) + require.Equal(t, "http", cfg.Scheme) require.Equal(t, "localhost:9000", cfg.Endpoint) require.True(t, cfg.PathStyle) } @@ -111,6 +114,11 @@ func TestConfig_BucketURL_PathStyle(t *testing.T) { require.Equal(t, "https://s3.example.com/my-bucket", c.BucketURL()) } +func TestConfig_BucketURL_PathStyle_EndpointHTTP(t *testing.T) { + c := &Config{Scheme: "http", Endpoint: "localhost:9000", Bucket: "b", PathStyle: true} + require.Equal(t, "http://localhost:9000/b", c.BucketURL()) +} + func TestConfig_BucketURL_VirtualHosted(t *testing.T) { c := &Config{Endpoint: "s3.us-east-1.amazonaws.com", Bucket: "my-bucket", PathStyle: false} require.Equal(t, "https://my-bucket.s3.us-east-1.amazonaws.com", c.BucketURL()) diff --git a/s3/types.go b/s3/types.go index 094a96d3..677721b6 100644 --- a/s3/types.go +++ b/s3/types.go @@ -11,6 +11,7 @@ import ( // Config holds the parsed fields from an S3 URL. Use ParseURL to create one from a URL string. type Config struct { + Scheme string // URL scheme, e.g. "https" or "http" Endpoint string // host[:port] only, e.g. "s3.us-east-1.amazonaws.com" PathStyle bool Bucket string @@ -24,10 +25,14 @@ type Config struct { // BucketURL returns the base URL for bucket-level operations. func (c *Config) BucketURL() string { - if c.PathStyle { - return fmt.Sprintf("https://%s/%s", c.Endpoint, c.Bucket) + scheme := "https" + if c.Scheme != "" { + scheme = c.Scheme } - return fmt.Sprintf("https://%s.%s", c.Bucket, c.Endpoint) + if c.PathStyle { + return fmt.Sprintf("%s://%s/%s", scheme, c.Endpoint, c.Bucket) + } + return fmt.Sprintf("%s://%s.%s", scheme, c.Bucket, c.Endpoint) } // HostHeader returns the value for the Host header. diff --git a/s3/util.go b/s3/util.go index 64f0f7c7..25dbe09f 100644 --- a/s3/util.go +++ b/s3/util.go @@ -70,21 +70,29 @@ func ParseURL(s3URL string) (*Config, error) { return nil, fmt.Errorf("s3: region query parameter is required") } endpointParam := u.Query().Get("endpoint") + var scheme string var endpoint string var pathStyle bool if endpointParam != "" { // Custom endpoint: strip scheme prefix to extract host[:port] + uep, err := url.Parse(endpointParam) + if err != nil { + return nil, fmt.Errorf("s3: invalid endpoint URL: %w", err) + } + scheme = uep.Scheme ep := strings.TrimRight(endpointParam, "/") ep = strings.TrimPrefix(ep, "https://") ep = strings.TrimPrefix(ep, "http://") endpoint = ep pathStyle = true } else { + scheme = "https" endpoint = fmt.Sprintf("s3.%s.amazonaws.com", region) pathStyle = false } disableHTTP2, _ := strconv.ParseBool(u.Query().Get("disable_http2")) return &Config{ + Scheme: scheme, Endpoint: endpoint, PathStyle: pathStyle, Bucket: bucket,