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) (publicURL string, err 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) (string, 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 err = 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"), }) return previewURL, err } func strPtr(s string) *string { return &s } func (s *mediaLibraryService) Delete(clientID *uuid.UUID, id uint) error { return s.Repo.SoftDelete(clientID, id) }