gf/os/gcfg/gcfg.go

415 lines
12 KiB
Go
Raw Normal View History

// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
2017-12-29 16:03:30 +08:00
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
2017-12-29 16:03:30 +08:00
2019-05-08 22:04:36 +08:00
// Package gcfg provides reading, caching and managing for configuration.
package gcfg
import (
2019-04-03 09:59:15 +08:00
"bytes"
"errors"
"fmt"
"github.com/gogf/gf/errors/gerror"
2021-01-22 15:30:39 +08:00
"github.com/gogf/gf/internal/intlog"
"github.com/gogf/gf/os/gcmd"
2020-02-14 21:57:35 +08:00
"github.com/gogf/gf/text/gstr"
"github.com/gogf/gf/util/gmode"
2019-06-21 22:23:07 +08:00
2019-08-19 21:02:44 +08:00
"github.com/gogf/gf/os/gres"
2019-07-29 21:01:19 +08:00
"github.com/gogf/gf/container/garray"
"github.com/gogf/gf/container/gmap"
"github.com/gogf/gf/encoding/gjson"
"github.com/gogf/gf/os/gfile"
"github.com/gogf/gf/os/gfsnotify"
"github.com/gogf/gf/os/glog"
"github.com/gogf/gf/os/gspath"
)
const (
DefaultConfigFile = "config.toml" // The default configuration file name.
cmdEnvKey = "gf.gcfg" // Configuration key for command argument or environment.
)
// Configuration struct.
type Config struct {
2021-01-27 00:55:45 +08:00
defaultName string // Default configuration file name.
searchPaths *garray.StrArray // Searching path array.
jsonMap *gmap.StrAnyMap // The pared JSON objects for configuration files.
violenceCheck bool // Whether do violence check in value index searching. It affects the performance when set true(false in default).
}
var (
supportedFileTypes = []string{"toml", "yaml", "yml", "json", "ini", "xml"}
resourceTryFiles = []string{"", "/", "config/", "config", "/config", "/config/"}
)
// New returns a new configuration management object.
// The parameter `file` specifies the default configuration file name for reading.
2019-06-19 09:06:52 +08:00
func New(file ...string) *Config {
name := DefaultConfigFile
2019-06-19 09:06:52 +08:00
if len(file) > 0 {
name = file[0]
} else {
// Custom default configuration file name from command line or environment.
if customFile := gcmd.GetOptWithEnv(fmt.Sprintf("%s.file", cmdEnvKey)).String(); customFile != "" {
name = customFile
}
2019-06-19 09:06:52 +08:00
}
c := &Config{
2021-01-27 00:55:45 +08:00
defaultName: name,
searchPaths: garray.NewStrArray(true),
jsonMap: gmap.NewStrAnyMap(true),
2019-06-19 09:06:52 +08:00
}
// Customized dir path from env/cmd.
if customPath := gcmd.GetOptWithEnv(fmt.Sprintf("%s.path", cmdEnvKey)).String(); customPath != "" {
if gfile.Exists(customPath) {
_ = c.SetPath(customPath)
} else {
if errorPrint() {
glog.Errorf("[gcfg] Configuration directory path does not exist: %s", customPath)
}
}
} else {
2021-03-03 14:29:01 +08:00
// Dir path of working dir.
if err := c.AddPath(gfile.Pwd()); err != nil {
intlog.Error(err)
}
// Dir path of main package.
if mainPath := gfile.MainPkgPath(); mainPath != "" && gfile.Exists(mainPath) {
if err := c.AddPath(mainPath); err != nil {
intlog.Error(err)
}
}
// Dir path of binary.
if selfPath := gfile.SelfDir(); selfPath != "" && gfile.Exists(selfPath) {
if err := c.AddPath(selfPath); err != nil {
intlog.Error(err)
}
}
2019-04-03 09:59:15 +08:00
}
2019-06-19 09:06:52 +08:00
return c
}
2019-04-05 00:23:59 +08:00
// SetPath sets the configuration directory path for file search.
// The parameter `path` can be absolute or relative path,
// but absolute path is strongly recommended.
func (c *Config) SetPath(path string) error {
var (
isDir = false
realPath = ""
)
2019-08-19 21:02:44 +08:00
if file := gres.Get(path); file != nil {
realPath = path
isDir = file.FileInfo().IsDir()
} else {
// Absolute path.
realPath = gfile.RealPath(path)
if realPath == "" {
// Relative path.
2021-01-27 00:55:45 +08:00
c.searchPaths.RLockFunc(func(array []string) {
2019-08-19 21:02:44 +08:00
for _, v := range array {
if path, _ := gspath.Search(v, path); path != "" {
realPath = path
break
}
2019-06-19 09:06:52 +08:00
}
2019-08-19 21:02:44 +08:00
})
}
if realPath != "" {
isDir = gfile.IsDir(realPath)
}
2019-06-19 09:06:52 +08:00
}
// Path not exist.
if realPath == "" {
buffer := bytes.NewBuffer(nil)
2021-01-27 00:55:45 +08:00
if c.searchPaths.Len() > 0 {
2019-06-19 09:06:52 +08:00
buffer.WriteString(fmt.Sprintf("[gcfg] SetPath failed: cannot find directory \"%s\" in following paths:", path))
2021-01-27 00:55:45 +08:00
c.searchPaths.RLockFunc(func(array []string) {
2019-06-19 09:06:52 +08:00
for k, v := range array {
buffer.WriteString(fmt.Sprintf("\n%d. %s", k+1, v))
}
})
} else {
buffer.WriteString(fmt.Sprintf(`[gcfg] SetPath failed: path "%s" does not exist`, path))
}
err := errors.New(buffer.String())
if errorPrint() {
glog.Error(err)
}
return err
}
// Should be a directory.
2019-08-19 21:02:44 +08:00
if !isDir {
2019-06-21 22:23:07 +08:00
err := fmt.Errorf(`[gcfg] SetPath failed: path "%s" should be directory type`, path)
2019-06-19 09:06:52 +08:00
if errorPrint() {
glog.Error(err)
}
return err
}
// Repeated path check.
2021-01-27 00:55:45 +08:00
if c.searchPaths.Search(realPath) != -1 {
2019-06-19 09:06:52 +08:00
return nil
}
2021-01-27 00:55:45 +08:00
c.jsonMap.Clear()
c.searchPaths.Clear()
c.searchPaths.Append(realPath)
2021-01-22 15:30:39 +08:00
intlog.Print("SetPath:", realPath)
2019-06-19 09:06:52 +08:00
return nil
2018-04-17 14:47:45 +08:00
}
2020-05-22 12:04:58 +08:00
// SetViolenceCheck sets whether to perform hierarchical conflict checking.
2019-04-05 00:23:59 +08:00
// This feature needs to be enabled when there is a level symbol in the key name.
2020-05-22 12:04:58 +08:00
// It is off in default.
//
// Note that, turning on this feature is quite expensive, and it is not recommended
// to allow separators in the key names. It is best to avoid this on the application side.
2018-07-01 00:38:35 +08:00
func (c *Config) SetViolenceCheck(check bool) {
2021-01-27 00:55:45 +08:00
c.violenceCheck = check
2019-06-19 09:06:52 +08:00
c.Clear()
2018-07-01 00:38:35 +08:00
}
2019-04-05 00:23:59 +08:00
// AddPath adds a absolute or relative path to the search paths.
func (c *Config) AddPath(path string) error {
2020-05-22 12:04:58 +08:00
var (
isDir = false
realPath = ""
)
// It firstly checks the resource manager,
// and then checks the filesystem for the path.
2019-08-19 21:02:44 +08:00
if file := gres.Get(path); file != nil {
realPath = path
isDir = file.FileInfo().IsDir()
} else {
// Absolute path.
realPath = gfile.RealPath(path)
if realPath == "" {
// Relative path.
2021-01-27 00:55:45 +08:00
c.searchPaths.RLockFunc(func(array []string) {
2019-08-19 21:02:44 +08:00
for _, v := range array {
if path, _ := gspath.Search(v, path); path != "" {
realPath = path
break
}
2019-06-19 09:06:52 +08:00
}
2019-08-19 21:02:44 +08:00
})
}
if realPath != "" {
isDir = gfile.IsDir(realPath)
}
2019-06-19 09:06:52 +08:00
}
if realPath == "" {
buffer := bytes.NewBuffer(nil)
2021-01-27 00:55:45 +08:00
if c.searchPaths.Len() > 0 {
2019-06-19 09:06:52 +08:00
buffer.WriteString(fmt.Sprintf("[gcfg] AddPath failed: cannot find directory \"%s\" in following paths:", path))
2021-01-27 00:55:45 +08:00
c.searchPaths.RLockFunc(func(array []string) {
2019-06-19 09:06:52 +08:00
for k, v := range array {
buffer.WriteString(fmt.Sprintf("\n%d. %s", k+1, v))
}
})
} else {
buffer.WriteString(fmt.Sprintf(`[gcfg] AddPath failed: path "%s" does not exist`, path))
}
err := gerror.New(buffer.String())
2019-06-19 09:06:52 +08:00
if errorPrint() {
glog.Error(err)
}
return err
}
2019-08-19 21:02:44 +08:00
if !isDir {
err := gerror.Newf(`[gcfg] AddPath failed: path "%s" should be directory type`, path)
2019-06-19 09:06:52 +08:00
if errorPrint() {
glog.Error(err)
}
return err
}
// Repeated path check.
2021-01-27 00:55:45 +08:00
if c.searchPaths.Search(realPath) != -1 {
2019-06-19 09:06:52 +08:00
return nil
}
2021-01-27 00:55:45 +08:00
c.searchPaths.Append(realPath)
2021-01-22 15:30:39 +08:00
intlog.Print("AddPath:", realPath)
2019-06-19 09:06:52 +08:00
return nil
}
// SetFileName sets the default configuration file name.
func (c *Config) SetFileName(name string) *Config {
c.defaultName = name
return c
}
// GetFileName returns the default configuration file name.
func (c *Config) GetFileName() string {
return c.defaultName
}
// Available checks and returns whether configuration of given `file` is available.
func (c *Config) Available(file ...string) bool {
var name string
if len(file) > 0 && file[0] != "" {
name = file[0]
} else {
name = c.defaultName
}
if path, _ := c.GetFilePath(name); path != "" {
return true
}
if GetContent(name) != "" {
return true
}
return false
}
// GetFilePath returns the absolute configuration file path for the given filename by `file`.
// If `file` is not passed, it returns the configuration file path of the default name.
// It returns an empty `path` string and an error if the given `file` does not exist.
func (c *Config) GetFilePath(file ...string) (path string, err error) {
2021-01-27 00:55:45 +08:00
name := c.defaultName
2019-06-19 09:06:52 +08:00
if len(file) > 0 {
name = file[0]
}
2019-10-31 23:37:33 +08:00
// Searching resource manager.
if !gres.IsEmpty() {
for _, v := range resourceTryFiles {
if file := gres.Get(v + name); file != nil {
path = file.Name()
return
}
}
2021-03-03 14:29:01 +08:00
c.searchPaths.RLockFunc(func(array []string) {
for _, prefix := range array {
for _, v := range resourceTryFiles {
if file := gres.Get(prefix + v + name); file != nil {
path = file.Name()
return
}
2019-08-19 21:02:44 +08:00
}
2019-06-19 09:06:52 +08:00
}
2021-03-03 14:29:01 +08:00
})
}
c.autoCheckAndAddMainPkgPathToSearchPaths()
2019-10-31 23:37:33 +08:00
// Searching the file system.
2021-03-03 14:29:01 +08:00
c.searchPaths.RLockFunc(func(array []string) {
for _, prefix := range array {
prefix = gstr.TrimRight(prefix, `\/`)
if path, _ = gspath.Search(prefix, name); path != "" {
return
}
if path, _ = gspath.Search(prefix+gfile.Separator+"config", name); path != "" {
return
}
2021-01-27 00:55:45 +08:00
}
2021-03-03 14:29:01 +08:00
})
// If it cannot find the path of `file`, it formats and returns a detailed error.
if path == "" {
var (
buffer = bytes.NewBuffer(nil)
)
if c.searchPaths.Len() > 0 {
buffer.WriteString(fmt.Sprintf(`[gcfg] cannot find config file "%s" in resource manager or the following paths:`, name))
c.searchPaths.RLockFunc(func(array []string) {
index := 1
for _, v := range array {
v = gstr.TrimRight(v, `\/`)
buffer.WriteString(fmt.Sprintf("\n%d. %s", index, v))
index++
buffer.WriteString(fmt.Sprintf("\n%d. %s", index, v+gfile.Separator+"config"))
index++
}
})
} else {
buffer.WriteString(fmt.Sprintf("[gcfg] cannot find config file \"%s\" with no path configured", name))
}
err = gerror.New(buffer.String())
}
return
}
// autoCheckAndAddMainPkgPathToSearchPaths automatically checks and adds directory path of package main
// to the searching path list if it's currently in development environment.
func (c *Config) autoCheckAndAddMainPkgPathToSearchPaths() {
if gmode.IsDevelop() {
mainPkgPath := gfile.MainPkgPath()
if mainPkgPath != "" {
if !c.searchPaths.Contains(mainPkgPath) {
c.searchPaths.Append(mainPkgPath)
}
}
}
}
// getJson returns a *gjson.Json object for the specified `file` content.
// It would print error if file reading fails. It return nil if any error occurs.
2019-06-19 09:06:52 +08:00
func (c *Config) getJson(file ...string) *gjson.Json {
var name string
if len(file) > 0 && file[0] != "" {
2019-06-19 09:06:52 +08:00
name = file[0]
} else {
2021-01-27 00:55:45 +08:00
name = c.defaultName
2019-06-19 09:06:52 +08:00
}
2021-01-27 00:55:45 +08:00
r := c.jsonMap.GetOrSetFuncLock(name, func() interface{} {
var (
err error
content string
filePath string
)
// The configured content can be any kind of data type different from its file type.
isFromConfigContent := true
2019-06-19 09:06:52 +08:00
if content = GetContent(name); content == "" {
isFromConfigContent = false
filePath, err = c.GetFilePath(name)
if err != nil && errorPrint() {
glog.Error(err)
}
2019-06-19 09:06:52 +08:00
if filePath == "" {
return nil
}
2019-08-19 21:02:44 +08:00
if file := gres.Get(filePath); file != nil {
content = string(file.Content())
} else {
content = gfile.GetContents(filePath)
}
2019-06-19 09:06:52 +08:00
}
// Note that the underlying configuration json object operations are concurrent safe.
var (
j *gjson.Json
)
dataType := gfile.ExtName(name)
if gjson.IsValidDataType(dataType) && !isFromConfigContent {
j, err = gjson.LoadContentType(dataType, content, true)
} else {
j, err = gjson.LoadContent(content, true)
}
if err == nil {
2021-01-27 00:55:45 +08:00
j.SetViolenceCheck(c.violenceCheck)
2019-06-19 09:06:52 +08:00
// Add monitor for this configuration file,
// any changes of this file will refresh its cache in Config object.
2019-08-19 22:54:37 +08:00
if filePath != "" && !gres.Contains(filePath) {
2019-06-21 22:23:07 +08:00
_, err = gfsnotify.Add(filePath, func(event *gfsnotify.Event) {
2021-01-27 00:55:45 +08:00
c.jsonMap.Remove(name)
2019-06-19 09:06:52 +08:00
})
2019-06-21 22:23:07 +08:00
if err != nil && errorPrint() {
glog.Error(err)
}
2019-06-19 09:06:52 +08:00
}
return j
2021-03-01 17:15:18 +08:00
}
if errorPrint() {
if filePath != "" {
glog.Criticalf(`[gcfg] load config file "%s" failed: %s`, filePath, err.Error())
2021-03-01 17:15:18 +08:00
} else {
glog.Criticalf(`[gcfg] load configuration failed: %s`, err.Error())
2019-06-19 09:06:52 +08:00
}
}
return nil
})
if r != nil {
return r.(*gjson.Json)
}
return nil
}