package app import ( "bytes" "context" "encoding/json" "errors" "io" "log" "net/http" "os" "strconv" "strings" "time" "evening_detective_server/internal/modules/processor_jwt" results "evening_detective_server/internal/services/game_results_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" ) func (s *server) DownloadGameResultsPackage(ctx context.Context, req *proto.GameResultsPackageReq) (*httpbody.HttpBody, error) { claims, err := claimsFromContext(ctx) if err != nil { return nil, err } if !canExportGame(claims) { return nil, status.Error(codes.PermissionDenied, "permission denied") } if req.GetID() <= 0 { return nil, status.Error(codes.InvalidArgument, "Некорректный идентификатор игры") } var output bytes.Buffer if err := s.gameResultsService.Prepare(ctx, int(req.GetID()), &output); err != nil { return nil, gameResultsErrorStatus(err) } return &httpbody.HttpBody{ContentType: "application/zip", Data: output.Bytes()}, nil } func (s *server) ApplyGameResultsPackage(ctx context.Context, req *proto.ApplyGameResultsPackageReq) (*proto.GameResultsPackageRsp, 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.GetPackage().GetContentType()) { return nil, status.Error(codes.InvalidArgument, "Требуется application/zip") } result, err := s.gameResultsApplyService.Apply(ctx, int(req.GetID()), bytes.NewReader(req.GetPackage().GetData()), claims.UserID) if err != nil { return nil, gameResultsErrorStatus(err) } return mapGameResultsPackage(result.GameID, result.PackageID, result.AppliedAt), nil } // GameResultsDownloadHandler stages a fresh ZIP before sending headers. func GameResultsDownloadHandler(jwt processor_jwt.IProcessorJWT, service *results.GameResultsPackageService) http.Handler { return gameResultsHandler(jwt, func(w http.ResponseWriter, r *http.Request, id, actor int) error { file, err := os.CreateTemp("", "game-results-download-*.zip") if err != nil { return err } defer func() { _ = file.Close(); _ = os.Remove(file.Name()) }() if err := service.Prepare(r.Context(), id, file); err != nil { return err } info, err := file.Stat() if err != nil { return err } if _, err := file.Seek(0, io.SeekStart); err != nil { return err } w.Header().Set("Content-Type", "application/zip") w.Header().Set("Content-Disposition", `attachment; filename="game-results-`+strconv.Itoa(id)+`.zip"`) w.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10)) if _, err := io.Copy(w, file); err != nil && r.Context().Err() == nil { log.Printf("game results download: %v", err) } return nil }) } // GameResultsApplyHandler accepts ZIP streams without Content-Length. func GameResultsApplyHandler(jwt processor_jwt.IProcessorJWT, service *results.GameResultsApplyService) http.Handler { return gameResultsHandler(jwt, func(w http.ResponseWriter, r *http.Request, id, actor int) error { if !isZIP(r.Header.Get("Content-Type")) { return status.Error(codes.InvalidArgument, "Требуется application/zip") } if r.ContentLength > 1<<30 { return results.ErrLimitExceeded } result, err := service.Apply(r.Context(), id, r.Body, actor) if err != nil { return err } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(mapGameResultsPackage(result.GameID, result.PackageID, result.AppliedAt)); err != nil { log.Printf("game results response: %v", err) } return nil }) } func gameResultsHandler(jwt processor_jwt.IProcessorJWT, action func(http.ResponseWriter, *http.Request, int, int) error) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store") w.Header().Set("X-Content-Type-Options", "nosniff") err := func() error { token, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ") if !ok || token == "" { return status.Error(codes.Unauthenticated, "Требуется авторизация") } claims, err := jwt.GetClaims(token) if err != nil { return status.Error(codes.Unauthenticated, "Недействительный токен") } if !canExportGame(claims) { return status.Error(codes.PermissionDenied, "permission denied") } id, err := strconv.ParseInt(r.PathValue("id"), 10, 32) if err != nil || id <= 0 { return status.Error(codes.InvalidArgument, "Некорректный идентификатор игры") } controller := http.NewResponseController(w) if err := controller.SetReadDeadline(time.Now().Add(30 * time.Minute)); err != nil && !errors.Is(err, http.ErrNotSupported) { return err } if err := controller.SetWriteDeadline(time.Now().Add(30 * time.Minute)); err != nil && !errors.Is(err, http.ErrNotSupported) { return err } return action(w, r, int(id), claims.UserID) }() if err == nil { return } if status.Code(err) == codes.Unknown { err = gameResultsErrorStatus(err) } code := runtime.HTTPStatusFromCode(status.Code(err)) if status.Code(err) == codes.ResourceExhausted { code = http.StatusRequestEntityTooLarge } w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) _ = json.NewEncoder(w).Encode(map[string]string{"error": status.Convert(err).Message()}) }) }