1
0

s3_client.go 7.2 KB

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