diff --git a/api/docs.go b/api/docs.go index f750e239..4abff7ea 100644 --- a/api/docs.go +++ b/api/docs.go @@ -322,6 +322,71 @@ const docTemplate = `{ "responses": {} } }, + "/challenges/{id}/attachment": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Challenge" + ], + "summary": "查找附件", + "responses": {} + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Challenge" + ], + "summary": "保存附件", + "parameters": [ + { + "type": "file", + "description": "attachment", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": {} + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Challenge" + ], + "summary": "删除附件", + "responses": {} + } + }, "/challenges/{id}/flags": { "post": { "security": [ @@ -983,31 +1048,6 @@ const docTemplate = `{ "responses": {} } }, - "/media/games/writeups/{id}": { - "get": { - "description": "通过团队 Id 获取比赛 Writeup", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Media" - ], - "summary": "通过团队 Id 获取比赛 Writeup", - "parameters": [ - { - "type": "string", - "description": "团队 Id", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": {} - } - }, "/pods/": { "get": { "security": [ diff --git a/api/swagger.json b/api/swagger.json index d56babbe..729ecb57 100644 --- a/api/swagger.json +++ b/api/swagger.json @@ -313,6 +313,71 @@ "responses": {} } }, + "/challenges/{id}/attachment": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Challenge" + ], + "summary": "查找附件", + "responses": {} + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Challenge" + ], + "summary": "保存附件", + "parameters": [ + { + "type": "file", + "description": "attachment", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": {} + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Challenge" + ], + "summary": "删除附件", + "responses": {} + } + }, "/challenges/{id}/flags": { "post": { "security": [ @@ -974,31 +1039,6 @@ "responses": {} } }, - "/media/games/writeups/{id}": { - "get": { - "description": "通过团队 Id 获取比赛 Writeup", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Media" - ], - "summary": "通过团队 Id 获取比赛 Writeup", - "parameters": [ - { - "type": "string", - "description": "团队 Id", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": {} - } - }, "/pods/": { "get": { "security": [ diff --git a/api/swagger.yaml b/api/swagger.yaml index 7574c3b1..018e04ad 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -995,6 +995,46 @@ paths: summary: 更新题目 tags: - Challenge + /challenges/{id}/attachment: + delete: + consumes: + - application/json + produces: + - application/json + responses: {} + security: + - ApiKeyAuth: [] + summary: 删除附件 + tags: + - Challenge + get: + consumes: + - application/json + produces: + - application/json + responses: {} + security: + - ApiKeyAuth: [] + summary: 查找附件 + tags: + - Challenge + post: + consumes: + - application/json + parameters: + - description: attachment + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: {} + security: + - ApiKeyAuth: [] + summary: 保存附件 + tags: + - Challenge /challenges/{id}/flags: post: consumes: @@ -1399,23 +1439,6 @@ paths: summary: 允许加入比赛 tags: - Game - /media/games/writeups/{id}: - get: - consumes: - - application/json - description: 通过团队 Id 获取比赛 Writeup - parameters: - - description: 团队 Id - in: path - name: id - required: true - type: string - produces: - - application/json - responses: {} - summary: 通过团队 Id 获取比赛 Writeup - tags: - - Media /pods/: get: description: 实例查询 diff --git a/internal/controller/challenge.go b/internal/controller/challenge.go index b34d75a8..5d896fbd 100644 --- a/internal/controller/challenge.go +++ b/internal/controller/challenge.go @@ -27,12 +27,16 @@ type IChallengeController interface { CreateFlag(ctx *gin.Context) UpdateFlag(ctx *gin.Context) DeleteFlag(ctx *gin.Context) + FindAttachment(ctx *gin.Context) + SaveAttachment(ctx *gin.Context) + DeleteAttachment(ctx *gin.Context) } type ChallengeController struct { challengeService service.IChallengeService flagService service.IFlagService hintService service.IHintService + mediaService service.IMediaService } func NewChallengeController(appService *service.Service) IChallengeController { @@ -40,6 +44,7 @@ func NewChallengeController(appService *service.Service) IChallengeController { challengeService: appService.ChallengeService, flagService: appService.FlagService, hintService: appService.HintService, + mediaService: appService.MediaService, } } @@ -338,3 +343,83 @@ func (c *ChallengeController) DeleteFlag(ctx *gin.Context) { "code": http.StatusOK, }) } + +// FindAttachment +// @Summary 查找附件 +// @Description +// @Tags Challenge +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Router /challenges/{id}/attachment [get] +func (c *ChallengeController) FindAttachment(ctx *gin.Context) { + id := convertor.ToUintD(ctx.Param("id"), 0) + filename, size, err := c.mediaService.FindChallengeAttachment(id) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": http.StatusBadRequest, + "msg": err.Error(), + }) + return + } + ctx.JSON(http.StatusOK, gin.H{ + "code": http.StatusOK, + "filename": filename, + "size": size, + }) +} + +// SaveAttachment +// @Summary 保存附件 +// @Description +// @Tags Challenge +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param file formData file true "attachment" +// @Router /challenges/{id}/attachment [post] +func (c *ChallengeController) SaveAttachment(ctx *gin.Context) { + id := convertor.ToUintD(ctx.Param("id"), 0) + fileHeader, err := ctx.FormFile("file") + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": http.StatusBadRequest, + "msg": err.Error(), + }) + return + } + err = c.mediaService.SaveChallengeAttachment(id, fileHeader) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": http.StatusBadRequest, + "msg": err.Error(), + }) + return + } + ctx.JSON(http.StatusOK, gin.H{ + "code": http.StatusOK, + }) +} + +// DeleteAttachment +// @Summary 删除附件 +// @Description +// @Tags Challenge +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Router /challenges/{id}/attachment [delete] +func (c *ChallengeController) DeleteAttachment(ctx *gin.Context) { + id := convertor.ToUintD(ctx.Param("id"), 0) + err := c.mediaService.DeleteChallengeAttachment(id) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": http.StatusBadRequest, + "msg": err.Error(), + }) + return + } + ctx.JSON(http.StatusOK, gin.H{ + "code": http.StatusOK, + }) +} diff --git a/internal/controller/media.go b/internal/controller/media.go index 36fa6a4e..29f1818d 100644 --- a/internal/controller/media.go +++ b/internal/controller/media.go @@ -1,18 +1,16 @@ package controller import ( - "fmt" "github.com/elabosak233/cloudsdale/internal/config" "github.com/elabosak233/cloudsdale/internal/service" - "github.com/gabriel-vasile/mimetype" "github.com/gin-gonic/gin" - "mime/multipart" "net/http" "os" + "path" ) type IMediaController interface { - FindGameWriteUpByTeamId(ctx *gin.Context) + GetFile(ctx *gin.Context) } type MediaController struct { @@ -25,37 +23,13 @@ func NewMediaController(appService *service.Service) IMediaController { } } -// FindGameWriteUpByTeamId -// @Summary 通过团队 Id 获取比赛 Writeup -// @Description 通过团队 Id 获取比赛 Writeup -// @Tags Media -// @Accept json -// @Produce json -// @Param id path string true "团队 Id" -// @Router /media/games/writeups/{id} [get] -func (c *MediaController) FindGameWriteUpByTeamId(ctx *gin.Context) { - id := ctx.Param("id") - path := fmt.Sprintf("%s/games/writeups/%s.pdf", config.AppCfg().Gin.Paths.Media, id) - _, err := os.Stat(path) - if err == nil { - ctx.File(path) - } else { +func (m *MediaController) GetFile(ctx *gin.Context) { + a := ctx.Param("path") + p := path.Join(config.AppCfg().Gin.Paths.Media, a) + _, err := os.Stat(p) + if os.IsNotExist(err) { ctx.Status(http.StatusNotFound) + return } -} - -func (c *MediaController) detectContentType(file *multipart.FileHeader) (mime *mimetype.MIME, err error) { - src, err := file.Open() - if err != nil { - return nil, err - } - defer func(src multipart.File) { - _ = src.Close() - }(src) - buffer := make([]byte, 512) - _, err = src.Read(buffer) - if err != nil { - return nil, err - } - return mimetype.Detect(buffer), nil + ctx.File(p) } diff --git a/internal/router/challenge.go b/internal/router/challenge.go index 87048a73..a5992ab6 100644 --- a/internal/router/challenge.go +++ b/internal/router/challenge.go @@ -34,6 +34,9 @@ func (c *ChallengeRouter) Register() { c.router.POST("/:id/flags", c.controller.CreateFlag) c.router.PUT("/:id/flags/:flag_id", c.controller.UpdateFlag) c.router.DELETE("/:id/flags/:flag_id", c.controller.DeleteFlag) + c.router.GET("/:id/attachment", c.controller.FindAttachment) + c.router.POST("/:id/attachment", c.controller.SaveAttachment) + c.router.DELETE("/:id/attachment", c.controller.DeleteAttachment) } func (c *ChallengeRouter) PreProcess() gin.HandlerFunc { diff --git a/internal/router/media.go b/internal/router/media.go index ad2418d1..e295b98c 100644 --- a/internal/router/media.go +++ b/internal/router/media.go @@ -22,5 +22,5 @@ func NewMediaRouter(mediaRouter *gin.RouterGroup, mediaController controller.IMe } func (m *MediaRouter) Register() { - m.router.GET("/games/writeups/:id", m.controller.FindGameWriteUpByTeamId) + m.router.GET("/*path", m.controller.GetFile) } diff --git a/internal/service/challenge.go b/internal/service/challenge.go index 4fbd2560..6f08d9da 100644 --- a/internal/service/challenge.go +++ b/internal/service/challenge.go @@ -45,9 +45,9 @@ func (t *ChallengeService) Create(req request.ChallengeCreateRequest) (err error } func (t *ChallengeService) Update(req request.ChallengeUpdateRequest) (err error) { - challengeModel := model.Challenge{} - _ = mapstructure.Decode(req, &challengeModel) - challengeModel, err = t.challengeRepository.Update(challengeModel) + challenge := model.Challenge{} + _ = mapstructure.Decode(req, &challenge) + challenge, err = t.challengeRepository.Update(challenge) return err } diff --git a/internal/service/media.go b/internal/service/media.go index d6b35c40..bf0d5dc0 100644 --- a/internal/service/media.go +++ b/internal/service/media.go @@ -1,6 +1,18 @@ package service +import ( + "fmt" + "github.com/elabosak233/cloudsdale/internal/config" + "io" + "mime/multipart" + "os" + "path" +) + type IMediaService interface { + FindChallengeAttachment(id uint) (filename string, size int64, err error) + SaveChallengeAttachment(id uint, fileHeader *multipart.FileHeader) (err error) + DeleteChallengeAttachment(id uint) (err error) } type MediaService struct{} @@ -8,3 +20,38 @@ type MediaService struct{} func NewMediaService() IMediaService { return &MediaService{} } + +func (m *MediaService) FindChallengeAttachment(id uint) (filename string, size int64, err error) { + p := path.Join(config.AppCfg().Gin.Paths.Media, "/challenges", fmt.Sprintf("%d", id)) + files, err := os.ReadDir(p) + for _, file := range files { + filename = file.Name() + info, _ := file.Info() + size = info.Size() + break + } + return filename, size, err +} + +func (m *MediaService) SaveChallengeAttachment(id uint, fileHeader *multipart.FileHeader) (err error) { + file, err := fileHeader.Open() + defer func(file multipart.File) { + _ = file.Close() + }(file) + data, err := io.ReadAll(file) + p := path.Join(config.AppCfg().Gin.Paths.Media, "/challenges", fmt.Sprintf("%d", id), fileHeader.Filename) + err = m.DeleteChallengeAttachment(id) + dir := path.Dir(p) + if _, err = os.Stat(dir); os.IsNotExist(err) { + if err = os.MkdirAll(dir, 0755); err != nil { + return err + } + } + err = os.WriteFile(p, data, 0644) + return err +} + +func (m *MediaService) DeleteChallengeAttachment(id uint) (err error) { + p := path.Join(config.AppCfg().Gin.Paths.Media, "/challenges", fmt.Sprintf("%d", id)) + return os.RemoveAll(p) +}