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/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..." diff --git a/README.md b/README.md index 65e1d7a..d63a30f 100644 --- a/README.md +++ b/README.md @@ -98,8 +98,17 @@ 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 + 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 fd877ba..a1671d1 100644 --- a/cmd/srv/controller/api_v1.go +++ b/cmd/srv/controller/api_v1.go @@ -22,11 +22,14 @@ 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), })) + 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 @@ -42,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/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..68974c1 100644 --- a/internal/gconfig/config.go +++ b/internal/gconfig/config.go @@ -27,8 +27,12 @@ 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 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/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/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/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/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 f115f25..b60b2e5 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" @@ -33,6 +34,7 @@ var ( func LoadSingleton() { LoadCronTasks() LoadNotifications() + LoadUpload() } func InitTimezoneAndCache() { @@ -66,12 +68,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 { 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, + } +}