diff --git a/.gitignore b/.gitignore index 25ae901..08c7ba8 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out -tmp/log/* +tmp/* .vscode/* conf/app.ini .idea/* diff --git a/config.yaml.example b/config.yaml.example index 982c8cb..3474161 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -4,6 +4,10 @@ app: jwt_expire: '7200' run_mode: 'debug' page_size: 10 + upload_save_path: 'tmp/uploads' + upload_server_path: '/static' + upload_image_max_size: 5 # MB + upload_image_allow_exts: ['.jpg','.jpeg','.png'] server: http_port: 8000 read_timeout: 60 diff --git a/config/app.go b/config/app.go index 3e99f32..69d14a4 100644 --- a/config/app.go +++ b/config/app.go @@ -1,15 +1,19 @@ /* * @Date: 2021-03-22 09:46:19 * @LastEditors: viletyy - * @LastEditTime: 2021-06-10 18:24:42 + * @LastEditTime: 2021-06-11 16:40:34 * @FilePath: /potato/config/app.go */ package config type App struct { - PageSize int64 `mapstructure:"page_size" json:"page_size" yaml:"page_size"` - JwtSecret string `mapstructure:"jwt_secret" json:"jwt_secret" yaml:"jwt_secret"` - JwtIssuser string `mapstructure:"jwt_issuser" json:"jwt_issuser" yaml:"jwt_issuser"` - JwtExpire int64 `mapstructure:"jwt_expire" json:"jwt_expire" yaml:"jwt_expire"` - RunMode string `mapstructure:"run_mode" json:"run_mode" yaml:"run_mode"` + PageSize int64 `mapstructure:"page_size" json:"page_size" yaml:"page_size"` + JwtSecret string `mapstructure:"jwt_secret" json:"jwt_secret" yaml:"jwt_secret"` + JwtIssuser string `mapstructure:"jwt_issuser" json:"jwt_issuser" yaml:"jwt_issuser"` + JwtExpire int64 `mapstructure:"jwt_expire" json:"jwt_expire" yaml:"jwt_expire"` + RunMode string `mapstructure:"run_mode" json:"run_mode" yaml:"run_mode"` + UploadSavePath string `mapstructure:"upload_save_path" json:"upload_save_path" yaml:"upload_save_path"` + UploadServerPath string `mapstructure:"upload_server_path" json:"upload_server_path" yaml:"upload_server_path"` + UploadImageMaxSize int64 `mapstructure:"upload_image_max_size" json:"upload_image_max_size" yaml:"upload_image_max_size"` + UploadImageAllowExts []string `mapstructure:"upload_image_allow_exts" json:"upload_image_allow_exts" yaml:"upload_image_allow_exts"` } diff --git a/docs/docs.go b/docs/docs.go index 1145059..aa22f8a 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -59,6 +59,48 @@ var doc = `{ } } }, + "/v1/upload": { + "post": { + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "summary": "上传文件", + "parameters": [ + { + "type": "string", + "description": "auth by /auth", + "name": "token", + "in": "header", + "required": true + }, + { + "type": "file", + "description": "文件", + "name": "file", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "文件类型", + "name": "type", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "请求成功", + "schema": { + "$ref": "#/definitions/errcode.Error" + } + } + } + } + }, "/v1/vendors": { "get": { "consumes": [ diff --git a/docs/swagger.json b/docs/swagger.json index 3db82b1..7569eea 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -43,6 +43,48 @@ } } }, + "/v1/upload": { + "post": { + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "summary": "上传文件", + "parameters": [ + { + "type": "string", + "description": "auth by /auth", + "name": "token", + "in": "header", + "required": true + }, + { + "type": "file", + "description": "文件", + "name": "file", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "文件类型", + "name": "type", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "请求成功", + "schema": { + "$ref": "#/definitions/errcode.Error" + } + } + } + } + }, "/v1/vendors": { "get": { "consumes": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 510b8e3..6532eb2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -40,6 +40,34 @@ paths: schema: $ref: '#/definitions/errcode.Error' summary: 鉴权验证 + /v1/upload: + post: + consumes: + - multipart/form-data + parameters: + - description: auth by /auth + in: header + name: token + required: true + type: string + - description: 文件 + in: formData + name: file + required: true + type: file + - description: 文件类型 + in: formData + name: type + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 请求成功 + schema: + $ref: '#/definitions/errcode.Error' + summary: 上传文件 /v1/vendors: get: consumes: diff --git a/internal/controller/api/v1/upload.go b/internal/controller/api/v1/upload.go new file mode 100644 index 0000000..4f30c70 --- /dev/null +++ b/internal/controller/api/v1/upload.go @@ -0,0 +1,64 @@ +/* + * @Date: 2021-06-11 17:27:25 + * @LastEditors: viletyy + * @LastEditTime: 2021-06-11 17:54:34 + * @FilePath: /potato/internal/controller/api/v1/upload.go + */ +package v1 + +import ( + "github.com/gin-gonic/gin" + "github.com/viletyy/potato/global" + "github.com/viletyy/potato/internal/service" + "github.com/viletyy/potato/pkg/app" + "github.com/viletyy/potato/pkg/errcode" + "github.com/viletyy/potato/pkg/upload" + "github.com/viletyy/yolk/convert" +) + +type Upload struct{} + +func NewUpload() Upload { + return Upload{} +} + +// @Summary 上传文件 +// @Description +// @Accept mpfd +// @Produce json +// @Param token header string true "auth by /auth" +// @Param file formData file true "文件" +// @Param type formData int true "文件类型" +// @Success 200 {object} errcode.Error "请求成功" +// @Router /v1/upload [post] +func (u Upload) Create(c *gin.Context) { + response := app.NewResponse(c) + file, fileHeader, err := c.Request.FormFile("file") + if err != nil { + global.GO_LOG.Sugar().Errorf("c.Request.FormFile err: %v", err) + errorCode := errcode.InvalidParams + errorCode.WithData(err) + response.ToErrorResponse(errorCode) + return + } + + fileType, err := convert.StrTo(c.PostForm("type")).Int() + if err != nil { + global.GO_LOG.Sugar().Errorf("convert.StrTo err: %v", err) + response.ToErrorResponse(errcode.InvalidParams) + } + + svc := service.New(c.Request.Context()) + fileInfo, err := svc.UploadFile(upload.FileType(fileType), file, fileHeader) + if err != nil { + global.GO_LOG.Sugar().Errorf("svc.UploadFile err :v", err) + errorCode := errcode.ErrorUploadFileFail + errorCode.WithData(err) + response.ToErrorResponse(errorCode) + return + } + + response.ToResponse(gin.H{ + "file_access_url": fileInfo.AccessUrl, + }) +} diff --git a/internal/routers/router.go b/internal/routers/router.go index ec9009a..7c0dd57 100644 --- a/internal/routers/router.go +++ b/internal/routers/router.go @@ -1,12 +1,14 @@ /* * @Date: 2021-03-21 19:54:57 * @LastEditors: viletyy - * @LastEditTime: 2021-06-10 19:02:31 + * @LastEditTime: 2021-06-11 17:57:21 * @FilePath: /potato/internal/routers/router.go */ package routers import ( + "net/http" + "github.com/gin-gonic/gin" _ "github.com/swaggo/gin-swagger" ginSwagger "github.com/swaggo/gin-swagger" @@ -32,6 +34,7 @@ func InitRouter() *gin.Engine { gin.SetMode(global.GO_CONFIG.App.RunMode) Engine.Use(middleware.CORS()) + Engine.StaticFS("/static", http.Dir(global.GO_CONFIG.App.UploadSavePath)) Engine.POST("/api/v1/auth", v1.GetAuth) Engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) @@ -43,5 +46,7 @@ func InitRouter() *gin.Engine { func V1InitModule() { V1RouterGroup.Use(middleware.JWT()) + upload := v1.NewUpload() + V1RouterGroup.POST("/upload", upload.Create) V1InitBasicRouter() } diff --git a/internal/service/upload.go b/internal/service/upload.go new file mode 100644 index 0000000..f32ada8 --- /dev/null +++ b/internal/service/upload.go @@ -0,0 +1,50 @@ +/* + * @Date: 2021-06-11 17:07:48 + * @LastEditors: viletyy + * @LastEditTime: 2021-06-11 17:24:45 + * @FilePath: /potato/internal/service/upload.go + */ +package service + +import ( + "errors" + "mime/multipart" + "os" + + "github.com/viletyy/potato/global" + "github.com/viletyy/potato/pkg/upload" +) + +type FileInfo struct { + Name string + AccessUrl string +} + +func (svc *Service) UploadFile(fileType upload.FileType, file multipart.File, fileHeader *multipart.FileHeader) (*FileInfo, error) { + fileName := upload.GetFileName(fileHeader.Filename) + if !upload.CheckContainExt(fileType, fileName) { + return nil, errors.New("file suffix is not supported.") + } + if !upload.CheckMaxSize(fileType, file) { + return nil, errors.New("exceeded maximum file limit.") + } + + uploadSavePath := upload.GetSavePath() + if upload.CheckSavePath(uploadSavePath) { + if err := upload.CreateSavePath(uploadSavePath, os.ModePerm); err != nil { + return nil, errors.New("failed to create save directory.") + } + } + + if upload.CheckPermission(uploadSavePath) { + return nil, errors.New("insufficient file permissions.") + } + + dst := uploadSavePath + "/" + fileName + if err := upload.SaveFile(fileHeader, dst); err != nil { + return nil, err + } + + accessUrl := global.GO_CONFIG.App.UploadServerPath + "/" + fileName + return &FileInfo{Name: fileName, AccessUrl: accessUrl}, nil +} diff --git a/pkg/errcode/module_code.go b/pkg/errcode/module_code.go index 248058f..5047b61 100644 --- a/pkg/errcode/module_code.go +++ b/pkg/errcode/module_code.go @@ -1,12 +1,13 @@ /* * @Date: 2021-06-10 23:09:09 * @LastEditors: viletyy - * @LastEditTime: 2021-06-11 15:16:59 + * @LastEditTime: 2021-06-11 17:25:58 * @FilePath: /potato/pkg/errcode/module_code.go */ package errcode var ( + ErrorUploadFileFail = NewError(20001, "上传文件失败") ErrorGetVendorListFail = NewError(20101, "获取系统厂商列表失败") ErrorGetVendorFail = NewError(20102, "获取系统厂商失败") ErrorCreateVendorFail = NewError(20103, "创建系统厂商失败") diff --git a/pkg/upload/file.go b/pkg/upload/file.go new file mode 100644 index 0000000..3e631fa --- /dev/null +++ b/pkg/upload/file.go @@ -0,0 +1,107 @@ +/* + * @Date: 2021-06-11 16:01:37 + * @LastEditors: viletyy + * @LastEditTime: 2021-06-11 17:07:43 + * @FilePath: /potato/pkg/upload/file.go + */ +package upload + +import ( + "io" + "io/ioutil" + "mime/multipart" + "os" + "path" + "strings" + + "github.com/viletyy/potato/global" + "github.com/viletyy/yolk/crypt" +) + +type FileType int + +const ( + TypeImage FileType = iota + 1 + TypeExcel + TypeTxt +) + +func GetFileName(name string) string { + ext := GetFileExt(name) + fileName := strings.TrimSuffix(name, ext) + fileName = crypt.Md5Encode(fileName) + + return fileName + ext +} + +func GetFileExt(name string) string { + return path.Ext(name) +} + +func GetSavePath() string { + return global.GO_CONFIG.App.UploadSavePath +} + +func CheckSavePath(dst string) bool { + _, err := os.Stat(dst) + return os.IsNotExist(err) +} + +func CheckContainExt(t FileType, name string) bool { + ext := GetFileExt(name) + ext = strings.ToUpper(ext) + switch t { + case TypeImage: + for _, allowExt := range global.GO_CONFIG.App.UploadImageAllowExts { + if strings.ToUpper(allowExt) == ext { + return true + } + } + } + + return false +} + +func CheckMaxSize(t FileType, f multipart.File) bool { + content, _ := ioutil.ReadAll(f) + size := len(content) + switch t { + case TypeImage: + if size <= int(global.GO_CONFIG.App.UploadImageMaxSize*1024*1024) { + return true + } + } + + return false +} + +func CheckPermission(dst string) bool { + _, err := os.Stat(dst) + return os.IsPermission(err) +} + +func CreateSavePath(dst string, perm os.FileMode) error { + err := os.MkdirAll(dst, perm) + if err != nil { + return err + } + + return nil +} + +func SaveFile(file *multipart.FileHeader, dst string) error { + src, err := file.Open() + if err != nil { + return err + } + defer src.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, src) + return err +}