generated from VLADIMIR/template
108 lines
3.5 KiB
Go
108 lines
3.5 KiB
Go
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"
|
|
}
|