Files
evening_detective_server/internal/app/scenario_archive.go
T
2026-10-01 13:19:59 +07:00

101 lines
3.2 KiB
Go

package app
import (
"context"
"errors"
"fmt"
"strings"
"evening_detective_server/internal/modules/roles"
"evening_detective_server/internal/modules/scenario_archive"
"evening_detective_server/internal/modules/string_tools"
"evening_detective_server/internal/repos/scenarios_repo"
"evening_detective_server/internal/services/scenarios_service"
proto "evening_detective_server/proto"
"google.golang.org/genproto/googleapis/api/httpbody"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
func (s *server) DownloadScenarioArchive(ctx context.Context, req *proto.DownloadScenarioArchiveReq) (*httpbody.HttpBody, error) {
claims, claimsErr := claimsFromContext(ctx)
if claimsErr != nil {
return nil, claimsErr
}
if !roles.HasRole(claims, roles.Author) {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
data, name, err := s.scenarioService.DownloadArchive(
ctx,
int(req.Id),
claims.UserID,
roles.HasRole(claims, roles.Admin),
)
if err != nil {
switch {
case errors.Is(err, scenarios_repo.ErrScenarioNotFound):
return nil, status.Errorf(codes.NotFound, "%v", err)
case errors.Is(err, scenarios_service.ErrScenarioNotOwner):
return nil, status.Errorf(codes.PermissionDenied, "%v", err)
default:
return nil, status.Errorf(codes.Internal, "%v", err)
}
}
// Имя файла транслитерируется (заголовки HTTP — только ASCII) и
// санитайзится от header injection; заголовок пробрасывается gateway.
transliterated := sanitizeFilename(string_tools.Transliterate(name))
if transliterated == "" {
transliterated = "scenario"
}
filename := transliterated + ".zip"
if err := grpc.SetHeader(ctx, metadata.Pairs("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))); err != nil {
return nil, status.Errorf(codes.Internal, "failed to set response header: %v", err)
}
return &httpbody.HttpBody{
Data: data,
ContentType: "application/zip",
}, nil
}
func (s *server) UploadScenarioArchive(ctx context.Context, req *httpbody.HttpBody) (*proto.UploadScenarioArchiveRsp, error) {
claims, claimsErr := claimsFromContext(ctx)
if claimsErr != nil {
return nil, claimsErr
}
if !roles.HasRole(claims, roles.Author) {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
if req == nil || len(req.Data) == 0 {
return nil, status.Errorf(codes.InvalidArgument, "пустое тело запроса")
}
if len(req.Data) > scenario_archive.MaxArchiveSize() {
return nil, status.Errorf(codes.InvalidArgument, "архив больше максимального размера (%d байт)", scenario_archive.MaxArchiveSize())
}
id, err := s.scenarioService.UploadArchive(ctx, req.Data, claims.UserID)
if err != nil {
return &proto.UploadScenarioArchiveRsp{
Error: err.Error(),
}, nil
}
return &proto.UploadScenarioArchiveRsp{
Id: int32(id),
}, nil
}
// sanitizeFilename — защита от инъекции заголовков в Content-Disposition.
func sanitizeFilename(name string) string {
replacer := strings.NewReplacer(
`"`, "_",
"\r", "_",
"\n", "_",
)
return replacer.Replace(name)
}