generated from VLADIMIR/template
154 lines
5.5 KiB
Go
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()})
|
|
})
|
|
}
|