This commit is contained in:
toom1996
2026-09-13 14:16:37 +08:00
parent b4a0c21ce1
commit 580697f17a
2 changed files with 274 additions and 0 deletions

121
internal/pkg/storage/s4.go Normal file
View File

@ -0,0 +1,121 @@
package storage
import (
"bytes"
"context"
"fmt"
"strings"
"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "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"
)
// s4DefaultEndpoint 缤纷云 S4 的 S3 兼容接入点(官方文档确认 https://s3.bitiful.net)。
// 采用虚拟主机风格寻址,请求落在 https://<bucket>.s3.bitiful.net/<key>。
const s4DefaultEndpoint = "https://s3.bitiful.net"
// S4Uploader 缤纷云 S4 对象存储上传器(兼容 S3 协议,基于 aws-sdk-go-v2)。
//
// 图片经 CoreIX 公开样式(如 key!style:high)对外提供,VIP 与免费用户同质量、匿名可访问。
// 库里只存对象 key(如 runway/abc.jpg),完整 URL 由 imgurl.Composer 拼装。
// 本类型同时实现 storage.Uploader 与 storage.Deleter。
type S4Uploader struct {
client *s3.Client
bucket string
baseURL string
enabled bool
}
// NewS4Uploader 构造缤纷云 S4 上传器。
// - ak/sk:缤纷云子账户 AccessKey/SecretKey(须有 PutObject/DeleteObject/GetObject 权限)。
// - bucket:存储桶名(须为「公开桶」,图片经 CoreIX 公开样式对外提供)。
// - endpoint:S3 接入点,默认 https://s3.bitiful.net;虚拟主机风格寻址。
// - baseURL:对外访问域名(GET 图片用)。为空时自动推导为 https://<bucket>.s3.bitiful.net,
// 须与该 S4 接入点的虚拟主机域名一致,保证展示图 URL 同源。
// - region:任意值(如 cn-east-1),缤纷云不校验。
func NewS4Uploader(ak, sk, bucket, endpoint, baseURL, region string) *S4Uploader {
if ak == "" || sk == "" || bucket == "" {
return &S4Uploader{enabled: false}
}
if endpoint == "" {
endpoint = s4DefaultEndpoint
}
if region == "" {
region = "cn-east-1"
}
cfg, err := awsconfig.LoadDefaultConfig(context.Background(),
awsconfig.WithRegion(region),
awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(ak, sk, "")),
)
if err != nil {
return &S4Uploader{enabled: false}
}
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String(endpoint)
o.UsePathStyle = false // 虚拟主机风格:https://<bucket>.s3.bitiful.net/<key>
})
if baseURL == "" {
baseURL = deriveS4BaseURL(endpoint, bucket)
}
return &S4Uploader{
client: client,
bucket: bucket,
baseURL: strings.TrimRight(baseURL, "/"),
enabled: true,
}
}
// deriveS4BaseURL 由 endpoint + bucket 推导虚拟主机风格访问域名:
// endpoint=https://s3.bitiful.net, bucket=toom → https://toom.s3.bitiful.net
func deriveS4BaseURL(endpoint, bucket string) string {
endpoint = strings.TrimRight(endpoint, "/")
scheme := "https://"
host := endpoint
if strings.HasPrefix(host, "https://") {
host = host[len("https://"):]
} else if strings.HasPrefix(host, "http://") {
scheme = "http://"
host = host[len("http://"):]
}
return scheme + bucket + "." + host
}
// PublicBaseURL 返回对外访问域名(可能由 NewS4Uploader 自动推导)。
func (s *S4Uploader) PublicBaseURL() string { return s.baseURL }
// Enabled 凭证齐全才视为启用。
func (s *S4Uploader) Enabled() bool { return s.enabled }
// Upload 把 data 直传到缤纷云 bucket 的 key,返回 key(库里只存 key,baseURL 由 Composer 拼)。
func (s *S4Uploader) Upload(ctx context.Context, key string, data []byte, contentType string) (string, error) {
if !s.enabled {
return "", fmt.Errorf("s4 uploader not enabled")
}
input := &s3.PutObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
Body: bytes.NewReader(data),
ContentType: aws.String(contentType),
}
if _, err := s.client.PutObject(ctx, input); err != nil {
return "", fmt.Errorf("s4 put object failed: %w", err)
}
return key, nil
}
// Delete 删除缤纷云 bucket 中的对象(按 key)。key 可能带 /uploads/ 等前缀,先剥离。
func (s *S4Uploader) Delete(ctx context.Context, key string) error {
if !s.enabled {
return nil
}
key = strings.TrimPrefix(key, "/")
if _, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
}); err != nil {
return fmt.Errorf("s4 delete object failed: %w", err)
}
return nil
}