package service import ( "context" "errors" "fmt" "io" "math/rand" "mime" "narasi-ahli-be/app/database/entity" "narasi-ahli-be/app/module/chat/mapper" "narasi-ahli-be/app/module/chat/repository" "narasi-ahli-be/app/module/chat/request" "narasi-ahli-be/app/module/chat/response" usersRepository "narasi-ahli-be/app/module/users/repository" config "narasi-ahli-be/config/config" minioStorage "narasi-ahli-be/config/config" utilSvc "narasi-ahli-be/utils/service" "path/filepath" "strconv" "strings" "time" "github.com/gofiber/fiber/v2" "github.com/minio/minio-go/v7" "github.com/rs/zerolog" ) type chatScheduleFileService struct { chatScheduleFileRepository repository.ChatScheduleFileRepository chatScheduleRepository repository.ChatScheduleRepository chatScheduleFileMapper *mapper.ChatScheduleFileMapper Log zerolog.Logger Cfg *config.Config MinioStorage *minioStorage.MinioStorage UsersRepo usersRepository.UsersRepository } type ChatScheduleFileService interface { // File management operations UploadChatScheduleFile(c *fiber.Ctx, chatScheduleID uint) error GetChatScheduleFiles(authToken string, req request.ChatScheduleFileQueryRequest) (files []*response.ChatScheduleFileResponse, err error) GetChatScheduleFileByID(authToken string, id uint) (file *response.ChatScheduleFileResponse, err error) UpdateChatScheduleFile(authToken string, id uint, req request.ChatScheduleFileUpdateRequest) (err error) DeleteChatScheduleFile(authToken string, id uint) (err error) Viewer(c *fiber.Ctx) error } func NewChatScheduleFileService( chatScheduleFileRepository repository.ChatScheduleFileRepository, chatScheduleRepository repository.ChatScheduleRepository, log zerolog.Logger, cfg *config.Config, minioStorage *minioStorage.MinioStorage, usersRepo usersRepository.UsersRepository, ) ChatScheduleFileService { return &chatScheduleFileService{ chatScheduleFileRepository: chatScheduleFileRepository, chatScheduleRepository: chatScheduleRepository, chatScheduleFileMapper: mapper.NewChatScheduleFileMapper(), Log: log, Cfg: cfg, MinioStorage: minioStorage, UsersRepo: usersRepo, } } // UploadChatScheduleFile - Upload files for chat schedule func (_i *chatScheduleFileService) UploadChatScheduleFile(c *fiber.Ctx, chatScheduleID uint) error { bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName form, err := c.MultipartForm() if err != nil { return err } // Create minio connection minioClient, err := _i.MinioStorage.ConnectMinio() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": true, "msg": err.Error(), }) } for _, files := range form.File { _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "ChatScheduleFile::Upload"). Interface("files", files).Msg("") for _, fileHeader := range files { _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "ChatScheduleFile::Upload"). Interface("data", fileHeader).Msg("") src, err := fileHeader.Open() if err != nil { return err } defer src.Close() filename := filepath.Base(fileHeader.Filename) filenameAlt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))]) filename = strings.ReplaceAll(filename, " ", "") filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))]) extension := filepath.Ext(fileHeader.Filename)[1:] now := time.Now() rand.New(rand.NewSource(now.UnixNano())) randUniqueId := rand.Intn(1000000) newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId) newFilename := newFilenameWithoutExt + "." + extension objectName := fmt.Sprintf("chat-schedules/upload/%d/%d/%s", now.Year(), now.Month(), newFilename) // Get file type from form data fileType := c.FormValue("file_type", "other") description := c.FormValue("description", "") isRequired := c.FormValue("is_required") == "true" // Create file entity fileEntity := &entity.ChatScheduleFiles{ ChatScheduleID: chatScheduleID, FileName: newFilename, OriginalName: filenameAlt, FilePath: objectName, FileSize: fileHeader.Size, MimeType: fileHeader.Header.Get("Content-Type"), FileType: fileType, Description: description, IsRequired: isRequired, } // Save to database _, err = _i.chatScheduleFileRepository.CreateChatScheduleFile(fileEntity) if err != nil { return err } // Upload file to MinIO _, err = minioClient.PutObject(context.Background(), bucketName, objectName, src, fileHeader.Size, minio.PutObjectOptions{}) if err != nil { return err } } } _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "ChatScheduleFile::Upload"). Interface("data", "Successfully uploaded").Msg("") return nil } // GetChatScheduleFiles - Get files for a chat schedule func (_i *chatScheduleFileService) GetChatScheduleFiles(authToken string, req request.ChatScheduleFileQueryRequest) (files []*response.ChatScheduleFileResponse, err error) { userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if userInfo == nil { return nil, errors.New("user not found") } userID := userInfo.ID // If chat schedule ID is provided, check if user has access if req.ChatScheduleID != nil { isParticipant, err := _i.chatScheduleRepository.CheckUserInChatSchedule(userID, *req.ChatScheduleID) if err != nil { return nil, err } if !isParticipant { return nil, errors.New("user is not a participant in this chat session") } } // Get files from repository fileEntities, err := _i.chatScheduleFileRepository.GetChatScheduleFiles(req) if err != nil { return nil, err } // Convert to response files = _i.chatScheduleFileMapper.ToResponseList(fileEntities) return } // GetChatScheduleFileByID - Get a specific chat schedule file func (_i *chatScheduleFileService) GetChatScheduleFileByID(authToken string, id uint) (file *response.ChatScheduleFileResponse, err error) { userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if userInfo == nil { return nil, errors.New("user not found") } userID := userInfo.ID // Get file from repository fileEntity, err := _i.chatScheduleFileRepository.GetChatScheduleFileByID(id) if err != nil { return nil, err } // Check if user has access to the chat schedule isParticipant, err := _i.chatScheduleRepository.CheckUserInChatSchedule(userID, fileEntity.ChatScheduleID) if err != nil { return nil, err } if !isParticipant { return nil, errors.New("user is not a participant in this chat session") } // Convert to response file = _i.chatScheduleFileMapper.ToResponse(fileEntity) return } // UpdateChatScheduleFile - Update a chat schedule file func (_i *chatScheduleFileService) UpdateChatScheduleFile(authToken string, id uint, req request.ChatScheduleFileUpdateRequest) (err error) { userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if userInfo == nil { return errors.New("user not found") } userID := userInfo.ID // Get existing file to check access existingFile, err := _i.chatScheduleFileRepository.GetChatScheduleFileByID(id) if err != nil { return err } // Check if user has access to the chat schedule isParticipant, err := _i.chatScheduleRepository.CheckUserInChatSchedule(userID, existingFile.ChatScheduleID) if err != nil { return err } if !isParticipant { return errors.New("user is not a participant in this chat session") } // Convert request to entity file := _i.chatScheduleFileMapper.ToUpdateEntity(req) // Update file err = _i.chatScheduleFileRepository.UpdateChatScheduleFile(id, file) return } // DeleteChatScheduleFile - Delete a chat schedule file func (_i *chatScheduleFileService) DeleteChatScheduleFile(authToken string, id uint) (err error) { userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if userInfo == nil { return errors.New("user not found") } userID := userInfo.ID // Get existing file to check access existingFile, err := _i.chatScheduleFileRepository.GetChatScheduleFileByID(id) if err != nil { return err } // Check if user has access to the chat schedule isParticipant, err := _i.chatScheduleRepository.CheckUserInChatSchedule(userID, existingFile.ChatScheduleID) if err != nil { return err } if !isParticipant { return errors.New("user is not a participant in this chat session") } // Delete file err = _i.chatScheduleFileRepository.DeleteChatScheduleFile(id) return } // Viewer - View chat schedule file func (_i *chatScheduleFileService) Viewer(c *fiber.Ctx) error { filename := c.Params("filename") // Find file by filename fileEntity, err := _i.chatScheduleFileRepository.GetChatScheduleFileByFilename(filename) if err != nil { return err } ctx := context.Background() bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName objectName := fileEntity.FilePath _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "ChatScheduleFile::Viewer"). Interface("data", objectName).Msg("") // Create minio connection minioClient, err := _i.MinioStorage.ConnectMinio() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": true, "msg": err.Error(), }) } fileContent, err := minioClient.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{}) if err != nil { return err } defer fileContent.Close() // Determine Content-Type based on file extension contentType := mime.TypeByExtension("." + getFileExtension(objectName)) if contentType == "" { contentType = "application/octet-stream" // fallback if no MIME type matches } c.Set("Content-Type", contentType) if _, err := io.Copy(c.Response().BodyWriter(), fileContent); err != nil { return err } return nil } // getFileExtension - Extract file extension from filename func getFileExtension(filename string) string { // split file name parts := strings.Split(filename, ".") // if no extension, return empty string if len(parts) == 1 || (len(parts) == 2 && parts[0] == "") { return "" } // get last extension return parts[len(parts)-1] }