diff --git a/apps/api-axum/src/storage.rs b/apps/api-axum/src/storage.rs index 7e5cb38..c099f2f 100644 --- a/apps/api-axum/src/storage.rs +++ b/apps/api-axum/src/storage.rs @@ -34,7 +34,12 @@ impl StorageClient { loader = loader.endpoint_url(endpoint); } let conf = loader.load().await; - let s3 = Client::new(&conf); + // LocalStack/S3 dev servers only resolve the bucket via path; R2 uses virtual-host style. + let force_path_style = std::env::var("R2_FORCE_PATH_STYLE").as_deref() == Ok("true"); + let s3_conf = aws_sdk_s3::config::Builder::from(&conf) + .force_path_style(force_path_style) + .build(); + let s3 = Client::from_conf(s3_conf); Self { s3, bucket: bucket.into(), diff --git a/apps/api-effect/.env.example b/apps/api-effect/.env.example index 969e435..6b86601 100644 --- a/apps/api-effect/.env.example +++ b/apps/api-effect/.env.example @@ -45,6 +45,8 @@ R2_SECRET_ACCESS_KEY=your_secret_key R2_BUCKET_NAME=webdevstudios-storage R2_PUBLIC_URL=https://pub-xxxxx.r2.dev R2_ENDPOINT=https://xxxxx.r2.cloudflarestorage.com +# LocalStack dev: R2_ENDPOINT=http://127.0.0.1:4566, R2_FORCE_PATH_STYLE=true, any dummy keys +R2_FORCE_PATH_STYLE=false # Redis REDIS_HOST="localhost" diff --git a/apps/api-effect/src/lib/storage.ts b/apps/api-effect/src/lib/storage.ts index 2e64c5e..e1b446f 100644 --- a/apps/api-effect/src/lib/storage.ts +++ b/apps/api-effect/src/lib/storage.ts @@ -33,6 +33,8 @@ function getClient(): ClientOptions | null { client: new S3Client({ region: 'auto', endpoint: process.env.R2_ENDPOINT, + // LocalStack/S3 dev servers only resolve the bucket via path; R2 uses virtual-host style. + forcePathStyle: process.env.R2_FORCE_PATH_STYLE === 'true', credentials: { accessKeyId: key, secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? '', diff --git a/apps/api-elysia/.env.example b/apps/api-elysia/.env.example index 5a84dbd..6e80898 100644 --- a/apps/api-elysia/.env.example +++ b/apps/api-elysia/.env.example @@ -45,6 +45,8 @@ R2_SECRET_ACCESS_KEY=your_secret_key R2_BUCKET_NAME=webdevstudios-storage R2_PUBLIC_URL=https://pub-xxxxx.r2.dev R2_ENDPOINT=https://xxxxx.r2.cloudflarestorage.com +# LocalStack dev: R2_ENDPOINT=http://127.0.0.1:4566, R2_FORCE_PATH_STYLE=true, any dummy keys +R2_FORCE_PATH_STYLE=false # Redis REDIS_HOST="localhost" diff --git a/apps/api-elysia/src/lib/storage.ts b/apps/api-elysia/src/lib/storage.ts index 2e64c5e..e1b446f 100644 --- a/apps/api-elysia/src/lib/storage.ts +++ b/apps/api-elysia/src/lib/storage.ts @@ -33,6 +33,8 @@ function getClient(): ClientOptions | null { client: new S3Client({ region: 'auto', endpoint: process.env.R2_ENDPOINT, + // LocalStack/S3 dev servers only resolve the bucket via path; R2 uses virtual-host style. + forcePathStyle: process.env.R2_FORCE_PATH_STYLE === 'true', credentials: { accessKeyId: key, secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? '', diff --git a/apps/api-go/internal/storage/storage.go b/apps/api-go/internal/storage/storage.go index 5844613..4c3dbcd 100644 --- a/apps/api-go/internal/storage/storage.go +++ b/apps/api-go/internal/storage/storage.go @@ -48,7 +48,8 @@ func New() *Service { endpoint := os.Getenv("R2_ENDPOINT") client := s3.NewFromConfig(cfg, func(o *s3.Options) { o.BaseEndpoint = &endpoint - o.UsePathStyle = false + // LocalStack/S3 dev servers only resolve the bucket via path; R2 uses virtual-host style. + o.UsePathStyle = os.Getenv("R2_FORCE_PATH_STYLE") == "true" }) return &Service{client: client, bucket: os.Getenv("R2_BUCKET_NAME"), publicURL: strings.TrimSuffix(os.Getenv("R2_PUBLIC_URL"), "/")} }