From 934618cbdfa158c92e34e277892c659f607270dd Mon Sep 17 00:00:00 2001 From: Leon Date: Wed, 21 Feb 2024 18:11:47 +0800 Subject: [PATCH 1/5] Add virtual path for file uploads --- README.md | 1 + cmd/srv/controller/controller.go | 2 +- config.example.yaml | 1 + internal/gconfig/config.go | 5 +++-- internal/gogin/common.go | 8 ++++++++ service/singleton/singleton.go | 7 +++++++ 6 files changed, 21 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 65e1d7a..2a4b86b 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ rate_limit: enable_cors: false # Enable CORS enable_user_registration: true # Enable user registration upload: + virtual_path: /upload # Virtual path dir: upload # Upload directory max_size: 10485760 # 10MB jwt: # JWT settings diff --git a/cmd/srv/controller/controller.go b/cmd/srv/controller/controller.go index b5d57d3..c7aca61 100644 --- a/cmd/srv/controller/controller.go +++ b/cmd/srv/controller/controller.go @@ -52,7 +52,7 @@ func serveStatic(r *gin.Engine) { r.StaticFS("/static", http.FS(staticFs)) // Serve uploaded files - r.Static("/upload", singleton.Conf.Upload.Dir) + r.Static(singleton.Conf.Upload.VirtualPath, singleton.Conf.Upload.Dir) } // Load templates diff --git a/config.example.yaml b/config.example.yaml index da9a06c..310fac8 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -14,6 +14,7 @@ rate_limit: enable_cors: false # Enable CORS enable_user_registration: true # Enable user registration upload: + virtual_path: /upload # Virtual path dir: upload # Upload directory max_size: 10485760 # 10MB jwt: # JWT settings diff --git a/internal/gconfig/config.go b/internal/gconfig/config.go index aab0c73..faabd0f 100644 --- a/internal/gconfig/config.go +++ b/internal/gconfig/config.go @@ -27,8 +27,9 @@ type Config struct { EnableCORS bool `mapstructure:"enable_cors"` EnableUserRegistration bool `mapstructure:"enable_user_registration"` Upload struct { - Dir string `mapstructure:"dir"` - MaxSize int `mapstructure:"max_size"` + Dir string `mapstructure:"dir"` + VirtualPath string `mapstructure:"virtual_path"` + MaxSize int `mapstructure:"max_size"` } `mapstructure:"upload"` Log struct { Level string `mapstructure:"level"` diff --git a/internal/gogin/common.go b/internal/gogin/common.go index d4b2a47..8c72bd7 100644 --- a/internal/gogin/common.go +++ b/internal/gogin/common.go @@ -35,6 +35,14 @@ func GetCurrentUser(c *gin.Context) (user model.User, err error) { return user, nil } +func GetCurrentUserId(c *gin.Context) uint64 { + user, err := GetCurrentUser(c) + if err != nil { + return 0 + } + return user.ID +} + func ShowErrorPage(c *gin.Context, i mygin.ErrInfo, isPage bool) { if isPage { c.HTML(i.Code, "error", CommonEnvironment(c, gin.H{ diff --git a/service/singleton/singleton.go b/service/singleton/singleton.go index f115f25..9f08d75 100644 --- a/service/singleton/singleton.go +++ b/service/singleton/singleton.go @@ -33,6 +33,13 @@ var ( func LoadSingleton() { LoadCronTasks() LoadNotifications() + InitUpload() +} + +func InitUpload() { + if err := file.MkdirAllIfNotExists(Conf.Upload.Dir, os.ModePerm); err != nil { + panic(err) + } } func InitTimezoneAndCache() { From 34f8bb40b2562f994a123790b6ef3f047e15c169 Mon Sep 17 00:00:00 2001 From: Leon Date: Wed, 21 Feb 2024 18:14:40 +0800 Subject: [PATCH 2/5] Update launch.json and api_v1.go --- .vscode/launch.json | 2 +- cmd/srv/controller/api_v1.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 35523d0..afe2418 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "type": "go", "request": "launch", "mode": "auto", - "program": "${file}" + "program": "cmd/main.go" }, { "name": "Development", diff --git a/cmd/srv/controller/api_v1.go b/cmd/srv/controller/api_v1.go index fd877ba..5114577 100644 --- a/cmd/srv/controller/api_v1.go +++ b/cmd/srv/controller/api_v1.go @@ -22,6 +22,7 @@ func (v *apiV1) serve() { r.Use(gogin.Authorize(gogin.AuthorizeOption{ User: true, IsPage: false, + AllowAPI: true, Msg: "Please log in first", Btn: "Log in", Redirect: fmt.Sprintf("%s/login", singleton.Conf.Site.BaseURL), From 8a078332eaa892dcd64b48eabdece3a74b087c4c Mon Sep 17 00:00:00 2001 From: Leon Date: Wed, 21 Feb 2024 19:22:16 +0800 Subject: [PATCH 3/5] Remove unused import and fix directory creation bug --- pkg/utils/file/file.go | 6 ++---- service/singleton/singleton.go | 7 ++++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pkg/utils/file/file.go b/pkg/utils/file/file.go index 993bac9..7ef60af 100644 --- a/pkg/utils/file/file.go +++ b/pkg/utils/file/file.go @@ -5,7 +5,6 @@ import ( "encoding/hex" "io" "os" - "path" "path/filepath" ) @@ -44,10 +43,9 @@ func FileMD5(filePath string) (string, error) { } func MkdirAllIfNotExists(pathname string, perm os.FileMode) error { - dir := path.Dir(pathname) - if _, err := os.Stat(dir); err != nil { + if _, err := os.Stat(pathname); err != nil { if os.IsNotExist(err) { - if err := os.MkdirAll(dir, perm); err != nil { + if err := os.MkdirAll(pathname, perm); err != nil { return err } } diff --git a/service/singleton/singleton.go b/service/singleton/singleton.go index 9f08d75..53cdc73 100644 --- a/service/singleton/singleton.go +++ b/service/singleton/singleton.go @@ -3,6 +3,7 @@ package singleton import ( "fmt" "os" + "path" "time" _ "github.com/ncruces/go-sqlite3/embed" @@ -73,12 +74,12 @@ func InitLog(conf *gconfig.Config) { } // InitDBFromPath initialize the database from the given path -func InitDBFromPath(path string) { +func InitDBFromPath(dbpath string) { var err error - if err = file.MkdirAllIfNotExists(path, os.ModePerm); err != nil { + if err = file.MkdirAllIfNotExists(path.Dir(dbpath), os.ModePerm); err != nil { panic(err) } - DB, err = gorm.Open(gormlite.Open(path), &gorm.Config{ + DB, err = gorm.Open(gormlite.Open(dbpath), &gorm.Config{ CreateBatchSize: 200, }) if err != nil { From 8da2ec3af73af05924532868798eea909b383e7c Mon Sep 17 00:00:00 2001 From: Leon Date: Wed, 21 Feb 2024 19:25:27 +0800 Subject: [PATCH 4/5] Add clean target to Makefile --- Makefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Makefile b/Makefile index 0cf4698..8fc4d97 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,12 @@ dev: @echo "Starting development service..." @go run cmd/main.go +.PHONY: clean +clean: + @echo "Cleaning up..." + @rm -rf ./release ./dist ./db ./logs ./upload ./cmd/db ./cmd/upload ./cmd/logs + @echo "Cleaned up." + .PHONY: deps deps: @echo "Installing dependencies..." From 7dc4f2489b9570ce9e70089754cf2426024cd0bf Mon Sep 17 00:00:00 2001 From: Leon Date: Wed, 21 Feb 2024 20:42:59 +0800 Subject: [PATCH 5/5] feat: add attachment upload api --- README.md | 10 +++- cmd/srv/controller/api_v1.go | 11 ++++ internal/gconfig/config.go | 9 ++- pkg/mygin/attchment_upload.go | 106 +++++++++++++++++++++++++++++++++ pkg/utils/image/image.go | 29 +++++++++ pkg/utils/utils.go | 40 +++++++++++++ service/singleton/singleton.go | 8 +-- service/singleton/upload.go | 21 +++++++ 8 files changed, 223 insertions(+), 11 deletions(-) create mode 100644 pkg/mygin/attchment_upload.go create mode 100644 pkg/utils/image/image.go create mode 100644 service/singleton/upload.go diff --git a/README.md b/README.md index 2a4b86b..d63a30f 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,15 @@ enable_user_registration: true # Enable user registration upload: virtual_path: /upload # Virtual path dir: upload # Upload directory - max_size: 10485760 # 10MB + max_size: 10485760 # 10MB, unit: byte + keep_original_name: true # Keep original file name + create_date_dir: true # Create date directory + allow_types: # Allowed file types + - image/jpeg + - image/jpg + - image/png + - image/gif + - image/bmp jwt: # JWT settings access_secret: qhkxjrRmYcVYKSEobqsvhxhtPVeTWquu # Access token secret refresh_secret: qhkxjrRmYcVYKSEobqsvhxhtPV3TWquu # Refresh token secret diff --git a/cmd/srv/controller/api_v1.go b/cmd/srv/controller/api_v1.go index 5114577..a1671d1 100644 --- a/cmd/srv/controller/api_v1.go +++ b/cmd/srv/controller/api_v1.go @@ -28,6 +28,8 @@ func (v *apiV1) serve() { Redirect: fmt.Sprintf("%s/login", singleton.Conf.Site.BaseURL), })) + r.POST("/attchment/upload", v.upload) // upload file + r.POST("/post", v.postPost) // create post r.GET("/post/:id", v.getPost) // get post r.DELETE("/post/:id", v.deletePost) // delete post @@ -43,6 +45,15 @@ func (v *apiV1) serve() { var authModel = model.Auth{} +func (v *apiV1) upload(c *gin.Context) { + result, err := singleton.AttchmentUpload.Upload(c) + if err != nil { + mygin.ResponseJSON(c, 400, gin.H{}, err.Error()) + return + } + mygin.ResponseJSON(c, 200, result, "upload success") +} + func (v *apiV1) logout(c *gin.Context) { isPage := parse.ParseBool(c.Query("page"), false) gogin.UserLogout(c) diff --git a/internal/gconfig/config.go b/internal/gconfig/config.go index faabd0f..68974c1 100644 --- a/internal/gconfig/config.go +++ b/internal/gconfig/config.go @@ -27,9 +27,12 @@ type Config struct { EnableCORS bool `mapstructure:"enable_cors"` EnableUserRegistration bool `mapstructure:"enable_user_registration"` Upload struct { - Dir string `mapstructure:"dir"` - VirtualPath string `mapstructure:"virtual_path"` - MaxSize int `mapstructure:"max_size"` + Dir string `mapstructure:"dir"` + VirtualPath string `mapstructure:"virtual_path"` + MaxSize int64 `mapstructure:"max_size"` + KeepOriginalName bool `mapstructure:"keep_original_name"` + CreateDateDir bool `mapstructure:"create_date_dir"` + AllowTypes []string `mapstructure:"allow_types"` } `mapstructure:"upload"` Log struct { Level string `mapstructure:"level"` diff --git a/pkg/mygin/attchment_upload.go b/pkg/mygin/attchment_upload.go new file mode 100644 index 0000000..ff86561 --- /dev/null +++ b/pkg/mygin/attchment_upload.go @@ -0,0 +1,106 @@ +package mygin + +import ( + "fmt" + "go-gin/pkg/utils" + "go-gin/pkg/utils/file" + "go-gin/pkg/utils/image" + "os" + "path" + "path/filepath" + "strings" + "time" + + "github.com/gin-gonic/gin" +) + +type AttchmentUpload struct { + BaseURL string // BaseURL is the base url for the uploaded file + MaxSize int64 // MaxSize is the max file size, default is 2MB + AllowTypes []string // AllowTypes is the allowed file types + FormName string // FormName is the form name for the file, default is "file" + StoreDir string // StoreDir is the directory to store the uploaded file + CreateDateDir bool // CreateDateDir is the flag to create date directory + KeepOriginalName bool // KeepOriginalName is the flag to keep the original file name +} + +type AttchmentUploadResult struct { + Url string `json:"url"` + Name string `json:"name"` + OriginalName string `json:"original_name"` + Size int64 `json:"size"` + MiMe string `json:"mime"` + With int `json:"width"` + Hei int `json:"height"` + Ext string `json:"ext"` + MD5 string `json:"md5"` + SavePath string `json:"save_path"` +} + +func (a *AttchmentUpload) Upload(c *gin.Context) (*AttchmentUploadResult, error) { + result := &AttchmentUploadResult{} + form_file, err := c.FormFile(a.FormName) + if err != nil { + return result, err + } + if form_file.Size > a.MaxSize { + return result, fmt.Errorf("file size too large") + } + form_file_ext := strings.ToLower(filepath.Ext(form_file.Filename)) // eg: .jpg + form_file_fileilename := form_file.Filename + form_file_fileize := form_file.Size + form_file_mime := form_file.Header.Get("Content-Type") + + if len(a.AllowTypes) > 0 && !utils.InArrayString(form_file_mime, a.AllowTypes) { + return result, fmt.Errorf("file type not allowed") + } + + now := time.Now() + year := now.Format("2006") + month := now.Format("01") + day := now.Format("02") + + saveName := utils.GenHexStr(32) + form_file_ext + if a.KeepOriginalName { + saveName = form_file_fileilename + } + + savePath := a.StoreDir + url := fmt.Sprintf("%s/%s", a.BaseURL, saveName) + if a.CreateDateDir { + savePath = path.Join(a.StoreDir, year, month, day) + url = fmt.Sprintf("%s/%s/%s/%s/%s", a.BaseURL, year, month, day, saveName) + } + if err := file.MkdirAllIfNotExists(savePath, os.ModePerm); err != nil { + return result, err + } + + if err := c.SaveUploadedFile(form_file, path.Join(savePath, saveName)); err != nil { + return result, err + } + + md5, _ := file.FileMD5(path.Join(savePath, saveName)) + w, h, _ := image.GetImageSize(path.Join(savePath, saveName)) + + result.Url = url + result.Name = saveName + result.OriginalName = form_file_fileilename + result.Size = form_file_fileize + result.MiMe = form_file_mime + result.Ext = form_file_ext + result.MD5 = md5 + result.With = w + result.Hei = h + result.SavePath = savePath + return result, nil +} + +func NewAttchmentUpload() *AttchmentUpload { + return &AttchmentUpload{ + BaseURL: "/upload", + MaxSize: 1024 * 1024 * 2, + AllowTypes: []string{"image/jpeg", "image/png", "image/gif", "image/jpg"}, + FormName: "file", + StoreDir: "./upload", + } +} diff --git a/pkg/utils/image/image.go b/pkg/utils/image/image.go new file mode 100644 index 0000000..9008c50 --- /dev/null +++ b/pkg/utils/image/image.go @@ -0,0 +1,29 @@ +package image + +import ( + "bytes" + "image" + _ "image/gif" + _ "image/jpeg" + _ "image/png" + + "os" +) + +func GetImageSize(imgpath string) (int, int, error) { + file, err := os.Open(imgpath) + if err != nil { + return 0, 0, err + } + defer file.Close() + img, _, err := image.DecodeConfig(file) + if err != nil { + return 0, 0, err + } + return img.Width, img.Height, nil +} + +func IsImage(data []byte) bool { + _, _, err := image.Decode(bytes.NewReader(data)) + return err == nil +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 456293a..1cbf1cd 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "fmt" "reflect" + "time" "github.com/twinj/uuid" ) @@ -73,3 +74,42 @@ func PrintStructFieldsAndValues(s interface{}, indent string) { } } } + +func InArrayString(needle string, haystack []string) bool { + for _, v := range haystack { + if v == needle { + return true + } + } + return false +} + +func InArrayInt(needle int, haystack []int) bool { + for _, v := range haystack { + if v == needle { + return true + } + } + return false +} + +func InArrayInt64(needle int64, haystack []int64) bool { + for _, v := range haystack { + if v == needle { + return true + } + } + return false +} + +func TimeFormatString(time time.Time, format string) string { + return time.Format(format) +} + +func TimeNowFormatString(format string) string { + return time.Now().Format(format) +} + +func TimeNowString() string { + return TimeNowFormatString("2006-01-02 15:04:05") +} diff --git a/service/singleton/singleton.go b/service/singleton/singleton.go index 53cdc73..b60b2e5 100644 --- a/service/singleton/singleton.go +++ b/service/singleton/singleton.go @@ -34,13 +34,7 @@ var ( func LoadSingleton() { LoadCronTasks() LoadNotifications() - InitUpload() -} - -func InitUpload() { - if err := file.MkdirAllIfNotExists(Conf.Upload.Dir, os.ModePerm); err != nil { - panic(err) - } + LoadUpload() } func InitTimezoneAndCache() { diff --git a/service/singleton/upload.go b/service/singleton/upload.go new file mode 100644 index 0000000..0c4be23 --- /dev/null +++ b/service/singleton/upload.go @@ -0,0 +1,21 @@ +package singleton + +import ( + "go-gin/pkg/mygin" +) + +var ( + AttchmentUpload *mygin.AttchmentUpload +) + +func LoadUpload() { + AttchmentUpload = &mygin.AttchmentUpload{ + BaseURL: Conf.Site.BaseURL + Conf.Upload.VirtualPath, + MaxSize: Conf.Upload.MaxSize, + AllowTypes: Conf.Upload.AllowTypes, + FormName: "file", + StoreDir: Conf.Upload.Dir, + CreateDateDir: Conf.Upload.CreateDateDir, + KeepOriginalName: Conf.Upload.KeepOriginalName, + } +}