backup_s3_store.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. package repository
  2. import (
  3. "bytes"
  4. "context"
  5. "fmt"
  6. "io"
  7. "os"
  8. "path"
  9. "time"
  10. "github.com/aws/aws-sdk-go-v2/service/s3"
  11. "github.com/Wei-Shaw/sub2api/internal/pkg/servertiming"
  12. "github.com/Wei-Shaw/sub2api/internal/service"
  13. )
  14. // S3BackupStore implements service.BackupObjectStore using AWS S3 compatible storage
  15. type S3BackupStore struct {
  16. client *s3.Client
  17. bucket string
  18. }
  19. // NewS3BackupStoreFactory returns a BackupObjectStoreFactory that creates S3-backed stores
  20. func NewS3BackupStoreFactory() service.BackupObjectStoreFactory {
  21. return func(ctx context.Context, cfg *service.BackupS3Config) (service.BackupObjectStore, error) {
  22. client, err := newS3Client(ctx, s3ClientParams{
  23. Endpoint: cfg.Endpoint,
  24. Region: cfg.Region,
  25. AccessKeyID: cfg.AccessKeyID,
  26. SecretAccessKey: cfg.SecretAccessKey,
  27. ForcePathStyle: cfg.ForcePathStyle,
  28. })
  29. if err != nil {
  30. return nil, err
  31. }
  32. return &S3BackupStore{client: client, bucket: cfg.Bucket}, nil
  33. }
  34. }
  35. func (s *S3BackupStore) Upload(ctx context.Context, key string, body io.Reader, contentType string) (int64, error) {
  36. // 读取全部内容以获取大小(S3 PutObject 需要知道内容长度)
  37. // 注意:阿里云 OSS 不兼容 s3manager 分片上传的签名方式,因此使用 PutObject
  38. data, err := io.ReadAll(body)
  39. if err != nil {
  40. return 0, fmt.Errorf("read body: %w", err)
  41. }
  42. finish := servertiming.ObserveDependency(ctx, "s3")
  43. _, err = s.client.PutObject(ctx, &s3.PutObjectInput{
  44. Bucket: &s.bucket,
  45. Key: &key,
  46. Body: bytes.NewReader(data),
  47. ContentType: &contentType,
  48. })
  49. finish()
  50. if err != nil {
  51. return 0, fmt.Errorf("S3 PutObject: %w", err)
  52. }
  53. return int64(len(data)), nil
  54. }
  55. func (s *S3BackupStore) UploadFile(ctx context.Context, key string, filePath string, contentType string) (int64, error) {
  56. file, err := os.Open(filePath)
  57. if err != nil {
  58. return 0, fmt.Errorf("open upload file: %w", err)
  59. }
  60. defer func() { _ = file.Close() }()
  61. info, err := file.Stat()
  62. if err != nil {
  63. return 0, fmt.Errorf("stat upload file: %w", err)
  64. }
  65. sizeBytes := info.Size()
  66. finish := servertiming.ObserveDependency(ctx, "s3")
  67. _, err = s.client.PutObject(ctx, &s3.PutObjectInput{
  68. Bucket: &s.bucket,
  69. Key: &key,
  70. Body: file,
  71. ContentLength: &sizeBytes,
  72. ContentType: &contentType,
  73. })
  74. finish()
  75. if err != nil {
  76. return 0, fmt.Errorf("S3 PutObject file: %w", err)
  77. }
  78. return sizeBytes, nil
  79. }
  80. func (s *S3BackupStore) Download(ctx context.Context, key string) (io.ReadCloser, error) {
  81. finish := servertiming.ObserveDependency(ctx, "s3")
  82. result, err := s.client.GetObject(ctx, &s3.GetObjectInput{
  83. Bucket: &s.bucket,
  84. Key: &key,
  85. })
  86. finish()
  87. if err != nil {
  88. return nil, fmt.Errorf("S3 GetObject: %w", err)
  89. }
  90. return result.Body, nil
  91. }
  92. func (s *S3BackupStore) Delete(ctx context.Context, key string) error {
  93. finish := servertiming.ObserveDependency(ctx, "s3")
  94. _, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
  95. Bucket: &s.bucket,
  96. Key: &key,
  97. })
  98. finish()
  99. return err
  100. }
  101. func (s *S3BackupStore) PresignURL(ctx context.Context, key string, expiry time.Duration) (string, error) {
  102. presignClient := s3.NewPresignClient(s.client)
  103. // 强制 attachment disposition:浏览器同页导航该 URL 时直接触发下载而非渲染,
  104. // 前端无需依赖会被弹窗拦截的新标签页。
  105. disposition := fmt.Sprintf("attachment; filename=%q", path.Base(key))
  106. result, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
  107. Bucket: &s.bucket,
  108. Key: &key,
  109. ResponseContentDisposition: &disposition,
  110. }, s3.WithPresignExpires(expiry))
  111. if err != nil {
  112. return "", fmt.Errorf("presign url: %w", err)
  113. }
  114. return result.URL, nil
  115. }
  116. func (s *S3BackupStore) HeadBucket(ctx context.Context) error {
  117. finish := servertiming.ObserveDependency(ctx, "s3")
  118. _, err := s.client.HeadBucket(ctx, &s3.HeadBucketInput{
  119. Bucket: &s.bucket,
  120. })
  121. finish()
  122. if err != nil {
  123. return fmt.Errorf("S3 HeadBucket failed: %w", err)
  124. }
  125. return nil
  126. }