Files
2026-10-11 13:26:38 +07:00

154 lines
5.5 KiB
Go

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()})
})
}