Files
go-sip/internal/oss/aliyun.go
T

155 lines
4.8 KiB
Go

package oss
import (
"context"
"errors"
"fmt"
"net/url"
"strings"
"time"
agentv1 "git.ipao.vip/rogee/go-sip/gen/agent/v1"
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 <= 0 || c.GrantTTL > 7*24*time.Hour {
return errors.New("OSS grant TTL must be between 1 second and 7 days")
}
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) (*agentv1.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 {
maxBytes = c.config.MaxAssetBytes
}
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([]*agentv1.Header, 0, len(presigned.SignedHeaders))
for name, value := range presigned.SignedHeaders {
headers = append(headers, &agentv1.Header{Name: name, Value: value})
}
return &agentv1.UploadGrant{
UploadId: uploadID,
TargetUrl: presigned.URL,
Headers: headers,
ExpiresAtUnixMs: presigned.Expiration.UnixMilli(),
ObjectKey: objectKey,
RequiredChecksumSha256: checksum,
MaxBytes: maxBytes,
}, nil
}
func (c *Client) Verify(ctx context.Context, objectKey, checksum string, size int64) error {
if c == nil || c.client == nil {
return errors.New("OSS client is not configured")
}
result, err := c.client.HeadObject(ctx, &aliyunoss.HeadObjectRequest{
Bucket: aliyunoss.Ptr(c.config.Bucket),
Key: aliyunoss.Ptr(objectKey),
})
if err != nil {
return fmt.Errorf("head OSS object: %w", err)
}
if result.ContentLength != size {
return fmt.Errorf("OSS object size mismatch: expected %d got %d", size, result.ContentLength)
}
storedChecksum := ""
for key, value := range result.Metadata {
key = strings.ToLower(strings.TrimSpace(key))
key = strings.TrimPrefix(key, "x-oss-meta-")
if key == "sha256" {
storedChecksum = value
break
}
}
if !strings.EqualFold(strings.TrimSpace(checksum), strings.TrimSpace(storedChecksum)) {
return errors.New("OSS object checksum metadata mismatch")
}
return nil
}
func (c *Client) ObjectID(objectKey string) string {
return "oss://" + c.config.Bucket + "/" + strings.TrimPrefix(objectKey, "/")
}