Files
silk/server-go/internal/service/s3.go
T

166 lines
4.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/url"
"os"
"strings"
"time"
"silk-server-go/internal/config"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
)
// S3Service Ceph S3 服务,提供 presigned URL、对象流、上传等能力
type S3Service struct {
client *s3.Client
endpoint string
region string
accessKey string
secretKey string
}
// NewS3Service 根据配置创建 S3 服务(forcePathStyle: true,兼容 Ceph
func NewS3Service(cfg *config.Config) *S3Service {
endpoint := strings.TrimSuffix(cfg.S3Endpoint, "/")
client := s3.New(s3.Options{
BaseEndpoint: aws.String(endpoint),
Region: cfg.S3Region,
Credentials: credentials.NewStaticCredentialsProvider(cfg.S3AccessKey, cfg.S3SecretKey, ""),
UsePathStyle: true,
})
return &S3Service{
client: client,
endpoint: endpoint,
region: cfg.S3Region,
accessKey: cfg.S3AccessKey,
secretKey: cfg.S3SecretKey,
}
}
// GetPresignedURL 手动实现 V4 签名 presigned GET URL1小时有效),与 NestJS s3.service.ts 一致
func (s *S3Service) GetPresignedURL(bucket, key string) (string, error) {
expiresIn := 3600
now := time.Now().UTC()
dateStamp := now.Format("20060102")
amzDate := now.Format("20060102T150405Z")
// 从 endpoint 提取 host(去掉 http:// 或 https:// 前缀)
host := strings.TrimPrefix(strings.TrimPrefix(s.endpoint, "https://"), "http://")
path := "/" + bucket + "/" + key
// 构造规范化查询字符串(按字母序排列)
params := url.Values{}
params.Set("X-Amz-Algorithm", "AWS4-HMAC-SHA256")
params.Set("X-Amz-Credential", s.accessKey+"/"+dateStamp+"/"+s.region+"/s3/aws4_request")
params.Set("X-Amz-Date", amzDate)
params.Set("X-Amz-Expires", fmt.Sprintf("%d", expiresIn))
params.Set("X-Amz-SignedHeaders", "host")
canonQuery := params.Encode()
// 规范化请求
canonHeaders := "host:" + host + "\n"
canonRequest := "GET\n" + path + "\n" + canonQuery + "\n" + canonHeaders + "\nhost\nUNSIGNED-PAYLOAD"
// 待签名字符串
scope := dateStamp + "/" + s.region + "/s3/aws4_request"
stringToSign := "AWS4-HMAC-SHA256\n" + amzDate + "\n" + scope + "\n" + sha256Hex(canonRequest)
// 签名密钥派生链:AWS4{secret} -> dateStamp -> region -> s3 -> aws4_request
kDate := hmacSHA256([]byte("AWS4"+s.secretKey), dateStamp)
kRegion := hmacSHA256(kDate, s.region)
kService := hmacSHA256(kRegion, "s3")
kSigning := hmacSHA256(kService, "aws4_request")
signature := hex.EncodeToString(hmacSHA256(kSigning, stringToSign))
return s.endpoint + path + "?" + canonQuery + "&X-Amz-Signature=" + signature, nil
}
// GetObjectStream 获取对象流(支持 Range 请求),用于视频流代理
func (s *S3Service) GetObjectStream(bucket, key string, rangeHeader string) (*s3.GetObjectOutput, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
input := &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
}
if rangeHeader != "" {
input.Range = aws.String(rangeHeader)
}
return s.client.GetObject(ctx, input)
}
// ListObjects 列举桶内对象(可按前缀过滤)
func (s *S3Service) ListObjects(bucket, prefix string) ([]types.Object, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
input := &s3.ListObjectsV2Input{
Bucket: aws.String(bucket),
MaxKeys: aws.Int32(100),
}
if prefix != "" {
input.Prefix = aws.String(prefix)
}
result, err := s.client.ListObjectsV2(ctx, input)
if err != nil {
return nil, err
}
return result.Contents, nil
}
// ObjectExists 检查对象是否存在
func (s *S3Service) ObjectExists(bucket, key string) bool {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_, err := s.client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
})
return err == nil
}
// UploadFile 上传本地文件到 S3
func (s *S3Service) UploadFile(bucket, key, filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
_, err = s.client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: file,
ContentType: aws.String("video/mp4"),
})
return err
}
// sha256Hex 计算 SHA256 十六进制摘要
func sha256Hex(data string) string {
h := sha256.Sum256([]byte(data))
return hex.EncodeToString(h[:])
}
// hmacSHA256 计算 HMAC-SHA256
func hmacSHA256(key []byte, data string) []byte {
h := hmac.New(sha256.New, key)
h.Write([]byte(data))
return h.Sum(nil)
}