package storage import ( "context" "fmt" "io" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/s3" ) // R2Store 走 Cloudflare R2 的 S3 兼容 API。endpoint 是账户级端点 // (https://.r2.cloudflarestorage.com),R2 两种寻址都支持, // 这里用 path-style 最稳。 // // 端点 URL 里的路径段(如果有)不交给 SDK —— path-style 下它会被折进 // 对象 key,导致「数据库 key」和「实际对象 key」对不上(直链 404)。 // 这里只取 scheme://host 当端点;路径段由 EndpointKeyPrefix 暴露给 // 上传 handler 拼进 key,三处(存储 / 数据库 / 直链)从此一致。 type R2Store struct { client *s3.Client bucket string } func NewR2(endpoint, bucket, accessKey, secretKey string) (*R2Store, error) { if endpoint == "" || bucket == "" { return nil, fmt.Errorf("r2: endpoint/bucket required") } // R2 不认 region,SDK 又必须有——官方文档给的值就是 "auto"。 cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("auto"), config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKey, secretKey, "")), config.WithBaseEndpoint(StripEndpointPath(endpoint)), ) if err != nil { return nil, fmt.Errorf("r2: load aws config: %w", err) } client := s3.NewFromConfig(cfg, func(o *s3.Options) { o.UsePathStyle = true }) return &R2Store{client: client, bucket: bucket}, nil } func (s *R2Store) Put(ctx context.Context, key string, r io.Reader, size int64, contentType string) error { _, err := s.client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(key), Body: r, ContentLength: aws.Int64(size), ContentType: aws.String(contentType), }) return err } func (s *R2Store) Open(ctx context.Context, key string) (io.ReadCloser, int64, error) { out, err := s.client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(key), }) if err != nil { return nil, 0, err } return out.Body, *out.ContentLength, nil } func (s *R2Store) Delete(ctx context.Context, key string) error { _, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(key), }) return err }