2018-03-25 17:18:30 +02:00
|
|
|
package cfg
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"bytes"
|
|
|
|
"errors"
|
2018-05-01 21:33:13 +02:00
|
|
|
"fmt"
|
2018-03-25 17:18:30 +02:00
|
|
|
"io"
|
2019-03-07 17:23:03 +01:00
|
|
|
"math"
|
2018-03-25 17:18:30 +02:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"reflect"
|
|
|
|
"strconv"
|
2018-04-01 20:17:41 +02:00
|
|
|
"strings"
|
2018-03-25 17:18:30 +02:00
|
|
|
"time"
|
|
|
|
"unicode"
|
|
|
|
)
|
|
|
|
|
2018-05-01 21:33:13 +02:00
|
|
|
const (
|
|
|
|
tagCfg = "cfg"
|
|
|
|
tagDefault = "default"
|
|
|
|
)
|
|
|
|
|
2018-06-24 23:01:01 +02:00
|
|
|
// ConfigFiles represents multiple file containing the config keys and values
|
2018-05-02 16:34:33 +02:00
|
|
|
type ConfigFiles struct {
|
2018-03-25 17:18:30 +02:00
|
|
|
Files []File
|
|
|
|
}
|
|
|
|
|
2018-06-24 23:01:01 +02:00
|
|
|
// File represents a file
|
|
|
|
// Required if an error should be thrown if file is absent
|
2018-03-25 17:18:30 +02:00
|
|
|
type File struct {
|
2018-06-24 23:01:01 +02:00
|
|
|
Name string
|
|
|
|
Path string
|
|
|
|
Required bool
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
|
|
|
|
2018-06-24 23:01:01 +02:00
|
|
|
// Default represents a default value for a field
|
2018-05-04 22:47:11 +02:00
|
|
|
type Default struct {
|
2018-05-01 21:33:13 +02:00
|
|
|
Value string
|
|
|
|
field reflect.Value
|
|
|
|
}
|
|
|
|
|
2018-06-24 23:01:01 +02:00
|
|
|
// CustomType can be implemented to unmarshal in a custom format
|
2018-03-29 14:11:41 +02:00
|
|
|
type CustomType interface {
|
|
|
|
Unmarshal(value string) error
|
|
|
|
}
|
|
|
|
|
2018-06-24 23:01:01 +02:00
|
|
|
// FileSize implements Unmarshal for parsing a file size config value.
|
|
|
|
// e.g. 10MB
|
2018-04-01 20:17:41 +02:00
|
|
|
type FileSize uint64
|
|
|
|
|
2019-03-07 17:23:03 +01:00
|
|
|
const (
|
|
|
|
B = 1 << (iota * 10)
|
|
|
|
KB
|
|
|
|
MB
|
|
|
|
GB
|
|
|
|
TB
|
|
|
|
)
|
|
|
|
|
|
|
|
var sizes = []string{"B", "KB", "MB", "GB", "TB"}
|
|
|
|
|
|
|
|
func (fs FileSize) HumanReadable() string {
|
2019-03-07 18:37:07 +01:00
|
|
|
exp := math.Floor(math.Log(float64(fs)) / math.Log(1024))
|
2019-03-07 17:23:03 +01:00
|
|
|
|
2019-03-07 18:37:07 +01:00
|
|
|
if exp > 4 {
|
|
|
|
exp = 4
|
|
|
|
}
|
2019-03-07 17:23:03 +01:00
|
|
|
s := sizes[int(exp)]
|
|
|
|
|
|
|
|
if exp == 0 {
|
|
|
|
return fmt.Sprintf("%d %s", fs, s)
|
|
|
|
}
|
|
|
|
|
|
|
|
val := float64(fs) / float64(math.Pow(1024, exp))
|
|
|
|
|
2019-03-07 18:37:07 +01:00
|
|
|
return fmt.Sprintf("%s %s", strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.2f", val), "0"), "."), s)
|
2019-03-07 17:23:03 +01:00
|
|
|
}
|
|
|
|
|
2018-04-01 20:17:41 +02:00
|
|
|
func (fs *FileSize) Unmarshal(value string) error {
|
|
|
|
size := FileSize(0)
|
|
|
|
|
|
|
|
if len(value) == 0 {
|
|
|
|
fs = &size
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-06-24 23:01:01 +02:00
|
|
|
value = strings.TrimSpace(strings.ToLower(value))
|
2018-04-01 20:17:41 +02:00
|
|
|
last := len(value) - 1
|
|
|
|
|
|
|
|
mp := uint64(1)
|
|
|
|
|
|
|
|
if value[last] == 'b' {
|
|
|
|
switch value[last-1] {
|
|
|
|
case 't':
|
|
|
|
mp = mp << 40
|
|
|
|
value = strings.TrimSpace(value[:last-1])
|
|
|
|
case 'g':
|
|
|
|
mp = mp << 30
|
|
|
|
value = strings.TrimSpace(value[:last-1])
|
|
|
|
case 'm':
|
|
|
|
mp = mp << 20
|
|
|
|
value = strings.TrimSpace(value[:last-1])
|
|
|
|
case 'k':
|
|
|
|
mp = mp << 10
|
|
|
|
value = strings.TrimSpace(value[:last-1])
|
|
|
|
default:
|
|
|
|
value = strings.TrimSpace(value[:last])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ps, err := strconv.ParseUint(value, 10, 64)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
*fs = FileSize(uint64(ps) * mp)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-05-01 21:33:13 +02:00
|
|
|
//AddConfig adds a config file
|
2018-06-24 23:01:01 +02:00
|
|
|
func (c *ConfigFiles) AddConfig(path, name string, required bool) {
|
2018-03-25 17:18:30 +02:00
|
|
|
f := File{
|
2018-06-24 23:01:01 +02:00
|
|
|
Path: path,
|
|
|
|
Name: name,
|
|
|
|
Required: required,
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
|
|
|
|
2018-03-27 22:11:26 +02:00
|
|
|
c.Files = append(c.Files, f)
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
|
|
|
|
2018-05-01 21:33:13 +02:00
|
|
|
//MergeConfigsInto merges multiple configs files into a struct
|
|
|
|
//returns the applied default values
|
2018-05-04 22:47:11 +02:00
|
|
|
func (c ConfigFiles) MergeConfigsInto(dest interface{}) (map[string]Default, error) {
|
2018-05-01 21:33:13 +02:00
|
|
|
kvs := make(map[string]string)
|
|
|
|
|
2018-03-25 17:18:30 +02:00
|
|
|
for _, v := range c.Files {
|
|
|
|
f, err := os.Open(filepath.Join(v.Path, v.Name))
|
|
|
|
|
|
|
|
if err != nil {
|
2018-06-24 23:01:01 +02:00
|
|
|
if !v.Required {
|
|
|
|
continue
|
|
|
|
}
|
2018-05-01 21:33:13 +02:00
|
|
|
return nil, err
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
defer f.Close()
|
|
|
|
|
2018-05-01 21:33:13 +02:00
|
|
|
kv, err := parse(f, dest)
|
2018-03-25 17:18:30 +02:00
|
|
|
|
|
|
|
if err != nil {
|
2018-05-01 21:33:13 +02:00
|
|
|
return nil, err
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
2018-04-18 15:16:39 +02:00
|
|
|
|
2018-05-01 21:33:13 +02:00
|
|
|
for k, v := range kv {
|
|
|
|
kvs[k] = v
|
|
|
|
}
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
|
|
|
|
2018-05-04 22:47:11 +02:00
|
|
|
defaults := make(map[string]Default)
|
2018-05-01 21:33:13 +02:00
|
|
|
err := setFields(kvs, defaults, dest)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return defaults, nil
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
|
|
|
|
2018-05-01 21:33:13 +02:00
|
|
|
//LoadConfigInto loads a single config into struct
|
|
|
|
//returns the applied default values
|
2018-05-04 22:47:11 +02:00
|
|
|
func LoadConfigInto(file string, dest interface{}) (map[string]Default, error) {
|
2018-04-01 20:17:41 +02:00
|
|
|
f, err := os.Open(file)
|
|
|
|
|
|
|
|
if err != nil {
|
2018-05-01 21:33:13 +02:00
|
|
|
return nil, err
|
2018-04-01 20:17:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
defer f.Close()
|
|
|
|
|
2018-05-01 21:33:13 +02:00
|
|
|
kvs, err := parse(f, dest)
|
2018-04-01 20:17:41 +02:00
|
|
|
|
|
|
|
if err != nil {
|
2018-05-01 21:33:13 +02:00
|
|
|
return nil, err
|
2018-04-01 20:17:41 +02:00
|
|
|
}
|
|
|
|
|
2018-05-04 22:47:11 +02:00
|
|
|
defaults := make(map[string]Default)
|
2018-05-01 21:33:13 +02:00
|
|
|
|
|
|
|
err = setFields(kvs, defaults, dest)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return defaults, nil
|
2018-04-01 20:17:41 +02:00
|
|
|
}
|
|
|
|
|
2018-05-01 21:33:13 +02:00
|
|
|
func parse(file *os.File, dest interface{}) (map[string]string, error) {
|
2018-03-25 17:18:30 +02:00
|
|
|
reader := bufio.NewReader(file)
|
|
|
|
kvmap := make(map[string]string)
|
|
|
|
|
|
|
|
for {
|
|
|
|
line, err := reader.ReadBytes('\n')
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
if err == io.EOF {
|
|
|
|
break
|
|
|
|
} else {
|
2018-05-01 21:33:13 +02:00
|
|
|
return nil, err
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
line = bytes.TrimLeftFunc(line, unicode.IsSpace)
|
|
|
|
|
|
|
|
if len(line) == 0 {
|
2018-03-25 18:12:04 +02:00
|
|
|
continue
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if line[0] == '#' {
|
2018-03-25 18:12:04 +02:00
|
|
|
continue
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
kv := bytes.SplitN(line, []byte("="), 2)
|
|
|
|
|
|
|
|
if len(kv) < 2 {
|
2018-03-25 18:12:04 +02:00
|
|
|
continue
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
key := string(bytes.TrimRightFunc(kv[0], unicode.IsSpace))
|
|
|
|
value := string(bytes.TrimSpace(bytes.TrimRight(kv[1], "\r\n")))
|
|
|
|
kvmap[key] = value
|
|
|
|
}
|
|
|
|
|
2018-05-01 21:33:13 +02:00
|
|
|
return kvmap, nil
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
|
|
|
|
2018-05-04 22:47:11 +02:00
|
|
|
func setFields(kv map[string]string, defaults map[string]Default, dest interface{}) error {
|
2018-03-25 17:18:30 +02:00
|
|
|
v := reflect.ValueOf(dest)
|
|
|
|
|
|
|
|
if v.Kind() != reflect.Ptr {
|
|
|
|
return errors.New("struct must be a pointer")
|
|
|
|
}
|
|
|
|
|
|
|
|
el := v.Elem()
|
|
|
|
|
|
|
|
for i := 0; i < el.NumField(); i++ {
|
2018-03-25 18:12:04 +02:00
|
|
|
if el.Field(i).Kind() == reflect.Struct {
|
2018-05-01 21:33:13 +02:00
|
|
|
err := setFields(kv, defaults, el.Field(i).Addr().Interface())
|
2018-04-18 15:16:39 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-03-25 18:12:04 +02:00
|
|
|
continue
|
|
|
|
}
|
2018-03-25 17:18:30 +02:00
|
|
|
if el.Field(i).CanSet() {
|
|
|
|
sKey := el.Type().Field(i).Tag.Get(tagCfg)
|
2018-05-01 21:33:13 +02:00
|
|
|
defValue := el.Type().Field(i).Tag.Get(tagDefault)
|
2018-03-25 17:18:30 +02:00
|
|
|
|
2018-03-28 00:34:49 +02:00
|
|
|
if sKey == "-" {
|
|
|
|
continue
|
|
|
|
}
|
2018-03-28 00:46:21 +02:00
|
|
|
|
2018-03-25 17:18:30 +02:00
|
|
|
if len(sKey) == 0 {
|
|
|
|
sKey = el.Type().Field(i).Name
|
|
|
|
}
|
|
|
|
|
2018-05-04 22:47:11 +02:00
|
|
|
def := Default{}
|
2018-05-01 21:33:13 +02:00
|
|
|
|
|
|
|
if len(defValue) > 0 {
|
2018-05-04 22:47:11 +02:00
|
|
|
def = Default{
|
2018-05-01 21:33:13 +02:00
|
|
|
Value: defValue,
|
|
|
|
field: el.Field(i),
|
|
|
|
}
|
|
|
|
|
|
|
|
defaults[sKey] = def
|
|
|
|
}
|
|
|
|
|
2018-03-25 17:18:30 +02:00
|
|
|
value, ok := kv[sKey]
|
|
|
|
|
|
|
|
if ok {
|
2018-03-25 18:12:04 +02:00
|
|
|
err := setField(el.Field(i), value)
|
2018-03-25 17:18:30 +02:00
|
|
|
|
|
|
|
if err != nil {
|
2018-05-04 22:47:11 +02:00
|
|
|
if def != (Default{}) {
|
2018-05-01 22:01:44 +02:00
|
|
|
//ignore error here if key has a default
|
2018-05-01 21:33:13 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
return fmt.Errorf("error while setting value [%s] for key [%s] error %v", value, sKey, err)
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
|
|
|
|
2018-05-01 21:33:13 +02:00
|
|
|
delete(defaults, sKey)
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-05-01 21:33:13 +02:00
|
|
|
for k, d := range defaults {
|
|
|
|
err := setField(d.field, d.Value)
|
2018-03-25 17:18:30 +02:00
|
|
|
if err != nil {
|
2018-05-01 21:33:13 +02:00
|
|
|
return fmt.Errorf("error while setting default value [%s] for key [%s] error %v", d.Value, k, err)
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-03-25 18:12:04 +02:00
|
|
|
func setField(field reflect.Value, value string) error {
|
2018-03-29 14:11:41 +02:00
|
|
|
customType := reflect.TypeOf((*CustomType)(nil)).Elem()
|
|
|
|
|
|
|
|
if reflect.PtrTo(field.Type()).Implements(customType) {
|
|
|
|
if c, ok := field.Addr().Interface().(CustomType); ok {
|
|
|
|
err := c.Unmarshal(value)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-03-25 17:18:30 +02:00
|
|
|
switch field.Kind() {
|
|
|
|
case reflect.String:
|
|
|
|
field.SetString(value)
|
|
|
|
case reflect.Int8, reflect.Int16, reflect.Int, reflect.Int64:
|
|
|
|
d, err := time.ParseDuration(value)
|
|
|
|
|
|
|
|
if err == nil {
|
|
|
|
field.Set(reflect.ValueOf(d))
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
iVal, err := strconv.ParseInt(value, 10, 64)
|
|
|
|
|
|
|
|
if err != nil {
|
2018-03-28 00:46:21 +02:00
|
|
|
return err
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
field.SetInt(int64(iVal))
|
|
|
|
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
|
|
iVal, err := strconv.ParseUint(value, 10, 64)
|
|
|
|
|
|
|
|
if err != nil {
|
2018-03-28 00:46:21 +02:00
|
|
|
return err
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
field.SetUint(uint64(iVal))
|
|
|
|
case reflect.Bool:
|
|
|
|
b := false
|
|
|
|
|
|
|
|
if value == "yes" || value == "YES" || value == "Yes" {
|
|
|
|
b = true
|
|
|
|
} else if value == "no" || value == "NO" || value == "No" {
|
|
|
|
b = false
|
|
|
|
} else {
|
|
|
|
var err error
|
|
|
|
b, err = strconv.ParseBool(value)
|
|
|
|
|
|
|
|
if err != nil {
|
2018-03-28 00:46:21 +02:00
|
|
|
return err
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
field.SetBool(b)
|
|
|
|
case reflect.Float32, reflect.Float64:
|
|
|
|
f, err := strconv.ParseFloat(value, 64)
|
|
|
|
|
|
|
|
if err != nil {
|
2018-03-28 00:46:21 +02:00
|
|
|
return err
|
2018-03-25 17:18:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
field.SetFloat(float64(f))
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|