diff --git a/src/server/config_load.go b/src/server/config_load.go new file mode 100644 index 0000000..4ace96b --- /dev/null +++ b/src/server/config_load.go @@ -0,0 +1,107 @@ +package server + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/ihexxa/gocfg" + + "github.com/ihexxa/quickshare/src/db/sitestore" + "github.com/ihexxa/quickshare/src/kvstore/boltdbpvd" +) + +type Opts struct { + Host string `short:"h" long:"host" description:"server hostname"` + Port int `short:"p" long:"port" description:"server port"` + DbPath string `short:"d" long:"db" description:"path of the quickshare.db"` + Configs []string `short:"c" long:"configs" description:"config path"` +} + +func LoadCfg(opts *Opts) (*gocfg.Cfg, error) { + defaultCfg, err := DefaultConfig() + if err != nil { + return nil, err + } + + cfg, err := gocfg.New(NewConfig()).Load(gocfg.JSONStr(defaultCfg)) + if err != nil { + return nil, err + } + + if opts.DbPath != "" { + cfg, err = mergeDbConfig(cfg, opts.DbPath) + if err != nil { + return nil, err + } + } else { + _, err := os.Stat(boltdbpvd.DBName) + if err == nil { + cfg, err = mergeDbConfig(cfg, boltdbpvd.DBName) + } else if err != nil { + if !os.IsNotExist(err) { + return nil, err + } + } + } + + cfg, err = mergeConfigFiles(cfg, opts.Configs) + if err != nil { + return nil, err + } + + return mergeArgs(cfg, opts) +} + +func mergeDbConfig(cfg *gocfg.Cfg, dbPath string) (*gocfg.Cfg, error) { + kv := boltdbpvd.New(dbPath, 1024) + defer kv.Close() + + siteStore, err := sitestore.NewSiteStore(kv) + if err != nil { + return nil, fmt.Errorf("fail to new site config store: %s", err) + } + + clientCfg, err := siteStore.GetCfg() + if err != nil { + return nil, err + } + clientCfgBytes, err := json.Marshal(clientCfg) + if err != nil { + return nil, err + } + + cfgStr := fmt.Sprintf(`{"site": %s}`, string(clientCfgBytes)) + return cfg.Load(gocfg.JSONStr(cfgStr)) +} + +func mergeConfigFiles(cfg *gocfg.Cfg, configPaths []string) (*gocfg.Cfg, error) { + var err error + + for _, configPath := range configPaths { + if strings.HasSuffix(configPath, ".yml") || strings.HasSuffix(configPath, ".yaml") { + cfg, err = cfg.Load(gocfg.YAML(configPath)) + } else if strings.HasSuffix(configPath, ".json") { + cfg, err = cfg.Load(gocfg.JSON(configPath)) + } else { + return nil, fmt.Errorf("unknown config file type (.yml .yaml .json are supported): %s", configPath) + } + if err != nil { + return nil, err + } + } + + return cfg, nil +} + +func mergeArgs(cfg *gocfg.Cfg, opts *Opts) (*gocfg.Cfg, error) { + if opts.Host != "" { + cfg.SetString("Server.Host", opts.Host) + } + if opts.Port != 0 { + cfg.SetInt("Server.Port", opts.Port) + } + + return cfg, nil +} diff --git a/src/server/config_load_test.go b/src/server/config_load_test.go new file mode 100644 index 0000000..87d863f --- /dev/null +++ b/src/server/config_load_test.go @@ -0,0 +1,352 @@ +package server + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/ihexxa/gocfg" + + "github.com/ihexxa/quickshare/src/db/sitestore" + "github.com/ihexxa/quickshare/src/db/userstore" +) + +func TestLoadCfg(t *testing.T) { + argsList := []*Opts{ + // default config + &Opts{ + Host: "", + Port: 0, + DbPath: "", + Configs: []string{}, + }, + // default config + db + &Opts{ + Host: "", + Port: 0, + DbPath: "testdata", + Configs: []string{}, + }, + // default config + db + config_1 + &Opts{ + Host: "", + Port: 0, + DbPath: "testdata", + Configs: []string{"testdata/config_1.yml"}, + }, + // default config + db + config_1 + config_2 + &Opts{ + Host: "", + Port: 0, + DbPath: "testdata", + Configs: []string{"testdata/config_1.yml", "testdata/config_4.yml"}, + }, + // config partial overwrite + &Opts{ + Host: "", + Port: 0, + DbPath: "testdata", + Configs: []string{ + "testdata/config_1.yml", + "testdata/config_4.yml", + "testdata/config_partial_users.yml", + }, + }, + // arg overwrite + } + + cfg1 := DefaultConfigStruct() + + cfg2 := DefaultConfigStruct() + cfg2.Site.ClientCfg.SiteName = "Quickshare" + cfg2.Site.ClientCfg.SiteDesc = "Quickshare" + cfg2.Site.ClientCfg.Bg.Url = "test.png" + cfg2.Site.ClientCfg.Bg.Repeat = "no-repeat" + cfg2.Site.ClientCfg.Bg.Position = "top" + cfg2.Site.ClientCfg.Bg.Align = "scroll" + + cfg3 := &Config{ + Fs: &FSConfig{ + Root: "1", + OpensLimit: 1, + OpenTTL: 1, + }, + Users: &UsersCfg{ + EnableAuth: true, + DefaultAdmin: "1", + DefaultAdminPwd: "1", + CookieTTL: 1, + CookieSecure: true, + CookieHttpOnly: true, + MinUserNameLen: 1, + MinPwdLen: 1, + CaptchaWidth: 1, + CaptchaHeight: 1, + CaptchaEnabled: true, + UploadSpeedLimit: 1, + DownloadSpeedLimit: 1, + SpaceLimit: 1, + LimiterCapacity: 1, + LimiterCyc: 1, + PredefinedUsers: []*userstore.UserCfg{ + &userstore.UserCfg{ + Name: "1", + Pwd: "1", + Role: "1", + }, + }, + }, + Secrets: &Secrets{ + TokenSecret: "1", + }, + Server: &ServerCfg{ + Debug: true, + Host: "1", + Port: 1, + ReadTimeout: 1, + WriteTimeout: 1, + MaxHeaderBytes: 1, + PublicPath: "1", + }, + Workers: &WorkerPoolCfg{ + QueueSize: 1, + SleepCyc: 1, + WorkerCount: 1, + }, + Site: &sitestore.SiteConfig{ + ClientCfg: &sitestore.ClientConfig{ + SiteName: "1", + SiteDesc: "1", + Bg: &sitestore.BgConfig{ + Url: "1", + Repeat: "1", + Position: "1", + Align: "1", + }, + }, + }, + } + + cfg4 := &Config{ + Fs: &FSConfig{ + Root: "4", + OpensLimit: 4, + OpenTTL: 4, + }, + Users: &UsersCfg{ + EnableAuth: false, + DefaultAdmin: "4", + DefaultAdminPwd: "4", + CookieTTL: 4, + CookieSecure: false, + CookieHttpOnly: false, + MinUserNameLen: 4, + MinPwdLen: 4, + CaptchaWidth: 4, + CaptchaHeight: 4, + CaptchaEnabled: false, + UploadSpeedLimit: 4, + DownloadSpeedLimit: 4, + SpaceLimit: 4, + LimiterCapacity: 4, + LimiterCyc: 4, + PredefinedUsers: []*userstore.UserCfg{ + &userstore.UserCfg{ + Name: "4", + Pwd: "4", + Role: "4", + }, + }, + }, + Secrets: &Secrets{ + TokenSecret: "4", + }, + Server: &ServerCfg{ + Debug: false, + Host: "4", + Port: 4, + ReadTimeout: 4, + WriteTimeout: 4, + MaxHeaderBytes: 4, + PublicPath: "4", + }, + Workers: &WorkerPoolCfg{ + QueueSize: 4, + SleepCyc: 4, + WorkerCount: 4, + }, + Site: &sitestore.SiteConfig{ + ClientCfg: &sitestore.ClientConfig{ + SiteName: "4", + SiteDesc: "4", + Bg: &sitestore.BgConfig{ + Url: "4", + Repeat: "4", + Position: "4", + Align: "4", + }, + }, + }, + } + + cfg5 := &Config{ + Fs: &FSConfig{ + Root: "4", + OpensLimit: 4, + OpenTTL: 4, + }, + Users: &UsersCfg{ + EnableAuth: true, + DefaultAdmin: "5", + DefaultAdminPwd: "5", + CookieTTL: 5, + CookieSecure: true, + CookieHttpOnly: true, + MinUserNameLen: 5, + MinPwdLen: 5, + CaptchaWidth: 5, + CaptchaHeight: 5, + CaptchaEnabled: true, + UploadSpeedLimit: 5, + DownloadSpeedLimit: 5, + SpaceLimit: 5, + LimiterCapacity: 5, + LimiterCyc: 5, + PredefinedUsers: []*userstore.UserCfg{ + &userstore.UserCfg{ + Name: "5", + Pwd: "5", + Role: "5", + }, + }, + }, + Secrets: &Secrets{ + TokenSecret: "4", + }, + Server: &ServerCfg{ + Debug: false, + Host: "4", + Port: 4, + ReadTimeout: 4, + WriteTimeout: 4, + MaxHeaderBytes: 4, + PublicPath: "4", + }, + Workers: &WorkerPoolCfg{ + QueueSize: 4, + SleepCyc: 4, + WorkerCount: 4, + }, + Site: &sitestore.SiteConfig{ + ClientCfg: &sitestore.ClientConfig{ + SiteName: "4", + SiteDesc: "4", + Bg: &sitestore.BgConfig{ + Url: "4", + Repeat: "4", + Position: "4", + Align: "4", + }, + }, + }, + } + + expects := []*Config{ + cfg1, + cfg2, + cfg3, + cfg4, + cfg5, + } + + testLoadCfg := func(t *testing.T) { + for i, opts := range argsList { + gotCfg, err := LoadCfg(opts) + if err != nil { + t.Fatal(err) + } + + expectCfgStruct := expects[i] + expectCfgBytes, err := json.Marshal(expectCfgStruct) + if err != nil { + t.Fatal(err) + } + + expectCfg, err := gocfg.New(NewConfig()).Load(gocfg.JSONStr(string(expectCfgBytes))) + if err != nil { + t.Fatal(err) + } + + if !Equal(gotCfg, expectCfg) { + t.Fatal("cfgs are not identical") + } + } + } + + t.Run("test LoadCfg", testLoadCfg) +} + +// TODO: use better comparing method +func Equal(cfg1, cfg2 *gocfg.Cfg) bool { + // check cfg1 + for k1, v1 := range cfg1.Bools() { + v2, ok := cfg2.Bool(k1) + if !ok || v2 != v1 { + fmt.Println(k1, v1, v2) + return false + } + } + for k1, v1 := range cfg1.Ints() { + v2, ok := cfg2.Int(k1) + if !ok || v2 != v1 { + fmt.Println(k1, v1, v2) + return false + } + } + for k1, v1 := range cfg1.Floats() { + v2, ok := cfg2.Float(k1) + if !ok || v2 != v1 { + fmt.Println(k1, v1, v2) + return false + } + } + for k1, v1 := range cfg1.Strings() { + v2, ok := cfg2.String(k1) + if !ok || v2 != v1 { + fmt.Println(k1, v1, v2) + return false + } + } + + // check cfg2 + for k2, v2 := range cfg2.Bools() { + v1, ok := cfg1.Bool(k2) + if !ok || v1 != v2 { + fmt.Println(k2, v1, v2) + return false + } + } + for k2, v2 := range cfg2.Ints() { + v1, ok := cfg1.Int(k2) + if !ok || v1 != v2 { + fmt.Println(k2, v1, v2) + return false + } + } + for k2, v2 := range cfg2.Floats() { + v1, ok := cfg1.Float(k2) + if !ok || v1 != v2 { + fmt.Println(k2, v1, v2) + return false + } + } + for k2, v2 := range cfg2.Strings() { + v1, ok := cfg1.String(k2) + if !ok || v1 != v2 { + fmt.Println(k2, v1, v2) + return false + } + } + + return true +} diff --git a/src/server/testdata/config_1.yml b/src/server/testdata/config_1.yml new file mode 100644 index 0000000..2ce9d4a --- /dev/null +++ b/src/server/testdata/config_1.yml @@ -0,0 +1,48 @@ +fs: + root: "1" + opensLimit: 1 + openTTL: 1 +secrets: + tokenSecret: "1" +server: + debug: true + host: "1" + port: 1 + readTimeout: 1 + writeTimeout: 1 + maxHeaderBytes: 1 + publicPath: "1" +users: + enableAuth: true + defaultAdmin: "1" + defaultAdminPwd: "1" + cookieTTL: 1 + cookieSecure: true + cookieHttpOnly: true + minUserNameLen: 1 + minPwdLen: 1 + captchaWidth: 1 + captchaHeight: 1 + captchaEnabled: true + uploadSpeedLimit: 1 + downloadSpeedLimit: 1 + spaceLimit: 1 + limiterCapacity: 1 + limiterCyc: 1 + predefinedUsers: + - name: "1" + pwd: "1" + role: "1" +workers: + queueSize: 1 + sleepCyc: 1 + workerCount: 1 +site: + clientCfg: + siteName: "1" + siteDesc: "1" + bg: + url: "1" + repeat: "1" + position: "1" + align: "1" diff --git a/src/server/testdata/config_4.yml b/src/server/testdata/config_4.yml new file mode 100644 index 0000000..4069ed8 --- /dev/null +++ b/src/server/testdata/config_4.yml @@ -0,0 +1,48 @@ +fs: + root: "4" + opensLimit: 4 + openTTL: 4 +secrets: + tokenSecret: "4" +server: + debug: false + host: "4" + port: 4 + readTimeout: 4 + writeTimeout: 4 + maxHeaderBytes: 4 + publicPath: "4" +users: + enableAuth: false + defaultAdmin: "4" + defaultAdminPwd: "4" + cookieTTL: 4 + cookieSecure: false + cookieHttpOnly: false + minUserNameLen: 4 + minPwdLen: 4 + captchaWidth: 4 + captchaHeight: 4 + captchaEnabled: false + uploadSpeedLimit: 4 + downloadSpeedLimit: 4 + spaceLimit: 4 + limiterCapacity: 4 + limiterCyc: 4 + predefinedUsers: + - name: "4" + pwd: "4" + role: "4" +workers: + queueSize: 4 + sleepCyc: 4 + workerCount: 4 +site: + clientCfg: + siteName: "4" + siteDesc: "4" + bg: + url: "4" + repeat: "4" + position: "4" + align: "4" diff --git a/src/server/testdata/config_partial_users.yml b/src/server/testdata/config_partial_users.yml new file mode 100644 index 0000000..0d0f5a0 --- /dev/null +++ b/src/server/testdata/config_partial_users.yml @@ -0,0 +1,21 @@ +users: + enableAuth: true + defaultAdmin: "5" + defaultAdminPwd: "5" + cookieTTL: 5 + cookieSecure: true + cookieHttpOnly: true + minUserNameLen: 5 + minPwdLen: 5 + captchaWidth: 5 + captchaHeight: 5 + captchaEnabled: true + uploadSpeedLimit: 5 + downloadSpeedLimit: 5 + spaceLimit: 5 + limiterCapacity: 5 + limiterCyc: 5 + predefinedUsers: + - name: "5" + pwd: "5" + role: "5"