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://.s3.bitiful.net/。 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://.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://.s3.bitiful.net/ }) 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 }