Files
backend_v2/internal/pkg/storage/s4.go
toom1996 580697f17a update
2026-09-13 14:16:37 +08:00

122 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}