diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8e7702e..f8f8e7c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -88,7 +88,7 @@ jobs: docker-compose.yml Dockerfile config.example.yaml - draft: true + draft: false generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.vscode/launch.json b/.vscode/launch.json index afe2418..3157f72 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,6 +6,7 @@ "type": "go", "request": "launch", "mode": "auto", + "cwd": "${workspaceFolder}", "program": "cmd/main.go" }, { @@ -13,7 +14,11 @@ "type": "go", "request": "launch", "mode": "auto", - "args": ["-c", "dev"], + "cwd": "${workspaceFolder}", + "args": [ + "-c", + "dev" + ], "program": "cmd/main.go" }, { @@ -21,7 +26,11 @@ "type": "go", "request": "launch", "mode": "auto", - "args": ["-c", "prod"], + "cwd": "${workspaceFolder}", + "args": [ + "-c", + "prod" + ], "program": "cmd/main.go" }, { @@ -29,7 +38,11 @@ "type": "go", "request": "launch", "mode": "auto", - "args": ["-c", "test"], + "cwd": "${workspaceFolder}", + "args": [ + "-c", + "test" + ], "program": "cmd/main.go" } ] diff --git a/README.md b/README.md index 1d08a58..34be1ab 100644 --- a/README.md +++ b/README.md @@ -19,25 +19,17 @@ If you want to develop with this project, you can follow the steps below. ```bash git clone git@github.com:funnyzak/go-gin.git && cd go-gin ``` - -2. Copy the `config.example.yaml` file to `config.yaml` and update the values. - ```bash - cp config.example.yaml config.yaml - ``` - -3. Run the application. +2. Run the application. ```bash go run cmd/main.go - # or make dev - - # You also specify the config file, e.g. dev, prod, etc. - go run cmd/main.go -c dev ``` +**Note:** The application will load the configuration from the `config.yaml` file in the root directory by default. If you want to use a different configuration file, you can copy `config.example.yaml` to `prod.yaml` and update the values. specify it using the `-c` parameter, for example: `go run cmd/main.go -c prod`, it will load the `prod.yaml` configuration file. + ### CI/CD You can fork this repository and add Secrets Keys: `DOCKER_USERNAME` and `DOCKER_PASSWORD` in the repository settings. And when you push the code, it will automatically build binary and docker image and push to the Docker Hub. @@ -79,84 +71,7 @@ You can fork this repository and add Secrets Keys: `DOCKER_USERNAME` and `DOCKER ## Configuration -The configuration file is in the `config.yaml` file, you can copy the `config.example.yaml` file to `config.yaml` and update the values, the configuration file is as follows: - -```yaml -server: - port: 8080 # Server port -site: - brand: Go-Gin # Site brand - description: A simple web application using Go and Gin # Site description - base_url: http://localhost:8080 # Site base URL, used for generating absolute URLs -debug: false # Debug mode, if true, the server will print detailed error messages -log: - level: debug # debug, info, warn, error, fatal, panic - path: logs/go-gin.log # Log file path -db_path: db/go-gin.sqlite # Database path -rate_limit: - max: 100 # requests per minute, 0 means no limit -enable_cors: false # Enable CORS -enable_user_registration: true # Enable user registration -upload: - virtual_path: /upload # Virtual path, used for generating absolute URLs, must start with / - dir: upload # Upload directory, relative to the project root directory. Or you can use an absolute path, such as /var/www/upload - max_size: 10485760 # 10MB, unit: byte - keep_original_name: false # Keep original file name, if false, the server will generate a random file name - create_date_dir: true # Create date directory, such as /upload/2021/01/01 - allow_types: # Allowed file types, if empty, all types are allowed - - 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 - access_token_expiration: 60 # minutes - refresh_token_expiration: 720 # minutes - access_token_cookie_name: go-gin-access # Access token cookie name - refresh_token_cookie_name: go-gin-refresh # Refresh token cookie name -location: Asia/Chongqing # Timezone -notifications: # Notification settings - - type: apprise # You must install apprise first, more details: https://github.com/caronc/apprise - instances: - - url: "apprise-url-1" - - url: "apprise-url-2" - - type: dingtalk - instances: - - webhook: "dingtalk-webhook-1" - - webhook: "dingtalk-webhook-2" - - type: ifttt - instances: - - key: "ifttt-key-1" - event: "event-1" - - key: "ifttt-key-2" - event: "event-2" - - type: smtp - instances: - - host: "smtp-host-1" - port: 587 - username: "user-1" - password: "password-1" - from: "from-1" - to: "to-1" - - host: "smtp-host-2" - port: 587 - username: "user-2" - password: "password-2" - from: "from-2" - to: "to-2" - - type: telegram - instances: - - botToken: "telegram-bot-token-1" - chatID: "chat-id-1" - - botToken: "telegram-bot-token-2" - chatID: "chat-id-2" - - type: wecom - instances: - - key: "wecom-key-1" - - key: "wecom-key-2" -``` +Service configuration via `yaml` format. The configuration file is located in the root directory of the project and is named `config.example.yaml`. You can copy this file to `config.yaml` and update the values. More details can be found in the [config.example.yaml](https://github.com/funnyzak/go-gin/blob/main/config.example.yaml) file. ## Build diff --git a/cmd/main.go b/cmd/main.go index d1e7211..689ea3f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "net/http" "github.com/gin-gonic/gin" "github.com/ory/graceful" @@ -15,89 +16,100 @@ import ( "go-gin/service/singleton" ) -type CliParam struct { +type ClIParam struct { Version bool // Show version ConfigName string // Config file name Port uint // Server port + Debug bool // Debug mode } -var ( - cliParam CliParam -) +var cliParam ClIParam -func init() { +func parseCommandLineParams() (cliParam ClIParam) { flag.CommandLine.ParseErrorsWhitelist.UnknownFlags = true flag.BoolVarP(&cliParam.Version, "version", "v", false, "show version") flag.StringVarP(&cliParam.ConfigName, "config", "c", "config", "config file name") flag.UintVarP(&cliParam.Port, "port", "p", 0, "server port") + flag.BoolVarP(&cliParam.Debug, "debug", "d", false, "debug mode") flag.Parse() flag.Lookup("config").NoOptDefVal = "config" - singleton.InitConfig(cliParam.ConfigName) - singleton.InitLog(singleton.Conf) - singleton.InitTimezoneAndCache() - singleton.InitDBFromPath(singleton.Conf.DBPath) - initService() + return cliParam } -func main() { - if cliParam.Version { - fmt.Println(singleton.Version) - return - } +func loadConfig() { + cliParam = parseCommandLineParams() - port := singleton.Conf.Server.Port - if cliParam.Port != 0 { - port = cliParam.Port + singleton.InitConfig(cliParam.ConfigName) + if cliParam.Port > 0 && cliParam.Port < 65536 { + singleton.Conf.Server.Port = cliParam.Port } + if cliParam.Debug { + singleton.Conf.Debug = cliParam.Debug + } +} - srv := controller.ServerWeb(port) - - startOutput := func() { +func startupOutput(httpserver *http.Server) { + if singleton.Conf.Debug { + fmt.Println() + fmt.Printf("Service version: %s\n", utils.Colorize(utils.ColorGreen, singleton.Version)) fmt.Println() fmt.Println("Server available routes:") - mygin.PrintRoute(srv.Handler.(*gin.Engine)) + mygin.PrintRoute(httpserver.Handler.(*gin.Engine)) fmt.Println() - fmt.Println("Server is running with config:") + fmt.Println("Server running with config:") utils.PrintStructFieldsAndValues(singleton.Conf, "") + } - fmt.Println() - ipv4s, err := ip.GetIPv4NetworkIPs() - if ipv4s != nil && err == nil { - fmt.Println("Server is running at:") - for _, ip := range ipv4s { - fmt.Printf(" - %-7s: %s\n", "Network", utils.Colorize(utils.ColorGreen, fmt.Sprintf("http://%s:%d", ip, port))) - } + fmt.Println() + fmt.Println("Server is running at:") + fmt.Printf(" - %-7s: %s\n", "Local", utils.Colorize(utils.ColorGreen, fmt.Sprintf("http://127.0.0.1:%d", singleton.Conf.Server.Port))) + ipv4s, err := ip.GetIPv4NetworkIPs() + if ipv4s != nil && err == nil { + for _, ip := range ipv4s { + fmt.Printf(" - %-7s: %s\n", "Network", utils.Colorize(utils.ColorGreen, fmt.Sprintf("http://%s:%d", ip, singleton.Conf.Server.Port))) } - fmt.Println() + } + fmt.Println() +} - fmt.Printf("Current service version: %s\n", utils.Colorize(utils.ColorGreen, singleton.Version)) - fmt.Println() +func init() { + loadConfig() + singleton.InitLog(singleton.Conf) + singleton.InitTimezoneAndCache() + singleton.InitDBFromPath(singleton.Conf.DBPath) + initService() +} + +func main() { + if cliParam.Version { + fmt.Println(singleton.Version) + return } + srv := controller.ServerWeb(singleton.Conf.Server.Port) if err := graceful.Graceful(func() error { - startOutput() + startupOutput(srv) return srv.ListenAndServe() }, func(c context.Context) error { fmt.Print(utils.Colorize("Server is shutting down", utils.ColorRed)) srv.Shutdown(c) return nil }); err != nil { - fmt.Println(utils.Colorize("Server is shutting down with error: %s", utils.ColorRed), err) + fmt.Printf("Server is shutting down with error: %s", utils.Colorize(utils.ColorRed, err.Error())) } } func initService() { - // Load all services in the singleton package singleton.LoadSingleton() - if _, err := singleton.Cron.AddFunc("0 * * * * *", sayHello); err != nil { - panic(err) - } + // if _, err := singleton.Cron.AddFunc("0 * * * * *", sayHello); err != nil { + // panic(err) + // } } -func sayHello() { - singleton.Log.Info().Msg("Hello world, I am a cron task") - // singleton.SendNotificationByType("wecom", "Hello world", "I am a cron task") -} +// func sayHello() { +// singleton.Log.Info().Msg("Hello world, I am a cron task") +// // singleton.SendNotificationByType("wecom", "Hello world", "I am a cron task") +// } diff --git a/cmd/srv/controller/api_v1.go b/cmd/srv/controller/api_v1.go index f29a09c..d2f1a59 100644 --- a/cmd/srv/controller/api_v1.go +++ b/cmd/srv/controller/api_v1.go @@ -46,7 +46,7 @@ func (v *apiV1) serve() { var authModel = model.Auth{} func (v *apiV1) upload(c *gin.Context) { - attachment, err := gogin.AttachmentUpload(c) + attachment, err := gogin.AttachmentUpload(c, c.Query("sub_dir")) if err != nil { mygin.ResponseJSON(c, 400, gin.H{}, err.Error()) return diff --git a/config.example.yaml b/config.example.yaml index e1dc6e1..de5eee6 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -3,18 +3,19 @@ server: site: brand: Go-Gin # Site brand description: A simple web application using Go and Gin # Site description - base_url: http://localhost:8080 # Site base URL, used for generating absolute URLs + base_url: http://localhost:8080 # Site base URL, used for generating absolute URLs, cant end with / debug: false # Debug mode, if true, the server will print detailed error messages log: level: debug # debug, info, warn, error, fatal, panic - path: logs/go-gin.log # Log file path -db_path: db/go-gin.sqlite # Database path + path: logs/go-gin.log # Log file path, relative to the project root directory. Or you can use an absolute path, such as /var/www/go-gin.log +db_path: db/go-gin.sqlite # Database path, relative to the project root directory. Or you can use an absolute path, such as /var/www/go-gin.sqlite rate_limit: max: 100 # requests per minute, 0 means no limit enable_cors: false # Enable CORS enable_user_registration: true # Enable user registration upload: - virtual_path: /upload # Virtual path, used for generating absolute URLs, must start with / + virtual_path: /upload # Virtual path, used for generating absolute URLs, must start with /, cant end with / + url_prefix: http://localhost:8080/upload # URL prefix, used for generating absolute URLs, must start with http:// or https:// or /, cant end with / dir: upload # Upload directory, relative to the project root directory. Or you can use an absolute path, such as /var/www/upload max_size: 10485760 # 10MB, unit: byte keep_original_name: false # Keep original file name, if false, the server will generate a random file name diff --git a/internal/gconfig/config.go b/internal/gconfig/config.go index 68974c1..62ed285 100644 --- a/internal/gconfig/config.go +++ b/internal/gconfig/config.go @@ -1,5 +1,11 @@ package gconfig +import ( + "go-gin/pkg/utils/file" + "os" + "path" +) + const ( DefaultLogPath = "logs/log.log" DefaultPprofRoutePath = "/debug/pprof" @@ -29,6 +35,7 @@ type Config struct { Upload struct { Dir string `mapstructure:"dir"` VirtualPath string `mapstructure:"virtual_path"` + URLPrefix string `mapstructure:"url_prefix"` MaxSize int64 `mapstructure:"max_size"` KeepOriginalName bool `mapstructure:"keep_original_name"` CreateDateDir bool `mapstructure:"create_date_dir"` @@ -49,3 +56,93 @@ type Config struct { Location string `mapstructure:"location"` Notifications []Notification `mapstructure:"notifications"` } + +func CreateDefaultConfigFile(config_path string) error { + err := file.MkdirAllIfNotExists(path.Dir(config_path), os.ModePerm) + if err != nil { + return err + } + err = file.WriteToFile(config_path, ConfigYamlTemplate, os.ModePerm) + if err != nil { + return err + } + return nil +} + +const ConfigYamlTemplate = ` +server: + port: 8080 # Server port +site: + brand: Go-Gin # Site brand + description: A simple web application using Go and Gin # Site description + base_url: http://localhost:8080 # Site base URL, used for generating absolute URLs, cant end with / +debug: false # Debug mode, if true, the server will print detailed error messages +log: + level: debug # debug, info, warn, error, fatal, panic + path: logs/go-gin.log # Log file path, relative to the project root directory. Or you can use an absolute path, such as /var/www/go-gin.log +db_path: db/go-gin.sqlite # Database path, relative to the project root directory. Or you can use an absolute path, such as /var/www/go-gin.sqlite +rate_limit: + max: 100 # requests per minute, 0 means no limit +enable_cors: false # Enable CORS +enable_user_registration: true # Enable user registration +upload: + virtual_path: /upload # Virtual path, used for generating absolute URLs, must start with /, cant end with / + url_prefix: http://localhost:8080/upload # URL prefix, used for generating absolute URLs, must start with http:// or https:// or /, cant end with / + dir: upload # Upload directory, relative to the project root directory. Or you can use an absolute path, such as /var/www/upload + max_size: 10485760 # 10MB, unit: byte + keep_original_name: false # Keep original file name, if false, the server will generate a random file name + create_date_dir: true # Create date directory, such as /upload/2021/01/01 + allow_types: # Allowed file types, if empty, all types are allowed + - 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 + access_token_expiration: 60 # minutes + refresh_token_expiration: 720 # minutes + access_token_cookie_name: go-gin-access # Access token cookie name + refresh_token_cookie_name: go-gin-refresh # Refresh token cookie name +location: Asia/Chongqing # Timezone +notifications: # Notification settings + - type: apprise # You must install apprise first, more details: https://github.com/caronc/apprise + instances: + - url: "apprise-url-1" + - url: "apprise-url-2" + - type: dingtalk + instances: + - webhook: "dingtalk-webhook-1" + - webhook: "dingtalk-webhook-2" + - type: ifttt + instances: + - key: "ifttt-key-1" + event: "event-1" + - key: "ifttt-key-2" + event: "event-2" + - type: smtp + instances: + - host: "smtp-host-1" + port: 587 + username: "user-1" + password: "password-1" + from: "from-1" + to: "to-1" + - host: "smtp-host-2" + port: 587 + username: "user-2" + password: "password-2" + from: "from-2" + to: "to-2" + - type: telegram + instances: + - botToken: "telegram-bot-token-1" + chatID: "chat-id-1" + - botToken: "telegram-bot-token-2" + chatID: "chat-id-2" + - type: wecom + instances: + - key: "wecom-key-1" + - key: "wecom-key-2" +` diff --git a/internal/gogin/common.go b/internal/gogin/common.go index 7ec742f..e60dc5f 100644 --- a/internal/gogin/common.go +++ b/internal/gogin/common.go @@ -80,8 +80,8 @@ func UserLogout(c *gin.Context) { c.SetCookie(singleton.Conf.JWT.RefreshTokenCookieName, "", -1, "/", "", false, true) } -func AttachmentUpload(c *gin.Context) (*model.Attachment, error) { - result, err := singleton.AttachmentUpload.Upload(c) +func AttachmentUpload(c *gin.Context, subDir ...string) (*model.Attachment, error) { + result, err := singleton.AttachmentUpload.Upload(c, subDir...) if err != nil { return nil, err } diff --git a/pkg/mygin/attachment_upload.go b/pkg/mygin/attachment_upload.go index ac60ca1..27b82d8 100644 --- a/pkg/mygin/attachment_upload.go +++ b/pkg/mygin/attachment_upload.go @@ -15,7 +15,7 @@ import ( ) type AttachmentUpload struct { - BaseURL string // BaseURL is the base url for the uploaded file + URLPrefix string // URLPrefix is the prefix of the uploaded file url 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" @@ -37,7 +37,8 @@ type AttachmentUploadedFile struct { SavePath string `json:"save_path"` // SavePath is the path to save the file } -func (a *AttachmentUpload) Upload(c *gin.Context) (*AttachmentUploadedFile, error) { +// Upload uploads the file, and returns the uploaded file info. +func (a *AttachmentUpload) Upload(c *gin.Context, subDir ...string) (*AttachmentUploadedFile, error) { result := &AttachmentUploadedFile{} form_file, err := c.FormFile(a.FormName) if err != nil { @@ -66,11 +67,20 @@ func (a *AttachmentUpload) Upload(c *gin.Context) (*AttachmentUploadedFile, erro } savePath := a.StoreDir - url := fmt.Sprintf("%s/%s", a.BaseURL, saveName) + url := a.URLPrefix + for _, v := range subDir { + if v != "" { + savePath = path.Join(savePath, v) + url = fmt.Sprintf("%s/%s", url, v) + } + } + 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) + savePath = path.Join(savePath, year+month+day) + url = fmt.Sprintf("%s/%s", url, year+month+day) } + url = fmt.Sprintf("%s/%s", url, saveName) + if err := file.MkdirAllIfNotExists(savePath, os.ModePerm); err != nil { return result, err } @@ -97,7 +107,7 @@ func (a *AttachmentUpload) Upload(c *gin.Context) (*AttachmentUploadedFile, erro func NewAttachmentUpload() *AttachmentUpload { return &AttachmentUpload{ - BaseURL: "/upload", + URLPrefix: "/upload", MaxSize: 1024 * 1024 * 2, AllowTypes: []string{"image/jpeg", "image/png", "image/gif", "image/jpg"}, FormName: "file", diff --git a/pkg/utils/ip/ip.go b/pkg/utils/ip/ip.go index d2efcfd..0bf6956 100644 --- a/pkg/utils/ip/ip.go +++ b/pkg/utils/ip/ip.go @@ -28,6 +28,9 @@ func GetNetworkIPs() ([]string, error) { ips := make([]string, 0) for _, i := range ifaces { + if i.Flags&net.FlagLoopback != 0 { + continue + } addrs, err := i.Addrs() if err != nil { continue diff --git a/service/singleton/singleton.go b/service/singleton/singleton.go index f5e4651..4b6d725 100644 --- a/service/singleton/singleton.go +++ b/service/singleton/singleton.go @@ -16,6 +16,7 @@ import ( "go-gin/internal/gconfig" "go-gin/model" "go-gin/pkg/logger" + "go-gin/pkg/utils" "go-gin/pkg/utils/conf" "go-gin/pkg/utils/file" ) @@ -50,7 +51,15 @@ func InitTimezoneAndCache() { func InitConfig(name string) { ViperConf, err := conf.ReadViperConfig(name, "yaml", []string{".", "./config", "../"}) if err != nil { - panic(fmt.Errorf("unable to read config: %s", err)) + fmt.Println(utils.Colorize(utils.ColorRed, err.Error())) + + gconfig.CreateDefaultConfigFile(name + ".yaml") + fmt.Printf("Successfully created default config file at %s\n", utils.Colorize(utils.ColorGreen, name+".yaml")) + + ViperConf, err = conf.ReadViperConfig(name, "yaml", []string{".", "./config", "../"}) + if err != nil { + panic(fmt.Errorf("unable to read config: %s", err)) + } } if err := ViperConf.Unmarshal(&Conf); err != nil { diff --git a/service/singleton/upload.go b/service/singleton/upload.go index 4a15159..06f3865 100644 --- a/service/singleton/upload.go +++ b/service/singleton/upload.go @@ -2,6 +2,8 @@ package singleton import ( "go-gin/pkg/mygin" + "go-gin/pkg/utils/file" + "os" ) var ( @@ -10,7 +12,7 @@ var ( func LoadUpload() { AttachmentUpload = &mygin.AttachmentUpload{ - BaseURL: Conf.Site.BaseURL + Conf.Upload.VirtualPath, + URLPrefix: Conf.Upload.URLPrefix, MaxSize: Conf.Upload.MaxSize, AllowTypes: Conf.Upload.AllowTypes, FormName: "file", @@ -18,4 +20,5 @@ func LoadUpload() { CreateDateDir: Conf.Upload.CreateDateDir, KeepOriginalName: Conf.Upload.KeepOriginalName, } + file.MkdirAllIfNotExists(Conf.Upload.Dir, os.ModePerm) }