Files
soft_usite/pkg/services/oss_service.go
T

353 lines
10 KiB
Go
Executable File

package services
import (
"context"
"fmt"
"io"
"log"
"time"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
// OSSObject representa un objeto/archivo listado en el bucket
type OSSObject struct {
Key string `json:"key"`
Size int64 `json:"size"`
LastModified time.Time `json:"last_modified"`
ETag string `json:"etag"`
}
// OSSListResult resultado paginado de listado de objetos
type OSSListResult struct {
Objects []OSSObject `json:"objects"`
CommonPrefixes []string `json:"prefixes"`
IsTruncated bool `json:"is_truncated"`
NextMarker string `json:"next_marker"`
}
// OSSProvider interface común para Alibaba OSS y S3-compatible (MinIO)
type OSSProvider interface {
UploadFile(objectKey, filePath string) error
DeleteFile(objectKey string) error
ListObjects(prefix, marker string, maxKeys int) (*OSSListResult, error)
SignedURL(objectKey string, expireSeconds int) (string, error)
UploadFromReader(objectKey, contentType string, r io.Reader) error
DeleteObjects(keys []string) error
BucketName() string
Endpoint() string
PublicURL(objectKey string) string
}
// ── Alibaba OSS ──────────────────────────────────────────────────────────────
type alibabaOSS struct {
client *oss.Client
bucket *oss.Bucket
bucketName string
endpoint string
}
func newAlibabaOSS(cfg *models.OssApi) (OSSProvider, error) {
client, err := oss.New(cfg.Endpoint, cfg.AccessKeyID, cfg.AccessKeySecret)
if err != nil {
return nil, fmt.Errorf("error creando cliente Alibaba OSS: %w", err)
}
bucket, err := client.Bucket(cfg.BucketName)
if err != nil {
return nil, fmt.Errorf("error obteniendo bucket Alibaba: %w", err)
}
return &alibabaOSS{
client: client,
bucket: bucket,
bucketName: cfg.BucketName,
endpoint: cfg.Endpoint,
}, nil
}
func (s *alibabaOSS) BucketName() string { return s.bucketName }
func (s *alibabaOSS) Endpoint() string { return s.endpoint }
func (s *alibabaOSS) PublicURL(objectKey string) string {
return fmt.Sprintf("https://%s.%s/%s", s.bucketName, s.endpoint, objectKey)
}
func (s *alibabaOSS) UploadFile(objectKey, filePath string) error {
if err := s.bucket.PutObjectFromFile(objectKey, filePath); err != nil {
return fmt.Errorf("error subiendo archivo a Alibaba OSS: %w", err)
}
log.Printf("Archivo '%s' subido como '%s' (Alibaba)", filePath, objectKey)
return nil
}
func (s *alibabaOSS) DeleteFile(objectKey string) error {
if err := s.bucket.DeleteObject(objectKey); err != nil {
return fmt.Errorf("error eliminando archivo de Alibaba OSS: %w", err)
}
log.Printf("Archivo eliminado de Alibaba OSS: %s", objectKey)
return nil
}
func (s *alibabaOSS) ListObjects(prefix, marker string, maxKeys int) (*OSSListResult, error) {
if maxKeys <= 0 || maxKeys > 1000 {
maxKeys = 100
}
opts := []oss.Option{
oss.MaxKeys(maxKeys),
oss.Delimiter("/"),
}
if prefix != "" {
opts = append(opts, oss.Prefix(prefix))
}
if marker != "" {
opts = append(opts, oss.Marker(marker))
}
resp, err := s.bucket.ListObjects(opts...)
if err != nil {
return nil, fmt.Errorf("error listando objetos Alibaba OSS: %w", err)
}
result := &OSSListResult{
IsTruncated: resp.IsTruncated,
NextMarker: resp.NextMarker,
}
for _, obj := range resp.Objects {
result.Objects = append(result.Objects, OSSObject{
Key: obj.Key,
Size: obj.Size,
LastModified: obj.LastModified,
ETag: obj.ETag,
})
}
for _, cp := range resp.CommonPrefixes {
result.CommonPrefixes = append(result.CommonPrefixes, cp)
}
return result, nil
}
func (s *alibabaOSS) SignedURL(objectKey string, expireSeconds int) (string, error) {
if expireSeconds <= 0 {
expireSeconds = 3600
}
url, err := s.bucket.SignURL(objectKey, oss.HTTPGet, int64(expireSeconds))
if err != nil {
return "", fmt.Errorf("error generando URL firmada Alibaba: %w", err)
}
return url, nil
}
func (s *alibabaOSS) UploadFromReader(objectKey, contentType string, r io.Reader) error {
var opts []oss.Option
if contentType != "" {
opts = append(opts, oss.ContentType(contentType))
}
if err := s.bucket.PutObject(objectKey, r, opts...); err != nil {
return fmt.Errorf("error subiendo objeto a Alibaba OSS: %w", err)
}
log.Printf("Objeto subido a Alibaba OSS: %s", objectKey)
return nil
}
func (s *alibabaOSS) DeleteObjects(keys []string) error {
_, err := s.bucket.DeleteObjects(keys)
if err != nil {
return fmt.Errorf("error eliminando objetos en batch de Alibaba OSS: %w", err)
}
return nil
}
// ── S3-compatible (MinIO) ────────────────────────────────────────────────────
type s3OSS struct {
client *minio.Client
bucketName string
endpoint string
publicURL string
}
func newS3OSS(cfg *models.OssApi) (OSSProvider, error) {
useSSL := false
endpoint := cfg.Endpoint
// Detectar si el endpoint usa HTTPS
if len(endpoint) > 8 && endpoint[:8] == "https://" {
useSSL = true
endpoint = endpoint[8:]
} else if len(endpoint) > 7 && endpoint[:7] == "http://" {
endpoint = endpoint[7:]
}
client, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(cfg.AccessKeyID, cfg.AccessKeySecret, ""),
Secure: useSSL,
Region: cfg.Region,
BucketLookup: minio.BucketLookupPath,
})
if err != nil {
return nil, fmt.Errorf("error creando cliente S3/MinIO: %w", err)
}
return &s3OSS{
client: client,
bucketName: cfg.BucketName,
endpoint: cfg.Endpoint,
publicURL: cfg.PublicURL,
}, nil
}
func (s *s3OSS) BucketName() string { return s.bucketName }
func (s *s3OSS) Endpoint() string { return s.endpoint }
func (s *s3OSS) PublicURL(objectKey string) string {
if s.publicURL != "" {
return fmt.Sprintf("%s/%s/%s", s.publicURL, s.bucketName, objectKey)
}
return fmt.Sprintf("%s/%s/%s", s.endpoint, s.bucketName, objectKey)
}
func (s *s3OSS) UploadFile(objectKey, filePath string) error {
ctx := context.Background()
_, err := s.client.FPutObject(ctx, s.bucketName, objectKey, filePath, minio.PutObjectOptions{
UserMetadata: map[string]string{
"Content-Disposition": "attachment",
},
})
if err != nil {
return fmt.Errorf("error subiendo archivo a S3: %w", err)
}
log.Printf("Archivo '%s' subido como '%s' (S3)", filePath, objectKey)
return nil
}
func (s *s3OSS) DeleteFile(objectKey string) error {
ctx := context.Background()
if err := s.client.RemoveObject(ctx, s.bucketName, objectKey, minio.RemoveObjectOptions{}); err != nil {
return fmt.Errorf("error eliminando archivo de S3: %w", err)
}
log.Printf("Archivo eliminado de S3: %s", objectKey)
return nil
}
func (s *s3OSS) ListObjects(prefix, marker string, maxKeys int) (*OSSListResult, error) {
ctx := context.Background()
if maxKeys <= 0 || maxKeys > 1000 {
maxKeys = 100
}
opts := minio.ListObjectsOptions{
Prefix: prefix,
Recursive: false, // false = usa '/' como delimiter
MaxKeys: maxKeys,
}
if marker != "" {
opts.StartAfter = marker
}
result := &OSSListResult{}
for obj := range s.client.ListObjects(ctx, s.bucketName, opts) {
if obj.Err != nil {
return nil, fmt.Errorf("error listando objetos S3: %w", obj.Err)
}
if len(obj.Key) > 0 && obj.Key[len(obj.Key)-1] == '/' {
result.CommonPrefixes = append(result.CommonPrefixes, obj.Key)
} else {
result.Objects = append(result.Objects, OSSObject{
Key: obj.Key,
Size: obj.Size,
LastModified: obj.LastModified,
ETag: obj.ETag,
})
}
}
// Determinar si hay más resultados y generar next_marker
if len(result.Objects) > 0 {
last := result.Objects[len(result.Objects)-1]
result.NextMarker = last.Key
result.IsTruncated = len(result.Objects) >= maxKeys
}
return result, nil
}
func (s *s3OSS) SignedURL(objectKey string, expireSeconds int) (string, error) {
if expireSeconds <= 0 {
expireSeconds = 3600
}
ctx := context.Background()
url, err := s.client.PresignedGetObject(ctx, s.bucketName, objectKey, time.Duration(expireSeconds)*time.Second, nil)
if err != nil {
return "", fmt.Errorf("error generando URL firmada S3: %w", err)
}
return url.String(), nil
}
func (s *s3OSS) UploadFromReader(objectKey, contentType string, r io.Reader) error {
ctx := context.Background()
opts := minio.PutObjectOptions{
UserMetadata: map[string]string{
"Content-Disposition": "attachment",
},
}
if contentType != "" {
opts.ContentType = contentType
}
_, err := s.client.PutObject(ctx, s.bucketName, objectKey, r, -1, opts)
if err != nil {
return fmt.Errorf("error subiendo objeto a S3: %w", err)
}
log.Printf("Objeto subido a S3: %s", objectKey)
return nil
}
func (s *s3OSS) DeleteObjects(keys []string) error {
ctx := context.Background()
opts := minio.RemoveObjectsOptions{}
ch := make(chan minio.ObjectInfo, len(keys))
go func() {
defer close(ch)
for _, k := range keys {
ch <- minio.ObjectInfo{Key: k}
}
}()
for err := range s.client.RemoveObjects(ctx, s.bucketName, ch, opts) {
if err.Err != nil {
return fmt.Errorf("error eliminando objetos en batch de S3: %w", err.Err)
}
}
return nil
}
// ── Factory ──────────────────────────────────────────────────────────────────
// NewOSSProvider crea el provider correcto según la configuración
func NewOSSProvider(cfg *models.OssApi) (OSSProvider, error) {
switch cfg.Provider {
case "s3":
return newS3OSS(cfg)
default:
return newAlibabaOSS(cfg)
}
}
// NewOSSProviderFromDB obtiene la configuración activa y crea el provider
func NewOSSProviderFromDB() (OSSProvider, error) {
cfg, err := models.GetLastActiveOssApi()
if err != nil {
return nil, fmt.Errorf("error obteniendo configuración OSS activa: %w", err)
}
return NewOSSProvider(cfg)
}
// NewOSSProviderByID obtiene una configuración por ID y crea el provider
func NewOSSProviderByID(id uint) (OSSProvider, error) {
cfg, err := models.GetOssApiByID(id)
if err != nil {
return nil, fmt.Errorf("error obteniendo configuración OSS #%d: %w", id, err)
}
if !cfg.IsActive {
return nil, fmt.Errorf("la configuración OSS #%d está inactiva", id)
}
return NewOSSProvider(cfg)
}