s3_client.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. package backup
  2. import (
  3. "bytes"
  4. "context"
  5. "fmt"
  6. "os"
  7. "path/filepath"
  8. "time"
  9. "github.com/0xJacky/Nginx-UI/model"
  10. "github.com/aws/aws-sdk-go-v2/aws"
  11. "github.com/aws/aws-sdk-go-v2/config"
  12. "github.com/aws/aws-sdk-go-v2/credentials"
  13. "github.com/aws/aws-sdk-go-v2/service/s3"
  14. "github.com/uozi-tech/cosy"
  15. "github.com/uozi-tech/cosy/logger"
  16. )
  17. // S3Client wraps the AWS S3 client with backup-specific functionality
  18. type S3Client struct {
  19. client *s3.Client
  20. bucket string
  21. }
  22. // NewS3Client creates a new S3 client from auto backup configuration.
  23. // This function initializes the AWS S3 client with the provided credentials and configuration.
  24. //
  25. // Parameters:
  26. // - autoBackup: The auto backup configuration containing S3 settings
  27. //
  28. // Returns:
  29. // - *S3Client: Configured S3 client wrapper
  30. // - error: CosyError if client creation fails
  31. func NewS3Client(autoBackup *model.AutoBackup) (*S3Client, error) {
  32. // Create AWS configuration with static credentials
  33. cfg, err := config.LoadDefaultConfig(context.TODO(),
  34. config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
  35. autoBackup.S3AccessKeyID,
  36. autoBackup.S3SecretAccessKey,
  37. "", // session token (not used for static credentials)
  38. )),
  39. config.WithRegion(getS3Region(autoBackup.S3Region)),
  40. )
  41. if err != nil {
  42. return nil, cosy.WrapErrorWithParams(ErrAutoBackupS3Upload, fmt.Sprintf("failed to load AWS config: %v", err))
  43. }
  44. // Create S3 client with custom endpoint if provided
  45. var s3Client *s3.Client
  46. if autoBackup.S3Endpoint != "" {
  47. s3Client = s3.NewFromConfig(cfg, func(o *s3.Options) {
  48. o.BaseEndpoint = aws.String(autoBackup.S3Endpoint)
  49. o.UsePathStyle = true // Use path-style addressing for custom endpoints
  50. })
  51. } else {
  52. s3Client = s3.NewFromConfig(cfg)
  53. }
  54. return &S3Client{
  55. client: s3Client,
  56. bucket: autoBackup.S3Bucket,
  57. }, nil
  58. }
  59. // UploadFile uploads a file to S3 with the specified key.
  60. // This function handles the actual upload operation with proper error handling and logging.
  61. //
  62. // Parameters:
  63. // - ctx: Context for the upload operation
  64. // - key: S3 object key (path) for the uploaded file
  65. // - data: File content to upload
  66. // - contentType: MIME type of the file content
  67. //
  68. // Returns:
  69. // - error: CosyError if upload fails
  70. func (s3c *S3Client) UploadFile(ctx context.Context, key string, data []byte, contentType string) error {
  71. logger.Infof("Uploading file to S3: bucket=%s, key=%s, size=%d bytes", s3c.bucket, key, len(data))
  72. // Create upload input
  73. input := &s3.PutObjectInput{
  74. Bucket: aws.String(s3c.bucket),
  75. Key: aws.String(key),
  76. Body: bytes.NewReader(data),
  77. ContentType: aws.String(contentType),
  78. Metadata: map[string]string{
  79. "uploaded-by": "nginx-ui",
  80. "upload-time": time.Now().UTC().Format(time.RFC3339),
  81. "content-length": fmt.Sprintf("%d", len(data)),
  82. },
  83. }
  84. // Perform the upload
  85. _, err := s3c.client.PutObject(ctx, input)
  86. if err != nil {
  87. return cosy.WrapErrorWithParams(ErrAutoBackupS3Upload, fmt.Sprintf("failed to upload to S3: %v", err))
  88. }
  89. logger.Infof("Successfully uploaded file to S3: bucket=%s, key=%s", s3c.bucket, key)
  90. return nil
  91. }
  92. // UploadBackupFiles uploads backup files to S3 with proper naming and organization.
  93. // This function handles uploading both the backup file and optional key file.
  94. //
  95. // Parameters:
  96. // - ctx: Context for the upload operations
  97. // - result: Backup execution result containing file paths
  98. // - autoBackup: Auto backup configuration for S3 path construction
  99. //
  100. // Returns:
  101. // - error: CosyError if any upload fails
  102. func (s3c *S3Client) UploadBackupFiles(ctx context.Context, result *BackupExecutionResult, autoBackup *model.AutoBackup) error {
  103. // Read backup file content
  104. backupData, err := readFileContent(result.FilePath)
  105. if err != nil {
  106. return cosy.WrapErrorWithParams(ErrAutoBackupS3Upload, fmt.Sprintf("failed to read backup file: %v", err))
  107. }
  108. // Construct S3 key for backup file
  109. backupFileName := filepath.Base(result.FilePath)
  110. backupKey := constructS3Key(autoBackup.StoragePath, backupFileName)
  111. // Upload backup file
  112. if err := s3c.UploadFile(ctx, backupKey, backupData, "application/zip"); err != nil {
  113. return err
  114. }
  115. // Upload key file if it exists (for encrypted backups)
  116. if result.KeyPath != "" {
  117. keyData, err := readFileContent(result.KeyPath)
  118. if err != nil {
  119. return cosy.WrapErrorWithParams(ErrAutoBackupS3Upload, fmt.Sprintf("failed to read key file: %v", err))
  120. }
  121. keyFileName := filepath.Base(result.KeyPath)
  122. keyKey := constructS3Key(autoBackup.StoragePath, keyFileName)
  123. if err := s3c.UploadFile(ctx, keyKey, keyData, "text/plain"); err != nil {
  124. return err
  125. }
  126. }
  127. return nil
  128. }
  129. // TestS3Connection tests the S3 connection and permissions.
  130. // This function verifies that the S3 configuration is valid and accessible.
  131. //
  132. // Parameters:
  133. // - ctx: Context for the test operation
  134. //
  135. // Returns:
  136. // - error: CosyError if connection test fails
  137. func (s3c *S3Client) TestS3Connection(ctx context.Context) error {
  138. logger.Infof("Testing S3 connection: bucket=%s", s3c.bucket)
  139. // Try to head the bucket to verify access
  140. _, err := s3c.client.HeadBucket(ctx, &s3.HeadBucketInput{
  141. Bucket: aws.String(s3c.bucket),
  142. })
  143. if err != nil {
  144. return cosy.WrapErrorWithParams(ErrAutoBackupS3Upload, fmt.Sprintf("S3 connection test failed: %v", err))
  145. }
  146. logger.Infof("S3 connection test successful: bucket=%s", s3c.bucket)
  147. return nil
  148. }
  149. // getS3Region returns the S3 region, defaulting to us-east-1 if not specified.
  150. // This function ensures a valid region is always provided to the AWS SDK.
  151. //
  152. // Parameters:
  153. // - region: The configured S3 region
  154. //
  155. // Returns:
  156. // - string: Valid AWS region string
  157. func getS3Region(region string) string {
  158. if region == "" {
  159. return "us-east-1" // Default region
  160. }
  161. return region
  162. }
  163. // constructS3Key constructs a proper S3 object key from storage path and filename.
  164. // This function ensures consistent S3 key formatting across the application.
  165. //
  166. // Parameters:
  167. // - storagePath: Base storage path in S3
  168. // - filename: Name of the file
  169. //
  170. // Returns:
  171. // - string: Properly formatted S3 object key
  172. func constructS3Key(storagePath, filename string) string {
  173. // Ensure storage path doesn't start with slash and ends with slash
  174. if storagePath == "" {
  175. return filename
  176. }
  177. // Remove leading slash if present
  178. if storagePath[0] == '/' {
  179. storagePath = storagePath[1:]
  180. }
  181. // Add trailing slash if not present
  182. if storagePath[len(storagePath)-1] != '/' {
  183. storagePath += "/"
  184. }
  185. return storagePath + filename
  186. }
  187. // readFileContent reads the entire content of a file into memory.
  188. // This function provides a centralized way to read file content for S3 uploads.
  189. //
  190. // Parameters:
  191. // - filePath: Path to the file to read
  192. //
  193. // Returns:
  194. // - []byte: File content
  195. // - error: Standard error if file reading fails
  196. func readFileContent(filePath string) ([]byte, error) {
  197. return os.ReadFile(filePath)
  198. }
  199. // TestS3ConnectionForConfig tests S3 connection for a given auto backup configuration.
  200. // This function is used by the API to validate S3 settings before saving.
  201. //
  202. // Parameters:
  203. // - autoBackup: Auto backup configuration with S3 settings
  204. //
  205. // Returns:
  206. // - error: CosyError if connection test fails
  207. func TestS3ConnectionForConfig(autoBackup *model.AutoBackup) error {
  208. s3Client, err := NewS3Client(autoBackup)
  209. if err != nil {
  210. return err
  211. }
  212. return s3Client.TestS3Connection(context.Background())
  213. }