qudoco-be/app/module/media_library/service/media_library.service.go

245 lines
6.9 KiB
Go
Raw Normal View History

package service
import (
"errors"
"mime/multipart"
"path/filepath"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/rs/zerolog"
"gorm.io/gorm"
"web-qudo-be/app/database/entity"
"web-qudo-be/app/module/media_library/repository"
"web-qudo-be/app/module/media_library/request"
"web-qudo-be/app/module/media_library/response"
appcfg "web-qudo-be/config/config"
"web-qudo-be/utils/paginator"
"web-qudo-be/utils/storage"
)
// RegisterInput is used from article/CMS upload hooks (same physical file, extra catalog row).
type RegisterInput struct {
ClientID *uuid.UUID
UserID int
PublicURL string
ObjectKey *string
OriginalFilename *string
FileCategory string
SizeBytes *int64
SourceType string
SourceLabel *string
ArticleFileID *uint
}
type MediaLibraryService interface {
UpsertRegister(in RegisterInput) error
RegisterFromRequest(clientID *uuid.UUID, userID int, req *request.MediaLibraryRegisterRequest) error
RegisterCMSAsset(publicURL, objectKey, sourceLabel string, file *multipart.FileHeader) error
All(clientID *uuid.UUID, q string, sourceType *string, p *paginator.Pagination) ([]*response.MediaLibraryItemResponse, *paginator.Pagination, error)
Upload(clientID *uuid.UUID, userID int, c *fiber.Ctx) error
Delete(clientID *uuid.UUID, id uint) error
}
type mediaLibraryService struct {
Repo repository.MediaLibraryRepository
Cfg *appcfg.Config
MinioStorage *appcfg.MinioStorage
Log zerolog.Logger
}
func NewMediaLibraryService(
repo repository.MediaLibraryRepository,
cfg *appcfg.Config,
minio *appcfg.MinioStorage,
log zerolog.Logger,
) MediaLibraryService {
return &mediaLibraryService{Repo: repo, Cfg: cfg, MinioStorage: minio, Log: log}
}
func ArticleFilePublicURL(cfg *appcfg.Config, fileName string) string {
base := strings.TrimSuffix(cfg.App.Domain, "/")
return base + "/article-files/viewer/" + strings.TrimPrefix(fileName, "/")
}
func CategoryFromFilename(name string) string {
ext := strings.ToLower(filepath.Ext(name))
switch ext {
case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg":
return "image"
case ".mp4", ".webm", ".mov":
return "video"
case ".mp3", ".wav", ".ogg", ".m4a":
return "audio"
case ".pdf", ".doc", ".docx", ".txt", ".csv":
return "document"
default:
return "other"
}
}
func (s *mediaLibraryService) UpsertRegister(in RegisterInput) error {
url := strings.TrimSpace(in.PublicURL)
if url == "" {
return nil
}
existing, err := s.Repo.FindByPublicURLAny(url)
if err == nil {
if !existing.IsActive {
cat := strings.TrimSpace(in.FileCategory)
if cat == "" {
cat = "other"
}
return s.Repo.Update(existing.ID, map[string]interface{}{
"is_active": true,
"object_key": in.ObjectKey,
"original_filename": in.OriginalFilename,
"file_category": cat,
"size_bytes": in.SizeBytes,
"source_type": in.SourceType,
"source_label": in.SourceLabel,
"article_file_id": in.ArticleFileID,
"updated_at": time.Now(),
})
}
return nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
cat := strings.TrimSpace(in.FileCategory)
if cat == "" {
cat = "other"
}
item := &entity.MediaLibraryItem{
PublicURL: url,
ObjectKey: in.ObjectKey,
OriginalFilename: in.OriginalFilename,
FileCategory: cat,
SizeBytes: in.SizeBytes,
SourceType: in.SourceType,
SourceLabel: in.SourceLabel,
ArticleFileID: in.ArticleFileID,
CreatedByID: in.UserID,
ClientID: in.ClientID,
IsActive: true,
}
return s.Repo.Create(item)
}
func (s *mediaLibraryService) RegisterCMSAsset(publicURL, objectKey, sourceLabel string, file *multipart.FileHeader) error {
if strings.TrimSpace(publicURL) == "" {
return nil
}
var keyPtr *string
if strings.TrimSpace(objectKey) != "" {
k := objectKey
keyPtr = &k
}
lbl := sourceLabel
var namePtr *string
var sz *int64
cat := "other"
if file != nil {
b := filepath.Base(file.Filename)
namePtr = &b
ss := file.Size
sz = &ss
cat = CategoryFromFilename(b)
}
return s.UpsertRegister(RegisterInput{
PublicURL: publicURL,
ObjectKey: keyPtr,
OriginalFilename: namePtr,
FileCategory: cat,
SizeBytes: sz,
SourceType: "cms",
SourceLabel: &lbl,
})
}
func (s *mediaLibraryService) RegisterFromRequest(clientID *uuid.UUID, userID int, req *request.MediaLibraryRegisterRequest) error {
cat := CategoryFromFilename(req.PublicURL)
if req.OriginalFilename != nil {
cat = CategoryFromFilename(*req.OriginalFilename)
}
if req.FileCategory != nil && strings.TrimSpace(*req.FileCategory) != "" {
cat = strings.TrimSpace(*req.FileCategory)
}
return s.UpsertRegister(RegisterInput{
ClientID: clientID,
UserID: userID,
PublicURL: strings.TrimSpace(req.PublicURL),
ObjectKey: req.ObjectKey,
OriginalFilename: req.OriginalFilename,
FileCategory: cat,
SizeBytes: req.SizeBytes,
SourceType: req.SourceType,
SourceLabel: req.SourceLabel,
ArticleFileID: req.ArticleFileID,
})
}
func toResponse(e *entity.MediaLibraryItem) *response.MediaLibraryItemResponse {
return &response.MediaLibraryItemResponse{
ID: e.ID,
PublicURL: e.PublicURL,
ObjectKey: e.ObjectKey,
OriginalFilename: e.OriginalFilename,
FileCategory: e.FileCategory,
SizeBytes: e.SizeBytes,
SourceType: e.SourceType,
SourceLabel: e.SourceLabel,
ArticleFileID: e.ArticleFileID,
CreatedByID: e.CreatedByID,
ClientID: e.ClientID,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
func (s *mediaLibraryService) All(clientID *uuid.UUID, q string, sourceType *string, p *paginator.Pagination) ([]*response.MediaLibraryItemResponse, *paginator.Pagination, error) {
rows, paging, err := s.Repo.GetAll(clientID, q, sourceType, p)
if err != nil {
return nil, paging, err
}
out := make([]*response.MediaLibraryItemResponse, 0, len(rows))
for _, r := range rows {
out = append(out, toResponse(r))
}
return out, paging, nil
}
func (s *mediaLibraryService) Upload(clientID *uuid.UUID, userID int, c *fiber.Ctx) error {
file, err := c.FormFile("file")
if err != nil {
return err
}
key, previewURL, err := storage.UploadMediaLibraryObject(s.MinioStorage, file)
if err != nil {
return err
}
name := filepath.Base(file.Filename)
sz := file.Size
return s.UpsertRegister(RegisterInput{
ClientID: clientID,
UserID: userID,
PublicURL: previewURL,
ObjectKey: &key,
OriginalFilename: &name,
FileCategory: CategoryFromFilename(name),
SizeBytes: &sz,
SourceType: "upload",
SourceLabel: strPtr("media_library_direct"),
})
}
func strPtr(s string) *string { return &s }
func (s *mediaLibraryService) Delete(clientID *uuid.UUID, id uint) error {
return s.Repo.SoftDelete(clientID, id)
}