gcfg.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. // Copyright GoFrame Author(https://github.com/gogf/gf). 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. // Package gcfg provides reading, caching and managing for configuration.
  7. package gcfg
  8. import (
  9. "bytes"
  10. "errors"
  11. "fmt"
  12. "github.com/gogf/gf/os/gcmd"
  13. "github.com/gogf/gf/text/gstr"
  14. "github.com/gogf/gf/os/gres"
  15. "github.com/gogf/gf/container/garray"
  16. "github.com/gogf/gf/container/gmap"
  17. "github.com/gogf/gf/encoding/gjson"
  18. "github.com/gogf/gf/os/gfile"
  19. "github.com/gogf/gf/os/gfsnotify"
  20. "github.com/gogf/gf/os/glog"
  21. "github.com/gogf/gf/os/gspath"
  22. )
  23. const (
  24. DefaultConfigFile = "config.toml" // The default configuration file name.
  25. cmdEnvKey = "gf.gcfg" // Configuration key for command argument or environment.
  26. )
  27. // Configuration struct.
  28. type Config struct {
  29. name string // Default configuration file name.
  30. paths *garray.StrArray // Searching path array.
  31. jsons *gmap.StrAnyMap // The pared JSON objects for configuration files.
  32. vc bool // Whether do violence check in value index searching. It affects the performance when set true(false in default).
  33. }
  34. var (
  35. supportedFileTypes = []string{"toml", "yaml", "json", "ini", "xml"}
  36. resourceTryFiles = []string{"", "/", "config/", "config", "/config", "/config/"}
  37. )
  38. // New returns a new configuration management object.
  39. // The parameter <file> specifies the default configuration file name for reading.
  40. func New(file ...string) *Config {
  41. name := DefaultConfigFile
  42. if len(file) > 0 {
  43. name = file[0]
  44. }
  45. c := &Config{
  46. name: name,
  47. paths: garray.NewStrArray(true),
  48. jsons: gmap.NewStrAnyMap(true),
  49. }
  50. // Customized dir path from env/cmd.
  51. if envPath := gcmd.GetWithEnv(fmt.Sprintf("%s.path", cmdEnvKey)).String(); envPath != "" {
  52. if gfile.Exists(envPath) {
  53. _ = c.SetPath(envPath)
  54. } else {
  55. if errorPrint() {
  56. glog.Errorf("Configuration directory path does not exist: %s", envPath)
  57. }
  58. }
  59. } else {
  60. // Dir path of working dir.
  61. _ = c.SetPath(gfile.Pwd())
  62. // Dir path of binary.
  63. if selfPath := gfile.SelfDir(); selfPath != "" && gfile.Exists(selfPath) {
  64. _ = c.AddPath(selfPath)
  65. }
  66. // Dir path of main package.
  67. if mainPath := gfile.MainPkgPath(); mainPath != "" && gfile.Exists(mainPath) {
  68. _ = c.AddPath(mainPath)
  69. }
  70. }
  71. return c
  72. }
  73. // filePath returns the absolute configuration file path for the given filename by <file>.
  74. func (c *Config) filePath(file ...string) (path string) {
  75. name := c.name
  76. if len(file) > 0 {
  77. name = file[0]
  78. }
  79. path = c.FilePath(name)
  80. if path == "" {
  81. buffer := bytes.NewBuffer(nil)
  82. if c.paths.Len() > 0 {
  83. buffer.WriteString(fmt.Sprintf("[gcfg] cannot find config file \"%s\" in following paths:", name))
  84. c.paths.RLockFunc(func(array []string) {
  85. index := 1
  86. for _, v := range array {
  87. v = gstr.TrimRight(v, `\/`)
  88. buffer.WriteString(fmt.Sprintf("\n%d. %s", index, v))
  89. index++
  90. buffer.WriteString(fmt.Sprintf("\n%d. %s", index, v+gfile.Separator+"config"))
  91. index++
  92. }
  93. })
  94. } else {
  95. buffer.WriteString(fmt.Sprintf("[gcfg] cannot find config file \"%s\" with no path set/add", name))
  96. }
  97. if errorPrint() {
  98. glog.Error(buffer.String())
  99. }
  100. }
  101. return path
  102. }
  103. // SetPath sets the configuration directory path for file search.
  104. // The parameter <path> can be absolute or relative path,
  105. // but absolute path is strongly recommended.
  106. func (c *Config) SetPath(path string) error {
  107. var (
  108. isDir = false
  109. realPath = ""
  110. )
  111. if file := gres.Get(path); file != nil {
  112. realPath = path
  113. isDir = file.FileInfo().IsDir()
  114. } else {
  115. // Absolute path.
  116. realPath = gfile.RealPath(path)
  117. if realPath == "" {
  118. // Relative path.
  119. c.paths.RLockFunc(func(array []string) {
  120. for _, v := range array {
  121. if path, _ := gspath.Search(v, path); path != "" {
  122. realPath = path
  123. break
  124. }
  125. }
  126. })
  127. }
  128. if realPath != "" {
  129. isDir = gfile.IsDir(realPath)
  130. }
  131. }
  132. // Path not exist.
  133. if realPath == "" {
  134. buffer := bytes.NewBuffer(nil)
  135. if c.paths.Len() > 0 {
  136. buffer.WriteString(fmt.Sprintf("[gcfg] SetPath failed: cannot find directory \"%s\" in following paths:", path))
  137. c.paths.RLockFunc(func(array []string) {
  138. for k, v := range array {
  139. buffer.WriteString(fmt.Sprintf("\n%d. %s", k+1, v))
  140. }
  141. })
  142. } else {
  143. buffer.WriteString(fmt.Sprintf(`[gcfg] SetPath failed: path "%s" does not exist`, path))
  144. }
  145. err := errors.New(buffer.String())
  146. if errorPrint() {
  147. glog.Error(err)
  148. }
  149. return err
  150. }
  151. // Should be a directory.
  152. if !isDir {
  153. err := fmt.Errorf(`[gcfg] SetPath failed: path "%s" should be directory type`, path)
  154. if errorPrint() {
  155. glog.Error(err)
  156. }
  157. return err
  158. }
  159. // Repeated path check.
  160. if c.paths.Search(realPath) != -1 {
  161. return nil
  162. }
  163. c.jsons.Clear()
  164. c.paths.Clear()
  165. c.paths.Append(realPath)
  166. return nil
  167. }
  168. // SetViolenceCheck sets whether to perform hierarchical conflict checking.
  169. // This feature needs to be enabled when there is a level symbol in the key name.
  170. // It is off in default.
  171. //
  172. // Note that, turning on this feature is quite expensive, and it is not recommended
  173. // to allow separators in the key names. It is best to avoid this on the application side.
  174. func (c *Config) SetViolenceCheck(check bool) {
  175. c.vc = check
  176. c.Clear()
  177. }
  178. // AddPath adds a absolute or relative path to the search paths.
  179. func (c *Config) AddPath(path string) error {
  180. var (
  181. isDir = false
  182. realPath = ""
  183. )
  184. // It firstly checks the resource manager,
  185. // and then checks the filesystem for the path.
  186. if file := gres.Get(path); file != nil {
  187. realPath = path
  188. isDir = file.FileInfo().IsDir()
  189. } else {
  190. // Absolute path.
  191. realPath = gfile.RealPath(path)
  192. if realPath == "" {
  193. // Relative path.
  194. c.paths.RLockFunc(func(array []string) {
  195. for _, v := range array {
  196. if path, _ := gspath.Search(v, path); path != "" {
  197. realPath = path
  198. break
  199. }
  200. }
  201. })
  202. }
  203. if realPath != "" {
  204. isDir = gfile.IsDir(realPath)
  205. }
  206. }
  207. if realPath == "" {
  208. buffer := bytes.NewBuffer(nil)
  209. if c.paths.Len() > 0 {
  210. buffer.WriteString(fmt.Sprintf("[gcfg] AddPath failed: cannot find directory \"%s\" in following paths:", path))
  211. c.paths.RLockFunc(func(array []string) {
  212. for k, v := range array {
  213. buffer.WriteString(fmt.Sprintf("\n%d. %s", k+1, v))
  214. }
  215. })
  216. } else {
  217. buffer.WriteString(fmt.Sprintf(`[gcfg] AddPath failed: path "%s" does not exist`, path))
  218. }
  219. err := errors.New(buffer.String())
  220. if errorPrint() {
  221. glog.Error(err)
  222. }
  223. return err
  224. }
  225. if !isDir {
  226. err := fmt.Errorf(`[gcfg] AddPath failed: path "%s" should be directory type`, path)
  227. if errorPrint() {
  228. glog.Error(err)
  229. }
  230. return err
  231. }
  232. // Repeated path check.
  233. if c.paths.Search(realPath) != -1 {
  234. return nil
  235. }
  236. c.paths.Append(realPath)
  237. //glog.Debug("[gcfg] AddPath:", realPath)
  238. return nil
  239. }
  240. // GetFilePath returns the absolute path of the specified configuration file.
  241. // If <file> is not passed, it returns the configuration file path of the default name.
  242. // If the specified configuration file does not exist,
  243. // an empty string is returned.
  244. func (c *Config) FilePath(file ...string) (path string) {
  245. name := c.name
  246. if len(file) > 0 {
  247. name = file[0]
  248. }
  249. // Searching resource manager.
  250. if !gres.IsEmpty() {
  251. for _, v := range resourceTryFiles {
  252. if file := gres.Get(v + name); file != nil {
  253. path = file.Name()
  254. return
  255. }
  256. }
  257. c.paths.RLockFunc(func(array []string) {
  258. for _, prefix := range array {
  259. for _, v := range resourceTryFiles {
  260. if file := gres.Get(prefix + v + name); file != nil {
  261. path = file.Name()
  262. return
  263. }
  264. }
  265. }
  266. })
  267. }
  268. // Searching the file system.
  269. c.paths.RLockFunc(func(array []string) {
  270. for _, prefix := range array {
  271. prefix = gstr.TrimRight(prefix, `\/`)
  272. if path, _ = gspath.Search(prefix, name); path != "" {
  273. return
  274. }
  275. if path, _ = gspath.Search(prefix+gfile.Separator+"config", name); path != "" {
  276. return
  277. }
  278. }
  279. })
  280. return
  281. }
  282. // SetFileName sets the default configuration file name.
  283. func (c *Config) SetFileName(name string) *Config {
  284. c.name = name
  285. return c
  286. }
  287. // GetFileName returns the default configuration file name.
  288. func (c *Config) GetFileName() string {
  289. return c.name
  290. }
  291. // Available checks and returns whether configuration of given <file> is available.
  292. func (c *Config) Available(file ...string) bool {
  293. var name string
  294. if len(file) > 0 && file[0] != "" {
  295. name = file[0]
  296. } else {
  297. name = c.name
  298. }
  299. if c.FilePath(name) != "" {
  300. return true
  301. }
  302. if GetContent(name) != "" {
  303. return true
  304. }
  305. return false
  306. }
  307. // getJson returns a *gjson.Json object for the specified <file> content.
  308. // It would print error if file reading fails. It return nil if any error occurs.
  309. func (c *Config) getJson(file ...string) *gjson.Json {
  310. var name string
  311. if len(file) > 0 && file[0] != "" {
  312. name = file[0]
  313. } else {
  314. name = c.name
  315. }
  316. r := c.jsons.GetOrSetFuncLock(name, func() interface{} {
  317. var (
  318. content = ""
  319. filePath = ""
  320. )
  321. // The configured content can be any kind of data type different from its file type.
  322. isFromConfigContent := true
  323. if content = GetContent(name); content == "" {
  324. isFromConfigContent = false
  325. filePath = c.filePath(name)
  326. if filePath == "" {
  327. return nil
  328. }
  329. if file := gres.Get(filePath); file != nil {
  330. content = string(file.Content())
  331. } else {
  332. content = gfile.GetContents(filePath)
  333. }
  334. }
  335. // Note that the underlying configuration json object operations are concurrent safe.
  336. var (
  337. j *gjson.Json
  338. err error
  339. )
  340. dataType := gfile.ExtName(name)
  341. if gjson.IsValidDataType(dataType) && !isFromConfigContent {
  342. j, err = gjson.LoadContentType(dataType, content, true)
  343. } else {
  344. j, err = gjson.LoadContent(content, true)
  345. }
  346. if err == nil {
  347. j.SetViolenceCheck(c.vc)
  348. // Add monitor for this configuration file,
  349. // any changes of this file will refresh its cache in Config object.
  350. if filePath != "" && !gres.Contains(filePath) {
  351. _, err = gfsnotify.Add(filePath, func(event *gfsnotify.Event) {
  352. c.jsons.Remove(name)
  353. })
  354. if err != nil && errorPrint() {
  355. glog.Error(err)
  356. }
  357. }
  358. return j
  359. } else {
  360. if errorPrint() {
  361. if filePath != "" {
  362. glog.Criticalf(`[gcfg] Load config file "%s" failed: %s`, filePath, err.Error())
  363. } else {
  364. glog.Criticalf(`[gcfg] Load configuration failed: %s`, err.Error())
  365. }
  366. }
  367. }
  368. return nil
  369. })
  370. if r != nil {
  371. return r.(*gjson.Json)
  372. }
  373. return nil
  374. }