feat: file upload imlemented
This commit is contained in:
parent
7623adf2a7
commit
184868b142
7 changed files with 355 additions and 8 deletions
|
|
@ -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:
|
||||
|
|
|
|||
109
api/api.gen.go
109
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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
37
api/paths/media_upload.yaml
Normal file
37
api/paths/media_upload.yaml
Normal file
|
|
@ -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
|
||||
16
go.mod
16
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
|
||||
)
|
||||
|
|
|
|||
20
go.sum
20
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=
|
||||
|
|
|
|||
141
modules/backend/handlers/images.go
Normal file
141
modules/backend/handlers/images.go
Normal file
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue