From 184868b142376c800d82988ccf05950db810c9a8 Mon Sep 17 00:00:00 2001 From: Iron_Felix Date: Sat, 6 Dec 2025 04:13:27 +0300 Subject: [PATCH] feat: file upload imlemented --- api/_build/openapi.yaml | 36 ++++++++ api/api.gen.go | 109 ++++++++++++++++++++++ api/openapi.yaml | 4 +- api/paths/media_upload.yaml | 37 ++++++++ go.mod | 16 ++-- go.sum | 20 ++++ modules/backend/handlers/images.go | 141 +++++++++++++++++++++++++++++ 7 files changed, 355 insertions(+), 8 deletions(-) create mode 100644 api/paths/media_upload.yaml create mode 100644 modules/backend/handlers/images.go diff --git a/api/_build/openapi.yaml b/api/_build/openapi.yaml index 5b6f731..9ed5b5f 100644 --- a/api/_build/openapi.yaml +++ b/api/_build/openapi.yaml @@ -527,6 +527,42 @@ paths: description: Internal server error security: - XsrfAuthHeader: [] + /media/upload: + post: + summary: 'Upload an image (PNG, JPEG, or WebP)' + description: | + Uploads a single image file. Supported formats: **PNG**, **JPEG/JPG**, **WebP**. + requestBody: + required: true + content: + encoding: + image: + contentType: 'image/png, image/jpeg, image/webp' + multipart/form-data: + schema: + image: + type: string + format: binary + description: 'Image file (PNG, JPEG, or WebP)' + responses: + '200': + description: Image uploaded successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Image' + '400': + description: 'Bad request — e.g., invalid/malformed image, empty file' + content: + application/json: + schema: + type: string + '415': + description: | + Unsupported Media Type — e.g., request `Content-Type` is not `multipart/form-data`, + or the `image` part has an unsupported `Content-Type` (not image/png, image/jpeg, or image/webp) + '500': + description: Internal server error components: parameters: cursor: diff --git a/api/api.gen.go b/api/api.gen.go index ff37ed9..d93e925 100644 --- a/api/api.gen.go +++ b/api/api.gen.go @@ -7,7 +7,10 @@ import ( "context" "encoding/json" "fmt" + "io" + "mime/multipart" "net/http" + "strings" "time" "github.com/gin-gonic/gin" @@ -181,6 +184,9 @@ type UserTitleStatus string // Cursor defines model for cursor. type Cursor = string +// PostMediaUploadMultipartBody defines parameters for PostMediaUpload. +type PostMediaUploadMultipartBody = interface{} + // GetTitlesParams defines parameters for GetTitles. type GetTitlesParams struct { Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` @@ -271,6 +277,9 @@ type UpdateUserTitleJSONBody struct { Status *UserTitleStatus `json:"status,omitempty"` } +// PostMediaUploadMultipartRequestBody defines body for PostMediaUpload for multipart/form-data ContentType. +type PostMediaUploadMultipartRequestBody = PostMediaUploadMultipartBody + // UpdateUserJSONRequestBody defines body for UpdateUser for application/json ContentType. type UpdateUserJSONRequestBody UpdateUserJSONBody @@ -282,6 +291,9 @@ type UpdateUserTitleJSONRequestBody UpdateUserTitleJSONBody // ServerInterface represents all server handlers. type ServerInterface interface { + // Upload an image (PNG, JPEG, or WebP) + // (POST /media/upload) + PostMediaUpload(c *gin.Context) // Get titles // (GET /titles) GetTitles(c *gin.Context, params GetTitlesParams) @@ -323,6 +335,19 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(c *gin.Context) +// PostMediaUpload operation middleware +func (siw *ServerInterfaceWrapper) PostMediaUpload(c *gin.Context) { + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.PostMediaUpload(c) +} + // GetTitles operation middleware func (siw *ServerInterfaceWrapper) GetTitles(c *gin.Context) { @@ -854,6 +879,7 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options ErrorHandler: errorHandler, } + router.POST(options.BaseURL+"/media/upload", wrapper.PostMediaUpload) router.GET(options.BaseURL+"/titles", wrapper.GetTitles) router.GET(options.BaseURL+"/titles/:title_id", wrapper.GetTitle) router.GET(options.BaseURL+"/users/", wrapper.GetUsers) @@ -866,6 +892,49 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options router.PATCH(options.BaseURL+"/users/:user_id/titles/:title_id", wrapper.UpdateUserTitle) } +type PostMediaUploadRequestObject struct { + Body io.Reader + MultipartBody *multipart.Reader +} + +type PostMediaUploadResponseObject interface { + VisitPostMediaUploadResponse(w http.ResponseWriter) error +} + +type PostMediaUpload200JSONResponse Image + +func (response PostMediaUpload200JSONResponse) VisitPostMediaUploadResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type PostMediaUpload400JSONResponse string + +func (response PostMediaUpload400JSONResponse) VisitPostMediaUploadResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type PostMediaUpload415Response struct { +} + +func (response PostMediaUpload415Response) VisitPostMediaUploadResponse(w http.ResponseWriter) error { + w.WriteHeader(415) + return nil +} + +type PostMediaUpload500Response struct { +} + +func (response PostMediaUpload500Response) VisitPostMediaUploadResponse(w http.ResponseWriter) error { + w.WriteHeader(500) + return nil +} + type GetTitlesRequestObject struct { Params GetTitlesParams } @@ -1403,6 +1472,9 @@ func (response UpdateUserTitle500Response) VisitUpdateUserTitleResponse(w http.R // StrictServerInterface represents all server handlers. type StrictServerInterface interface { + // Upload an image (PNG, JPEG, or WebP) + // (POST /media/upload) + PostMediaUpload(ctx context.Context, request PostMediaUploadRequestObject) (PostMediaUploadResponseObject, error) // Get titles // (GET /titles) GetTitles(ctx context.Context, request GetTitlesRequestObject) (GetTitlesResponseObject, error) @@ -1447,6 +1519,43 @@ type strictHandler struct { middlewares []StrictMiddlewareFunc } +// PostMediaUpload operation middleware +func (sh *strictHandler) PostMediaUpload(ctx *gin.Context) { + var request PostMediaUploadRequestObject + + if strings.HasPrefix(ctx.GetHeader("Content-Type"), "encoding") { + request.Body = ctx.Request.Body + } + if strings.HasPrefix(ctx.GetHeader("Content-Type"), "multipart/form-data") { + if reader, err := ctx.Request.MultipartReader(); err == nil { + request.MultipartBody = reader + } else { + ctx.Error(err) + return + } + } + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.PostMediaUpload(ctx, request.(PostMediaUploadRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PostMediaUpload") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(PostMediaUploadResponseObject); ok { + if err := validResponse.VisitPostMediaUploadResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + // GetTitles operation middleware func (sh *strictHandler) GetTitles(ctx *gin.Context, params GetTitlesParams) { var request GetTitlesRequestObject diff --git a/api/openapi.yaml b/api/openapi.yaml index 0759a54..26813fc 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -19,7 +19,9 @@ paths: $ref: "./paths/users-id-titles.yaml" /users/{user_id}/titles/{title_id}: $ref: "./paths/users-id-titles-id.yaml" - + /media/upload: + $ref: "./paths/media_upload.yaml" + components: parameters: $ref: "./parameters/_index.yaml" diff --git a/api/paths/media_upload.yaml b/api/paths/media_upload.yaml new file mode 100644 index 0000000..0453952 --- /dev/null +++ b/api/paths/media_upload.yaml @@ -0,0 +1,37 @@ +post: + summary: Upload an image (PNG, JPEG, or WebP) + description: | + Uploads a single image file. Supported formats: **PNG**, **JPEG/JPG**, **WebP**. + requestBody: + required: true + content: + multipart/form-data: + schema: + image: + type: string + format: binary + description: Image file (PNG, JPEG, or WebP) + encoding: + image: + contentType: image/png, image/jpeg, image/webp + + responses: + '200': + description: Image uploaded successfully + content: + application/json: + schema: + $ref: "../schemas/Image.yaml" + '400': + description: Bad request — e.g., invalid/malformed image, empty file + content: + application/json: + schema: + type: string + '415': + description: | + Unsupported Media Type — e.g., request `Content-Type` is not `multipart/form-data`, + or the `image` part has an unsupported `Content-Type` (not image/png, image/jpeg, or image/webp) + + '500': + description: Internal server error \ No newline at end of file diff --git a/go.mod b/go.mod index 6662bc1..08a3dc1 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/disintegration/imaging v1.6.2 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -42,12 +43,13 @@ require ( github.com/ugorji/go/codec v1.3.0 // indirect go.uber.org/mock v0.5.0 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.40.0 // indirect - golang.org/x/mod v0.25.0 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.27.0 // indirect - golang.org/x/tools v0.34.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/image v0.33.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/tools v0.38.0 // indirect google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/go.sum b/go.sum index 520a22b..dc41797 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= @@ -103,10 +105,18 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ= +golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -114,11 +124,15 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -131,6 +145,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -144,12 +160,16 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= diff --git a/modules/backend/handlers/images.go b/modules/backend/handlers/images.go new file mode 100644 index 0000000..5309480 --- /dev/null +++ b/modules/backend/handlers/images.go @@ -0,0 +1,141 @@ +package handlers + +import ( + "bytes" + "context" + "fmt" + "image" + "image/jpeg" + "image/png" + "io" + "net/http" + oapi "nyanimedb/api" + "os" + "path/filepath" + "strings" + + "github.com/disintegration/imaging" + log "github.com/sirupsen/logrus" + "golang.org/x/image/webp" +) + +// PostMediaUpload implements oapi.StrictServerInterface. +func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUploadRequestObject) (oapi.PostMediaUploadResponseObject, error) { + // Получаем multipart body + mp := request.MultipartBody + if mp == nil { + log.Errorf("PostMedia without body") + return oapi.PostMediaUpload400JSONResponse("Multipart body is required"), nil + } + + // Парсим первую часть (предполагаем, что файл в поле "file") + part, err := mp.NextPart() + if err != nil { + log.Errorf("PostMedia without file") + return oapi.PostMediaUpload400JSONResponse("File required"), nil + } + defer part.Close() + + // Читаем ВЕСЬ файл в память (для небольших изображений — нормально) + // Если файлы могут быть большими — используйте лимитированный буфер (см. ниже) + data, err := io.ReadAll(part) + if err != nil { + log.Errorf("PostMedia cannot read file") + return oapi.PostMediaUpload400JSONResponse("File required"), nil + } + + if len(data) == 0 { + log.Errorf("PostMedia empty file") + return oapi.PostMediaUpload400JSONResponse("Empty file"), nil + } + + // Проверка MIME по первым 512 байтам + mimeType := http.DetectContentType(data) + if mimeType != "image/jpeg" && mimeType != "image/png" && mimeType != "image/webp" { + log.Errorf("PostMedia bad type") + return oapi.PostMediaUpload400JSONResponse("Bad data type"), nil + } + + // Декодируем изображение из буфера + var img image.Image + switch mimeType { + case "image/jpeg": + { + img, err = jpeg.Decode(bytes.NewReader(data)) + if err != nil { + log.Errorf("PostMedia cannot decode file: %v", err) + return oapi.PostMediaUpload500Response{}, nil + } + } + case "image/png": + { + img, err = png.Decode(bytes.NewReader(data)) + if err != nil { + log.Errorf("PostMedia cannot decode file: %v", err) + return oapi.PostMediaUpload500Response{}, nil + } + } + case "image/webp": + { + img, err = webp.Decode(bytes.NewReader(data)) + if err != nil { + log.Errorf("PostMedia cannot decode file: %v", err) + return oapi.PostMediaUpload500Response{}, nil + } + } + } + + // Перекодируем в чистый JPEG (без EXIF, сжатие, RGB) + var buf bytes.Buffer + err = imaging.Encode(&buf, img, imaging.PNG) + if err != nil { + log.Errorf("PostMedia failed to re-encode JPEG: %v", err) + return oapi.PostMediaUpload500Response{}, nil + } + + // TODO: to delete + filename := part.FileName() + if filename == "" { + filename = "upload_" + generateRandomHex(8) + ".jpg" + } else { + filename = sanitizeFilename(filename) + if !strings.HasSuffix(strings.ToLower(filename), ".jpg") { + filename += ".jpg" + } + } + + // TODO: пойти на хуй ( вызвать файловую помойку) + err = os.WriteFile(filepath.Join("/uploads", filename), buf.Bytes(), 0644) + if err != nil { + log.Errorf("PostMedia failed to write: %v", err) + return oapi.PostMediaUpload500Response{}, nil + } + + return oapi.PostMediaUpload200JSONResponse{}, nil +} + +// Вспомогательные функции — как раньше +func generateRandomHex(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = byte('a' + (i % 16)) + } + return fmt.Sprintf("%x", b) +} + +func sanitizeFilename(name string) string { + var clean strings.Builder + for _, r := range name { + if (r >= 'a' && r <= 'z') || + (r >= 'A' && r <= 'Z') || + (r >= '0' && r <= '9') || + r == '.' || r == '_' || r == '-' { + clean.WriteRune(r) + } + } + s := clean.String() + if s == "" { + return "file" + } + return s +}