122 lines
4.2 KiB
Go
122 lines
4.2 KiB
Go
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
|
||
}
|