245 lines
6.9 KiB
Go
245 lines
6.9 KiB
Go
|
|
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)
|
||
|
|
}
|