package object_storage import ( "context" "encoding/json" "fmt" "strings" "time" "github.com/atlasos/calypso/internal/common/logger" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" madmin "github.com/minio/madmin-go/v3" ) // Service handles MinIO object storage operations type Service struct { client *minio.Client adminClient *madmin.AdminClient logger *logger.Logger endpoint string accessKey string secretKey string } // NewService creates a new MinIO service func NewService(endpoint, accessKey, secretKey string, log *logger.Logger) (*Service, error) { // Create MinIO client minioClient, err := minio.New(endpoint, &minio.Options{ Creds: credentials.NewStaticV4(accessKey, secretKey, ""), Secure: false, // Set to true if using HTTPS }) if err != nil { return nil, fmt.Errorf("failed to create MinIO client: %w", err) } // Create MinIO Admin client adminClient, err := madmin.New(endpoint, accessKey, secretKey, false) if err != nil { return nil, fmt.Errorf("failed to create MinIO admin client: %w", err) } return &Service{ client: minioClient, adminClient: adminClient, logger: log, endpoint: endpoint, accessKey: accessKey, secretKey: secretKey, }, nil } // Bucket represents a MinIO bucket type Bucket struct { Name string `json:"name"` CreationDate time.Time `json:"creation_date"` Size int64 `json:"size"` // Total size in bytes Objects int64 `json:"objects"` // Number of objects AccessPolicy string `json:"access_policy"` // private, public-read, public-read-write } // ListBuckets lists all buckets in MinIO func (s *Service) ListBuckets(ctx context.Context) ([]*Bucket, error) { buckets, err := s.client.ListBuckets(ctx) if err != nil { return nil, fmt.Errorf("failed to list buckets: %w", err) } result := make([]*Bucket, 0, len(buckets)) for _, bucket := range buckets { bucketInfo, err := s.getBucketInfo(ctx, bucket.Name) if err != nil { s.logger.Warn("Failed to get bucket info", "bucket", bucket.Name, "error", err) // Continue with basic info result = append(result, &Bucket{ Name: bucket.Name, CreationDate: bucket.CreationDate, Size: 0, Objects: 0, AccessPolicy: "private", }) continue } result = append(result, bucketInfo) } return result, nil } // getBucketInfo gets detailed information about a bucket func (s *Service) getBucketInfo(ctx context.Context, bucketName string) (*Bucket, error) { // Get bucket creation date buckets, err := s.client.ListBuckets(ctx) if err != nil { return nil, err } var creationDate time.Time for _, b := range buckets { if b.Name == bucketName { creationDate = b.CreationDate break } } // Get bucket size and object count by listing objects var size int64 var objects int64 // List objects in bucket to calculate size and count objectCh := s.client.ListObjects(ctx, bucketName, minio.ListObjectsOptions{ Recursive: true, }) for object := range objectCh { if object.Err != nil { s.logger.Warn("Error listing object", "bucket", bucketName, "error", object.Err) continue } objects++ size += object.Size } return &Bucket{ Name: bucketName, CreationDate: creationDate, Size: size, Objects: objects, AccessPolicy: s.getBucketPolicy(ctx, bucketName), }, nil } // getBucketPolicy gets the access policy for a bucket func (s *Service) getBucketPolicy(ctx context.Context, bucketName string) string { policy, err := s.client.GetBucketPolicy(ctx, bucketName) if err != nil { return "private" } // Parse policy JSON to determine access type // For simplicity, check if policy allows public read if policy != "" { // Check if policy contains public read access if strings.Contains(policy, "s3:GetObject") && strings.Contains(policy, "Principal") && strings.Contains(policy, "*") { if strings.Contains(policy, "s3:PutObject") { return "public-read-write" } return "public-read" } } return "private" } // CreateBucket creates a new bucket func (s *Service) CreateBucket(ctx context.Context, bucketName string) error { err := s.client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{}) if err != nil { return fmt.Errorf("failed to create bucket: %w", err) } return nil } // DeleteBucket deletes a bucket func (s *Service) DeleteBucket(ctx context.Context, bucketName string) error { err := s.client.RemoveBucket(ctx, bucketName) if err != nil { return fmt.Errorf("failed to delete bucket: %w", err) } return nil } // GetBucketStats gets statistics for a bucket func (s *Service) GetBucketStats(ctx context.Context, bucketName string) (*Bucket, error) { return s.getBucketInfo(ctx, bucketName) } // User represents a MinIO IAM user type User struct { AccessKey string `json:"access_key"` Status string `json:"status"` // "enabled" or "disabled" CreatedAt time.Time `json:"created_at"` } // ListUsers lists all IAM users in MinIO func (s *Service) ListUsers(ctx context.Context) ([]*User, error) { users, err := s.adminClient.ListUsers(ctx) if err != nil { return nil, fmt.Errorf("failed to list users: %w", err) } result := make([]*User, 0, len(users)) for accessKey, userInfo := range users { status := "enabled" if userInfo.Status == madmin.AccountDisabled { status = "disabled" } // MinIO doesn't provide creation date, use current time result = append(result, &User{ AccessKey: accessKey, Status: status, CreatedAt: time.Now(), }) } return result, nil } // CreateUser creates a new IAM user in MinIO func (s *Service) CreateUser(ctx context.Context, accessKey, secretKey string) error { err := s.adminClient.AddUser(ctx, accessKey, secretKey) if err != nil { return fmt.Errorf("failed to create user: %w", err) } return nil } // DeleteUser deletes an IAM user from MinIO func (s *Service) DeleteUser(ctx context.Context, accessKey string) error { err := s.adminClient.RemoveUser(ctx, accessKey) if err != nil { return fmt.Errorf("failed to delete user: %w", err) } return nil } // ServiceAccount represents a MinIO service account (access key) type ServiceAccount struct { AccessKey string `json:"access_key"` SecretKey string `json:"secret_key,omitempty"` // Only returned on creation ParentUser string `json:"parent_user"` Expiration time.Time `json:"expiration,omitempty"` CreatedAt time.Time `json:"created_at"` } // ListServiceAccounts lists all service accounts in MinIO func (s *Service) ListServiceAccounts(ctx context.Context) ([]*ServiceAccount, error) { accounts, err := s.adminClient.ListServiceAccounts(ctx, "") if err != nil { return nil, fmt.Errorf("failed to list service accounts: %w", err) } result := make([]*ServiceAccount, 0, len(accounts.Accounts)) for _, account := range accounts.Accounts { var expiration time.Time if account.Expiration != nil { expiration = *account.Expiration } result = append(result, &ServiceAccount{ AccessKey: account.AccessKey, ParentUser: account.ParentUser, Expiration: expiration, CreatedAt: time.Now(), // MinIO doesn't provide creation date }) } return result, nil } // CreateServiceAccount creates a new service account (access key) in MinIO func (s *Service) CreateServiceAccount(ctx context.Context, parentUser string, policy string, expiration *time.Time) (*ServiceAccount, error) { opts := madmin.AddServiceAccountReq{ TargetUser: parentUser, } if policy != "" { opts.Policy = json.RawMessage(policy) } if expiration != nil { opts.Expiration = expiration } creds, err := s.adminClient.AddServiceAccount(ctx, opts) if err != nil { return nil, fmt.Errorf("failed to create service account: %w", err) } return &ServiceAccount{ AccessKey: creds.AccessKey, SecretKey: creds.SecretKey, ParentUser: parentUser, Expiration: creds.Expiration, CreatedAt: time.Now(), }, nil } // DeleteServiceAccount deletes a service account from MinIO func (s *Service) DeleteServiceAccount(ctx context.Context, accessKey string) error { err := s.adminClient.DeleteServiceAccount(ctx, accessKey) if err != nil { return fmt.Errorf("failed to delete service account: %w", err) } return nil }