package oss import ( "context" "errors" "fmt" "net/url" "strings" "time" agentpb "git.ipao.vip/rogee/go-sip/gen/agent" aliyunoss "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss" "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials" ) const defaultGrantTTL = 15 * time.Minute // Config is Dispatcher-owned OSS configuration. Access keys never leave the // Dispatcher; Agents receive only presigned upload grants. type Config struct { Endpoint string Region string Bucket string AccessKeyID string AccessKeySecret string KeyPrefix string GrantTTL time.Duration MaxAssetBytes int64 } func (c Config) Validate() error { if strings.TrimSpace(c.Endpoint) == "" || strings.TrimSpace(c.Region) == "" || strings.TrimSpace(c.Bucket) == "" { return errors.New("OSS endpoint, region and bucket are required") } if strings.TrimSpace(c.AccessKeyID) == "" || strings.TrimSpace(c.AccessKeySecret) == "" { return errors.New("OSS access key ID and secret are required") } endpoint := c.Endpoint if !strings.Contains(endpoint, "://") { endpoint = "https://" + endpoint } parsed, err := url.Parse(endpoint) if err != nil || parsed.Host == "" || parsed.Scheme != "https" { return errors.New("OSS endpoint must be a valid HTTPS URL") } if c.GrantTTL != defaultGrantTTL { return errors.New("OSS grant TTL must be exactly 15 minutes") } if c.MaxAssetBytes <= 0 { return errors.New("OSS max asset bytes must be positive") } return nil } // Client signs and verifies object operations using Alibaba's official OSS // SDK. It is intentionally only constructed in the Dispatcher process. type Client struct { config Config client *aliyunoss.Client } func NewClient(config Config) (*Client, error) { if config.GrantTTL == 0 { config.GrantTTL = defaultGrantTTL } if err := config.Validate(); err != nil { return nil, err } endpoint := config.Endpoint if !strings.Contains(endpoint, "://") { endpoint = "https://" + endpoint } sdkConfig := aliyunoss.LoadDefaultConfig(). WithCredentialsProvider(credentials.NewStaticCredentialsProvider(config.AccessKeyID, config.AccessKeySecret)). WithRegion(config.Region). WithEndpoint(endpoint). WithSignatureVersion(aliyunoss.SignatureVersionV4) return &Client{config: config, client: aliyunoss.NewClient(sdkConfig)}, nil } func (c *Client) Config() Config { return c.config } func (c *Client) Grant(ctx context.Context, uploadID, objectKey, checksum string, maxBytes int64, now time.Time) (*agentpb.UploadGrant, error) { if c == nil || c.client == nil { return nil, errors.New("OSS client is not configured") } if uploadID == "" || objectKey == "" || checksum == "" { return nil, errors.New("upload ID, object key and checksum are required") } if maxBytes <= 0 || maxBytes > c.config.MaxAssetBytes { return nil, errors.New("requested upload size must be positive and within the configured limit") } if now.IsZero() { now = time.Now() } expiresAt := now.Add(c.config.GrantTTL) request := &aliyunoss.PutObjectRequest{ Bucket: aliyunoss.Ptr(c.config.Bucket), Key: aliyunoss.Ptr(objectKey), Metadata: map[string]string{ "sha256": checksum, }, } presigned, err := c.client.Presign(ctx, request, aliyunoss.PresignExpiration(expiresAt)) if err != nil { return nil, fmt.Errorf("presign OSS PUT: %w", err) } headers := make([]*agentpb.Header, 0, len(presigned.SignedHeaders)) for name, value := range presigned.SignedHeaders { headers = append(headers, &agentpb.Header{Name: name, Value: value}) } return &agentpb.UploadGrant{ UploadId: uploadID, TargetUrl: presigned.URL, Headers: headers, ExpiresAtUnixMs: presigned.Expiration.UnixMilli(), ObjectKey: objectKey, Bucket: c.config.Bucket, RequiredChecksumSha256: checksum, MaxBytes: maxBytes, }, nil }