package service import ( "context" "fmt" "mime" "mime/multipart" "narasi-ahli-be/app/module/knowledge_base/mapper" "narasi-ahli-be/app/module/knowledge_base/repository" "narasi-ahli-be/app/module/knowledge_base/request" "narasi-ahli-be/app/module/knowledge_base/response" config "narasi-ahli-be/config/config" minioStorage "narasi-ahli-be/config/config" "narasi-ahli-be/utils/paginator" "path/filepath" "strings" "time" "github.com/gofiber/fiber/v2" "github.com/minio/minio-go/v7" "github.com/rs/zerolog" ) type KnowledgeBaseService struct { Repo repository.KnowledgeBaseRepositoryInterface Log zerolog.Logger Cfg *config.Config MinioStorage *minioStorage.MinioStorage } type KnowledgeBaseServiceInterface interface { All(req request.KnowledgeBaseQueryRequest) (data []*response.KnowledgeBaseResponse, paging paginator.Pagination, err error) Show(id uint) (data *response.KnowledgeBaseResponse, err error) Create(c *fiber.Ctx) (data *response.KnowledgeBaseResponse, err error) Update(id uint, req request.KnowledgeBaseUpdateRequest) (data *response.KnowledgeBaseResponse, err error) Delete(id uint) error Viewer(c *fiber.Ctx) error UpdateStatus(id uint, status int) (data *response.KnowledgeBaseResponse, err error) UpdateDocumentId(id uint, documentId int) (data *response.KnowledgeBaseResponse, err error) } func NewKnowledgeBaseService( repo repository.KnowledgeBaseRepositoryInterface, log zerolog.Logger, cfg *config.Config, minioStorage *minioStorage.MinioStorage, ) KnowledgeBaseServiceInterface { return &KnowledgeBaseService{ Repo: repo, Log: log, Cfg: cfg, MinioStorage: minioStorage, } } func (s *KnowledgeBaseService) uploadFileToMinio( ctx context.Context, minioClient *minio.Client, bucketName string, agentId string, fileHeader *multipart.FileHeader, ) (*string, error) { if fileHeader == nil { return nil, nil } src, err := fileHeader.Open() if err != nil { return nil, err } defer src.Close() filename := filepath.Base(fileHeader.Filename) filename = strings.ReplaceAll(filename, " ", "") ext := filepath.Ext(filename) filenameWithoutExt := strings.TrimSuffix(filename, ext) now := time.Now() newFilename := fmt.Sprintf("%s_%d%s", filenameWithoutExt, now.UnixNano(), ext) // ⭐ INI PATH ASLI MINIO objectName := fmt.Sprintf( "knowledge-base/%s/%s", agentId, newFilename, ) _, err = minioClient.PutObject( ctx, bucketName, objectName, src, fileHeader.Size, minio.PutObjectOptions{ ContentType: fileHeader.Header.Get("Content-Type"), }, ) if err != nil { return nil, err } // ⭐ KEMBALIKAN OBJECT PATH return &objectName, nil } func (s *KnowledgeBaseService) All(req request.KnowledgeBaseQueryRequest) (data []*response.KnowledgeBaseResponse, paging paginator.Pagination, err error) { results, paging, err := s.Repo.GetAll(req) if err != nil { return nil, paging, err } host := s.Cfg.App.Domain for _, item := range results { data = append(data, mapper.KnowledgeBaseResponseMapper(item, host)) } return data, paging, nil } func getFileExtension(filename string) string { parts := strings.Split(filename, ".") if len(parts) == 1 || (len(parts) == 2 && parts[0] == "") { return "" } return parts[len(parts)-1] } func (s *KnowledgeBaseService) Viewer(c *fiber.Ctx) error { filename := c.Params("filename") _, objectName, err := s.Repo.FindByFilename(filename) if err != nil { return c.Status(404).SendString("file not found") } ctx := context.Background() bucketName := s.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName minioClient, err := s.MinioStorage.ConnectMinio() if err != nil { return c.Status(500).SendString(err.Error()) } opts := minio.GetObjectOptions{} // support video streaming if rangeHeader := c.Get("Range"); rangeHeader != "" { opts.Set("Range", rangeHeader) c.Status(fiber.StatusPartialContent) c.Set("Accept-Ranges", "bytes") } object, err := minioClient.GetObject(ctx, bucketName, objectName, opts) if err != nil { return c.Status(404).SendString("file not found") } defer object.Close() stat, err := object.Stat() if err != nil { return c.Status(404).SendString("file not found") } contentType := stat.ContentType if contentType == "" { contentType = mime.TypeByExtension(filepath.Ext(objectName)) } c.Set("Content-Type", contentType) c.Set("Content-Disposition", "inline") return c.SendStream(object) } func (s *KnowledgeBaseService) Show(id uint) (data *response.KnowledgeBaseResponse, err error) { result, err := s.Repo.FindOne(id) if err != nil { return nil, err } host := s.Cfg.App.Domain return mapper.KnowledgeBaseResponseMapper(result, host), nil } func (s *KnowledgeBaseService) Create(c *fiber.Ctx) (data *response.KnowledgeBaseResponse, err error) { req := new(request.KnowledgeBaseCreateRequest) if err := c.BodyParser(req); err != nil { return nil, err } if req.AgentId == nil || *req.AgentId == "" { return nil, fiber.NewError(fiber.StatusBadRequest, "agentId is required") } if req.AgentName == nil || *req.AgentName == "" { return nil, fiber.NewError(fiber.StatusBadRequest, "agentName is required") } if req.Title == nil || *req.Title == "" { return nil, fiber.NewError(fiber.StatusBadRequest, "title is required") } fileJournal, _ := c.FormFile("fileJournal") fileAudio, _ := c.FormFile("fileAudio") fileVideo, _ := c.FormFile("fileVideo") minioClient, err := s.MinioStorage.ConnectMinio() if err != nil { return nil, err } ctx := context.Background() bucketName := s.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName journalPath, _ := s.uploadFileToMinio(ctx, minioClient, bucketName, *req.AgentId, fileJournal) audioPath, _ := s.uploadFileToMinio(ctx, minioClient, bucketName, *req.AgentId, fileAudio) videoPath, _ := s.uploadFileToMinio(ctx, minioClient, bucketName, *req.AgentId, fileVideo) entity := req.ToEntity() entity.FileJournalUrl = journalPath entity.FileAudioUrl = audioPath entity.FileVideoUrl = videoPath if req.Status < 0 { entity.Status = 0 } err = s.Repo.Create(entity) if err != nil { return nil, err } result, err := s.Repo.FindOne(entity.ID) if err != nil { return nil, err } host := s.Cfg.App.Domain return mapper.KnowledgeBaseResponseMapper(result, host), nil } func (s *KnowledgeBaseService) Update(id uint, req request.KnowledgeBaseUpdateRequest) (data *response.KnowledgeBaseResponse, err error) { old, err := s.Repo.FindOne(id) if err != nil { return nil, err } old.AgentId = req.AgentId old.AgentName = req.AgentName old.Title = req.Title old.Status = req.Status old.FileJournalUrl = req.FileJournalUrl old.FileAudioUrl = req.FileAudioUrl old.FileVideoUrl = req.FileVideoUrl if req.IsActive != nil { old.IsActive = *req.IsActive } old.UpdatedAt = time.Now() err = s.Repo.Update(id, old) if err != nil { return nil, err } updated, err := s.Repo.FindOne(id) if err != nil { return nil, err } host := s.Cfg.App.Domain return mapper.KnowledgeBaseResponseMapper(updated, host), nil } func (s *KnowledgeBaseService) Delete(id uint) error { result, err := s.Repo.FindOne(id) if err != nil { return err } result.IsActive = false result.UpdatedAt = time.Now() return s.Repo.Delete(id) } func (s *KnowledgeBaseService) UpdateStatus(id uint, status int) (data *response.KnowledgeBaseResponse, err error) { old, err := s.Repo.FindOne(id) if err != nil { return nil, err } old.Status = status old.UpdatedAt = time.Now() err = s.Repo.Update(id, old) if err != nil { return nil, err } updated, err := s.Repo.FindOne(id) if err != nil { return nil, err } host := s.Cfg.App.Domain return mapper.KnowledgeBaseResponseMapper(updated, host), nil } func (s *KnowledgeBaseService) UpdateDocumentId(id uint, documentId int) (data *response.KnowledgeBaseResponse, err error) { // cek data exist _, err = s.Repo.FindOne(id) if err != nil { return nil, err } err = s.Repo.UpdateDocumentId(id, documentId) if err != nil { return nil, err } updated, err := s.Repo.FindOne(id) if err != nil { return nil, err } host := s.Cfg.App.Domain return mapper.KnowledgeBaseResponseMapper(updated, host), nil }