package app import ( "bytes" "context" "encoding/json" "errors" "io" "log" "mime" "net/http" "strings" "time" "evening_detective_server/internal/modules/processor_jwt" "evening_detective_server/internal/services/game_import_service" "evening_detective_server/internal/services/game_package_service" "evening_detective_server/proto" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/genproto/googleapis/api/httpbody" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // IGameImporter is shared by the streaming HTTP and bounded gRPC transports. type IGameImporter interface { Import(context.Context, io.Reader, int) (*game_import_service.Result, error) } func (s *server) ImportGame(ctx context.Context, req *httpbody.HttpBody) (*proto.GameImportRsp, error) { claims, err := claimsFromContext(ctx) if err != nil { return nil, err } if !canExportGame(claims) { return nil, status.Error(codes.PermissionDenied, "permission denied") } if !isZIP(req.GetContentType()) { return nil, status.Error(codes.InvalidArgument, "Требуется application/zip") } result, err := s.gameImportService.Import(ctx, bytes.NewReader(req.GetData()), claims.UserID) if err != nil { return nil, gameImportErrorStatus(err) } return mapGameImport(result), nil } // GameImportHandler receives ZIPs directly, without the gRPC message buffer or multipart encoding. func GameImportHandler(jwt processor_jwt.IProcessorJWT, importer IGameImporter) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "application/json") w.Header().Set("X-Content-Type-Options", "nosniff") fail := func(err error) { code := runtime.HTTPStatusFromCode(status.Code(err)) if status.Code(err) == codes.ResourceExhausted { code = http.StatusRequestEntityTooLarge } w.WriteHeader(code) _ = json.NewEncoder(w).Encode(map[string]string{"error": status.Convert(err).Message()}) } token, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ") if !ok || token == "" { fail(status.Error(codes.Unauthenticated, "Требуется авторизация")) return } claims, err := jwt.GetClaims(token) if err != nil { fail(status.Error(codes.Unauthenticated, "Недействительный токен")) return } if !canExportGame(claims) { fail(status.Error(codes.PermissionDenied, "permission denied")) return } if !isZIP(r.Header.Get("Content-Type")) { fail(status.Error(codes.InvalidArgument, "Требуется application/zip")) return } if r.ContentLength > game_package_service.MaxArchiveSize { fail(gameImportErrorStatus(game_package_service.ErrLimitExceeded)) return } if err := http.NewResponseController(w).SetReadDeadline(time.Now().Add(30 * time.Minute)); err != nil && !errors.Is(err, http.ErrNotSupported) { fail(gameImportErrorStatus(err)) return } r.Body = http.MaxBytesReader(w, r.Body, game_package_service.MaxArchiveSize) result, err := importer.Import(r.Context(), r.Body, claims.UserID) if err != nil { fail(gameImportErrorStatus(err)) return } if err := json.NewEncoder(w).Encode(map[string]any{ "gameID": result.GameID, "packageID": result.PackageID, "repeated": result.Repeated, }); err != nil { log.Printf("game import response: %v", err) } }) } func isZIP(contentType string) bool { value, _, err := mime.ParseMediaType(contentType) return err == nil && value == "application/zip" }