chore: 初始化仓库基线(AGENTS.md、git 规范、敏感文件排除)
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
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 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user