add media service
This commit is contained in:
176
cmd/main.go
176
cmd/main.go
@@ -1,135 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"lendry-erp/media/internal/processor"
|
||||
"lendry-erp/media/internal/storage"
|
||||
"lendry-erp/media/internal/application/usecases"
|
||||
"lendry-erp/media/internal/config"
|
||||
infrastructureGrpc "lendry-erp/media/internal/infrastructure/grpc"
|
||||
"lendry-erp/media/internal/infrastructure/images"
|
||||
"lendry-erp/media/internal/infrastructure/storage"
|
||||
"lendry-erp/media/pkg/logger"
|
||||
)
|
||||
|
||||
// InternalAuthMiddleware защищает Go-сервис.
|
||||
// Он пропускает только те запросы, в которых API Gateway заботливо положил X-User-Id
|
||||
func InternalAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userId := r.Header.Get("X-User-Id")
|
||||
isModerator := r.Header.Get("X-Is-Admin") == "true"
|
||||
func main() {
|
||||
cfg := config.Load()
|
||||
|
||||
if userId == "" {
|
||||
slog.Warn("Блокировка несанкционированного доступа (отсутствует X-User-Id)", "ip", r.RemoteAddr)
|
||||
http.Error(w, `{"error": "Unauthorized: API Gateway only"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
logger.Init(cfg.Logging.Level)
|
||||
logger.Info("🚀 Starting media-service (gRPC only) in %s mode", cfg.App.Env)
|
||||
|
||||
slog.Debug("Запрос авторизован Gateway", "user_id", userId, "moderator", isModerator)
|
||||
next.ServeHTTP(w, r)
|
||||
// 1. Инициализация хранилища
|
||||
mediaStorage, err := storage.NewS3Storage(cfg)
|
||||
if err != nil {
|
||||
logger.Fatal("failed to init S3 storage: %v", err)
|
||||
}
|
||||
logger.Info("✅ S3 storage connected (bucket: %s)", cfg.Storage.Bucket)
|
||||
|
||||
// 2. Инициализация UseCases (Бизнес-логика)
|
||||
imgProcessor := images.NewImageProcessor()
|
||||
uploadUC := usecases.NewUploadUseCase(mediaStorage, imgProcessor)
|
||||
presignUC := usecases.NewPresignUseCase(mediaStorage)
|
||||
|
||||
// 3. Создание gRPC сервера
|
||||
grpcServer := infrastructureGrpc.NewServer(uploadUC, presignUC)
|
||||
|
||||
// 4. Запуск прослушивания порта
|
||||
grpcListener, err := net.Listen("tcp", fmt.Sprintf(":%s", cfg.GRPC.Port))
|
||||
if err != nil {
|
||||
logger.Fatal("failed to listen gRPC: %v", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
logger.Info("gRPC listening on :%s", cfg.GRPC.Port)
|
||||
if err := grpcServer.Serve(grpcListener); err != nil {
|
||||
logger.Fatal("gRPC serve error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 5. Ожидание сигнала остановки
|
||||
waitForShutdown(func() {
|
||||
logger.Warn("🛑 Graceful shutdown started...")
|
||||
grpcServer.GracefulStop()
|
||||
mediaStorage.Close()
|
||||
logger.Info("✅ Shutdown complete")
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Подключение к MinIO
|
||||
minioStore, err := storage.NewMinioStorage("minio:9000", "admin", "admin12345", "erp-media")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
func waitForShutdown(cleanup func()) {
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
// === Эндпоинт 1: ЗАГРУЗКА ПРИВАТНЫХ ФАЙЛОВ ===
|
||||
http.HandleFunc("/upload", InternalAuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseMultipartForm(50 << 20) // Лимит 50 МБ
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, "Файл не найден", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
mode := r.FormValue("mode")
|
||||
if mode == "" {
|
||||
mode = "chat"
|
||||
}
|
||||
|
||||
fileBytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка чтения файла", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
finalBytes, contentType, err := processor.ProcessImage(fileBytes, mode)
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка обработки изображения", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ext := filepath.Ext(header.Filename)
|
||||
if ext == "" {
|
||||
ext = ".jpg"
|
||||
}
|
||||
|
||||
objectName := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
|
||||
|
||||
fileName, err := minioStore.Upload(r.Context(), objectName, bytes.NewReader(finalBytes), int64(len(finalBytes)), contentType)
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка загрузки в MinIO", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"success": true, "fileName": "%s", "size": %d}`, fileName, len(finalBytes))
|
||||
}))
|
||||
|
||||
// === Эндпоинт 2: ПОЛУЧЕНИЕ ПРИВАТНОЙ ССЫЛКИ ===
|
||||
http.HandleFunc("/media/url", InternalAuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
fileName := r.URL.Query().Get("file")
|
||||
if fileName == "" {
|
||||
http.Error(w, "Укажите имя файла", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// ВАЖНО: Раз запрос дошел сюда, значит API Gateway УЖЕ проверил
|
||||
// JWT токен и принадлежность пользователя к чату (через gRPC).
|
||||
// Никакие проверки БД в Go больше не нужны. Мы доверяем Gateway!
|
||||
|
||||
expiry := time.Minute * 2 // Приватные ссылки живут 2 минуты
|
||||
presignedURL, err := minioStore.GeneratePresignedURL(r.Context(), fileName, expiry)
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка генерации ссылки", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"url": "%s"}`, presignedURL)
|
||||
}))
|
||||
|
||||
// === Эндпоинт 3: ПОЛУЧЕНИЕ ПУБЛИЧНОЙ ССЫЛКИ (Аватарки, Баннеры) ===
|
||||
// Обрати внимание: этот эндпоинт НЕ обернут в InternalAuthMiddleware
|
||||
http.HandleFunc("/media/public/url", func(w http.ResponseWriter, r *http.Request) {
|
||||
fileName := r.URL.Query().Get("file")
|
||||
if fileName == "" {
|
||||
http.Error(w, "Укажите имя файла", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Публичные ссылки можно делать более долгоживущими (например, на 24 часа),
|
||||
// чтобы браузер мог их эффективно кэшировать
|
||||
expiry := time.Hour * 24
|
||||
presignedURL, err := minioStore.GeneratePresignedURL(r.Context(), fileName, expiry)
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка генерации ссылки", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"url": "%s"}`, presignedURL)
|
||||
})
|
||||
|
||||
slog.Info("Media Service запущен на внутреннем порту :8081")
|
||||
if err := http.ListenAndServe(":8081", nil); err != nil {
|
||||
slog.Error("Ошибка работы HTTP сервера", "err", err)
|
||||
}
|
||||
cleanup()
|
||||
<-ctx.Done()
|
||||
}
|
||||
Reference in New Issue
Block a user