github.com/gogf/gf@v1.16.9/os/gcfg/gcfg_config.go (about)

     1  // Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
     2  //
     3  // This Source Code Form is subject to the terms of the MIT License.
     4  // If a copy of the MIT was not distributed with this file,
     5  // You can obtain one at https://github.com/gogf/gf.
     6  
     7  package gcfg
     8  
     9  import (
    10  	"bytes"
    11  	"context"
    12  	"fmt"
    13  	"github.com/gogf/gf/container/garray"
    14  	"github.com/gogf/gf/container/gmap"
    15  	"github.com/gogf/gf/encoding/gjson"
    16  	"github.com/gogf/gf/errors/gcode"
    17  	"github.com/gogf/gf/errors/gerror"
    18  	"github.com/gogf/gf/internal/intlog"
    19  	"github.com/gogf/gf/os/gcmd"
    20  	"github.com/gogf/gf/os/gfile"
    21  	"github.com/gogf/gf/os/gfsnotify"
    22  	"github.com/gogf/gf/os/glog"
    23  	"github.com/gogf/gf/os/gres"
    24  	"github.com/gogf/gf/os/gspath"
    25  	"github.com/gogf/gf/text/gstr"
    26  	"github.com/gogf/gf/util/gmode"
    27  )
    28  
    29  // New returns a new configuration management object.
    30  // The parameter `file` specifies the default configuration file name for reading.
    31  func New(file ...string) *Config {
    32  	name := DefaultConfigFile
    33  	if len(file) > 0 {
    34  		name = file[0]
    35  	} else {
    36  		// Custom default configuration file name from command line or environment.
    37  		if customFile := gcmd.GetOptWithEnv(commandEnvKeyForFile).String(); customFile != "" {
    38  			name = customFile
    39  		}
    40  	}
    41  	c := &Config{
    42  		defaultName: name,
    43  		searchPaths: garray.NewStrArray(true),
    44  		jsonMap:     gmap.NewStrAnyMap(true),
    45  	}
    46  	// Customized dir path from env/cmd.
    47  	if customPath := gcmd.GetOptWithEnv(commandEnvKeyForPath).String(); customPath != "" {
    48  		if gfile.Exists(customPath) {
    49  			_ = c.SetPath(customPath)
    50  		} else {
    51  			if errorPrint() {
    52  				glog.Errorf("[gcfg] Configuration directory path does not exist: %s", customPath)
    53  			}
    54  		}
    55  	} else {
    56  		// Dir path of working dir.
    57  		if err := c.AddPath(gfile.Pwd()); err != nil {
    58  			intlog.Error(context.TODO(), err)
    59  		}
    60  
    61  		// Dir path of main package.
    62  		if mainPath := gfile.MainPkgPath(); mainPath != "" && gfile.Exists(mainPath) {
    63  			if err := c.AddPath(mainPath); err != nil {
    64  				intlog.Error(context.TODO(), err)
    65  			}
    66  		}
    67  
    68  		// Dir path of binary.
    69  		if selfPath := gfile.SelfDir(); selfPath != "" && gfile.Exists(selfPath) {
    70  			if err := c.AddPath(selfPath); err != nil {
    71  				intlog.Error(context.TODO(), err)
    72  			}
    73  		}
    74  	}
    75  	return c
    76  }
    77  
    78  // Instance returns an instance of Config with default settings.
    79  // The parameter `name` is the name for the instance. But very note that, if the file "name.toml"
    80  // exists in the configuration directory, it then sets it as the default configuration file. The
    81  // toml file type is the default configuration file type.
    82  func Instance(name ...string) *Config {
    83  	key := DefaultName
    84  	if len(name) > 0 && name[0] != "" {
    85  		key = name[0]
    86  	}
    87  	return instances.GetOrSetFuncLock(key, func() interface{} {
    88  		c := New()
    89  		// If it's not using default configuration or its configuration file is not available,
    90  		// it searches the possible configuration file according to the name and all supported
    91  		// file types.
    92  		if key != DefaultName || !c.Available() {
    93  			for _, fileType := range supportedFileTypes {
    94  				if file := fmt.Sprintf(`%s.%s`, key, fileType); c.Available(file) {
    95  					c.SetFileName(file)
    96  					break
    97  				}
    98  			}
    99  		}
   100  		return c
   101  	}).(*Config)
   102  }
   103  
   104  // SetPath sets the configuration directory path for file search.
   105  // The parameter `path` can be absolute or relative path,
   106  // but absolute path is strongly recommended.
   107  func (c *Config) SetPath(path string) error {
   108  	var (
   109  		isDir    = false
   110  		realPath = ""
   111  	)
   112  	if file := gres.Get(path); file != nil {
   113  		realPath = path
   114  		isDir = file.FileInfo().IsDir()
   115  	} else {
   116  		// Absolute path.
   117  		realPath = gfile.RealPath(path)
   118  		if realPath == "" {
   119  			// Relative path.
   120  			c.searchPaths.RLockFunc(func(array []string) {
   121  				for _, v := range array {
   122  					if path, _ := gspath.Search(v, path); path != "" {
   123  						realPath = path
   124  						break
   125  					}
   126  				}
   127  			})
   128  		}
   129  		if realPath != "" {
   130  			isDir = gfile.IsDir(realPath)
   131  		}
   132  	}
   133  	// Path not exist.
   134  	if realPath == "" {
   135  		buffer := bytes.NewBuffer(nil)
   136  		if c.searchPaths.Len() > 0 {
   137  			buffer.WriteString(fmt.Sprintf("[gcfg] SetPath failed: cannot find directory \"%s\" in following paths:", path))
   138  			c.searchPaths.RLockFunc(func(array []string) {
   139  				for k, v := range array {
   140  					buffer.WriteString(fmt.Sprintf("\n%d. %s", k+1, v))
   141  				}
   142  			})
   143  		} else {
   144  			buffer.WriteString(fmt.Sprintf(`[gcfg] SetPath failed: path "%s" does not exist`, path))
   145  		}
   146  		err := gerror.NewCode(gcode.CodeOperationFailed, buffer.String())
   147  		if errorPrint() {
   148  			glog.Error(err)
   149  		}
   150  		return err
   151  	}
   152  	// Should be a directory.
   153  	if !isDir {
   154  		err := fmt.Errorf(`[gcfg] SetPath failed: path "%s" should be directory type`, path)
   155  		if errorPrint() {
   156  			glog.Error(err)
   157  		}
   158  		return err
   159  	}
   160  	// Repeated path check.
   161  	if c.searchPaths.Search(realPath) != -1 {
   162  		return nil
   163  	}
   164  	c.jsonMap.Clear()
   165  	c.searchPaths.Clear()
   166  	c.searchPaths.Append(realPath)
   167  	intlog.Print(context.TODO(), "SetPath:", realPath)
   168  	return nil
   169  }
   170  
   171  // SetViolenceCheck sets whether to perform hierarchical conflict checking.
   172  // This feature needs to be enabled when there is a level symbol in the key name.
   173  // It is off in default.
   174  //
   175  // Note that, turning on this feature is quite expensive, and it is not recommended
   176  // to allow separators in the key names. It is best to avoid this on the application side.
   177  func (c *Config) SetViolenceCheck(check bool) {
   178  	c.violenceCheck = check
   179  	c.Clear()
   180  }
   181  
   182  // AddPath adds a absolute or relative path to the search paths.
   183  func (c *Config) AddPath(path string) error {
   184  	var (
   185  		isDir    = false
   186  		realPath = ""
   187  	)
   188  	// It firstly checks the resource manager,
   189  	// and then checks the filesystem for the path.
   190  	if file := gres.Get(path); file != nil {
   191  		realPath = path
   192  		isDir = file.FileInfo().IsDir()
   193  	} else {
   194  		// Absolute path.
   195  		realPath = gfile.RealPath(path)
   196  		if realPath == "" {
   197  			// Relative path.
   198  			c.searchPaths.RLockFunc(func(array []string) {
   199  				for _, v := range array {
   200  					if path, _ := gspath.Search(v, path); path != "" {
   201  						realPath = path
   202  						break
   203  					}
   204  				}
   205  			})
   206  		}
   207  		if realPath != "" {
   208  			isDir = gfile.IsDir(realPath)
   209  		}
   210  	}
   211  	if realPath == "" {
   212  		buffer := bytes.NewBuffer(nil)
   213  		if c.searchPaths.Len() > 0 {
   214  			buffer.WriteString(fmt.Sprintf("[gcfg] AddPath failed: cannot find directory \"%s\" in following paths:", path))
   215  			c.searchPaths.RLockFunc(func(array []string) {
   216  				for k, v := range array {
   217  					buffer.WriteString(fmt.Sprintf("\n%d. %s", k+1, v))
   218  				}
   219  			})
   220  		} else {
   221  			buffer.WriteString(fmt.Sprintf(`[gcfg] AddPath failed: path "%s" does not exist`, path))
   222  		}
   223  		err := gerror.NewCode(gcode.CodeOperationFailed, buffer.String())
   224  		if errorPrint() {
   225  			glog.Error(err)
   226  		}
   227  		return err
   228  	}
   229  	if !isDir {
   230  		err := gerror.NewCodef(gcode.CodeInvalidParameter, `[gcfg] AddPath failed: path "%s" should be directory type`, path)
   231  		if errorPrint() {
   232  			glog.Error(err)
   233  		}
   234  		return err
   235  	}
   236  	// Repeated path check.
   237  	if c.searchPaths.Search(realPath) != -1 {
   238  		return nil
   239  	}
   240  	c.searchPaths.Append(realPath)
   241  	intlog.Print(context.TODO(), "AddPath:", realPath)
   242  	return nil
   243  }
   244  
   245  // SetFileName sets the default configuration file name.
   246  func (c *Config) SetFileName(name string) *Config {
   247  	c.defaultName = name
   248  	return c
   249  }
   250  
   251  // GetFileName returns the default configuration file name.
   252  func (c *Config) GetFileName() string {
   253  	return c.defaultName
   254  }
   255  
   256  // Available checks and returns whether configuration of given `file` is available.
   257  func (c *Config) Available(file ...string) bool {
   258  	var name string
   259  	if len(file) > 0 && file[0] != "" {
   260  		name = file[0]
   261  	} else {
   262  		name = c.defaultName
   263  	}
   264  	if path, _ := c.GetFilePath(name); path != "" {
   265  		return true
   266  	}
   267  	if GetContent(name) != "" {
   268  		return true
   269  	}
   270  	return false
   271  }
   272  
   273  // GetFilePath returns the absolute configuration file path for the given filename by `file`.
   274  // If `file` is not passed, it returns the configuration file path of the default name.
   275  // It returns an empty `path` string and an error if the given `file` does not exist.
   276  func (c *Config) GetFilePath(file ...string) (path string, err error) {
   277  	name := c.defaultName
   278  	if len(file) > 0 {
   279  		name = file[0]
   280  	}
   281  	// Searching resource manager.
   282  	if !gres.IsEmpty() {
   283  		for _, v := range resourceTryFiles {
   284  			if file := gres.Get(v + name); file != nil {
   285  				path = file.Name()
   286  				return
   287  			}
   288  		}
   289  		c.searchPaths.RLockFunc(func(array []string) {
   290  			for _, prefix := range array {
   291  				for _, v := range resourceTryFiles {
   292  					if file := gres.Get(prefix + v + name); file != nil {
   293  						path = file.Name()
   294  						return
   295  					}
   296  				}
   297  			}
   298  		})
   299  	}
   300  	c.autoCheckAndAddMainPkgPathToSearchPaths()
   301  	// Searching the file system.
   302  	c.searchPaths.RLockFunc(func(array []string) {
   303  		for _, prefix := range array {
   304  			prefix = gstr.TrimRight(prefix, `\/`)
   305  			if path, _ = gspath.Search(prefix, name); path != "" {
   306  				return
   307  			}
   308  			if path, _ = gspath.Search(prefix+gfile.Separator+"config", name); path != "" {
   309  				return
   310  			}
   311  		}
   312  	})
   313  	// If it cannot find the path of `file`, it formats and returns a detailed error.
   314  	if path == "" {
   315  		var (
   316  			buffer = bytes.NewBuffer(nil)
   317  		)
   318  		if c.searchPaths.Len() > 0 {
   319  			buffer.WriteString(fmt.Sprintf(`[gcfg] cannot find config file "%s" in resource manager or the following paths:`, name))
   320  			c.searchPaths.RLockFunc(func(array []string) {
   321  				index := 1
   322  				for _, v := range array {
   323  					v = gstr.TrimRight(v, `\/`)
   324  					buffer.WriteString(fmt.Sprintf("\n%d. %s", index, v))
   325  					index++
   326  					buffer.WriteString(fmt.Sprintf("\n%d. %s", index, v+gfile.Separator+"config"))
   327  					index++
   328  				}
   329  			})
   330  		} else {
   331  			buffer.WriteString(fmt.Sprintf("[gcfg] cannot find config file \"%s\" with no path configured", name))
   332  		}
   333  		err = gerror.NewCode(gcode.CodeOperationFailed, buffer.String())
   334  	}
   335  	return
   336  }
   337  
   338  // autoCheckAndAddMainPkgPathToSearchPaths automatically checks and adds directory path of package main
   339  // to the searching path list if it's currently in development environment.
   340  func (c *Config) autoCheckAndAddMainPkgPathToSearchPaths() {
   341  	if gmode.IsDevelop() {
   342  		mainPkgPath := gfile.MainPkgPath()
   343  		if mainPkgPath != "" {
   344  			if !c.searchPaths.Contains(mainPkgPath) {
   345  				c.searchPaths.Append(mainPkgPath)
   346  			}
   347  		}
   348  	}
   349  }
   350  
   351  // getJson returns a *gjson.Json object for the specified `file` content.
   352  // It would print error if file reading fails. It return nil if any error occurs.
   353  func (c *Config) getJson(file ...string) *gjson.Json {
   354  	var name string
   355  	if len(file) > 0 && file[0] != "" {
   356  		name = file[0]
   357  	} else {
   358  		name = c.defaultName
   359  	}
   360  	r := c.jsonMap.GetOrSetFuncLock(name, func() interface{} {
   361  		var (
   362  			err      error
   363  			content  string
   364  			filePath string
   365  		)
   366  		// The configured content can be any kind of data type different from its file type.
   367  		isFromConfigContent := true
   368  		if content = GetContent(name); content == "" {
   369  			isFromConfigContent = false
   370  			filePath, err = c.GetFilePath(name)
   371  			if err != nil && errorPrint() {
   372  				glog.Error(err)
   373  			}
   374  			if filePath == "" {
   375  				return nil
   376  			}
   377  			if file := gres.Get(filePath); file != nil {
   378  				content = string(file.Content())
   379  			} else {
   380  				content = gfile.GetContents(filePath)
   381  			}
   382  		}
   383  		// Note that the underlying configuration json object operations are concurrent safe.
   384  		var (
   385  			j *gjson.Json
   386  		)
   387  		dataType := gfile.ExtName(name)
   388  		if gjson.IsValidDataType(dataType) && !isFromConfigContent {
   389  			j, err = gjson.LoadContentType(dataType, content, true)
   390  		} else {
   391  			j, err = gjson.LoadContent(content, true)
   392  		}
   393  		if err == nil {
   394  			j.SetViolenceCheck(c.violenceCheck)
   395  			// Add monitor for this configuration file,
   396  			// any changes of this file will refresh its cache in Config object.
   397  			if filePath != "" && !gres.Contains(filePath) {
   398  				_, err = gfsnotify.Add(filePath, func(event *gfsnotify.Event) {
   399  					c.jsonMap.Remove(name)
   400  				})
   401  				if err != nil && errorPrint() {
   402  					glog.Error(err)
   403  				}
   404  			}
   405  			return j
   406  		}
   407  		if errorPrint() {
   408  			if filePath != "" {
   409  				glog.Criticalf(`[gcfg] load config file "%s" failed: %s`, filePath, err.Error())
   410  			} else {
   411  				glog.Criticalf(`[gcfg] load configuration failed: %s`, err.Error())
   412  			}
   413  		}
   414  		return nil
   415  	})
   416  	if r != nil {
   417  		return r.(*gjson.Json)
   418  	}
   419  	return nil
   420  }