diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 7b7a6937..7634a817 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -179,7 +179,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon) notificationController := controller.NewNotificationController(notificationService) dashboardController := controller.NewDashboardController(dashboardService) - answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController) + uploadController := controller.NewUploadController(uploaderService) + answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController) swaggerRouter := router.NewSwaggerRouter(swaggerConf) uiRouter := router.NewUIRouter() authUserMiddleware := middleware.NewAuthUserMiddleware(authService) diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index 6765a883..6594df2a 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -43,4 +43,5 @@ const ( InstallCreateTableFailed = "error.database.create_table_failed" InstallConfigFailed = "error.install.create_config_failed" SiteInfoNotFound = "error.site_info.not_found" + UploadFileSourceUnsupported = "error.upload.source_unsupported" ) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 4a76acf7..e23230b1 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -21,4 +21,5 @@ var ProviderSetController = wire.NewSet( NewNotificationController, NewSiteinfoController, NewDashboardController, + NewUploadController, ) diff --git a/internal/controller/upload_controller.go b/internal/controller/upload_controller.go new file mode 100644 index 00000000..20e55fe3 --- /dev/null +++ b/internal/controller/upload_controller.go @@ -0,0 +1,61 @@ +package controller + +import ( + "github.com/answerdev/answer/internal/base/handler" + "github.com/answerdev/answer/internal/base/reason" + "github.com/answerdev/answer/internal/service/uploader" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/errors" +) + +const ( + // file is uploaded by markdown(or something else) editor + fileFromPost = "post" + // file is used to change the user's avatar + fileFromAvatar = "avatar" + // file is logo/icon images + fileFromBranding = "branding" +) + +// UploadController upload controller +type UploadController struct { + uploaderService *uploader.UploaderService +} + +// NewUploadController new controller +func NewUploadController(uploaderService *uploader.UploaderService) *UploadController { + return &UploadController{ + uploaderService: uploaderService, + } +} + +// UploadFile upload file +// @Summary upload file +// @Description upload file +// @Tags Upload +// @Accept multipart/form-data +// @Security ApiKeyAuth +// @Param source formData string true "identify the source of the file upload" Enums(post, avatar, branding) +// @Param file formData file true "file" +// @Success 200 {object} handler.RespBody{data=string} +// @Router /answer/api/v1/file [post] +func (uc *UploadController) UploadFile(ctx *gin.Context) { + var ( + url string + err error + ) + + source := ctx.PostForm("source") + switch source { + case fileFromAvatar: + url, err = uc.uploaderService.UploadAvatarFile(ctx) + case fileFromPost: + url, err = uc.uploaderService.UploadPostFile(ctx) + case fileFromBranding: + url, err = uc.uploaderService.UploadBrandingFile(ctx) + default: + handler.HandleResponse(ctx, errors.BadRequest(reason.UploadFileSourceUnsupported), nil) + return + } + handler.HandleResponse(ctx, err, url) +} diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 92d491fe..e972110a 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -1,10 +1,6 @@ package controller import ( - "net/http" - "path" - "strings" - "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/middleware" "github.com/answerdev/answer/internal/base/reason" @@ -17,7 +13,6 @@ import ( "github.com/answerdev/answer/internal/service/uploader" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" - "github.com/segmentfault/pacman/log" ) // UserController user controller @@ -378,66 +373,6 @@ func (uc *UserController) UserUpdateInterface(ctx *gin.Context) { handler.HandleResponse(ctx, err, nil) } -// UploadUserAvatar godoc -// @Summary UserUpdateInfo -// @Description UserUpdateInfo -// @Tags User -// @Accept multipart/form-data -// @Security ApiKeyAuth -// @Param file formData file true "file" -// @Success 200 {object} handler.RespBody{data=string} -// @Router /answer/api/v1/user/avatar/upload [post] -func (uc *UserController) UploadUserAvatar(ctx *gin.Context) { - // max size - var filesMax int64 = 5 << 20 - var valuesMax int64 = 5 - ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, filesMax+valuesMax) - _, header, err := ctx.Request.FormFile("file") - if err != nil { - log.Error(err.Error()) - handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) - return - } - fileExt := strings.ToLower(path.Ext(header.Filename)) - if fileExt != ".jpg" && fileExt != ".png" && fileExt != ".jpeg" { - log.Errorf("upload file format is not supported: %s", fileExt) - handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) - return - } - - url, err := uc.uploaderService.UploadAvatarFile(ctx, header, fileExt) - handler.HandleResponse(ctx, err, url) -} - -// UploadUserPostFile godoc -// @Summary upload user post file -// @Description upload user post file -// @Tags User -// @Accept multipart/form-data -// @Security ApiKeyAuth -// @Param file formData file true "file" -// @Success 200 {object} handler.RespBody{data=string} -// @Router /answer/api/v1/user/post/file [post] -func (uc *UserController) UploadUserPostFile(ctx *gin.Context) { - // max size - ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024) - _, header, err := ctx.Request.FormFile("file") - if err != nil { - log.Error(err.Error()) - handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) - return - } - fileExt := strings.ToLower(path.Ext(header.Filename)) - if fileExt != ".jpg" && fileExt != ".png" && fileExt != ".jpeg" { - log.Errorf("upload file format is not supported: %s", fileExt) - handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) - return - } - - url, err := uc.uploaderService.UploadPostFile(ctx, header, fileExt) - handler.HandleResponse(ctx, err, url) -} - // ActionRecord godoc // @Summary ActionRecord // @Description ActionRecord diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 53b3e3fe..7020b82f 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -28,6 +28,7 @@ type AnswerAPIRouter struct { siteinfoController *controller.SiteinfoController notificationController *controller.NotificationController dashboardController *controller.DashboardController + uploadController *controller.UploadController } func NewAnswerAPIRouter( @@ -52,7 +53,7 @@ func NewAnswerAPIRouter( siteinfoController *controller.SiteinfoController, notificationController *controller.NotificationController, dashboardController *controller.DashboardController, - + uploadController *controller.UploadController, ) *AnswerAPIRouter { return &AnswerAPIRouter{ langController: langController, @@ -76,6 +77,7 @@ func NewAnswerAPIRouter( notificationController: notificationController, siteinfoController: siteinfoController, dashboardController: dashboardController, + uploadController: uploadController, } } @@ -180,8 +182,6 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { r.PUT("/user/password", a.userController.UserModifyPassWord) r.PUT("/user/info", a.userController.UserUpdateInfo) r.PUT("/user/interface", a.userController.UserUpdateInterface) - r.POST("/user/avatar/upload", a.userController.UploadUserAvatar) - r.POST("/user/post/file", a.userController.UploadUserPostFile) r.POST("/user/notice/set", a.userController.UserNoticeSet) // vote @@ -196,6 +196,9 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { r.GET("/notification/page", a.notificationController.GetList) r.PUT("/notification/read/state/all", a.notificationController.ClearUnRead) r.PUT("/notification/read/state", a.notificationController.ClearIDUnRead) + + // upload file + r.POST("/file", a.uploadController.UploadFile) } func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) { diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go index 1d6d2491..3751623f 100644 --- a/internal/service/uploader/upload.go +++ b/internal/service/uploader/upload.go @@ -4,12 +4,14 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "mime/multipart" + "net/http" "os" "path" "path/filepath" + "strings" + "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/service/service_config" "github.com/answerdev/answer/internal/service/siteinfo_common" @@ -24,6 +26,25 @@ const ( avatarSubPath = "avatar" avatarThumbSubPath = "avatar_thumb" postSubPath = "post" + brandingSubPath = "branding" +) + +var ( + subPathList = []string{ + avatarSubPath, + avatarThumbSubPath, + postSubPath, + brandingSubPath, + } + FormatExts = map[string]imaging.Format{ + ".jpg": imaging.JPEG, + ".jpeg": imaging.JPEG, + ".png": imaging.PNG, + ".gif": imaging.GIF, + ".tif": imaging.TIFF, + ".tiff": imaging.TIFF, + ".bmp": imaging.BMP, + } ) // UploaderService user service @@ -35,13 +56,11 @@ type UploaderService struct { // NewUploaderService new upload service func NewUploaderService(serviceConfig *service_config.ServiceConfig, siteInfoService *siteinfo_common.SiteInfoCommonService) *UploaderService { - err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, avatarSubPath)) - if err != nil { - panic(err) - } - err = dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, postSubPath)) - if err != nil { - panic(err) + for _, subPath := range subPathList { + err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, subPath)) + if err != nil { + panic(err) + } } return &UploaderService{ serviceConfig: serviceConfig, @@ -49,23 +68,26 @@ func NewUploaderService(serviceConfig *service_config.ServiceConfig, } } -func (us *UploaderService) UploadAvatarFile(ctx *gin.Context, file *multipart.FileHeader, fileExt string) ( - url string, err error) { +// UploadAvatarFile upload avatar file +func (us *UploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err error) { + // max size + ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 5*1024*1024) + _, file, err := ctx.Request.FormFile("file") + if err != nil { + handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) + return + } + fileExt := strings.ToLower(path.Ext(file.Filename)) + if _, ok := FormatExts[fileExt]; !ok { + handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) + return + } + newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) avatarFilePath := path.Join(avatarSubPath, newFilename) return us.uploadFile(ctx, file, avatarFilePath) } -var FormatExts = map[string]imaging.Format{ - ".jpg": imaging.JPEG, - ".jpeg": imaging.JPEG, - ".png": imaging.PNG, - ".gif": imaging.GIF, - ".tif": imaging.TIFF, - ".tiff": imaging.TIFF, - ".bmp": imaging.BMP, -} - func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, size int) ( avatarfile []byte, err error) { if size > 1024 { @@ -73,12 +95,12 @@ func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam } thumbFileName := fmt.Sprintf("%d_%d@%s", size, size, fileName) thumbfilePath := fmt.Sprintf("%s/%s/%s", uploadPath, avatarThumbSubPath, thumbFileName) - avatarfile, err = ioutil.ReadFile(thumbfilePath) + avatarfile, err = os.ReadFile(thumbfilePath) if err == nil { return avatarfile, nil } filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, fileName) - avatarfile, err = ioutil.ReadFile(filePath) + avatarfile, err = os.ReadFile(filePath) if err != nil { return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } @@ -117,13 +139,46 @@ func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam return buf.Bytes(), nil } -func (us *UploaderService) UploadPostFile(ctx *gin.Context, file *multipart.FileHeader, fileExt string) ( +func (us *UploaderService) UploadPostFile(ctx *gin.Context) ( url string, err error) { + // max size + ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024) + _, file, err := ctx.Request.FormFile("file") + if err != nil { + handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) + return + } + fileExt := strings.ToLower(path.Ext(file.Filename)) + if _, ok := FormatExts[fileExt]; !ok { + handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) + return + } + newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) avatarFilePath := path.Join(postSubPath, newFilename) return us.uploadFile(ctx, file, avatarFilePath) } +func (us *UploaderService) UploadBrandingFile(ctx *gin.Context) ( + url string, err error) { + // max size + ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024) + _, file, err := ctx.Request.FormFile("file") + if err != nil { + handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) + return + } + fileExt := strings.ToLower(path.Ext(file.Filename)) + if _, ok := FormatExts[fileExt]; !ok { + handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) + return + } + + newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) + avatarFilePath := path.Join(brandingSubPath, newFilename) + return us.uploadFile(ctx, file, avatarFilePath) +} + func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) ( url string, err error) { siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx)