feat: unified file upload interface

This commit is contained in:
LinkinStar 2022-11-14 16:27:10 +08:00
parent 23217f92b7
commit f6fa850aa0
7 changed files with 149 additions and 92 deletions

View File

@ -179,7 +179,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon) notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon)
notificationController := controller.NewNotificationController(notificationService) notificationController := controller.NewNotificationController(notificationService)
dashboardController := controller.NewDashboardController(dashboardService) 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) swaggerRouter := router.NewSwaggerRouter(swaggerConf)
uiRouter := router.NewUIRouter() uiRouter := router.NewUIRouter()
authUserMiddleware := middleware.NewAuthUserMiddleware(authService) authUserMiddleware := middleware.NewAuthUserMiddleware(authService)

View File

@ -43,4 +43,5 @@ const (
InstallCreateTableFailed = "error.database.create_table_failed" InstallCreateTableFailed = "error.database.create_table_failed"
InstallConfigFailed = "error.install.create_config_failed" InstallConfigFailed = "error.install.create_config_failed"
SiteInfoNotFound = "error.site_info.not_found" SiteInfoNotFound = "error.site_info.not_found"
UploadFileSourceUnsupported = "error.upload.source_unsupported"
) )

View File

@ -21,4 +21,5 @@ var ProviderSetController = wire.NewSet(
NewNotificationController, NewNotificationController,
NewSiteinfoController, NewSiteinfoController,
NewDashboardController, NewDashboardController,
NewUploadController,
) )

View File

@ -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)
}

View File

@ -1,10 +1,6 @@
package controller package controller
import ( import (
"net/http"
"path"
"strings"
"github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware" "github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/reason"
@ -17,7 +13,6 @@ import (
"github.com/answerdev/answer/internal/service/uploader" "github.com/answerdev/answer/internal/service/uploader"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
) )
// UserController user controller // UserController user controller
@ -378,66 +373,6 @@ func (uc *UserController) UserUpdateInterface(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil) 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 // ActionRecord godoc
// @Summary ActionRecord // @Summary ActionRecord
// @Description ActionRecord // @Description ActionRecord

View File

@ -28,6 +28,7 @@ type AnswerAPIRouter struct {
siteinfoController *controller.SiteinfoController siteinfoController *controller.SiteinfoController
notificationController *controller.NotificationController notificationController *controller.NotificationController
dashboardController *controller.DashboardController dashboardController *controller.DashboardController
uploadController *controller.UploadController
} }
func NewAnswerAPIRouter( func NewAnswerAPIRouter(
@ -52,7 +53,7 @@ func NewAnswerAPIRouter(
siteinfoController *controller.SiteinfoController, siteinfoController *controller.SiteinfoController,
notificationController *controller.NotificationController, notificationController *controller.NotificationController,
dashboardController *controller.DashboardController, dashboardController *controller.DashboardController,
uploadController *controller.UploadController,
) *AnswerAPIRouter { ) *AnswerAPIRouter {
return &AnswerAPIRouter{ return &AnswerAPIRouter{
langController: langController, langController: langController,
@ -76,6 +77,7 @@ func NewAnswerAPIRouter(
notificationController: notificationController, notificationController: notificationController,
siteinfoController: siteinfoController, siteinfoController: siteinfoController,
dashboardController: dashboardController, 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/password", a.userController.UserModifyPassWord)
r.PUT("/user/info", a.userController.UserUpdateInfo) r.PUT("/user/info", a.userController.UserUpdateInfo)
r.PUT("/user/interface", a.userController.UserUpdateInterface) 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) r.POST("/user/notice/set", a.userController.UserNoticeSet)
// vote // vote
@ -196,6 +196,9 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
r.GET("/notification/page", a.notificationController.GetList) r.GET("/notification/page", a.notificationController.GetList)
r.PUT("/notification/read/state/all", a.notificationController.ClearUnRead) r.PUT("/notification/read/state/all", a.notificationController.ClearUnRead)
r.PUT("/notification/read/state", a.notificationController.ClearIDUnRead) r.PUT("/notification/read/state", a.notificationController.ClearIDUnRead)
// upload file
r.POST("/file", a.uploadController.UploadFile)
} }
func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) { func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {

View File

@ -4,12 +4,14 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"mime/multipart" "mime/multipart"
"net/http"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strings"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/service/service_config" "github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/answerdev/answer/internal/service/siteinfo_common"
@ -24,6 +26,25 @@ const (
avatarSubPath = "avatar" avatarSubPath = "avatar"
avatarThumbSubPath = "avatar_thumb" avatarThumbSubPath = "avatar_thumb"
postSubPath = "post" 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 // UploaderService user service
@ -35,13 +56,11 @@ type UploaderService struct {
// NewUploaderService new upload service // NewUploaderService new upload service
func NewUploaderService(serviceConfig *service_config.ServiceConfig, func NewUploaderService(serviceConfig *service_config.ServiceConfig,
siteInfoService *siteinfo_common.SiteInfoCommonService) *UploaderService { siteInfoService *siteinfo_common.SiteInfoCommonService) *UploaderService {
err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, avatarSubPath)) for _, subPath := range subPathList {
err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, subPath))
if err != nil { if err != nil {
panic(err) panic(err)
} }
err = dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, postSubPath))
if err != nil {
panic(err)
} }
return &UploaderService{ return &UploaderService{
serviceConfig: serviceConfig, serviceConfig: serviceConfig,
@ -49,23 +68,26 @@ func NewUploaderService(serviceConfig *service_config.ServiceConfig,
} }
} }
func (us *UploaderService) UploadAvatarFile(ctx *gin.Context, file *multipart.FileHeader, fileExt string) ( // UploadAvatarFile upload avatar file
url string, err error) { 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) newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
avatarFilePath := path.Join(avatarSubPath, newFilename) avatarFilePath := path.Join(avatarSubPath, newFilename)
return us.uploadFile(ctx, file, avatarFilePath) 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) ( func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, size int) (
avatarfile []byte, err error) { avatarfile []byte, err error) {
if size > 1024 { 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) thumbFileName := fmt.Sprintf("%d_%d@%s", size, size, fileName)
thumbfilePath := fmt.Sprintf("%s/%s/%s", uploadPath, avatarThumbSubPath, thumbFileName) thumbfilePath := fmt.Sprintf("%s/%s/%s", uploadPath, avatarThumbSubPath, thumbFileName)
avatarfile, err = ioutil.ReadFile(thumbfilePath) avatarfile, err = os.ReadFile(thumbfilePath)
if err == nil { if err == nil {
return avatarfile, nil return avatarfile, nil
} }
filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, fileName) filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, fileName)
avatarfile, err = ioutil.ReadFile(filePath) avatarfile, err = os.ReadFile(filePath)
if err != nil { if err != nil {
return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack() 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 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) { 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) newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
avatarFilePath := path.Join(postSubPath, newFilename) avatarFilePath := path.Join(postSubPath, newFilename)
return us.uploadFile(ctx, file, avatarFilePath) 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) ( func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) (
url string, err error) { url string, err error) {
siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx) siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx)