155 lines
4.8 KiB
Go
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, "/")
|
|
}
|