package service import ( "context" "fmt" "io" "path/filepath" "strings" "time" "netidhub-saas-be/config/config" "github.com/gofiber/fiber/v2" "github.com/minio/minio-go/v7" "github.com/rs/zerolog" ) // ClientLogoUploadService handles client logo uploads to MinIO type ClientLogoUploadService struct { MinioStorage *config.MinioStorage Log zerolog.Logger } // NewClientLogoUploadService creates a new client logo upload service func NewClientLogoUploadService(minioStorage *config.MinioStorage, log zerolog.Logger) *ClientLogoUploadService { return &ClientLogoUploadService{ MinioStorage: minioStorage, Log: log, } } // UploadLogo uploads client logo to MinIO and returns the image path func (s *ClientLogoUploadService) UploadLogo(c *fiber.Ctx, clientId, clientSlug string) (string, string, error) { s.Log.Info(). Str("UploadLogo", clientId). Msg("Client upload logo process") // Get the uploaded file file, err := c.FormFile("logo") if err != nil { return "", "", fmt.Errorf("failed to get uploaded file: %w", err) } s.Log.Info(). Str("Upload Logo File", clientId).Interface("file", file).Msg("Client upload logo files") // Validate file type if !s.isValidImageType(file.Filename) { return "", "", fmt.Errorf("invalid file type. Only jpg, jpeg, png, gif, webp are allowed") } s.Log.Info(). Str("Upload Logo File", clientId).Interface("file", s.isValidImageType(file.Filename)).Msg("Client upload logo files") // Validate file size (max 5MB) const maxSize = 5 * 1024 * 1024 // 5MB if file.Size > maxSize { return "", "", fmt.Errorf("file size too large. Maximum size is 5MB") } // Create MinIO connection minioClient, err := s.MinioStorage.ConnectMinio() if err != nil { return "", "", fmt.Errorf("failed to connect to MinIO: %w", err) } bucketName := s.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName s.Log.Info(). Str("Upload Logo bucketName", clientId).Interface("bucketName", bucketName).Msg("Client upload logo files") // Generate unique filename using client name filename := s.generateUniqueFilename(file.Filename, clientSlug) objectName := fmt.Sprintf("clients/logos/%s/%s", clientId, filename) // Open file src, err := file.Open() if err != nil { return "", "", fmt.Errorf("failed to open uploaded file: %w", err) } defer src.Close() // Upload to MinIO _, err = minioClient.PutObject( context.Background(), bucketName, objectName, src, file.Size, minio.PutObjectOptions{ ContentType: s.getContentType(file.Filename), }, ) if err != nil { return "", "", fmt.Errorf("failed to upload file to MinIO: %w", err) } s.Log.Info(). Str("clientId", clientId). Str("filename", filename). Str("objectName", objectName). Int64("fileSize", file.Size). Msg("Client logo uploaded successfully") return objectName, filename, nil } // DeleteLogo deletes client logo from MinIO func (s *ClientLogoUploadService) DeleteLogo(clientId, imagePath string) error { if imagePath == "" { return nil // Nothing to delete } // Create MinIO connection minioClient, err := s.MinioStorage.ConnectMinio() if err != nil { return fmt.Errorf("failed to connect to MinIO: %w", err) } bucketName := s.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName // Delete from MinIO err = minioClient.RemoveObject(context.Background(), bucketName, imagePath, minio.RemoveObjectOptions{}) if err != nil { return fmt.Errorf("failed to delete file from MinIO: %w", err) } s.Log.Info(). Str("clientId", clientId). Str("imagePath", imagePath). Msg("Client logo deleted successfully") return nil } // GetLogoURL generates a presigned URL for the logo func (s *ClientLogoUploadService) GetLogoURL(imagePath string, expiry time.Duration) (string, error) { if imagePath == "" { return "", nil } // Create MinIO connection minioClient, err := s.MinioStorage.ConnectMinio() if err != nil { return "", fmt.Errorf("failed to connect to MinIO: %w", err) } bucketName := s.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName // Generate presigned URL url, err := minioClient.PresignedGetObject(context.Background(), bucketName, imagePath, expiry, nil) if err != nil { return "", fmt.Errorf("failed to generate presigned URL: %w", err) } return url.String(), nil } // GetLogoFile retrieves logo file from MinIO and returns file data and content type func (s *ClientLogoUploadService) GetLogoFile(clientId, filename string) ([]byte, string, error) { if filename == "" { return nil, "", fmt.Errorf("filename is required") } // Create MinIO connection minioClient, err := s.MinioStorage.ConnectMinio() if err != nil { return nil, "", fmt.Errorf("failed to connect to MinIO: %w", err) } bucketName := s.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName objectName := fmt.Sprintf("clients/logos/%s/%s", clientId, filename) // Get object from MinIO object, err := minioClient.GetObject(context.Background(), bucketName, objectName, minio.GetObjectOptions{}) if err != nil { return nil, "", fmt.Errorf("failed to get object from MinIO: %w", err) } defer object.Close() // Read object data data, err := io.ReadAll(object) if err != nil { return nil, "", fmt.Errorf("failed to read object data: %w", err) } // Get content type based on file extension contentType := s.getContentType(filename) s.Log.Info(). Str("filename", filename). Str("contentType", contentType). Int("dataSize", len(data)). Msg("Logo file retrieved successfully") return data, contentType, nil } // isValidImageType checks if the file extension is a valid image type func (s *ClientLogoUploadService) isValidImageType(filename string) bool { ext := strings.ToLower(filepath.Ext(filename)) validExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"} for _, validExt := range validExts { if ext == validExt { return true } } return false } // generateUniqueFilename generates a unique filename using client name slug + Unix timestamp func (s *ClientLogoUploadService) generateUniqueFilename(originalFilename, clientSlug string) string { ext := filepath.Ext(originalFilename) // Get Unix timestamp now := time.Now() timestamp := now.Unix() // Format: clientname_unix_timestamp.ext return fmt.Sprintf("%s_%d%s", clientSlug, timestamp, ext) } // createSlug converts client name to URL-friendly slug func (s *ClientLogoUploadService) createSlug(name string) string { // Convert to lowercase slug := strings.ToLower(name) // Replace spaces with underscores slug = strings.ReplaceAll(slug, " ", "_") // Replace hyphens with underscores slug = strings.ReplaceAll(slug, "-", "_") // Remove special characters (keep only alphanumeric and underscores) var result strings.Builder for _, char := range slug { if (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char == '_' { result.WriteRune(char) } } slug = result.String() // Remove multiple consecutive underscores for strings.Contains(slug, "__") { slug = strings.ReplaceAll(slug, "__", "_") } // Remove leading/trailing underscores slug = strings.Trim(slug, "_") // If empty after cleaning, use "client" if slug == "" { slug = "client" } return slug } // getContentType returns the MIME type based on file extension func (s *ClientLogoUploadService) getContentType(filename string) string { ext := strings.ToLower(filepath.Ext(filename)) switch ext { case ".jpg", ".jpeg": return "image/jpeg" case ".png": return "image/png" case ".gif": return "image/gif" case ".webp": return "image/webp" default: return "application/octet-stream" } }