194 lines
5.8 KiB
Go
194 lines
5.8 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"crypto/hmac"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"io"
|
||
"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 URL(1小时有效),与 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
|
||
}
|
||
|
||
// EnsureBucket 确保 bucket 存在,不存在则创建(幂等)
|
||
func (s *S3Service) EnsureBucket(bucket string) error {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||
defer cancel()
|
||
if _, err := s.client.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: aws.String(bucket)}); err == nil {
|
||
return nil
|
||
}
|
||
_, err := s.client.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: aws.String(bucket)})
|
||
return err
|
||
}
|
||
|
||
// UploadImage 上传图片对象(支持指定 Content-Type)
|
||
func (s *S3Service) UploadImage(bucket, key string, body io.Reader, contentType string) error {
|
||
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: body,
|
||
ContentType: aws.String(contentType),
|
||
})
|
||
return err
|
||
}
|
||
|
||
// Endpoint 返回 S3 endpoint(用于拼接对象公开 URL)
|
||
func (s *S3Service) Endpoint() string { return s.endpoint }
|
||
|
||
// 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)
|
||
}
|