// Package tencentcos 封装腾讯云 COS 对象上传能力。 package tencentcos import ( "context" "fmt" "io" "net/http" "net/url" "strings" "time" cos "github.com/tencentyun/cos-go-sdk-v5" ) // Config 是 gateway 上传文件所需的 COS 最小配置。 type Config struct { SecretID string SecretKey string Bucket string Region string AccessURL string } // Uploader 负责把 gateway 收到的文件流写入 COS,并返回 CDN/自定义域名访问地址。 type Uploader struct { client *cos.Client accessURL string } // NewUploader 按 bucket + region 装配 COS SDK client。 func NewUploader(cfg Config, transport http.RoundTripper) (*Uploader, error) { cfg.SecretID = strings.TrimSpace(cfg.SecretID) cfg.SecretKey = strings.TrimSpace(cfg.SecretKey) cfg.Bucket = strings.TrimSpace(cfg.Bucket) cfg.Region = strings.TrimSpace(cfg.Region) cfg.AccessURL = strings.TrimRight(strings.TrimSpace(cfg.AccessURL), "/") if cfg.SecretID == "" || cfg.SecretKey == "" || cfg.Bucket == "" || cfg.Region == "" || cfg.AccessURL == "" { return nil, fmt.Errorf("tencent cos config is incomplete") } if _, err := url.ParseRequestURI(cfg.AccessURL); err != nil { return nil, fmt.Errorf("tencent cos access_url is invalid: %w", err) } bucketURL, err := url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com", cfg.Bucket, cfg.Region)) if err != nil { return nil, fmt.Errorf("tencent cos bucket url is invalid: %w", err) } if transport == nil { transport = http.DefaultTransport } client := cos.NewClient(&cos.BaseURL{BucketURL: bucketURL}, &http.Client{ Timeout: 30 * time.Second, Transport: &cos.AuthorizationTransport{ SecretID: cfg.SecretID, SecretKey: cfg.SecretKey, Transport: transport, }, }) return &Uploader{client: client, accessURL: cfg.AccessURL}, nil } // PutObject 上传对象并返回对外访问 URL。 func (u *Uploader) PutObject(ctx context.Context, key string, reader io.Reader, sizeBytes int64, contentType string) (string, error) { key = strings.TrimLeft(strings.TrimSpace(key), "/") if key == "" || reader == nil || sizeBytes <= 0 { return "", fmt.Errorf("object upload input is invalid") } options := &cos.ObjectPutOptions{ ObjectPutHeaderOptions: &cos.ObjectPutHeaderOptions{ ContentLength: sizeBytes, ContentType: strings.TrimSpace(contentType), }, } if _, err := u.client.Object.Put(ctx, key, reader, options); err != nil { return "", err } publicURL, err := url.JoinPath(u.accessURL, key) if err != nil { return "", fmt.Errorf("build public object url: %w", err) } return publicURL, nil }