package services import ( "bytes" "context" "fmt" "io" "os" "path/filepath" "strings" "clawreef/internal/config" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) type ObjectStorageService interface { PutObject(ctx context.Context, objectKey string, body []byte, contentType string) error GetObject(ctx context.Context, objectKey string) ([]byte, error) } type objectStorageService struct { minioClient *minio.Client bucket string basePath string localPath string } func NewObjectStorageService(cfg config.ObjectStorageConfig) (ObjectStorageService, error) { service := &objectStorageService{ bucket: strings.TrimSpace(cfg.Bucket), basePath: strings.Trim(strings.TrimSpace(cfg.BasePath), "/"), localPath: strings.TrimSpace(cfg.LocalFallback), } if service.localPath == "" { service.localPath = ".data/object-storage" } if strings.TrimSpace(cfg.Endpoint) == "" { return service, nil } client, err := minio.New(cfg.Endpoint, &minio.Options{ Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""), Secure: cfg.UseSSL, Region: cfg.Region, BucketLookup: minio.BucketLookupPath, }) if err != nil { return nil, fmt.Errorf("failed to initialize object storage client: %w", err) } service.minioClient = client return service, nil } func (s *objectStorageService) PutObject(ctx context.Context, objectKey string, body []byte, contentType string) error { if s.minioClient == nil { target := filepath.Join(s.localPath, filepath.FromSlash(s.resolveObjectKey(objectKey))) if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { return fmt.Errorf("failed to prepare local object storage directory: %w", err) } if err := os.WriteFile(target, body, 0o644); err != nil { return fmt.Errorf("failed to write local object storage object: %w", err) } return nil } exists, err := s.minioClient.BucketExists(ctx, s.bucket) if err != nil { return fmt.Errorf("failed to check object storage bucket: %w", err) } if !exists { if err := s.minioClient.MakeBucket(ctx, s.bucket, minio.MakeBucketOptions{}); err != nil { return fmt.Errorf("failed to create object storage bucket: %w", err) } } _, err = s.minioClient.PutObject(ctx, s.bucket, s.resolveObjectKey(objectKey), bytes.NewReader(body), int64(len(body)), minio.PutObjectOptions{ ContentType: contentType, }) if err != nil { return fmt.Errorf("failed to upload object: %w", err) } return nil } func (s *objectStorageService) GetObject(ctx context.Context, objectKey string) ([]byte, error) { if s.minioClient == nil { target := filepath.Join(s.localPath, filepath.FromSlash(s.resolveObjectKey(objectKey))) content, err := os.ReadFile(target) if err != nil { return nil, fmt.Errorf("failed to read local object storage object: %w", err) } return content, nil } reader, err := s.minioClient.GetObject(ctx, s.bucket, s.resolveObjectKey(objectKey), minio.GetObjectOptions{}) if err != nil { return nil, fmt.Errorf("failed to open object: %w", err) } defer reader.Close() content, err := io.ReadAll(reader) if err != nil { return nil, fmt.Errorf("failed to read object: %w", err) } return content, nil } func (s *objectStorageService) resolveObjectKey(objectKey string) string { objectKey = strings.Trim(strings.TrimSpace(objectKey), "/") if s.basePath == "" { return objectKey } return s.basePath + "/" + objectKey }