diff --git a/.gitignore b/.gitignore index adac6a6..f38a348 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Binaries for programs and plugins +supervisor-eventlistener *.exe *.dll *.so @@ -11,5 +12,7 @@ *.out # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 -.glide/ -.idear +.*/ +vendor/**/*.yml +vendor/**/.gitignore +build/ diff --git a/LICENSE b/LICENSE index 2316abe..12c3281 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 qiang.ou +Copyright (c) 2017-2021 qiang.ou Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..464b535 --- /dev/null +++ b/Makefile @@ -0,0 +1,58 @@ +project_name:=supervisor-eventlistener +project_version:=1.2.3 +root_dir := $(abspath $(CURDIR)) +build_dir := $(root_dir)/build +GOPATH := ${HOME}/go + +all: clean release + + +clean: + rm -fr $(build_dir) + + +build-bydocker: + sudo docker run -it --rm \ + -v $(GOPATH)/:/root/go/ \ + -v $(root_dir)/:/$(project_name) \ + -w /$(project_name)/ \ + golang:1.17.0-buster \ + make build + + +build: + GO111MODULE=on go build -o $(project_name) ./main.go + + +release: clean build-bydocker + mkdir -p $(build_dir)/$(project_name)/ + mv ./supervisor-eventlistener $(build_dir)/$(project_name)/ + cp ./conf/config.toml $(build_dir)/$(project_name)/ + cd $(build_dir) && tar -zcvf $(project_name)-$(project_version).tar.gz $(project_name) + @echo ...created $(build_dir)/$(project_name)-$(project_version).tar.gz + @echo ...done. + + +log: + tmux new-session -d -s dev + tmux split-window -t "dev:0" + tmux split-window -t "dev:0.0" -h + tmux split-window -t "dev:0.2" -h + tmux send-keys -t "dev:0.0" "bash -c 'tail -f /tmp/supervisor-eventlistener.log'" Enter + tmux send-keys -t "dev:0.1" "bash -c 'sudo supervisorctl tail -f $(project_name)'" Enter + tmux set-option -g mouse on + tmux attach -t dev + tmux kill-session -t dev + + +test-integration: + go build + sudo supervisorctl stop $(project_name) + sudo cp ./$(project_name) /usr/local/bin/ + sudo cp ./tests/supervisor-app.ini /etc/supervisor.d/ + sudo supervisorctl remove $(project_name) + sudo supervisorctl update $(project_name) + sudo supervisorctl tail -f $(project_name) stderr + + +.PHONY: clean build build-bydocker release log test-integration diff --git a/README.md b/README.md index f98e7be..2fa6492 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,57 @@ -# supervisor-event-listener -Supervisor事件通知, 支持邮件, Slack, WebHook - -## 简介 -Supervisor是*nix环境下的进程管理工具, 可以把前台进程转换为守护进程, 当进程异常退出时自动重启. -supervisor-event-listener监听进程异常退出事件, 并发送通知. - -## 下载 -[v1.0](https://github.com/ouqiang/supervisor-event-listener/releases) - -### 源码安装 -* `go get -u github.com/ouqiang/supervisor-event-listener` - -## Supervisor配置 -```ini -[eventlistener:supervisor-event-listener] -; 默认读取配置文件/etc/supervisor-event-listener.ini -command=/path/to/supervisor-event-listener -; 指定配置文件路径 -;command=/path/to/supervisor-event-listener -c /path/to/supervisor-event-listener.ini -events=PROCESS_STATE_EXITED -...... -``` - -## 配置文件, 默认读取`/etc/supervisor-event-listener.ini` - -```ini -[default] -# 通知类型 mail,slack,webhook 只能选择一种 -notify_type = mail - -# 邮件服务器配置 -mail.server.user = test@163.com -mail.server.password = 123456 -mail.server.host = smtp.163.com -mail.server.port = 25 - -# 邮件收件人配置, 多个收件人, 逗号分隔 -mail.user = hello@163.com - -# Slack配置 -slack.webhook_url = https://hooks.slack.com/services/xxxx/xxx/xxxx -slack.channel = exception - -# WebHook通知URL配置 -webhook_url = http://my.webhook.com - -``` - -## 通知内容 -邮件、Slack -```shell -Host: ip(hostname) -Process: process-name -PID: 6152 -EXITED FROM state: RUNNING -``` -WebHook, Post请求, 字段含义查看Supervisor文档 -```json -{ - "Header": { - "Ver": "3.0", - "Server": "supervisor", - "Serial": 11, - "Pool": "supervisor-listener", - "PoolSerial": 11, - "EventName": "PROCESS_STATE_EXITED", - "Len": 84 - }, - "Payload": { - "Ip": "ip(hostname)", - "ProcessName": "process-name", - "GroupName": "group-name", - "FromState": "RUNNING", - "Expected": 0, - "Pid": 6371 - } -} -``` +# Introduction +A `eventlistener` for supervisor, it may listen and redirect events to e-mail, webhook, slack and so on. +More details http://supervisord.org/events.html + +# Features +* support e-mail, webhook, slack +* support bearychat, lack(feishu) + +# Usage + +1. setup supervisor-eventlistener + ```toml + [mail] + receivers = ["hello@163.com", "world@163.com"] + server_user = "test@163.com" + server_password = "123456" + server_host = "smtp.163.com" + server_port = 25 + + [slack] + url = "https://hooks.slack.com/services/xxxx/xxx/xxxx" + channel = "exception" + timeout = 6 + + [webhook] + url = "http://my.webhook.com" + timeout = 6 + + [bearychat] + url = "https://hook.bearychat.com/xxx/xxxx" + channel = "alert" + timeout = 6 + + [feishu] + url = "https://hook.feishu.com/xxx/xxxx?signKey=it_is_optional" + timeout = 6 + ``` + +2. setup supervisor + ```ini + [eventlistener:supervisor-event-listener] + command=/usr/local/bin/supervisor-event-listener -c /etc/supervisor-event-listener.toml + user=nobody + group=nobody + events= + PROCESS_STATE_EXITED, + PROCESS_STATE_FATAL, + PROCESS_STATE_STOPPED, + PROCESS_STATE_RUNNING + ``` + +3. start supervisor-eventlistener + ```bash + supervisorctl start supervisor-event-listener + ``` + +That's all. \ No newline at end of file diff --git a/build.sh b/build.sh deleted file mode 100644 index 8062b5c..0000000 --- a/build.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash - -# set -x -u -# 构建应用, supervisor-event-listener.tar.gz -# ./build.sh -p linux -a amd64 -# 参数含义 -# -p 指定平台(linux|darwin) -# -a 指定体系架构(amd64|386), 默认amd64 - - -TEMP_DIR=`date +%s`-temp-`echo $RANDOM` - -# 目标平台 linux,darwin -OS='' -# 目标平台架构 -ARCH='' -# 应用名称 -APP_NAME='supervisor-event-listener' -# 可执行文件名 -EXEC_NAME='' -# 压缩包名称 -COMPRESS_FILE='' - - -# -p 平台 -a 架构 -while getopts "p:a:" OPT; -do - case $OPT in - p) OS=$OPTARG - ;; - a) ARCH=$OPTARG - ;; - esac -done - -if [[ -z $OS ]];then - echo "平台不能为空" - exit 1 -fi - -if [[ $OS && $OS != 'linux' && $OS != 'darwin' ]];then - echo '平台错误,支持的平台 linux darmin(osx)' - exit 1 -fi - -if [[ -z $ARCH ]];then - ARCH='amd64' -fi - -if [[ $ARCH != '386' && $ARCH != 'amd64' ]];then - echo 'arch错误,仅支持 386 amd64' - exit 1 -fi - -echo '开始编译' - -GOOS=$OS GOARCH=$ARCH go build -ldflags '-w' - -if [[ $? != 0 ]];then - exit 1 -fi -echo '编译完成' - - -EXEC_NAME=${APP_NAME} -COMPRESS_FILE=${APP_NAME}-${OS}-${ARCH}.tar.gz - -mkdir -p $TEMP_DIR/$APP_NAME -if [[ $? != 0 ]]; then - exit 1 -fi - -# 需要打包的文件 -PACKAGE_FILENAME=(supervisor-event-listener.ini ${EXEC_NAME}) - -echo '复制文件到临时目录' -# 复制文件到临时目录 -for i in ${PACKAGE_FILENAME[*]} -do - cp -r $i $TEMP_DIR/$APP_NAME -done - - -echo '压缩文件' -# 压缩文件 -cd $TEMP_DIR - -tar czf $COMPRESS_FILE * -mv $COMPRESS_FILE ../ -cd ../ - -rm $EXEC_NAME -rm -rf $TEMP_DIR - -echo '打包完成' -echo '生成压缩文件--' $COMPRESS_FILE \ No newline at end of file diff --git a/conf/config.go b/conf/config.go new file mode 100644 index 0000000..f5c13c1 --- /dev/null +++ b/conf/config.go @@ -0,0 +1,56 @@ +package conf + +import ( + "fmt" + "sync/atomic" + + validator "github.com/go-playground/validator/v10" +) + +var ( + // c = &Config{} + cfg atomic.Value +) + +type Config struct { + DryRun *DryRun + WebHook *WebHook + Mail *Mail + Slack *Slack + BearyChat *BearyChat + Feishu *Feishu +} + +func (c *Config) Validate() error { + if c == nil { + return fmt.Errorf("nil config") + } + + if c.BearyChat != nil { + if c.BearyChat.URL == "" { + return fmt.Errorf("Invalid bearcychat config") + } + } + if c.Mail != nil { + if len(c.Mail.Receivers) <= 0 { + return fmt.Errorf("Invalid mail config") + } + } + if c.Slack != nil { + if c.Slack.URL == "" { + return fmt.Errorf("Invalid slack config") + } + } + + if c.WebHook != nil { + if c.WebHook.URL == "" { + return fmt.Errorf("Invalid slack config") + } + } + + validate := validator.New() + if err := validate.Struct(c); err != nil { + return err + } + return nil +} diff --git a/conf/config.toml b/conf/config.toml new file mode 100644 index 0000000..9e7ec34 --- /dev/null +++ b/conf/config.toml @@ -0,0 +1,24 @@ +[mail] +receivers = ["hello@163.com", "world@163.com"] +server_user = "test@163.com" +server_password = "123456" +server_host = "smtp.163.com" +server_port = 25 + +[slack] +url = "https://hooks.slack.com/services/xxxx/xxx/xxxx" +channel = "exception" +timeout = 6 + +[webhook] +url = "http://my.webhook.com" +timeout = 6 + +[bearychat] +url = "https://hook.bearychat.com/xxx/xxxx" +channel = "alert" +timeout = 6 + +[feishu] +url = "https://hook.feishu.com/xxx/xxxx?signKey=it_is_optional" +timeout = 6 \ No newline at end of file diff --git a/conf/init.go b/conf/init.go new file mode 100644 index 0000000..e4882b1 --- /dev/null +++ b/conf/init.go @@ -0,0 +1,36 @@ +package conf + +import ( + "io" + "os" + + "github.com/pelletier/go-toml" +) + +func Init(fpath string) error { + f, err := os.Open(fpath) + if err != nil { + return err + } + data, err := io.ReadAll(f) + if err != nil { + return err + } + c := Config{} + if err := toml.Unmarshal(data, &c); err != nil { + return err + } + if err := c.Validate(); err != nil { + return err + } + cfg.Store(&c) + return nil +} + +func Reload(fpath string) error { + return Init(fpath) +} + +func Get() *Config { + return cfg.Load().(*Config) +} diff --git a/conf/listeners.go b/conf/listeners.go new file mode 100644 index 0000000..bdd92ef --- /dev/null +++ b/conf/listeners.go @@ -0,0 +1,35 @@ +package conf + +type DryRun struct { + Text string `toml:"text"` +} + +// 邮件服务器 +type Mail struct { + Receivers []string `toml:"receivers" validate:"required"` + ServerUser string `toml:"server_user" validate:"required"` + ServerPassword string `toml:"server_password" validate:"required"` + ServerHost string `toml:"server_host" validate:"required"` + ServerPort int `toml:"server_port" validate:"required"` +} + +type WebHook struct { + URL string `toml:"url" validate:"required"` + Timeout int `toml:"timeout"` +} + +type Slack struct { + URL string `toml:"url" validate:"required"` + Channel string `toml:"channel" validate:"required"` +} + +type BearyChat struct { + URL string `toml:"url" validate:"required"` + Channel string `toml:"channel"` + Timeout int `toml:"timeout"` +} + +type Feishu struct { + URL string `toml:"url" validate:"required"` + Timeout int `toml:"timeout"` +} diff --git a/config/config.go b/config/config.go deleted file mode 100644 index 06475a8..0000000 --- a/config/config.go +++ /dev/null @@ -1,140 +0,0 @@ -package config - -import ( - "flag" - "fmt" - "github.com/ouqiang/supervisor-event-listener/utils" - "gopkg.in/ini.v1" - "os" - "strings" -) - -type Config struct { - NotifyType string - WebHook WebHook - MailServer MailServer - MailUser MailUser - Slack Slack -} - -type WebHook struct { - Url string -} - -type Slack struct { - WebHookUrl string - Channel string -} - -// 邮件服务器 -type MailServer struct { - User string - Password string - Host string - Port int -} - -// 接收邮件的用户 -type MailUser struct { - Email []string -} - -func ParseConfig() *Config { - var configFile string - flag.StringVar(&configFile, "c", "/etc/supervisor-event-listener.ini", "config file") - flag.Parse() - configFile = strings.TrimSpace(configFile) - if configFile == "" { - Exit("请指定配置文件路径") - } - file, err := ini.Load(configFile) - if err != nil { - Exit("读取配置文件失败#" + err.Error()) - } - section := file.Section("default") - notifyType := section.Key("notify_type").String() - notifyType = strings.TrimSpace(notifyType) - if !utils.InStringSlice([]string{"mail", "slack", "webhook"}, notifyType) { - Exit("不支持的通知类型-" + notifyType) - } - - config := &Config{} - config.NotifyType = notifyType - switch notifyType { - case "mail": - config.MailServer = parseMailServer(section) - config.MailUser = parseMailUser(section) - case "slack": - config.Slack = parseSlack(section) - case "webhook": - config.WebHook = parseWebHook(section) - } - - return config -} - -func parseMailServer(section *ini.Section) MailServer { - user := section.Key("mail.server.user").String() - user = strings.TrimSpace(user) - password := section.Key("mail.server.password").String() - password = strings.TrimSpace(password) - host := section.Key("mail.server.host").String() - host = strings.TrimSpace(host) - port, portErr := section.Key("mail.server.port").Int() - if user == "" || password == "" || host == "" || portErr != nil { - Exit("邮件服务器配置错误") - } - - mailServer := MailServer{} - mailServer.User = user - mailServer.Password = password - mailServer.Host = host - mailServer.Port = port - - return mailServer -} - -func parseMailUser(section *ini.Section) MailUser { - user := section.Key("mail.user").String() - user = strings.TrimSpace(user) - if user == "" { - Exit("邮件收件人配置错误") - } - mailUser := MailUser{} - mailUser.Email = strings.Split(user, ",") - - return mailUser -} - -func parseSlack(section *ini.Section) Slack { - webHookUrl := section.Key("slack.webhook_url").String() - webHookUrl = strings.TrimSpace(webHookUrl) - channel := section.Key("slack.channel").String() - channel = strings.TrimSpace(channel) - if webHookUrl == "" || channel == "" { - Exit("Slack配置错误") - } - - slack := Slack{} - slack.WebHookUrl = webHookUrl - slack.Channel = channel - - return slack -} - -func parseWebHook(section *ini.Section) WebHook { - url := section.Key("webhook_url").String() - url = strings.TrimSpace(url) - if url == "" { - Exit("WebHookUrl配置错误") - } - webHook := WebHook{} - webHook.Url = url - - return webHook -} - -func Exit(msg string) { - fmt.Fprintln(os.Stderr, msg) - os.Exit(1) -} diff --git a/event/errs.go b/event/errs.go new file mode 100644 index 0000000..fc7f277 --- /dev/null +++ b/event/errs.go @@ -0,0 +1,11 @@ +package event + +import ( + "errors" +) + +var ( + ErrParseHeader = errors.New("parse header failed") + ErrParsePayload = errors.New("parse payload failed") + ErrPayloadLength = errors.New("invalid payload length") +) diff --git a/event/event.go b/event/event.go deleted file mode 100644 index bf07cc0..0000000 --- a/event/event.go +++ /dev/null @@ -1,110 +0,0 @@ -package event - -import ( - "errors" - "fmt" - "github.com/ouqiang/supervisor-event-listener/utils" - "os" - "strconv" - "strings" -) - -// Message 消息格式 -type Message struct { - Header *Header - Payload *Payload -} - -func (msg *Message) String() string { - return fmt.Sprintf("Host: %s\nProcess: %s\nPID: %d\nEXITED FROM state: %s", msg.Payload.Ip, msg.Payload.ProcessName, msg.Payload.Pid, msg.Payload.FromState) - -} - -// Header Supervisord触发事件时会先发送Header,根据Header中len字段去读取Payload -type Header struct { - Ver string - Server string - Serial int - Pool string - PoolSerial int - EventName string // 事件名称 - Len int // Payload长度 -} - -// Payload -type Payload struct { - Ip string - ProcessName string // 进程名称 - GroupName string // 进程组名称 - FromState string - Expected int - Pid int -} - -// Fields -type Fields map[string]string - -var ( - ErrParseHeader = errors.New("解析Header失败") - ErrParsePayload = errors.New("解析Payload失败") -) - -func ParseHeader(header string) (*Header, error) { - h := &Header{} - fields := parseFields(header) - if len(fields) == 0 { - return h, ErrParseHeader - } - - h.Ver = fields["ver"] - h.Server = fields["server"] - h.Serial, _ = strconv.Atoi(fields["serial"]) - h.Pool = fields["pool"] - h.PoolSerial, _ = strconv.Atoi(fields["poolserial"]) - h.EventName = fields["eventname"] - h.Len, _ = strconv.Atoi(fields["len"]) - - return h, nil -} - -func ParsePayload(payload string) (*Payload, error) { - p := &Payload{} - fields := parseFields(payload) - if len(fields) == 0 { - return p, ErrParsePayload - } - hostname, _ := os.Hostname() - p.Ip = fmt.Sprintf("%s(%s)", utils.GetLocalIp(), hostname) - p.ProcessName = fields["processname"] - p.GroupName = fields["groupname"] - p.FromState = fields["from_state"] - p.Expected, _ = strconv.Atoi(fields["expected"]) - p.Pid, _ = strconv.Atoi(fields["pid"]) - - return p, nil -} - -func parseFields(data string) Fields { - fields := make(Fields) - data = strings.TrimSpace(data) - if data == "" { - return fields - } - // 格式如下 - // ver:3.0 server:supervisor serial:5 - slice := strings.Split(data, " ") - if len(slice) == 0 { - return fields - } - for _, item := range slice { - group := strings.Split(item, ":") - if len(group) < 2 { - continue - } - key := strings.TrimSpace(group[0]) - value := strings.TrimSpace(group[1]) - fields[key] = value - } - - return fields -} diff --git a/event/header.go b/event/header.go new file mode 100644 index 0000000..2981363 --- /dev/null +++ b/event/header.go @@ -0,0 +1,31 @@ +package event + +import "strconv" + +type Header struct { + Ver string + Server string + Serial int + Pool string + PoolSerial int + EventName string // 事件名称 + Len int // Payload长度 +} + +func ParseHeader(header string) (*Header, error) { + h := &Header{} + fields := parseFields(header) + if len(fields) == 0 { + return h, ErrParseHeader + } + + h.Ver = fields["ver"] + h.Server = fields["server"] + h.Serial, _ = strconv.Atoi(fields["serial"]) + h.Pool = fields["pool"] + h.PoolSerial, _ = strconv.Atoi(fields["poolserial"]) + h.EventName = fields["eventname"] + h.Len, _ = strconv.Atoi(fields["len"]) + + return h, nil +} diff --git a/event/message.go b/event/message.go new file mode 100644 index 0000000..9da905e --- /dev/null +++ b/event/message.go @@ -0,0 +1,113 @@ +package event + +import ( + "bufio" + "encoding/json" + "fmt" + "time" + + "github.com/ouqiang/supervisor-event-listener/utils/errlog" + "github.com/pkg/errors" +) + +type Message struct { + TS time.Time + Header *Header + Payload *Payload +} + +func NewMessage(h *Header, p *Payload) Message { + return Message{ + TS: time.Now(), + Header: h, + Payload: p, + } +} + +func ReadMessage(reader *bufio.Reader) (Message, error) { + header, err := readHeader(reader) + if err != nil { + errlog.Error("header:%+v err:%+v", header, err) + return Message{}, err + } + payload, err := readPayload(reader, header.Len) + if err != nil { + errlog.Error("payload:%+v err:%+v", payload, err) + return Message{}, err + } + return NewMessage(header, payload), nil +} + +// 读取header +func readHeader(reader *bufio.Reader) (*Header, error) { + // 读取Header + data, err := reader.ReadString('\n') + if err != nil { + return nil, err + } + // 解析Header + header, err := ParseHeader(data) + if err != nil { + return nil, err + } + return header, nil +} + +// 读取payload +func readPayload(reader *bufio.Reader, payloadLen int) (*Payload, error) { + // 读取payload + buf := make([]byte, payloadLen) + length, err := reader.Read(buf) + if err != nil { + return nil, err + } + if payloadLen != length { + err := ErrPayloadLength + err = errors.Wrapf(err, " payloadLen:%d != length:%d", payloadLen, length) + return nil, err + } + // 解析payload + payload, err := ParsePayload(string(buf)) + if err != nil { + return nil, err + } + return payload, nil +} + +func (msg *Message) String() string { + tmpl := `Proc: %s +Host: %s +PID: %d State: %s +Date: %s` + return fmt.Sprintf(tmpl, + msg.Payload.ProcessName, + msg.Payload.IP, + msg.Payload.PID, msg.Payload.FromState, + msg.TS.Format(time.RFC3339), + ) +} + +func (msg *Message) ToJson(indent ...int) string { + realIndent := 0 + if len(indent) > 0 { + realIndent = indent[0] + } + t := "" + switch realIndent { + case 0: + case 1: + t = " " + case 2: + t = " " + case 3: + t = " " + case 4: + t = " " + default: + t = " " + } + _bytes, _ := json.MarshalIndent(msg, "", t) + return string(_bytes) +} + +// Header Supervisord触发事件时会先发送Header,根据Header中len字段去读取Payload diff --git a/event/payload.go b/event/payload.go new file mode 100644 index 0000000..75cbe39 --- /dev/null +++ b/event/payload.go @@ -0,0 +1,62 @@ +package event + +import ( + "fmt" + "os" + "strconv" + "strings" + + "github.com/ouqiang/supervisor-event-listener/utils" +) + +type Payload struct { + IP string + ProcessName string // 进程名称 + GroupName string // 进程组名称 + FromState string + Expected int + PID int +} + +type Fields map[string]string + +func ParsePayload(payload string) (*Payload, error) { + p := &Payload{} + fields := parseFields(payload) + if len(fields) == 0 { + return nil, ErrParsePayload + } + hostname, _ := os.Hostname() + p.IP = fmt.Sprintf("%s(%s)", utils.GetLocalIp(), hostname) + p.ProcessName = fields["processname"] + p.GroupName = fields["groupname"] + p.FromState = fields["from_state"] + p.Expected, _ = strconv.Atoi(fields["expected"]) + p.PID, _ = strconv.Atoi(fields["pid"]) + return p, nil +} + +func parseFields(data string) Fields { + fields := make(Fields) + data = strings.TrimSpace(data) + if data == "" { + return fields + } + // 格式如下 + // ver:3.0 server:supervisor serial:5 + slice := strings.Split(data, " ") + if len(slice) == 0 { + return fields + } + for _, item := range slice { + group := strings.Split(item, ":") + if len(group) < 2 { + continue + } + key := strings.TrimSpace(group[0]) + value := strings.TrimSpace(group[1]) + fields[key] = value + } + + return fields +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7fb2136 --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module github.com/ouqiang/supervisor-event-listener + +go 1.17 + +require ( + github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df + github.com/go-playground/validator/v10 v10.9.0 + github.com/pelletier/go-toml v1.9.4 + github.com/pkg/errors v0.9.1 +) + +require ( + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect + golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect + golang.org/x/text v0.3.6 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..413d5a5 --- /dev/null +++ b/go.sum @@ -0,0 +1,58 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df h1:Bao6dhmbTA1KFVxmJ6nBoMuOJit2yjEgLJpIMYpop0E= +github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A= +github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= +github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/listener/listener.go b/listener/listener.go index 39b9bce..8944431 100644 --- a/listener/listener.go +++ b/listener/listener.go @@ -4,23 +4,34 @@ import ( "bufio" "errors" "fmt" - "github.com/ouqiang/supervisor-event-listener/event" - "github.com/ouqiang/supervisor-event-listener/listener/notify" "log" "os" + "time" + + "github.com/ouqiang/supervisor-event-listener/event" + "github.com/ouqiang/supervisor-event-listener/notify" ) var ( + // ErrPayloadLength ... ErrPayloadLength = errors.New("Header中len长度与实际读取长度不一致") ) +// Start ... func Start() { - defer func() { - if err := recover(); err != nil { - log.Print("panic", err) - } - }() - listen() + go run() +} + +func run() { + for { + defer func() { + if err := recover(); err != nil { + log.Print("panic", err) + } + }() + listen() + time.Sleep(time.Second) + } } // 监听事件, 从标准输入获取事件内容 @@ -28,60 +39,16 @@ func listen() { reader := bufio.NewReader(os.Stdin) for { ready() - header, err := readHeader(reader) - if err != nil { - failure(err) - continue - } - payload, err := readPayload(reader, header.Len) + msg, err := event.ReadMessage(reader) if err != nil { failure(err) continue } - // 只处理进程异常退出事件 - if header.EventName == "PROCESS_STATE_EXITED" { - notify.Push(header, payload) - } success() + notify.Push(&msg) } } -// 读取header -func readHeader(reader *bufio.Reader) (*event.Header, error) { - // 读取Header - data, err := reader.ReadString('\n') - if err != nil { - return nil, err - } - // 解析Header - header, err := event.ParseHeader(data) - if err != nil { - return nil, err - } - - return header, nil -} - -// 读取payload -func readPayload(reader *bufio.Reader, payloadLen int) (*event.Payload, error) { - // 读取payload - buf := make([]byte, payloadLen) - length, err := reader.Read(buf) - if err != nil { - return nil, err - } - if payloadLen != length { - return nil, ErrPayloadLength - } - // 解析payload - payload, err := event.ParsePayload(string(buf)) - if err != nil { - return nil, err - } - - return payload, nil -} - func ready() { fmt.Fprint(os.Stdout, "READY\n") } @@ -92,5 +59,5 @@ func success() { func failure(err error) { fmt.Fprintln(os.Stderr, err) - fmt.Fprint(os.Stdout, "Result 2\nFAIL") + fmt.Fprint(os.Stdout, "RESULT 2\nFAIL") } diff --git a/listener/notify/mail.go b/listener/notify/mail.go deleted file mode 100644 index dd3907a..0000000 --- a/listener/notify/mail.go +++ /dev/null @@ -1,34 +0,0 @@ -package notify - -import ( - "errors" - "fmt" - "github.com/go-gomail/gomail" - "github.com/ouqiang/supervisor-event-listener/event" - "strings" -) - -type Mail struct{} - -func (mail *Mail) Send(message event.Message) error { - body := message.String() - body = strings.Replace(body, "\n", "
", -1) - gomailMessage := gomail.NewMessage() - gomailMessage.SetHeader("From", Conf.MailServer.User) - gomailMessage.SetHeader("To", Conf.MailUser.Email...) - gomailMessage.SetHeader("Subject", "Supervisor事件通知") - gomailMessage.SetBody("text/html", body) - mailer := gomail.NewPlainDialer( - Conf.MailServer.Host, - Conf.MailServer.Port, - Conf.MailServer.User, - Conf.MailServer.Password, - ) - err := mailer.DialAndSend(gomailMessage) - if err == nil { - return nil - } - errorMessage := fmt.Sprintf("邮件发送失败#%s", err.Error()) - - return errors.New(errorMessage) -} diff --git a/listener/notify/notify.go b/listener/notify/notify.go deleted file mode 100644 index 7c8a654..0000000 --- a/listener/notify/notify.go +++ /dev/null @@ -1,65 +0,0 @@ -package notify - -import ( - "github.com/ouqiang/supervisor-event-listener/config" - "github.com/ouqiang/supervisor-event-listener/event" - - "fmt" - "os" - "time" -) - -var ( - Conf *config.Config - queue chan event.Message -) - -func init() { - Conf = config.ParseConfig() - queue = make(chan event.Message, 10) - go start() -} - -type Notifiable interface { - Send(event.Message) error -} - -func Push(header *event.Header, payload *event.Payload) { - queue <- event.Message{header, payload} -} - -func start() { - var message event.Message - var notifyHandler Notifiable - for { - message = <-queue - switch Conf.NotifyType { - case "mail": - notifyHandler = &Mail{} - case "slack": - notifyHandler = &Slack{} - case "webhook": - notifyHandler = &WebHook{} - } - if notifyHandler == nil { - continue - } - go send(notifyHandler, message) - time.Sleep(1 * time.Second) - } -} - -func send(notifyHandler Notifiable, message event.Message) { - // 最多重试3次 - tryTimes := 3 - i := 0 - for i < tryTimes { - err := notifyHandler.Send(message) - if err == nil { - break - } - fmt.Fprintln(os.Stderr, err) - time.Sleep(30 * time.Second) - i++ - } -} diff --git a/listener/notify/webhook.go b/listener/notify/webhook.go deleted file mode 100644 index 8f04ace..0000000 --- a/listener/notify/webhook.go +++ /dev/null @@ -1,26 +0,0 @@ -package notify - -import ( - "encoding/json" - "errors" - "fmt" - "github.com/ouqiang/supervisor-event-listener/event" - "github.com/ouqiang/supervisor-event-listener/utils/httpclient" -) - -type WebHook struct{} - -func (hook *WebHook) Send(message event.Message) error { - encodeMessage, err := json.Marshal(message) - if err != nil { - return err - } - timeout := 60 - response := httpclient.PostJson(Conf.WebHook.Url, string(encodeMessage), timeout) - - if response.StatusCode == 200 { - return nil - } - errorMessage := fmt.Sprintf("webhook执行失败#HTTP状态码-%d#HTTP-BODY-%s", response.StatusCode, response.Body) - return errors.New(errorMessage) -} diff --git a/main.go b/main.go new file mode 100644 index 0000000..9cee757 --- /dev/null +++ b/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "flag" + "log" + "os" + "os/signal" + "syscall" + + "github.com/ouqiang/supervisor-event-listener/conf" + "github.com/ouqiang/supervisor-event-listener/listener" + "github.com/ouqiang/supervisor-event-listener/notify" + "github.com/ouqiang/supervisor-event-listener/utils/errlog" +) + +var ( + chanSig = make(chan os.Signal, 128) +) + +func main() { + defer func() { + if err := recover(); err != nil { + log.Print("panic", err) + os.Exit(127) + } + }() + + var configFile = flag.String("c", "/etc/supervisor-event-listener.ini", "config file") + var dryRun = flag.Bool("dryRun", false, "dry run, lint config file") + flag.Parse() + + if err := conf.Init(*configFile); err != nil { + errlog.Error("config init failed. err: %v", err) + os.Exit(127) + return + } + if err := notify.Init(conf.Get()); err != nil { + errlog.Error("notify init failed. err: %v", err) + os.Exit(127) + return + } + if *dryRun { + return + } + notify.Start() + listener.Start() + + signal.Notify(chanSig, syscall.SIGHUP) + for sig := range chanSig { + switch sig { + case syscall.SIGHUP: + if err := conf.Reload(*configFile); err != nil { + errlog.Error("config init failed. err: %v", err) + continue + } + notify.Reload(conf.Get()) + default: + errlog.Info("unexpected signal %v", sig) + } + } +} diff --git a/notify/bearychat.go b/notify/bearychat.go new file mode 100644 index 0000000..d33fe8a --- /dev/null +++ b/notify/bearychat.go @@ -0,0 +1,41 @@ +package notify + +import ( + "encoding/json" + + "github.com/ouqiang/supervisor-event-listener/conf" + "github.com/ouqiang/supervisor-event-listener/event" + "github.com/ouqiang/supervisor-event-listener/utils/errlog" + "github.com/ouqiang/supervisor-event-listener/utils/httpclient" +) + +type BearyChat conf.BearyChat + +func (this *BearyChat) Send(msg *event.Message) error { + url := this.URL + channel := this.Channel + timeout := this.Timeout + + params := map[string]interface{}{ + "text": this.format(msg), + } + if channel != "" { + params["channel"] = channel + } + + body, err := json.Marshal(params) + if err != nil { + return err + } + resp := httpclient.PostJson(url, string(body), timeout) + if !resp.IsOK() { + errlog.Error("params: %v err: %v", params, resp.Error()) + return resp + } + return nil +} + +func (this *BearyChat) format(msg *event.Message) string { + // return msg.ToJson(4) + return msg.String() +} diff --git a/notify/feishu.go b/notify/feishu.go new file mode 100644 index 0000000..1d03162 --- /dev/null +++ b/notify/feishu.go @@ -0,0 +1,80 @@ +package notify + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "net/url" + "strings" + "time" + + "github.com/ouqiang/supervisor-event-listener/conf" + "github.com/ouqiang/supervisor-event-listener/event" + "github.com/ouqiang/supervisor-event-listener/utils/errlog" + "github.com/ouqiang/supervisor-event-listener/utils/httpclient" +) + +type Feishu conf.Feishu + +func (this *Feishu) Send(msg *event.Message) error { + url := this.URL + timeout := this.Timeout + return send2feishu(url, msg.String(), timeout) +} + +func send2feishu(_url string, text string, timeout int) error { + parse := func(_url string) (string, string) { + tmpArr := strings.Split(_url, "?") + if len(tmpArr) == 1 { + return _url, "" + } else if len(tmpArr) == 2 { + _url = tmpArr[0] + q, err := url.ParseQuery(tmpArr[1]) + if err != nil { + panic(err) + } + return _url, q.Get("signKey") + } else { + panic(fmt.Errorf("invalid url: %s", _url)) + } + } + + sign := func(secret string, timestamp int64) string { + //timestamp + key do sha256, then base64 encode + stringToSign := fmt.Sprintf("%v\n%s", timestamp, secret) + + var data []byte + h := hmac.New(sha256.New, []byte(stringToSign)) + _, err := h.Write(data) + if err != nil { + panic(err) + } + return base64.StdEncoding.EncodeToString(h.Sum(nil)) + } + + params := map[string]interface{}{ + "msg_type": "text", + "content": map[string]interface{}{ + "text": text, + }, + } + + _url, signKey := parse(_url) + if signKey != "" { + ts := time.Now().Unix() + params["timestamp"] = ts + params["sign"] = sign(signKey, ts) + } + body, err := json.Marshal(params) + if err != nil { + return err + } + resp := httpclient.PostJson(_url, string(body), timeout) + if !resp.IsOK() { + errlog.Error("params: %v err: %v", params, resp.Error()) + return resp + } + return nil +} diff --git a/notify/init.go b/notify/init.go new file mode 100644 index 0000000..db473c8 --- /dev/null +++ b/notify/init.go @@ -0,0 +1,116 @@ +package notify + +import ( + "fmt" + "os" + "strings" + "sync/atomic" + "time" + + "github.com/ouqiang/supervisor-event-listener/conf" + "github.com/ouqiang/supervisor-event-listener/event" + "github.com/ouqiang/supervisor-event-listener/utils/errlog" +) + +var ( + chanMsg = make(chan *event.Message, 2048) + chanReload = make(chan *conf.Config, 16) + // notifiables map[string]Notifiable + notifiables atomic.Value +) + +func Init(conf *conf.Config) error { + if conf == nil { + return fmt.Errorf("nil config") + } + + m := map[string]Notifiable{} + if conf.WebHook != nil { + m["webhook"] = (*WebHook)(conf.WebHook) + } + if conf.Mail != nil { + m["mail"] = (*Mail)(conf.Mail) + } + if conf.Slack != nil { + m["slack"] = (*Slack)(conf.Slack) + } + if conf.BearyChat != nil { + m["bearychat"] = (*BearyChat)(conf.BearyChat) + } + if conf.Feishu != nil { + m["feishu"] = (*Feishu)(conf.Feishu) + } + notifiables.Store(m) + return nil +} + +func Reload(conf *conf.Config) { + chanReload <- conf +} + +func reload(conf *conf.Config) error { + if err := Init(conf); err != nil { + errlog.Info("loading config failed. %v", err) + return err + } + names := []string{} + for name := range Get() { + names = append(names, name) + } + errlog.Info("reloaded config: %s", strings.Join(names, " ")) + return nil +} + +func Push(msg *event.Message) { + chanMsg <- msg +} + +func Start() { + go run() +} + +func run() { + for { + select { + case msg := <-chanMsg: + errlog.Info("msg=%s", msg.ToJson(2)) + handleMessage(msg) + time.Sleep(50 * time.Millisecond) + case conf := <-chanReload: + _ = reload(conf) + default: + time.Sleep(50 * time.Millisecond) + } + } +} + +func handleMessage(msg *event.Message) error { + errlog.Debug("message: %+v\n", msg) + m := Get() + for name, notifyHandler := range m { + if notifyHandler == nil { + errlog.Error("nil notify handler %s", name) + } + go send(notifyHandler, msg) + } + return nil +} + +func send(notifyHandler Notifiable, msg *event.Message) { + // 最多重试3次 + tryTimes := 3 + i := 0 + for i < tryTimes { + err := notifyHandler.Send(msg) + if err == nil { + break + } + fmt.Fprintln(os.Stderr, err) + time.Sleep(30 * time.Second) + i++ + } +} + +func Get() map[string]Notifiable { + return notifiables.Load().(map[string]Notifiable) +} diff --git a/notify/mail.go b/notify/mail.go new file mode 100644 index 0000000..e7f3f43 --- /dev/null +++ b/notify/mail.go @@ -0,0 +1,33 @@ +package notify + +import ( + "fmt" + "strings" + + "github.com/go-gomail/gomail" + "github.com/ouqiang/supervisor-event-listener/conf" + "github.com/ouqiang/supervisor-event-listener/event" +) + +type Mail conf.Mail + +func (mail *Mail) Send(msg *event.Message) error { + body := msg.String() + body = strings.Replace(body, "\n", "
", -1) + m := gomail.NewMessage() + m.SetHeader("From", mail.ServerUser) + m.SetHeader("To", mail.Receivers...) + m.SetHeader("Subject", "Supervisor事件通知") + m.SetBody("text/html", body) + mailer := gomail.NewPlainDialer( + mail.ServerHost, + mail.ServerPort, + mail.ServerUser, + mail.ServerPassword, + ) + err := mailer.DialAndSend(m) + if err == nil { + return nil + } + return fmt.Errorf("邮件发送失败#%v", err) +} diff --git a/notify/notifiable.go b/notify/notifiable.go new file mode 100644 index 0000000..1feff16 --- /dev/null +++ b/notify/notifiable.go @@ -0,0 +1,7 @@ +package notify + +import "github.com/ouqiang/supervisor-event-listener/event" + +type Notifiable interface { + Send(*event.Message) error +} diff --git a/listener/notify/slack.go b/notify/slack.go similarity index 53% rename from listener/notify/slack.go rename to notify/slack.go index 6076faf..a253445 100644 --- a/listener/notify/slack.go +++ b/notify/slack.go @@ -1,31 +1,28 @@ package notify import ( - "errors" "fmt" - "github.com/ouqiang/supervisor-event-listener/utils/httpclient" + + "github.com/ouqiang/supervisor-event-listener/conf" "github.com/ouqiang/supervisor-event-listener/event" "github.com/ouqiang/supervisor-event-listener/utils" + "github.com/ouqiang/supervisor-event-listener/utils/httpclient" ) -type Slack struct{} +type Slack conf.Slack -func (slack *Slack) Send(message event.Message) error { - body := slack.format(message.String(), Conf.Slack.Channel) +func (s *Slack) Send(msg *event.Message) error { + body := s.format(msg.String(), s.Channel) timeout := 120 - response := httpclient.PostJson(Conf.Slack.WebHookUrl, body, timeout) - if response.StatusCode == 200 { - return nil + resp := httpclient.PostJson(s.URL, body, timeout) + if !resp.IsOK() { + return resp } - - errorMessage := fmt.Sprintf("发送Slack消息失败#HTTP状态码-%d#HTTP-Body-%s", - response.StatusCode, response.Body) - - return errors.New(errorMessage) + return nil } // 格式化消息内容 -func (slack *Slack) format(content string, channel string) string { +func (s *Slack) format(content string, channel string) string { content = utils.EscapeJson(content) specialChars := []string{"&", "<", ">"} replaceChars := []string{"&", "<", ">"} diff --git a/notify/webhook.go b/notify/webhook.go new file mode 100644 index 0000000..c73095f --- /dev/null +++ b/notify/webhook.go @@ -0,0 +1,25 @@ +package notify + +import ( + "encoding/json" + + "github.com/ouqiang/supervisor-event-listener/conf" + "github.com/ouqiang/supervisor-event-listener/event" + "github.com/ouqiang/supervisor-event-listener/utils/httpclient" +) + +type WebHook conf.WebHook + +func (hook *WebHook) Send(msg *event.Message) error { + url := hook.URL + timeout := hook.Timeout + data, err := json.Marshal(msg) + if err != nil { + return err + } + resp := httpclient.PostJson(url, string(data), timeout) + if !resp.IsOK() { + return resp + } + return nil +} diff --git a/supervisor-event-listener.go b/supervisor-event-listener.go deleted file mode 100644 index 0982b21..0000000 --- a/supervisor-event-listener.go +++ /dev/null @@ -1,11 +0,0 @@ -package main - -import ( - "github.com/ouqiang/supervisor-event-listener/listener" -) - -func main() { - for { - listener.Start() - } -} diff --git a/supervisor-event-listener.ini b/supervisor-event-listener.ini deleted file mode 100644 index dbe00ed..0000000 --- a/supervisor-event-listener.ini +++ /dev/null @@ -1,19 +0,0 @@ -[default] -# 通知类型 mail,slack,webhook 只能选择一种 -notify_type = mail - -# 邮件服务器配置 -mail.server.user = test@163.com -mail.server.password = 123456 -mail.server.host = smtp.163.com -mail.server.port = 25 - -# 邮件收件人配置, 多个收件人, 逗号分隔 -mail.user = hello@163.com - -# Slack配置 -slack.webhook_url = https://hooks.slack.com/services/xxxx/xxx/xxxx -slack.channel = exception - -# WebHook通知URL配置 -webhook_url = http://my.webhook.com \ No newline at end of file diff --git a/tests/supervisor-app.ini b/tests/supervisor-app.ini new file mode 100644 index 0000000..7e6f2e2 --- /dev/null +++ b/tests/supervisor-app.ini @@ -0,0 +1,13 @@ +[eventlistener:supervisor-event-listener] +command=/usr/local/bin/supervisor-event-listener + -c /etc/supervisor-event-listener.toml +events= + PROCESS_STATE_EXITED, + PROCESS_STATE_FATAL, + PROCESS_STATE_STOPPED, + PROCESS_STATE_RUNNING +user=nobody +group=nobody + +[program:sleep-then-exit] +command=sleep 3 && exit 1 diff --git a/utils/errlog/log.go b/utils/errlog/log.go new file mode 100644 index 0000000..ec98b54 --- /dev/null +++ b/utils/errlog/log.go @@ -0,0 +1,80 @@ +package errlog + +import ( + "fmt" + "os" + "path" + "runtime" + "time" +) + +const ( + OFF = iota + FATAL + ERROR + WARN + INFO + DEBUG + ALL +) + +var LEVELS_NAME = map[int]string{ + OFF: "off", + FATAL: "fatal", + ERROR: "error", + WARN: "warn", + INFO: "info", + DEBUG: "debug", + ALL: "all", +} + +var f = os.Stderr +var curLogLevel = INFO + +func init() { + // fpath := "/tmp/supervisor-event-listener.log" + // f = newLogFile(fpath) +} + +func newLogFile(fpath string) *os.File { + mode := os.O_CREATE | os.O_WRONLY + file, err := os.OpenFile(fpath, mode, 0644) + if err != nil { + panic(err) + } + return file +} + +func logFormat(level int, _fmt string, args ...interface{}) { + if level > curLogLevel { + return + } + + _, fn, lineno, _ := runtime.Caller(2) + fn = path.Base(fn) + now := time.Now() + levelName := LEVELS_NAME[level] + prefix := fmt.Sprintf("%s [%s] %12s:%d ", + now.Format(time.RFC3339), levelName, fn, lineno) + f.WriteString(prefix) + f.WriteString(fmt.Sprintf(_fmt, args...)) + f.WriteString("\n") +} + +func Debug(fmt string, args ...interface{}) { + logFormat(DEBUG, fmt, args...) +} + +func Info(fmt string, args ...interface{}) { + logFormat(INFO, fmt, args...) +} + +func Error(fmt string, args ...interface{}) { + logFormat(ERROR, fmt, args...) +} + +func Fatal(fmt string, args ...interface{}) { + logFormat(FATAL, fmt, args...) + f.Close() + os.Exit(127) +} diff --git a/utils/httpclient/http_client.go b/utils/httpclient/http_client.go index 29f4334..2b0808d 100644 --- a/utils/httpclient/http_client.go +++ b/utils/httpclient/http_client.go @@ -10,13 +10,42 @@ import ( "time" ) +var ( + client = &http.Client{Timeout: 6 * time.Second} +) + type ResponseWrapper struct { StatusCode int Body string Header http.Header } -func Get(url string, timeout int) ResponseWrapper { +func (this *ResponseWrapper) IsOK() bool { + return this != nil && this.StatusCode == 200 +} + +func (this *ResponseWrapper) String() string { + if this == nil { + return "null" + } + body := this.Body + if runes := []rune(this.Body); len(runes) > 256 { + body = string(runes[0:256]) + } + return fmt.Sprintf("resp[%d] %s", this.StatusCode, body) +} + +func (this *ResponseWrapper) Error() string { + if this == nil { + return "null" + } + if this.Body == "" { + return "empty body" + } + return this.Body +} + +func Get(url string, timeout int) *ResponseWrapper { req, err := http.NewRequest("GET", url, nil) if err != nil { return createRequestError(err) @@ -25,7 +54,7 @@ func Get(url string, timeout int) ResponseWrapper { return request(req, timeout) } -func PostParams(url string, params string, timeout int) ResponseWrapper { +func PostParams(url string, params string, timeout int) *ResponseWrapper { buf := bytes.NewBufferString(params) req, err := http.NewRequest("POST", url, buf) if err != nil { @@ -36,7 +65,7 @@ func PostParams(url string, params string, timeout int) ResponseWrapper { return request(req, timeout) } -func PostJson(url string, body string, timeout int) ResponseWrapper { +func PostJson(url string, body string, timeout int) *ResponseWrapper { buf := bytes.NewBufferString(body) req, err := http.NewRequest("POST", url, buf) if err != nil { @@ -47,9 +76,8 @@ func PostJson(url string, body string, timeout int) ResponseWrapper { return request(req, timeout) } -func request(req *http.Request, timeout int) ResponseWrapper { +func request(req *http.Request, timeout int) *ResponseWrapper { wrapper := ResponseWrapper{StatusCode: 0, Body: "", Header: make(http.Header)} - client := &http.Client{} if timeout > 0 { client.Timeout = time.Duration(timeout) * time.Second } @@ -57,19 +85,18 @@ func request(req *http.Request, timeout int) ResponseWrapper { resp, err := client.Do(req) if err != nil { wrapper.Body = fmt.Sprintf("执行HTTP请求错误-%s", err.Error()) - return wrapper + return &wrapper } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { wrapper.Body = fmt.Sprintf("读取HTTP请求返回值失败-%s", err.Error()) - return wrapper + return &wrapper } wrapper.StatusCode = resp.StatusCode wrapper.Body = string(body) wrapper.Header = resp.Header - - return wrapper + return &wrapper } func setRequestHeader(req *http.Request) { @@ -77,7 +104,7 @@ func setRequestHeader(req *http.Request) { req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 golang/gocron") } -func createRequestError(err error) ResponseWrapper { +func createRequestError(err error) *ResponseWrapper { errorMessage := fmt.Sprintf("创建HTTP请求错误-%s", err.Error()) - return ResponseWrapper{0, errorMessage, make(http.Header)} + return &ResponseWrapper{0, errorMessage, make(http.Header)} } diff --git a/vendor/github.com/go-gomail/gomail/CHANGELOG.md b/vendor/github.com/go-gomail/gomail/CHANGELOG.md deleted file mode 100644 index a797ab4..0000000 --- a/vendor/github.com/go-gomail/gomail/CHANGELOG.md +++ /dev/null @@ -1,20 +0,0 @@ -# Change Log -All notable changes to this project will be documented in this file. -This project adheres to [Semantic Versioning](http://semver.org/). - -## [2.0.0] - 2015-09-02 - -- Mailer has been removed. It has been replaced by Dialer and Sender. -- `File` type and the `CreateFile` and `OpenFile` functions have been removed. -- `Message.Attach` and `Message.Embed` have a new signature. -- `Message.GetBodyWriter` has been removed. Use `Message.AddAlternativeWriter` -instead. -- `Message.Export` has been removed. `Message.WriteTo` can be used instead. -- `Message.DelHeader` has been removed. -- The `Bcc` header field is no longer sent. It is far more simpler and -efficient: the same message is sent to all recipients instead of sending a -different email to each Bcc address. -- LoginAuth has been removed. `NewPlainDialer` now implements the LOGIN -authentication mechanism when needed. -- Go 1.2 is now required instead of Go 1.3. No external dependency are used when -using Go 1.5. diff --git a/vendor/github.com/go-gomail/gomail/CONTRIBUTING.md b/vendor/github.com/go-gomail/gomail/CONTRIBUTING.md deleted file mode 100644 index d5601c2..0000000 --- a/vendor/github.com/go-gomail/gomail/CONTRIBUTING.md +++ /dev/null @@ -1,20 +0,0 @@ -Thank you for contributing to Gomail! Here are a few guidelines: - -## Bugs - -If you think you found a bug, create an issue and supply the minimum amount -of code triggering the bug so it can be reproduced. - - -## Fixing a bug - -If you want to fix a bug, you can send a pull request. It should contains a -new test or update an existing one to cover that bug. - - -## New feature proposal - -If you think Gomail lacks a feature, you can open an issue or send a pull -request. I want to keep Gomail code and API as simple as possible so please -describe your needs so we can discuss whether this feature should be added to -Gomail or not. diff --git a/vendor/github.com/go-gomail/gomail/LICENSE b/vendor/github.com/go-gomail/gomail/LICENSE deleted file mode 100644 index 5f5c12a..0000000 --- a/vendor/github.com/go-gomail/gomail/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Alexandre Cesaro - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/go-gomail/gomail/README.md b/vendor/github.com/go-gomail/gomail/README.md deleted file mode 100644 index b3be9e1..0000000 --- a/vendor/github.com/go-gomail/gomail/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# Gomail -[![Build Status](https://travis-ci.org/go-gomail/gomail.svg?branch=v2)](https://travis-ci.org/go-gomail/gomail) [![Code Coverage](http://gocover.io/_badge/gopkg.in/gomail.v2)](http://gocover.io/gopkg.in/gomail.v2) [![Documentation](https://godoc.org/gopkg.in/gomail.v2?status.svg)](https://godoc.org/gopkg.in/gomail.v2) - -## Introduction - -Gomail is a simple and efficient package to send emails. It is well tested and -documented. - -Gomail can only send emails using an SMTP server. But the API is flexible and it -is easy to implement other methods for sending emails using a local Postfix, an -API, etc. - -It is versioned using [gopkg.in](https://gopkg.in) so I promise -there will never be backward incompatible changes within each version. - -It requires Go 1.2 or newer. With Go 1.5, no external dependencies are used. - - -## Features - -Gomail supports: -- Attachments -- Embedded images -- HTML and text templates -- Automatic encoding of special characters -- SSL and TLS -- Sending multiple emails with the same SMTP connection - - -## Documentation - -https://godoc.org/gopkg.in/gomail.v2 - - -## Download - - go get gopkg.in/gomail.v2 - - -## Examples - -See the [examples in the documentation](https://godoc.org/gopkg.in/gomail.v2#example-package). - - -## FAQ - -### x509: certificate signed by unknown authority - -If you get this error it means the certificate used by the SMTP server is not -considered valid by the client running Gomail. As a quick workaround you can -bypass the verification of the server's certificate chain and host name by using -`SetTLSConfig`: - - package main - - import ( - "crypto/tls" - - "gopkg.in/gomail.v2" - ) - - func main() { - d := gomail.NewDialer("smtp.example.com", 587, "user", "123456") - d.TLSConfig = &tls.Config{InsecureSkipVerify: true} - - // Send emails using d. - } - -Note, however, that this is insecure and should not be used in production. - - -## Contribute - -Contributions are more than welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for -more info. - - -## Change log - -See [CHANGELOG.md](CHANGELOG.md). - - -## License - -[MIT](LICENSE) - - -## Contact - -You can ask questions on the [Gomail -thread](https://groups.google.com/d/topic/golang-nuts/jMxZHzvvEVg/discussion) -in the Go mailing-list. diff --git a/vendor/github.com/go-gomail/gomail/auth.go b/vendor/github.com/go-gomail/gomail/auth.go deleted file mode 100644 index d28b83a..0000000 --- a/vendor/github.com/go-gomail/gomail/auth.go +++ /dev/null @@ -1,49 +0,0 @@ -package gomail - -import ( - "bytes" - "errors" - "fmt" - "net/smtp" -) - -// loginAuth is an smtp.Auth that implements the LOGIN authentication mechanism. -type loginAuth struct { - username string - password string - host string -} - -func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { - if !server.TLS { - advertised := false - for _, mechanism := range server.Auth { - if mechanism == "LOGIN" { - advertised = true - break - } - } - if !advertised { - return "", nil, errors.New("gomail: unencrypted connection") - } - } - if server.Name != a.host { - return "", nil, errors.New("gomail: wrong host name") - } - return "LOGIN", nil, nil -} - -func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { - if !more { - return nil, nil - } - - switch { - case bytes.Equal(fromServer, []byte("Username:")): - return []byte(a.username), nil - case bytes.Equal(fromServer, []byte("Password:")): - return []byte(a.password), nil - default: - return nil, fmt.Errorf("gomail: unexpected server challenge: %s", fromServer) - } -} diff --git a/vendor/github.com/go-gomail/gomail/doc.go b/vendor/github.com/go-gomail/gomail/doc.go deleted file mode 100644 index a8f5091..0000000 --- a/vendor/github.com/go-gomail/gomail/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package gomail provides a simple interface to compose emails and to mail them -// efficiently. -// -// More info on Github: https://github.com/go-gomail/gomail -package gomail diff --git a/vendor/github.com/go-gomail/gomail/message.go b/vendor/github.com/go-gomail/gomail/message.go deleted file mode 100644 index 4bffb1e..0000000 --- a/vendor/github.com/go-gomail/gomail/message.go +++ /dev/null @@ -1,322 +0,0 @@ -package gomail - -import ( - "bytes" - "io" - "os" - "path/filepath" - "time" -) - -// Message represents an email. -type Message struct { - header header - parts []*part - attachments []*file - embedded []*file - charset string - encoding Encoding - hEncoder mimeEncoder - buf bytes.Buffer -} - -type header map[string][]string - -type part struct { - contentType string - copier func(io.Writer) error - encoding Encoding -} - -// NewMessage creates a new message. It uses UTF-8 and quoted-printable encoding -// by default. -func NewMessage(settings ...MessageSetting) *Message { - m := &Message{ - header: make(header), - charset: "UTF-8", - encoding: QuotedPrintable, - } - - m.applySettings(settings) - - if m.encoding == Base64 { - m.hEncoder = bEncoding - } else { - m.hEncoder = qEncoding - } - - return m -} - -// Reset resets the message so it can be reused. The message keeps its previous -// settings so it is in the same state that after a call to NewMessage. -func (m *Message) Reset() { - for k := range m.header { - delete(m.header, k) - } - m.parts = nil - m.attachments = nil - m.embedded = nil -} - -func (m *Message) applySettings(settings []MessageSetting) { - for _, s := range settings { - s(m) - } -} - -// A MessageSetting can be used as an argument in NewMessage to configure an -// email. -type MessageSetting func(m *Message) - -// SetCharset is a message setting to set the charset of the email. -func SetCharset(charset string) MessageSetting { - return func(m *Message) { - m.charset = charset - } -} - -// SetEncoding is a message setting to set the encoding of the email. -func SetEncoding(enc Encoding) MessageSetting { - return func(m *Message) { - m.encoding = enc - } -} - -// Encoding represents a MIME encoding scheme like quoted-printable or base64. -type Encoding string - -const ( - // QuotedPrintable represents the quoted-printable encoding as defined in - // RFC 2045. - QuotedPrintable Encoding = "quoted-printable" - // Base64 represents the base64 encoding as defined in RFC 2045. - Base64 Encoding = "base64" - // Unencoded can be used to avoid encoding the body of an email. The headers - // will still be encoded using quoted-printable encoding. - Unencoded Encoding = "8bit" -) - -// SetHeader sets a value to the given header field. -func (m *Message) SetHeader(field string, value ...string) { - m.encodeHeader(value) - m.header[field] = value -} - -func (m *Message) encodeHeader(values []string) { - for i := range values { - values[i] = m.encodeString(values[i]) - } -} - -func (m *Message) encodeString(value string) string { - return m.hEncoder.Encode(m.charset, value) -} - -// SetHeaders sets the message headers. -func (m *Message) SetHeaders(h map[string][]string) { - for k, v := range h { - m.SetHeader(k, v...) - } -} - -// SetAddressHeader sets an address to the given header field. -func (m *Message) SetAddressHeader(field, address, name string) { - m.header[field] = []string{m.FormatAddress(address, name)} -} - -// FormatAddress formats an address and a name as a valid RFC 5322 address. -func (m *Message) FormatAddress(address, name string) string { - if name == "" { - return address - } - - enc := m.encodeString(name) - if enc == name { - m.buf.WriteByte('"') - for i := 0; i < len(name); i++ { - b := name[i] - if b == '\\' || b == '"' { - m.buf.WriteByte('\\') - } - m.buf.WriteByte(b) - } - m.buf.WriteByte('"') - } else if hasSpecials(name) { - m.buf.WriteString(bEncoding.Encode(m.charset, name)) - } else { - m.buf.WriteString(enc) - } - m.buf.WriteString(" <") - m.buf.WriteString(address) - m.buf.WriteByte('>') - - addr := m.buf.String() - m.buf.Reset() - return addr -} - -func hasSpecials(text string) bool { - for i := 0; i < len(text); i++ { - switch c := text[i]; c { - case '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '.', '"': - return true - } - } - - return false -} - -// SetDateHeader sets a date to the given header field. -func (m *Message) SetDateHeader(field string, date time.Time) { - m.header[field] = []string{m.FormatDate(date)} -} - -// FormatDate formats a date as a valid RFC 5322 date. -func (m *Message) FormatDate(date time.Time) string { - return date.Format(time.RFC1123Z) -} - -// GetHeader gets a header field. -func (m *Message) GetHeader(field string) []string { - return m.header[field] -} - -// SetBody sets the body of the message. It replaces any content previously set -// by SetBody, AddAlternative or AddAlternativeWriter. -func (m *Message) SetBody(contentType, body string, settings ...PartSetting) { - m.parts = []*part{m.newPart(contentType, newCopier(body), settings)} -} - -// AddAlternative adds an alternative part to the message. -// -// It is commonly used to send HTML emails that default to the plain text -// version for backward compatibility. AddAlternative appends the new part to -// the end of the message. So the plain text part should be added before the -// HTML part. See http://en.wikipedia.org/wiki/MIME#Alternative -func (m *Message) AddAlternative(contentType, body string, settings ...PartSetting) { - m.AddAlternativeWriter(contentType, newCopier(body), settings...) -} - -func newCopier(s string) func(io.Writer) error { - return func(w io.Writer) error { - _, err := io.WriteString(w, s) - return err - } -} - -// AddAlternativeWriter adds an alternative part to the message. It can be -// useful with the text/template or html/template packages. -func (m *Message) AddAlternativeWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) { - m.parts = append(m.parts, m.newPart(contentType, f, settings)) -} - -func (m *Message) newPart(contentType string, f func(io.Writer) error, settings []PartSetting) *part { - p := &part{ - contentType: contentType, - copier: f, - encoding: m.encoding, - } - - for _, s := range settings { - s(p) - } - - return p -} - -// A PartSetting can be used as an argument in Message.SetBody, -// Message.AddAlternative or Message.AddAlternativeWriter to configure the part -// added to a message. -type PartSetting func(*part) - -// SetPartEncoding sets the encoding of the part added to the message. By -// default, parts use the same encoding than the message. -func SetPartEncoding(e Encoding) PartSetting { - return PartSetting(func(p *part) { - p.encoding = e - }) -} - -type file struct { - Name string - Header map[string][]string - CopyFunc func(w io.Writer) error -} - -func (f *file) setHeader(field, value string) { - f.Header[field] = []string{value} -} - -// A FileSetting can be used as an argument in Message.Attach or Message.Embed. -type FileSetting func(*file) - -// SetHeader is a file setting to set the MIME header of the message part that -// contains the file content. -// -// Mandatory headers are automatically added if they are not set when sending -// the email. -func SetHeader(h map[string][]string) FileSetting { - return func(f *file) { - for k, v := range h { - f.Header[k] = v - } - } -} - -// Rename is a file setting to set the name of the attachment if the name is -// different than the filename on disk. -func Rename(name string) FileSetting { - return func(f *file) { - f.Name = name - } -} - -// SetCopyFunc is a file setting to replace the function that runs when the -// message is sent. It should copy the content of the file to the io.Writer. -// -// The default copy function opens the file with the given filename, and copy -// its content to the io.Writer. -func SetCopyFunc(f func(io.Writer) error) FileSetting { - return func(fi *file) { - fi.CopyFunc = f - } -} - -func (m *Message) appendFile(list []*file, name string, settings []FileSetting) []*file { - f := &file{ - Name: filepath.Base(name), - Header: make(map[string][]string), - CopyFunc: func(w io.Writer) error { - h, err := os.Open(name) - if err != nil { - return err - } - if _, err := io.Copy(w, h); err != nil { - h.Close() - return err - } - return h.Close() - }, - } - - for _, s := range settings { - s(f) - } - - if list == nil { - return []*file{f} - } - - return append(list, f) -} - -// Attach attaches the files to the email. -func (m *Message) Attach(filename string, settings ...FileSetting) { - m.attachments = m.appendFile(m.attachments, filename, settings) -} - -// Embed embeds the images to the email. -func (m *Message) Embed(filename string, settings ...FileSetting) { - m.embedded = m.appendFile(m.embedded, filename, settings) -} diff --git a/vendor/github.com/go-gomail/gomail/mime.go b/vendor/github.com/go-gomail/gomail/mime.go deleted file mode 100644 index 194d4a7..0000000 --- a/vendor/github.com/go-gomail/gomail/mime.go +++ /dev/null @@ -1,21 +0,0 @@ -// +build go1.5 - -package gomail - -import ( - "mime" - "mime/quotedprintable" - "strings" -) - -var newQPWriter = quotedprintable.NewWriter - -type mimeEncoder struct { - mime.WordEncoder -} - -var ( - bEncoding = mimeEncoder{mime.BEncoding} - qEncoding = mimeEncoder{mime.QEncoding} - lastIndexByte = strings.LastIndexByte -) diff --git a/vendor/github.com/go-gomail/gomail/mime_go14.go b/vendor/github.com/go-gomail/gomail/mime_go14.go deleted file mode 100644 index 3dc26aa..0000000 --- a/vendor/github.com/go-gomail/gomail/mime_go14.go +++ /dev/null @@ -1,25 +0,0 @@ -// +build !go1.5 - -package gomail - -import "gopkg.in/alexcesaro/quotedprintable.v3" - -var newQPWriter = quotedprintable.NewWriter - -type mimeEncoder struct { - quotedprintable.WordEncoder -} - -var ( - bEncoding = mimeEncoder{quotedprintable.BEncoding} - qEncoding = mimeEncoder{quotedprintable.QEncoding} - lastIndexByte = func(s string, c byte) int { - for i := len(s) - 1; i >= 0; i-- { - - if s[i] == c { - return i - } - } - return -1 - } -) diff --git a/vendor/github.com/go-gomail/gomail/send.go b/vendor/github.com/go-gomail/gomail/send.go deleted file mode 100644 index 9115ebe..0000000 --- a/vendor/github.com/go-gomail/gomail/send.go +++ /dev/null @@ -1,116 +0,0 @@ -package gomail - -import ( - "errors" - "fmt" - "io" - "net/mail" -) - -// Sender is the interface that wraps the Send method. -// -// Send sends an email to the given addresses. -type Sender interface { - Send(from string, to []string, msg io.WriterTo) error -} - -// SendCloser is the interface that groups the Send and Close methods. -type SendCloser interface { - Sender - Close() error -} - -// A SendFunc is a function that sends emails to the given addresses. -// -// The SendFunc type is an adapter to allow the use of ordinary functions as -// email senders. If f is a function with the appropriate signature, SendFunc(f) -// is a Sender object that calls f. -type SendFunc func(from string, to []string, msg io.WriterTo) error - -// Send calls f(from, to, msg). -func (f SendFunc) Send(from string, to []string, msg io.WriterTo) error { - return f(from, to, msg) -} - -// Send sends emails using the given Sender. -func Send(s Sender, msg ...*Message) error { - for i, m := range msg { - if err := send(s, m); err != nil { - return fmt.Errorf("gomail: could not send email %d: %v", i+1, err) - } - } - - return nil -} - -func send(s Sender, m *Message) error { - from, err := m.getFrom() - if err != nil { - return err - } - - to, err := m.getRecipients() - if err != nil { - return err - } - - if err := s.Send(from, to, m); err != nil { - return err - } - - return nil -} - -func (m *Message) getFrom() (string, error) { - from := m.header["Sender"] - if len(from) == 0 { - from = m.header["From"] - if len(from) == 0 { - return "", errors.New(`gomail: invalid message, "From" field is absent`) - } - } - - return parseAddress(from[0]) -} - -func (m *Message) getRecipients() ([]string, error) { - n := 0 - for _, field := range []string{"To", "Cc", "Bcc"} { - if addresses, ok := m.header[field]; ok { - n += len(addresses) - } - } - list := make([]string, 0, n) - - for _, field := range []string{"To", "Cc", "Bcc"} { - if addresses, ok := m.header[field]; ok { - for _, a := range addresses { - addr, err := parseAddress(a) - if err != nil { - return nil, err - } - list = addAddress(list, addr) - } - } - } - - return list, nil -} - -func addAddress(list []string, addr string) []string { - for _, a := range list { - if addr == a { - return list - } - } - - return append(list, addr) -} - -func parseAddress(field string) (string, error) { - addr, err := mail.ParseAddress(field) - if err != nil { - return "", fmt.Errorf("gomail: invalid address %q: %v", field, err) - } - return addr.Address, nil -} diff --git a/vendor/github.com/go-gomail/gomail/smtp.go b/vendor/github.com/go-gomail/gomail/smtp.go deleted file mode 100644 index 2aa49c8..0000000 --- a/vendor/github.com/go-gomail/gomail/smtp.go +++ /dev/null @@ -1,202 +0,0 @@ -package gomail - -import ( - "crypto/tls" - "fmt" - "io" - "net" - "net/smtp" - "strings" - "time" -) - -// A Dialer is a dialer to an SMTP server. -type Dialer struct { - // Host represents the host of the SMTP server. - Host string - // Port represents the port of the SMTP server. - Port int - // Username is the username to use to authenticate to the SMTP server. - Username string - // Password is the password to use to authenticate to the SMTP server. - Password string - // Auth represents the authentication mechanism used to authenticate to the - // SMTP server. - Auth smtp.Auth - // SSL defines whether an SSL connection is used. It should be false in - // most cases since the authentication mechanism should use the STARTTLS - // extension instead. - SSL bool - // TSLConfig represents the TLS configuration used for the TLS (when the - // STARTTLS extension is used) or SSL connection. - TLSConfig *tls.Config - // LocalName is the hostname sent to the SMTP server with the HELO command. - // By default, "localhost" is sent. - LocalName string -} - -// NewDialer returns a new SMTP Dialer. The given parameters are used to connect -// to the SMTP server. -func NewDialer(host string, port int, username, password string) *Dialer { - return &Dialer{ - Host: host, - Port: port, - Username: username, - Password: password, - SSL: port == 465, - } -} - -// NewPlainDialer returns a new SMTP Dialer. The given parameters are used to -// connect to the SMTP server. -// -// Deprecated: Use NewDialer instead. -func NewPlainDialer(host string, port int, username, password string) *Dialer { - return NewDialer(host, port, username, password) -} - -// Dial dials and authenticates to an SMTP server. The returned SendCloser -// should be closed when done using it. -func (d *Dialer) Dial() (SendCloser, error) { - conn, err := netDialTimeout("tcp", addr(d.Host, d.Port), 10*time.Second) - if err != nil { - return nil, err - } - - if d.SSL { - conn = tlsClient(conn, d.tlsConfig()) - } - - c, err := smtpNewClient(conn, d.Host) - if err != nil { - return nil, err - } - - if d.LocalName != "" { - if err := c.Hello(d.LocalName); err != nil { - return nil, err - } - } - - if !d.SSL { - if ok, _ := c.Extension("STARTTLS"); ok { - if err := c.StartTLS(d.tlsConfig()); err != nil { - c.Close() - return nil, err - } - } - } - - if d.Auth == nil && d.Username != "" { - if ok, auths := c.Extension("AUTH"); ok { - if strings.Contains(auths, "CRAM-MD5") { - d.Auth = smtp.CRAMMD5Auth(d.Username, d.Password) - } else if strings.Contains(auths, "LOGIN") && - !strings.Contains(auths, "PLAIN") { - d.Auth = &loginAuth{ - username: d.Username, - password: d.Password, - host: d.Host, - } - } else { - d.Auth = smtp.PlainAuth("", d.Username, d.Password, d.Host) - } - } - } - - if d.Auth != nil { - if err = c.Auth(d.Auth); err != nil { - c.Close() - return nil, err - } - } - - return &smtpSender{c, d}, nil -} - -func (d *Dialer) tlsConfig() *tls.Config { - if d.TLSConfig == nil { - return &tls.Config{ServerName: d.Host} - } - return d.TLSConfig -} - -func addr(host string, port int) string { - return fmt.Sprintf("%s:%d", host, port) -} - -// DialAndSend opens a connection to the SMTP server, sends the given emails and -// closes the connection. -func (d *Dialer) DialAndSend(m ...*Message) error { - s, err := d.Dial() - if err != nil { - return err - } - defer s.Close() - - return Send(s, m...) -} - -type smtpSender struct { - smtpClient - d *Dialer -} - -func (c *smtpSender) Send(from string, to []string, msg io.WriterTo) error { - if err := c.Mail(from); err != nil { - if err == io.EOF { - // This is probably due to a timeout, so reconnect and try again. - sc, derr := c.d.Dial() - if derr == nil { - if s, ok := sc.(*smtpSender); ok { - *c = *s - return c.Send(from, to, msg) - } - } - } - return err - } - - for _, addr := range to { - if err := c.Rcpt(addr); err != nil { - return err - } - } - - w, err := c.Data() - if err != nil { - return err - } - - if _, err = msg.WriteTo(w); err != nil { - w.Close() - return err - } - - return w.Close() -} - -func (c *smtpSender) Close() error { - return c.Quit() -} - -// Stubbed out for tests. -var ( - netDialTimeout = net.DialTimeout - tlsClient = tls.Client - smtpNewClient = func(conn net.Conn, host string) (smtpClient, error) { - return smtp.NewClient(conn, host) - } -) - -type smtpClient interface { - Hello(string) error - Extension(string) (bool, string) - StartTLS(*tls.Config) error - Auth(smtp.Auth) error - Mail(string) error - Rcpt(string) error - Data() (io.WriteCloser, error) - Quit() error - Close() error -} diff --git a/vendor/github.com/go-gomail/gomail/writeto.go b/vendor/github.com/go-gomail/gomail/writeto.go deleted file mode 100644 index 9fb6b86..0000000 --- a/vendor/github.com/go-gomail/gomail/writeto.go +++ /dev/null @@ -1,306 +0,0 @@ -package gomail - -import ( - "encoding/base64" - "errors" - "io" - "mime" - "mime/multipart" - "path/filepath" - "strings" - "time" -) - -// WriteTo implements io.WriterTo. It dumps the whole message into w. -func (m *Message) WriteTo(w io.Writer) (int64, error) { - mw := &messageWriter{w: w} - mw.writeMessage(m) - return mw.n, mw.err -} - -func (w *messageWriter) writeMessage(m *Message) { - if _, ok := m.header["Mime-Version"]; !ok { - w.writeString("Mime-Version: 1.0\r\n") - } - if _, ok := m.header["Date"]; !ok { - w.writeHeader("Date", m.FormatDate(now())) - } - w.writeHeaders(m.header) - - if m.hasMixedPart() { - w.openMultipart("mixed") - } - - if m.hasRelatedPart() { - w.openMultipart("related") - } - - if m.hasAlternativePart() { - w.openMultipart("alternative") - } - for _, part := range m.parts { - w.writePart(part, m.charset) - } - if m.hasAlternativePart() { - w.closeMultipart() - } - - w.addFiles(m.embedded, false) - if m.hasRelatedPart() { - w.closeMultipart() - } - - w.addFiles(m.attachments, true) - if m.hasMixedPart() { - w.closeMultipart() - } -} - -func (m *Message) hasMixedPart() bool { - return (len(m.parts) > 0 && len(m.attachments) > 0) || len(m.attachments) > 1 -} - -func (m *Message) hasRelatedPart() bool { - return (len(m.parts) > 0 && len(m.embedded) > 0) || len(m.embedded) > 1 -} - -func (m *Message) hasAlternativePart() bool { - return len(m.parts) > 1 -} - -type messageWriter struct { - w io.Writer - n int64 - writers [3]*multipart.Writer - partWriter io.Writer - depth uint8 - err error -} - -func (w *messageWriter) openMultipart(mimeType string) { - mw := multipart.NewWriter(w) - contentType := "multipart/" + mimeType + ";\r\n boundary=" + mw.Boundary() - w.writers[w.depth] = mw - - if w.depth == 0 { - w.writeHeader("Content-Type", contentType) - w.writeString("\r\n") - } else { - w.createPart(map[string][]string{ - "Content-Type": {contentType}, - }) - } - w.depth++ -} - -func (w *messageWriter) createPart(h map[string][]string) { - w.partWriter, w.err = w.writers[w.depth-1].CreatePart(h) -} - -func (w *messageWriter) closeMultipart() { - if w.depth > 0 { - w.writers[w.depth-1].Close() - w.depth-- - } -} - -func (w *messageWriter) writePart(p *part, charset string) { - w.writeHeaders(map[string][]string{ - "Content-Type": {p.contentType + "; charset=" + charset}, - "Content-Transfer-Encoding": {string(p.encoding)}, - }) - w.writeBody(p.copier, p.encoding) -} - -func (w *messageWriter) addFiles(files []*file, isAttachment bool) { - for _, f := range files { - if _, ok := f.Header["Content-Type"]; !ok { - mediaType := mime.TypeByExtension(filepath.Ext(f.Name)) - if mediaType == "" { - mediaType = "application/octet-stream" - } - f.setHeader("Content-Type", mediaType+`; name="`+f.Name+`"`) - } - - if _, ok := f.Header["Content-Transfer-Encoding"]; !ok { - f.setHeader("Content-Transfer-Encoding", string(Base64)) - } - - if _, ok := f.Header["Content-Disposition"]; !ok { - var disp string - if isAttachment { - disp = "attachment" - } else { - disp = "inline" - } - f.setHeader("Content-Disposition", disp+`; filename="`+f.Name+`"`) - } - - if !isAttachment { - if _, ok := f.Header["Content-ID"]; !ok { - f.setHeader("Content-ID", "<"+f.Name+">") - } - } - w.writeHeaders(f.Header) - w.writeBody(f.CopyFunc, Base64) - } -} - -func (w *messageWriter) Write(p []byte) (int, error) { - if w.err != nil { - return 0, errors.New("gomail: cannot write as writer is in error") - } - - var n int - n, w.err = w.w.Write(p) - w.n += int64(n) - return n, w.err -} - -func (w *messageWriter) writeString(s string) { - n, _ := io.WriteString(w.w, s) - w.n += int64(n) -} - -func (w *messageWriter) writeHeader(k string, v ...string) { - w.writeString(k) - if len(v) == 0 { - w.writeString(":\r\n") - return - } - w.writeString(": ") - - // Max header line length is 78 characters in RFC 5322 and 76 characters - // in RFC 2047. So for the sake of simplicity we use the 76 characters - // limit. - charsLeft := 76 - len(k) - len(": ") - - for i, s := range v { - // If the line is already too long, insert a newline right away. - if charsLeft < 1 { - if i == 0 { - w.writeString("\r\n ") - } else { - w.writeString(",\r\n ") - } - charsLeft = 75 - } else if i != 0 { - w.writeString(", ") - charsLeft -= 2 - } - - // While the header content is too long, fold it by inserting a newline. - for len(s) > charsLeft { - s = w.writeLine(s, charsLeft) - charsLeft = 75 - } - w.writeString(s) - if i := lastIndexByte(s, '\n'); i != -1 { - charsLeft = 75 - (len(s) - i - 1) - } else { - charsLeft -= len(s) - } - } - w.writeString("\r\n") -} - -func (w *messageWriter) writeLine(s string, charsLeft int) string { - // If there is already a newline before the limit. Write the line. - if i := strings.IndexByte(s, '\n'); i != -1 && i < charsLeft { - w.writeString(s[:i+1]) - return s[i+1:] - } - - for i := charsLeft - 1; i >= 0; i-- { - if s[i] == ' ' { - w.writeString(s[:i]) - w.writeString("\r\n ") - return s[i+1:] - } - } - - // We could not insert a newline cleanly so look for a space or a newline - // even if it is after the limit. - for i := 75; i < len(s); i++ { - if s[i] == ' ' { - w.writeString(s[:i]) - w.writeString("\r\n ") - return s[i+1:] - } - if s[i] == '\n' { - w.writeString(s[:i+1]) - return s[i+1:] - } - } - - // Too bad, no space or newline in the whole string. Just write everything. - w.writeString(s) - return "" -} - -func (w *messageWriter) writeHeaders(h map[string][]string) { - if w.depth == 0 { - for k, v := range h { - if k != "Bcc" { - w.writeHeader(k, v...) - } - } - } else { - w.createPart(h) - } -} - -func (w *messageWriter) writeBody(f func(io.Writer) error, enc Encoding) { - var subWriter io.Writer - if w.depth == 0 { - w.writeString("\r\n") - subWriter = w.w - } else { - subWriter = w.partWriter - } - - if enc == Base64 { - wc := base64.NewEncoder(base64.StdEncoding, newBase64LineWriter(subWriter)) - w.err = f(wc) - wc.Close() - } else if enc == Unencoded { - w.err = f(subWriter) - } else { - wc := newQPWriter(subWriter) - w.err = f(wc) - wc.Close() - } -} - -// As required by RFC 2045, 6.7. (page 21) for quoted-printable, and -// RFC 2045, 6.8. (page 25) for base64. -const maxLineLen = 76 - -// base64LineWriter limits text encoded in base64 to 76 characters per line -type base64LineWriter struct { - w io.Writer - lineLen int -} - -func newBase64LineWriter(w io.Writer) *base64LineWriter { - return &base64LineWriter{w: w} -} - -func (w *base64LineWriter) Write(p []byte) (int, error) { - n := 0 - for len(p)+w.lineLen > maxLineLen { - w.w.Write(p[:maxLineLen-w.lineLen]) - w.w.Write([]byte("\r\n")) - p = p[maxLineLen-w.lineLen:] - n += maxLineLen - w.lineLen - w.lineLen = 0 - } - - w.w.Write(p) - w.lineLen += len(p) - - return n + len(p), nil -} - -// Stubbed out for testing. -var now = time.Now diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE deleted file mode 100644 index 5f5c12a..0000000 --- a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Alexandre Cesaro - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md deleted file mode 100644 index 98ddf82..0000000 --- a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# quotedprintable - -## Introduction - -Package quotedprintable implements quoted-printable and message header encoding -as specified by RFC 2045 and RFC 2047. - -It is a copy of the Go 1.5 package `mime/quotedprintable`. It also includes -the new functions of package `mime` concerning RFC 2047. - -This code has minor changes with the standard library code in order to work -with Go 1.0 and newer. - -## Documentation - -https://godoc.org/gopkg.in/alexcesaro/quotedprintable.v3 diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go deleted file mode 100644 index cfd0261..0000000 --- a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go +++ /dev/null @@ -1,279 +0,0 @@ -package quotedprintable - -import ( - "bytes" - "encoding/base64" - "errors" - "fmt" - "io" - "strings" - "unicode" - "unicode/utf8" -) - -// A WordEncoder is a RFC 2047 encoded-word encoder. -type WordEncoder byte - -const ( - // BEncoding represents Base64 encoding scheme as defined by RFC 2045. - BEncoding = WordEncoder('b') - // QEncoding represents the Q-encoding scheme as defined by RFC 2047. - QEncoding = WordEncoder('q') -) - -var ( - errInvalidWord = errors.New("mime: invalid RFC 2047 encoded-word") -) - -// Encode returns the encoded-word form of s. If s is ASCII without special -// characters, it is returned unchanged. The provided charset is the IANA -// charset name of s. It is case insensitive. -func (e WordEncoder) Encode(charset, s string) string { - if !needsEncoding(s) { - return s - } - return e.encodeWord(charset, s) -} - -func needsEncoding(s string) bool { - for _, b := range s { - if (b < ' ' || b > '~') && b != '\t' { - return true - } - } - return false -} - -// encodeWord encodes a string into an encoded-word. -func (e WordEncoder) encodeWord(charset, s string) string { - buf := getBuffer() - defer putBuffer(buf) - - buf.WriteString("=?") - buf.WriteString(charset) - buf.WriteByte('?') - buf.WriteByte(byte(e)) - buf.WriteByte('?') - - if e == BEncoding { - w := base64.NewEncoder(base64.StdEncoding, buf) - io.WriteString(w, s) - w.Close() - } else { - enc := make([]byte, 3) - for i := 0; i < len(s); i++ { - b := s[i] - switch { - case b == ' ': - buf.WriteByte('_') - case b <= '~' && b >= '!' && b != '=' && b != '?' && b != '_': - buf.WriteByte(b) - default: - enc[0] = '=' - enc[1] = upperhex[b>>4] - enc[2] = upperhex[b&0x0f] - buf.Write(enc) - } - } - } - buf.WriteString("?=") - return buf.String() -} - -const upperhex = "0123456789ABCDEF" - -// A WordDecoder decodes MIME headers containing RFC 2047 encoded-words. -type WordDecoder struct { - // CharsetReader, if non-nil, defines a function to generate - // charset-conversion readers, converting from the provided - // charset into UTF-8. - // Charsets are always lower-case. utf-8, iso-8859-1 and us-ascii charsets - // are handled by default. - // One of the the CharsetReader's result values must be non-nil. - CharsetReader func(charset string, input io.Reader) (io.Reader, error) -} - -// Decode decodes an encoded-word. If word is not a valid RFC 2047 encoded-word, -// word is returned unchanged. -func (d *WordDecoder) Decode(word string) (string, error) { - fields := strings.Split(word, "?") // TODO: remove allocation? - if len(fields) != 5 || fields[0] != "=" || fields[4] != "=" || len(fields[2]) != 1 { - return "", errInvalidWord - } - - content, err := decode(fields[2][0], fields[3]) - if err != nil { - return "", err - } - - buf := getBuffer() - defer putBuffer(buf) - - if err := d.convert(buf, fields[1], content); err != nil { - return "", err - } - - return buf.String(), nil -} - -// DecodeHeader decodes all encoded-words of the given string. It returns an -// error if and only if CharsetReader of d returns an error. -func (d *WordDecoder) DecodeHeader(header string) (string, error) { - // If there is no encoded-word, returns before creating a buffer. - i := strings.Index(header, "=?") - if i == -1 { - return header, nil - } - - buf := getBuffer() - defer putBuffer(buf) - - buf.WriteString(header[:i]) - header = header[i:] - - betweenWords := false - for { - start := strings.Index(header, "=?") - if start == -1 { - break - } - cur := start + len("=?") - - i := strings.Index(header[cur:], "?") - if i == -1 { - break - } - charset := header[cur : cur+i] - cur += i + len("?") - - if len(header) < cur+len("Q??=") { - break - } - encoding := header[cur] - cur++ - - if header[cur] != '?' { - break - } - cur++ - - j := strings.Index(header[cur:], "?=") - if j == -1 { - break - } - text := header[cur : cur+j] - end := cur + j + len("?=") - - content, err := decode(encoding, text) - if err != nil { - betweenWords = false - buf.WriteString(header[:start+2]) - header = header[start+2:] - continue - } - - // Write characters before the encoded-word. White-space and newline - // characters separating two encoded-words must be deleted. - if start > 0 && (!betweenWords || hasNonWhitespace(header[:start])) { - buf.WriteString(header[:start]) - } - - if err := d.convert(buf, charset, content); err != nil { - return "", err - } - - header = header[end:] - betweenWords = true - } - - if len(header) > 0 { - buf.WriteString(header) - } - - return buf.String(), nil -} - -func decode(encoding byte, text string) ([]byte, error) { - switch encoding { - case 'B', 'b': - return base64.StdEncoding.DecodeString(text) - case 'Q', 'q': - return qDecode(text) - } - return nil, errInvalidWord -} - -func (d *WordDecoder) convert(buf *bytes.Buffer, charset string, content []byte) error { - switch { - case strings.EqualFold("utf-8", charset): - buf.Write(content) - case strings.EqualFold("iso-8859-1", charset): - for _, c := range content { - buf.WriteRune(rune(c)) - } - case strings.EqualFold("us-ascii", charset): - for _, c := range content { - if c >= utf8.RuneSelf { - buf.WriteRune(unicode.ReplacementChar) - } else { - buf.WriteByte(c) - } - } - default: - if d.CharsetReader == nil { - return fmt.Errorf("mime: unhandled charset %q", charset) - } - r, err := d.CharsetReader(strings.ToLower(charset), bytes.NewReader(content)) - if err != nil { - return err - } - if _, err = buf.ReadFrom(r); err != nil { - return err - } - } - return nil -} - -// hasNonWhitespace reports whether s (assumed to be ASCII) contains at least -// one byte of non-whitespace. -func hasNonWhitespace(s string) bool { - for _, b := range s { - switch b { - // Encoded-words can only be separated by linear white spaces which does - // not include vertical tabs (\v). - case ' ', '\t', '\n', '\r': - default: - return true - } - } - return false -} - -// qDecode decodes a Q encoded string. -func qDecode(s string) ([]byte, error) { - dec := make([]byte, len(s)) - n := 0 - for i := 0; i < len(s); i++ { - switch c := s[i]; { - case c == '_': - dec[n] = ' ' - case c == '=': - if i+2 >= len(s) { - return nil, errInvalidWord - } - b, err := readHexByte(s[i+1], s[i+2]) - if err != nil { - return nil, err - } - dec[n] = b - i += 2 - case (c <= '~' && c >= ' ') || c == '\n' || c == '\r' || c == '\t': - dec[n] = c - default: - return nil, errInvalidWord - } - n++ - } - - return dec[:n], nil -} diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go deleted file mode 100644 index 24283c5..0000000 --- a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go +++ /dev/null @@ -1,26 +0,0 @@ -// +build go1.3 - -package quotedprintable - -import ( - "bytes" - "sync" -) - -var bufPool = sync.Pool{ - New: func() interface{} { - return new(bytes.Buffer) - }, -} - -func getBuffer() *bytes.Buffer { - return bufPool.Get().(*bytes.Buffer) -} - -func putBuffer(buf *bytes.Buffer) { - if buf.Len() > 1024 { - return - } - buf.Reset() - bufPool.Put(buf) -} diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go deleted file mode 100644 index d335b4a..0000000 --- a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go +++ /dev/null @@ -1,24 +0,0 @@ -// +build !go1.3 - -package quotedprintable - -import "bytes" - -var ch = make(chan *bytes.Buffer, 32) - -func getBuffer() *bytes.Buffer { - select { - case buf := <-ch: - return buf - default: - } - return new(bytes.Buffer) -} - -func putBuffer(buf *bytes.Buffer) { - buf.Reset() - select { - case ch <- buf: - default: - } -} diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go deleted file mode 100644 index 955edca..0000000 --- a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go +++ /dev/null @@ -1,121 +0,0 @@ -// Package quotedprintable implements quoted-printable encoding as specified by -// RFC 2045. -package quotedprintable - -import ( - "bufio" - "bytes" - "fmt" - "io" -) - -// Reader is a quoted-printable decoder. -type Reader struct { - br *bufio.Reader - rerr error // last read error - line []byte // to be consumed before more of br -} - -// NewReader returns a quoted-printable reader, decoding from r. -func NewReader(r io.Reader) *Reader { - return &Reader{ - br: bufio.NewReader(r), - } -} - -func fromHex(b byte) (byte, error) { - switch { - case b >= '0' && b <= '9': - return b - '0', nil - case b >= 'A' && b <= 'F': - return b - 'A' + 10, nil - // Accept badly encoded bytes. - case b >= 'a' && b <= 'f': - return b - 'a' + 10, nil - } - return 0, fmt.Errorf("quotedprintable: invalid hex byte 0x%02x", b) -} - -func readHexByte(a, b byte) (byte, error) { - var hb, lb byte - var err error - if hb, err = fromHex(a); err != nil { - return 0, err - } - if lb, err = fromHex(b); err != nil { - return 0, err - } - return hb<<4 | lb, nil -} - -func isQPDiscardWhitespace(r rune) bool { - switch r { - case '\n', '\r', ' ', '\t': - return true - } - return false -} - -var ( - crlf = []byte("\r\n") - lf = []byte("\n") - softSuffix = []byte("=") -) - -// Read reads and decodes quoted-printable data from the underlying reader. -func (r *Reader) Read(p []byte) (n int, err error) { - // Deviations from RFC 2045: - // 1. in addition to "=\r\n", "=\n" is also treated as soft line break. - // 2. it will pass through a '\r' or '\n' not preceded by '=', consistent - // with other broken QP encoders & decoders. - for len(p) > 0 { - if len(r.line) == 0 { - if r.rerr != nil { - return n, r.rerr - } - r.line, r.rerr = r.br.ReadSlice('\n') - - // Does the line end in CRLF instead of just LF? - hasLF := bytes.HasSuffix(r.line, lf) - hasCR := bytes.HasSuffix(r.line, crlf) - wholeLine := r.line - r.line = bytes.TrimRightFunc(wholeLine, isQPDiscardWhitespace) - if bytes.HasSuffix(r.line, softSuffix) { - rightStripped := wholeLine[len(r.line):] - r.line = r.line[:len(r.line)-1] - if !bytes.HasPrefix(rightStripped, lf) && !bytes.HasPrefix(rightStripped, crlf) { - r.rerr = fmt.Errorf("quotedprintable: invalid bytes after =: %q", rightStripped) - } - } else if hasLF { - if hasCR { - r.line = append(r.line, '\r', '\n') - } else { - r.line = append(r.line, '\n') - } - } - continue - } - b := r.line[0] - - switch { - case b == '=': - if len(r.line[1:]) < 2 { - return n, io.ErrUnexpectedEOF - } - b, err = readHexByte(r.line[1], r.line[2]) - if err != nil { - return n, err - } - r.line = r.line[2:] // 2 of the 3; other 1 is done below - case b == '\t' || b == '\r' || b == '\n': - break - case b < ' ' || b > '~': - return n, fmt.Errorf("quotedprintable: invalid unescaped byte 0x%02x in body", b) - } - p[0] = b - p = p[1:] - r.line = r.line[1:] - n++ - } - return n, nil -} diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go deleted file mode 100644 index 43359d5..0000000 --- a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go +++ /dev/null @@ -1,166 +0,0 @@ -package quotedprintable - -import "io" - -const lineMaxLen = 76 - -// A Writer is a quoted-printable writer that implements io.WriteCloser. -type Writer struct { - // Binary mode treats the writer's input as pure binary and processes end of - // line bytes as binary data. - Binary bool - - w io.Writer - i int - line [78]byte - cr bool -} - -// NewWriter returns a new Writer that writes to w. -func NewWriter(w io.Writer) *Writer { - return &Writer{w: w} -} - -// Write encodes p using quoted-printable encoding and writes it to the -// underlying io.Writer. It limits line length to 76 characters. The encoded -// bytes are not necessarily flushed until the Writer is closed. -func (w *Writer) Write(p []byte) (n int, err error) { - for i, b := range p { - switch { - // Simple writes are done in batch. - case b >= '!' && b <= '~' && b != '=': - continue - case isWhitespace(b) || !w.Binary && (b == '\n' || b == '\r'): - continue - } - - if i > n { - if err := w.write(p[n:i]); err != nil { - return n, err - } - n = i - } - - if err := w.encode(b); err != nil { - return n, err - } - n++ - } - - if n == len(p) { - return n, nil - } - - if err := w.write(p[n:]); err != nil { - return n, err - } - - return len(p), nil -} - -// Close closes the Writer, flushing any unwritten data to the underlying -// io.Writer, but does not close the underlying io.Writer. -func (w *Writer) Close() error { - if err := w.checkLastByte(); err != nil { - return err - } - - return w.flush() -} - -// write limits text encoded in quoted-printable to 76 characters per line. -func (w *Writer) write(p []byte) error { - for _, b := range p { - if b == '\n' || b == '\r' { - // If the previous byte was \r, the CRLF has already been inserted. - if w.cr && b == '\n' { - w.cr = false - continue - } - - if b == '\r' { - w.cr = true - } - - if err := w.checkLastByte(); err != nil { - return err - } - if err := w.insertCRLF(); err != nil { - return err - } - continue - } - - if w.i == lineMaxLen-1 { - if err := w.insertSoftLineBreak(); err != nil { - return err - } - } - - w.line[w.i] = b - w.i++ - w.cr = false - } - - return nil -} - -func (w *Writer) encode(b byte) error { - if lineMaxLen-1-w.i < 3 { - if err := w.insertSoftLineBreak(); err != nil { - return err - } - } - - w.line[w.i] = '=' - w.line[w.i+1] = upperhex[b>>4] - w.line[w.i+2] = upperhex[b&0x0f] - w.i += 3 - - return nil -} - -// checkLastByte encodes the last buffered byte if it is a space or a tab. -func (w *Writer) checkLastByte() error { - if w.i == 0 { - return nil - } - - b := w.line[w.i-1] - if isWhitespace(b) { - w.i-- - if err := w.encode(b); err != nil { - return err - } - } - - return nil -} - -func (w *Writer) insertSoftLineBreak() error { - w.line[w.i] = '=' - w.i++ - - return w.insertCRLF() -} - -func (w *Writer) insertCRLF() error { - w.line[w.i] = '\r' - w.line[w.i+1] = '\n' - w.i += 2 - - return w.flush() -} - -func (w *Writer) flush() error { - if _, err := w.w.Write(w.line[:w.i]); err != nil { - return err - } - - w.i = 0 - return nil -} - -func isWhitespace(b byte) bool { - return b == ' ' || b == '\t' -} diff --git a/vendor/gopkg.in/ini.v1/LICENSE b/vendor/gopkg.in/ini.v1/LICENSE deleted file mode 100644 index 37ec93a..0000000 --- a/vendor/gopkg.in/ini.v1/LICENSE +++ /dev/null @@ -1,191 +0,0 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and -distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright -owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities -that control, are controlled by, or are under common control with that entity. -For the purposes of this definition, "control" means (i) the power, direct or -indirect, to cause the direction or management of such entity, whether by -contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising -permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including -but not limited to software source code, documentation source, and configuration -files. - -"Object" form shall mean any form resulting from mechanical transformation or -translation of a Source form, including but not limited to compiled object code, -generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made -available under the License, as indicated by a copyright notice that is included -in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that -is based on (or derived from) the Work and for which the editorial revisions, -annotations, elaborations, or other modifications represent, as a whole, an -original work of authorship. For the purposes of this License, Derivative Works -shall not include works that remain separable from, or merely link (or bind by -name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version -of the Work and any modifications or additions to that Work or Derivative Works -thereof, that is intentionally submitted to Licensor for inclusion in the Work -by the copyright owner or by an individual or Legal Entity authorized to submit -on behalf of the copyright owner. For the purposes of this definition, -"submitted" means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, and -issue tracking systems that are managed by, or on behalf of, the Licensor for -the purpose of discussing and improving the Work, but excluding communication -that is conspicuously marked or otherwise designated in writing by the copyright -owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf -of whom a Contribution has been received by Licensor and subsequently -incorporated within the Work. - -2. Grant of Copyright License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the Work and such -Derivative Works in Source or Object form. - -3. Grant of Patent License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable (except as stated in this section) patent license to make, have -made, use, offer to sell, sell, import, and otherwise transfer the Work, where -such license applies only to those patent claims licensable by such Contributor -that are necessarily infringed by their Contribution(s) alone or by combination -of their Contribution(s) with the Work to which such Contribution(s) was -submitted. If You institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work or a -Contribution incorporated within the Work constitutes direct or contributory -patent infringement, then any patent licenses granted to You under this License -for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. - -You may reproduce and distribute copies of the Work or Derivative Works thereof -in any medium, with or without modifications, and in Source or Object form, -provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of -this License; and -You must cause any modified files to carry prominent notices stating that You -changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, -all copyright, patent, trademark, and attribution notices from the Source form -of the Work, excluding those notices that do not pertain to any part of the -Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any -Derivative Works that You distribute must include a readable copy of the -attribution notices contained within such NOTICE file, excluding those notices -that do not pertain to any part of the Derivative Works, in at least one of the -following places: within a NOTICE text file distributed as part of the -Derivative Works; within the Source form or documentation, if provided along -with the Derivative Works; or, within a display generated by the Derivative -Works, if and wherever such third-party notices normally appear. The contents of -the NOTICE file are for informational purposes only and do not modify the -License. You may add Your own attribution notices within Derivative Works that -You distribute, alongside or as an addendum to the NOTICE text from the Work, -provided that such additional attribution notices cannot be construed as -modifying the License. -You may add Your own copyright statement to Your modifications and may provide -additional or different license terms and conditions for use, reproduction, or -distribution of Your modifications, or for any such Derivative Works as a whole, -provided Your use, reproduction, and distribution of the Work otherwise complies -with the conditions stated in this License. - -5. Submission of Contributions. - -Unless You explicitly state otherwise, any Contribution intentionally submitted -for inclusion in the Work by You to the Licensor shall be under the terms and -conditions of this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify the terms of -any separate license agreement you may have executed with Licensor regarding -such Contributions. - -6. Trademarks. - -This License does not grant permission to use the trade names, trademarks, -service marks, or product names of the Licensor, except as required for -reasonable and customary use in describing the origin of the Work and -reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. - -Unless required by applicable law or agreed to in writing, Licensor provides the -Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, -including, without limitation, any warranties or conditions of TITLE, -NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are -solely responsible for determining the appropriateness of using or -redistributing the Work and assume any risks associated with Your exercise of -permissions under this License. - -8. Limitation of Liability. - -In no event and under no legal theory, whether in tort (including negligence), -contract, or otherwise, unless required by applicable law (such as deliberate -and grossly negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, incidental, -or consequential damages of any character arising as a result of this License or -out of the use or inability to use the Work (including but not limited to -damages for loss of goodwill, work stoppage, computer failure or malfunction, or -any and all other commercial damages or losses), even if such Contributor has -been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. - -While redistributing the Work or Derivative Works thereof, You may choose to -offer, and charge a fee for, acceptance of support, warranty, indemnity, or -other liability obligations and/or rights consistent with this License. However, -in accepting such obligations, You may act only on Your own behalf and on Your -sole responsibility, not on behalf of any other Contributor, and only if You -agree to indemnify, defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason of your -accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work - -To apply the Apache License to your work, attach the following boilerplate -notice, with the fields enclosed by brackets "[]" replaced with your own -identifying information. (Don't include the brackets!) The text should be -enclosed in the appropriate comment syntax for the file format. We also -recommend that a file or class name and description of purpose be included on -the same "printed page" as the copyright notice for easier identification within -third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/vendor/gopkg.in/ini.v1/Makefile b/vendor/gopkg.in/ini.v1/Makefile deleted file mode 100644 index ac034e5..0000000 --- a/vendor/gopkg.in/ini.v1/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -.PHONY: build test bench vet - -build: vet bench - -test: - go test -v -cover -race - -bench: - go test -v -cover -race -test.bench=. -test.benchmem - -vet: - go vet diff --git a/vendor/gopkg.in/ini.v1/README.md b/vendor/gopkg.in/ini.v1/README.md deleted file mode 100644 index e67d51f..0000000 --- a/vendor/gopkg.in/ini.v1/README.md +++ /dev/null @@ -1,746 +0,0 @@ -INI [![Build Status](https://travis-ci.org/go-ini/ini.svg?branch=master)](https://travis-ci.org/go-ini/ini) [![Sourcegraph](https://sourcegraph.com/github.com/go-ini/ini/-/badge.svg)](https://sourcegraph.com/github.com/go-ini/ini?badge) -=== - -![](https://avatars0.githubusercontent.com/u/10216035?v=3&s=200) - -Package ini provides INI file read and write functionality in Go. - -[简体中文](README_ZH.md) - -## Feature - -- Load multiple data sources(`[]byte`, file and `io.ReadCloser`) with overwrites. -- Read with recursion values. -- Read with parent-child sections. -- Read with auto-increment key names. -- Read with multiple-line values. -- Read with tons of helper methods. -- Read and convert values to Go types. -- Read and **WRITE** comments of sections and keys. -- Manipulate sections, keys and comments with ease. -- Keep sections and keys in order as you parse and save. - -## Installation - -To use a tagged revision: - - go get gopkg.in/ini.v1 - -To use with latest changes: - - go get github.com/go-ini/ini - -Please add `-u` flag to update in the future. - -### Testing - -If you want to test on your machine, please apply `-t` flag: - - go get -t gopkg.in/ini.v1 - -Please add `-u` flag to update in the future. - -## Getting Started - -### Loading from data sources - -A **Data Source** is either raw data in type `[]byte`, a file name with type `string` or `io.ReadCloser`. You can load **as many data sources as you want**. Passing other types will simply return an error. - -```go -cfg, err := ini.Load([]byte("raw data"), "filename", ioutil.NopCloser(bytes.NewReader([]byte("some other data")))) -``` - -Or start with an empty object: - -```go -cfg := ini.Empty() -``` - -When you cannot decide how many data sources to load at the beginning, you will still be able to **Append()** them later. - -```go -err := cfg.Append("other file", []byte("other raw data")) -``` - -If you have a list of files with possibilities that some of them may not available at the time, and you don't know exactly which ones, you can use `LooseLoad` to ignore nonexistent files without returning error. - -```go -cfg, err := ini.LooseLoad("filename", "filename_404") -``` - -The cool thing is, whenever the file is available to load while you're calling `Reload` method, it will be counted as usual. - -#### Ignore cases of key name - -When you do not care about cases of section and key names, you can use `InsensitiveLoad` to force all names to be lowercased while parsing. - -```go -cfg, err := ini.InsensitiveLoad("filename") -//... - -// sec1 and sec2 are the exactly same section object -sec1, err := cfg.GetSection("Section") -sec2, err := cfg.GetSection("SecTIOn") - -// key1 and key2 are the exactly same key object -key1, err := sec1.GetKey("Key") -key2, err := sec2.GetKey("KeY") -``` - -#### MySQL-like boolean key - -MySQL's configuration allows a key without value as follows: - -```ini -[mysqld] -... -skip-host-cache -skip-name-resolve -``` - -By default, this is considered as missing value. But if you know you're going to deal with those cases, you can assign advanced load options: - -```go -cfg, err := LoadSources(LoadOptions{AllowBooleanKeys: true}, "my.cnf")) -``` - -The value of those keys are always `true`, and when you save to a file, it will keep in the same foramt as you read. - -To generate such keys in your program, you could use `NewBooleanKey`: - -```go -key, err := sec.NewBooleanKey("skip-host-cache") -``` - -#### Comment - -Take care that following format will be treated as comment: - -1. Line begins with `#` or `;` -2. Words after `#` or `;` -3. Words after section name (i.e words after `[some section name]`) - -If you want to save a value with `#` or `;`, please quote them with ``` ` ``` or ``` """ ```. - -Alternatively, you can use following `LoadOptions` to completely ignore inline comments: - -```go -cfg, err := LoadSources(LoadOptions{IgnoreInlineComment: true}, "app.ini")) -``` - -### Working with sections - -To get a section, you would need to: - -```go -section, err := cfg.GetSection("section name") -``` - -For a shortcut for default section, just give an empty string as name: - -```go -section, err := cfg.GetSection("") -``` - -When you're pretty sure the section exists, following code could make your life easier: - -```go -section := cfg.Section("section name") -``` - -What happens when the section somehow does not exist? Don't panic, it automatically creates and returns a new section to you. - -To create a new section: - -```go -err := cfg.NewSection("new section") -``` - -To get a list of sections or section names: - -```go -sections := cfg.Sections() -names := cfg.SectionStrings() -``` - -### Working with keys - -To get a key under a section: - -```go -key, err := cfg.Section("").GetKey("key name") -``` - -Same rule applies to key operations: - -```go -key := cfg.Section("").Key("key name") -``` - -To check if a key exists: - -```go -yes := cfg.Section("").HasKey("key name") -``` - -To create a new key: - -```go -err := cfg.Section("").NewKey("name", "value") -``` - -To get a list of keys or key names: - -```go -keys := cfg.Section("").Keys() -names := cfg.Section("").KeyStrings() -``` - -To get a clone hash of keys and corresponding values: - -```go -hash := cfg.Section("").KeysHash() -``` - -### Working with values - -To get a string value: - -```go -val := cfg.Section("").Key("key name").String() -``` - -To validate key value on the fly: - -```go -val := cfg.Section("").Key("key name").Validate(func(in string) string { - if len(in) == 0 { - return "default" - } - return in -}) -``` - -If you do not want any auto-transformation (such as recursive read) for the values, you can get raw value directly (this way you get much better performance): - -```go -val := cfg.Section("").Key("key name").Value() -``` - -To check if raw value exists: - -```go -yes := cfg.Section("").HasValue("test value") -``` - -To get value with types: - -```go -// For boolean values: -// true when value is: 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On -// false when value is: 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off -v, err = cfg.Section("").Key("BOOL").Bool() -v, err = cfg.Section("").Key("FLOAT64").Float64() -v, err = cfg.Section("").Key("INT").Int() -v, err = cfg.Section("").Key("INT64").Int64() -v, err = cfg.Section("").Key("UINT").Uint() -v, err = cfg.Section("").Key("UINT64").Uint64() -v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339) -v, err = cfg.Section("").Key("TIME").Time() // RFC3339 - -v = cfg.Section("").Key("BOOL").MustBool() -v = cfg.Section("").Key("FLOAT64").MustFloat64() -v = cfg.Section("").Key("INT").MustInt() -v = cfg.Section("").Key("INT64").MustInt64() -v = cfg.Section("").Key("UINT").MustUint() -v = cfg.Section("").Key("UINT64").MustUint64() -v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339) -v = cfg.Section("").Key("TIME").MustTime() // RFC3339 - -// Methods start with Must also accept one argument for default value -// when key not found or fail to parse value to given type. -// Except method MustString, which you have to pass a default value. - -v = cfg.Section("").Key("String").MustString("default") -v = cfg.Section("").Key("BOOL").MustBool(true) -v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25) -v = cfg.Section("").Key("INT").MustInt(10) -v = cfg.Section("").Key("INT64").MustInt64(99) -v = cfg.Section("").Key("UINT").MustUint(3) -v = cfg.Section("").Key("UINT64").MustUint64(6) -v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now()) -v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339 -``` - -What if my value is three-line long? - -```ini -[advance] -ADDRESS = """404 road, -NotFound, State, 5000 -Earth""" -``` - -Not a problem! - -```go -cfg.Section("advance").Key("ADDRESS").String() - -/* --- start --- -404 road, -NotFound, State, 5000 -Earth ------- end --- */ -``` - -That's cool, how about continuation lines? - -```ini -[advance] -two_lines = how about \ - continuation lines? -lots_of_lines = 1 \ - 2 \ - 3 \ - 4 -``` - -Piece of cake! - -```go -cfg.Section("advance").Key("two_lines").String() // how about continuation lines? -cfg.Section("advance").Key("lots_of_lines").String() // 1 2 3 4 -``` - -Well, I hate continuation lines, how do I disable that? - -```go -cfg, err := ini.LoadSources(ini.LoadOptions{ - IgnoreContinuation: true, -}, "filename") -``` - -Holy crap! - -Note that single quotes around values will be stripped: - -```ini -foo = "some value" // foo: some value -bar = 'some value' // bar: some value -``` - -That's all? Hmm, no. - -#### Helper methods of working with values - -To get value with given candidates: - -```go -v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"}) -v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75}) -v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30}) -v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30}) -v = cfg.Section("").Key("UINT").InUint(4, []int{3, 6, 9}) -v = cfg.Section("").Key("UINT64").InUint64(8, []int64{3, 6, 9}) -v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3}) -v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339 -``` - -Default value will be presented if value of key is not in candidates you given, and default value does not need be one of candidates. - -To validate value in a given range: - -```go -vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2) -vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20) -vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20) -vals = cfg.Section("").Key("UINT").RangeUint(0, 3, 9) -vals = cfg.Section("").Key("UINT64").RangeUint64(0, 3, 9) -vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime) -vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339 -``` - -##### Auto-split values into a slice - -To use zero value of type for invalid inputs: - -```go -// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] -// Input: how, 2.2, are, you -> [0.0 2.2 0.0 0.0] -vals = cfg.Section("").Key("STRINGS").Strings(",") -vals = cfg.Section("").Key("FLOAT64S").Float64s(",") -vals = cfg.Section("").Key("INTS").Ints(",") -vals = cfg.Section("").Key("INT64S").Int64s(",") -vals = cfg.Section("").Key("UINTS").Uints(",") -vals = cfg.Section("").Key("UINT64S").Uint64s(",") -vals = cfg.Section("").Key("TIMES").Times(",") -``` - -To exclude invalid values out of result slice: - -```go -// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] -// Input: how, 2.2, are, you -> [2.2] -vals = cfg.Section("").Key("FLOAT64S").ValidFloat64s(",") -vals = cfg.Section("").Key("INTS").ValidInts(",") -vals = cfg.Section("").Key("INT64S").ValidInt64s(",") -vals = cfg.Section("").Key("UINTS").ValidUints(",") -vals = cfg.Section("").Key("UINT64S").ValidUint64s(",") -vals = cfg.Section("").Key("TIMES").ValidTimes(",") -``` - -Or to return nothing but error when have invalid inputs: - -```go -// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] -// Input: how, 2.2, are, you -> error -vals = cfg.Section("").Key("FLOAT64S").StrictFloat64s(",") -vals = cfg.Section("").Key("INTS").StrictInts(",") -vals = cfg.Section("").Key("INT64S").StrictInt64s(",") -vals = cfg.Section("").Key("UINTS").StrictUints(",") -vals = cfg.Section("").Key("UINT64S").StrictUint64s(",") -vals = cfg.Section("").Key("TIMES").StrictTimes(",") -``` - -### Save your configuration - -Finally, it's time to save your configuration to somewhere. - -A typical way to save configuration is writing it to a file: - -```go -// ... -err = cfg.SaveTo("my.ini") -err = cfg.SaveToIndent("my.ini", "\t") -``` - -Another way to save is writing to a `io.Writer` interface: - -```go -// ... -cfg.WriteTo(writer) -cfg.WriteToIndent(writer, "\t") -``` - -By default, spaces are used to align "=" sign between key and values, to disable that: - -```go -ini.PrettyFormat = false -``` - -## Advanced Usage - -### Recursive Values - -For all value of keys, there is a special syntax `%()s`, where `` is the key name in same section or default section, and `%()s` will be replaced by corresponding value(empty string if key not found). You can use this syntax at most 99 level of recursions. - -```ini -NAME = ini - -[author] -NAME = Unknwon -GITHUB = https://github.com/%(NAME)s - -[package] -FULL_NAME = github.com/go-ini/%(NAME)s -``` - -```go -cfg.Section("author").Key("GITHUB").String() // https://github.com/Unknwon -cfg.Section("package").Key("FULL_NAME").String() // github.com/go-ini/ini -``` - -### Parent-child Sections - -You can use `.` in section name to indicate parent-child relationship between two or more sections. If the key not found in the child section, library will try again on its parent section until there is no parent section. - -```ini -NAME = ini -VERSION = v1 -IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s - -[package] -CLONE_URL = https://%(IMPORT_PATH)s - -[package.sub] -``` - -```go -cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1 -``` - -#### Retrieve parent keys available to a child section - -```go -cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"] -``` - -### Unparseable Sections - -Sometimes, you have sections that do not contain key-value pairs but raw content, to handle such case, you can use `LoadOptions.UnparsableSections`: - -```go -cfg, err := LoadSources(LoadOptions{UnparseableSections: []string{"COMMENTS"}}, `[COMMENTS] -<1> This slide has the fuel listed in the wrong units `)) - -body := cfg.Section("COMMENTS").Body() - -/* --- start --- -<1> This slide has the fuel listed in the wrong units ------- end --- */ -``` - -### Auto-increment Key Names - -If key name is `-` in data source, then it would be seen as special syntax for auto-increment key name start from 1, and every section is independent on counter. - -```ini -[features] --: Support read/write comments of keys and sections --: Support auto-increment of key names --: Support load multiple files to overwrite key values -``` - -```go -cfg.Section("features").KeyStrings() // []{"#1", "#2", "#3"} -``` - -### Map To Struct - -Want more objective way to play with INI? Cool. - -```ini -Name = Unknwon -age = 21 -Male = true -Born = 1993-01-01T20:17:05Z - -[Note] -Content = Hi is a good man! -Cities = HangZhou, Boston -``` - -```go -type Note struct { - Content string - Cities []string -} - -type Person struct { - Name string - Age int `ini:"age"` - Male bool - Born time.Time - Note - Created time.Time `ini:"-"` -} - -func main() { - cfg, err := ini.Load("path/to/ini") - // ... - p := new(Person) - err = cfg.MapTo(p) - // ... - - // Things can be simpler. - err = ini.MapTo(p, "path/to/ini") - // ... - - // Just map a section? Fine. - n := new(Note) - err = cfg.Section("Note").MapTo(n) - // ... -} -``` - -Can I have default value for field? Absolutely. - -Assign it before you map to struct. It will keep the value as it is if the key is not presented or got wrong type. - -```go -// ... -p := &Person{ - Name: "Joe", -} -// ... -``` - -It's really cool, but what's the point if you can't give me my file back from struct? - -### Reflect From Struct - -Why not? - -```go -type Embeded struct { - Dates []time.Time `delim:"|"` - Places []string `ini:"places,omitempty"` - None []int `ini:",omitempty"` -} - -type Author struct { - Name string `ini:"NAME"` - Male bool - Age int - GPA float64 - NeverMind string `ini:"-"` - *Embeded -} - -func main() { - a := &Author{"Unknwon", true, 21, 2.8, "", - &Embeded{ - []time.Time{time.Now(), time.Now()}, - []string{"HangZhou", "Boston"}, - []int{}, - }} - cfg := ini.Empty() - err = ini.ReflectFrom(cfg, a) - // ... -} -``` - -So, what do I get? - -```ini -NAME = Unknwon -Male = true -Age = 21 -GPA = 2.8 - -[Embeded] -Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00 -places = HangZhou,Boston -``` - -#### Name Mapper - -To save your time and make your code cleaner, this library supports [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) between struct field and actual section and key name. - -There are 2 built-in name mappers: - -- `AllCapsUnderscore`: it converts to format `ALL_CAPS_UNDERSCORE` then match section or key. -- `TitleUnderscore`: it converts to format `title_underscore` then match section or key. - -To use them: - -```go -type Info struct { - PackageName string -} - -func main() { - err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("package_name=ini")) - // ... - - cfg, err := ini.Load([]byte("PACKAGE_NAME=ini")) - // ... - info := new(Info) - cfg.NameMapper = ini.AllCapsUnderscore - err = cfg.MapTo(info) - // ... -} -``` - -Same rules of name mapper apply to `ini.ReflectFromWithMapper` function. - -#### Value Mapper - -To expand values (e.g. from environment variables), you can use the `ValueMapper` to transform values: - -```go -type Env struct { - Foo string `ini:"foo"` -} - -func main() { - cfg, err := ini.Load([]byte("[env]\nfoo = ${MY_VAR}\n") - cfg.ValueMapper = os.ExpandEnv - // ... - env := &Env{} - err = cfg.Section("env").MapTo(env) -} -``` - -This would set the value of `env.Foo` to the value of the environment variable `MY_VAR`. - -#### Other Notes On Map/Reflect - -Any embedded struct is treated as a section by default, and there is no automatic parent-child relations in map/reflect feature: - -```go -type Child struct { - Age string -} - -type Parent struct { - Name string - Child -} - -type Config struct { - City string - Parent -} -``` - -Example configuration: - -```ini -City = Boston - -[Parent] -Name = Unknwon - -[Child] -Age = 21 -``` - -What if, yes, I'm paranoid, I want embedded struct to be in the same section. Well, all roads lead to Rome. - -```go -type Child struct { - Age string -} - -type Parent struct { - Name string - Child `ini:"Parent"` -} - -type Config struct { - City string - Parent -} -``` - -Example configuration: - -```ini -City = Boston - -[Parent] -Name = Unknwon -Age = 21 -``` - -## Getting Help - -- [API Documentation](https://gowalker.org/gopkg.in/ini.v1) -- [File An Issue](https://github.com/go-ini/ini/issues/new) - -## FAQs - -### What does `BlockMode` field do? - -By default, library lets you read and write values so we need a locker to make sure your data is safe. But in cases that you are very sure about only reading data through the library, you can set `cfg.BlockMode = false` to speed up read operations about **50-70%** faster. - -### Why another INI library? - -Many people are using my another INI library [goconfig](https://github.com/Unknwon/goconfig), so the reason for this one is I would like to make more Go style code. Also when you set `cfg.BlockMode = false`, this one is about **10-30%** faster. - -To make those changes I have to confirm API broken, so it's safer to keep it in another place and start using `gopkg.in` to version my package at this time.(PS: shorter import path) - -## License - -This project is under Apache v2 License. See the [LICENSE](LICENSE) file for the full license text. diff --git a/vendor/gopkg.in/ini.v1/README_ZH.md b/vendor/gopkg.in/ini.v1/README_ZH.md deleted file mode 100644 index 0cf4194..0000000 --- a/vendor/gopkg.in/ini.v1/README_ZH.md +++ /dev/null @@ -1,733 +0,0 @@ -本包提供了 Go 语言中读写 INI 文件的功能。 - -## 功能特性 - -- 支持覆盖加载多个数据源(`[]byte`、文件和 `io.ReadCloser`) -- 支持递归读取键值 -- 支持读取父子分区 -- 支持读取自增键名 -- 支持读取多行的键值 -- 支持大量辅助方法 -- 支持在读取时直接转换为 Go 语言类型 -- 支持读取和 **写入** 分区和键的注释 -- 轻松操作分区、键值和注释 -- 在保存文件时分区和键值会保持原有的顺序 - -## 下载安装 - -使用一个特定版本: - - go get gopkg.in/ini.v1 - -使用最新版: - - go get github.com/go-ini/ini - -如需更新请添加 `-u` 选项。 - -### 测试安装 - -如果您想要在自己的机器上运行测试,请使用 `-t` 标记: - - go get -t gopkg.in/ini.v1 - -如需更新请添加 `-u` 选项。 - -## 开始使用 - -### 从数据源加载 - -一个 **数据源** 可以是 `[]byte` 类型的原始数据,`string` 类型的文件路径或 `io.ReadCloser`。您可以加载 **任意多个** 数据源。如果您传递其它类型的数据源,则会直接返回错误。 - -```go -cfg, err := ini.Load([]byte("raw data"), "filename", ioutil.NopCloser(bytes.NewReader([]byte("some other data")))) -``` - -或者从一个空白的文件开始: - -```go -cfg := ini.Empty() -``` - -当您在一开始无法决定需要加载哪些数据源时,仍可以使用 **Append()** 在需要的时候加载它们。 - -```go -err := cfg.Append("other file", []byte("other raw data")) -``` - -当您想要加载一系列文件,但是不能够确定其中哪些文件是不存在的,可以通过调用函数 `LooseLoad` 来忽略它们(`Load` 会因为文件不存在而返回错误): - -```go -cfg, err := ini.LooseLoad("filename", "filename_404") -``` - -更牛逼的是,当那些之前不存在的文件在重新调用 `Reload` 方法的时候突然出现了,那么它们会被正常加载。 - -#### 忽略键名的大小写 - -有时候分区和键的名称大小写混合非常烦人,这个时候就可以通过 `InsensitiveLoad` 将所有分区和键名在读取里强制转换为小写: - -```go -cfg, err := ini.InsensitiveLoad("filename") -//... - -// sec1 和 sec2 指向同一个分区对象 -sec1, err := cfg.GetSection("Section") -sec2, err := cfg.GetSection("SecTIOn") - -// key1 和 key2 指向同一个键对象 -key1, err := sec1.GetKey("Key") -key2, err := sec2.GetKey("KeY") -``` - -#### 类似 MySQL 配置中的布尔值键 - -MySQL 的配置文件中会出现没有具体值的布尔类型的键: - -```ini -[mysqld] -... -skip-host-cache -skip-name-resolve -``` - -默认情况下这被认为是缺失值而无法完成解析,但可以通过高级的加载选项对它们进行处理: - -```go -cfg, err := LoadSources(LoadOptions{AllowBooleanKeys: true}, "my.cnf")) -``` - -这些键的值永远为 `true`,且在保存到文件时也只会输出键名。 - -如果您想要通过程序来生成此类键,则可以使用 `NewBooleanKey`: - -```go -key, err := sec.NewBooleanKey("skip-host-cache") -``` - -#### 关于注释 - -下述几种情况的内容将被视为注释: - -1. 所有以 `#` 或 `;` 开头的行 -2. 所有在 `#` 或 `;` 之后的内容 -3. 分区标签后的文字 (即 `[分区名]` 之后的内容) - -如果你希望使用包含 `#` 或 `;` 的值,请使用 ``` ` ``` 或 ``` """ ``` 进行包覆。 - -除此之外,您还可以通过 `LoadOptions` 完全忽略行内注释: - -```go -cfg, err := LoadSources(LoadOptions{IgnoreInlineComment: true}, "app.ini")) -``` - -### 操作分区(Section) - -获取指定分区: - -```go -section, err := cfg.GetSection("section name") -``` - -如果您想要获取默认分区,则可以用空字符串代替分区名: - -```go -section, err := cfg.GetSection("") -``` - -当您非常确定某个分区是存在的,可以使用以下简便方法: - -```go -section := cfg.Section("section name") -``` - -如果不小心判断错了,要获取的分区其实是不存在的,那会发生什么呢?没事的,它会自动创建并返回一个对应的分区对象给您。 - -创建一个分区: - -```go -err := cfg.NewSection("new section") -``` - -获取所有分区对象或名称: - -```go -sections := cfg.Sections() -names := cfg.SectionStrings() -``` - -### 操作键(Key) - -获取某个分区下的键: - -```go -key, err := cfg.Section("").GetKey("key name") -``` - -和分区一样,您也可以直接获取键而忽略错误处理: - -```go -key := cfg.Section("").Key("key name") -``` - -判断某个键是否存在: - -```go -yes := cfg.Section("").HasKey("key name") -``` - -创建一个新的键: - -```go -err := cfg.Section("").NewKey("name", "value") -``` - -获取分区下的所有键或键名: - -```go -keys := cfg.Section("").Keys() -names := cfg.Section("").KeyStrings() -``` - -获取分区下的所有键值对的克隆: - -```go -hash := cfg.Section("").KeysHash() -``` - -### 操作键值(Value) - -获取一个类型为字符串(string)的值: - -```go -val := cfg.Section("").Key("key name").String() -``` - -获取值的同时通过自定义函数进行处理验证: - -```go -val := cfg.Section("").Key("key name").Validate(func(in string) string { - if len(in) == 0 { - return "default" - } - return in -}) -``` - -如果您不需要任何对值的自动转变功能(例如递归读取),可以直接获取原值(这种方式性能最佳): - -```go -val := cfg.Section("").Key("key name").Value() -``` - -判断某个原值是否存在: - -```go -yes := cfg.Section("").HasValue("test value") -``` - -获取其它类型的值: - -```go -// 布尔值的规则: -// true 当值为:1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On -// false 当值为:0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off -v, err = cfg.Section("").Key("BOOL").Bool() -v, err = cfg.Section("").Key("FLOAT64").Float64() -v, err = cfg.Section("").Key("INT").Int() -v, err = cfg.Section("").Key("INT64").Int64() -v, err = cfg.Section("").Key("UINT").Uint() -v, err = cfg.Section("").Key("UINT64").Uint64() -v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339) -v, err = cfg.Section("").Key("TIME").Time() // RFC3339 - -v = cfg.Section("").Key("BOOL").MustBool() -v = cfg.Section("").Key("FLOAT64").MustFloat64() -v = cfg.Section("").Key("INT").MustInt() -v = cfg.Section("").Key("INT64").MustInt64() -v = cfg.Section("").Key("UINT").MustUint() -v = cfg.Section("").Key("UINT64").MustUint64() -v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339) -v = cfg.Section("").Key("TIME").MustTime() // RFC3339 - -// 由 Must 开头的方法名允许接收一个相同类型的参数来作为默认值, -// 当键不存在或者转换失败时,则会直接返回该默认值。 -// 但是,MustString 方法必须传递一个默认值。 - -v = cfg.Seciont("").Key("String").MustString("default") -v = cfg.Section("").Key("BOOL").MustBool(true) -v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25) -v = cfg.Section("").Key("INT").MustInt(10) -v = cfg.Section("").Key("INT64").MustInt64(99) -v = cfg.Section("").Key("UINT").MustUint(3) -v = cfg.Section("").Key("UINT64").MustUint64(6) -v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now()) -v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339 -``` - -如果我的值有好多行怎么办? - -```ini -[advance] -ADDRESS = """404 road, -NotFound, State, 5000 -Earth""" -``` - -嗯哼?小 case! - -```go -cfg.Section("advance").Key("ADDRESS").String() - -/* --- start --- -404 road, -NotFound, State, 5000 -Earth ------- end --- */ -``` - -赞爆了!那要是我属于一行的内容写不下想要写到第二行怎么办? - -```ini -[advance] -two_lines = how about \ - continuation lines? -lots_of_lines = 1 \ - 2 \ - 3 \ - 4 -``` - -简直是小菜一碟! - -```go -cfg.Section("advance").Key("two_lines").String() // how about continuation lines? -cfg.Section("advance").Key("lots_of_lines").String() // 1 2 3 4 -``` - -可是我有时候觉得两行连在一起特别没劲,怎么才能不自动连接两行呢? - -```go -cfg, err := ini.LoadSources(ini.LoadOptions{ - IgnoreContinuation: true, -}, "filename") -``` - -哇靠给力啊! - -需要注意的是,值两侧的单引号会被自动剔除: - -```ini -foo = "some value" // foo: some value -bar = 'some value' // bar: some value -``` - -这就是全部了?哈哈,当然不是。 - -#### 操作键值的辅助方法 - -获取键值时设定候选值: - -```go -v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"}) -v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75}) -v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30}) -v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30}) -v = cfg.Section("").Key("UINT").InUint(4, []int{3, 6, 9}) -v = cfg.Section("").Key("UINT64").InUint64(8, []int64{3, 6, 9}) -v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3}) -v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339 -``` - -如果获取到的值不是候选值的任意一个,则会返回默认值,而默认值不需要是候选值中的一员。 - -验证获取的值是否在指定范围内: - -```go -vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2) -vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20) -vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20) -vals = cfg.Section("").Key("UINT").RangeUint(0, 3, 9) -vals = cfg.Section("").Key("UINT64").RangeUint64(0, 3, 9) -vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime) -vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339 -``` - -##### 自动分割键值到切片(slice) - -当存在无效输入时,使用零值代替: - -```go -// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] -// Input: how, 2.2, are, you -> [0.0 2.2 0.0 0.0] -vals = cfg.Section("").Key("STRINGS").Strings(",") -vals = cfg.Section("").Key("FLOAT64S").Float64s(",") -vals = cfg.Section("").Key("INTS").Ints(",") -vals = cfg.Section("").Key("INT64S").Int64s(",") -vals = cfg.Section("").Key("UINTS").Uints(",") -vals = cfg.Section("").Key("UINT64S").Uint64s(",") -vals = cfg.Section("").Key("TIMES").Times(",") -``` - -从结果切片中剔除无效输入: - -```go -// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] -// Input: how, 2.2, are, you -> [2.2] -vals = cfg.Section("").Key("FLOAT64S").ValidFloat64s(",") -vals = cfg.Section("").Key("INTS").ValidInts(",") -vals = cfg.Section("").Key("INT64S").ValidInt64s(",") -vals = cfg.Section("").Key("UINTS").ValidUints(",") -vals = cfg.Section("").Key("UINT64S").ValidUint64s(",") -vals = cfg.Section("").Key("TIMES").ValidTimes(",") -``` - -当存在无效输入时,直接返回错误: - -```go -// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] -// Input: how, 2.2, are, you -> error -vals = cfg.Section("").Key("FLOAT64S").StrictFloat64s(",") -vals = cfg.Section("").Key("INTS").StrictInts(",") -vals = cfg.Section("").Key("INT64S").StrictInt64s(",") -vals = cfg.Section("").Key("UINTS").StrictUints(",") -vals = cfg.Section("").Key("UINT64S").StrictUint64s(",") -vals = cfg.Section("").Key("TIMES").StrictTimes(",") -``` - -### 保存配置 - -终于到了这个时刻,是时候保存一下配置了。 - -比较原始的做法是输出配置到某个文件: - -```go -// ... -err = cfg.SaveTo("my.ini") -err = cfg.SaveToIndent("my.ini", "\t") -``` - -另一个比较高级的做法是写入到任何实现 `io.Writer` 接口的对象中: - -```go -// ... -cfg.WriteTo(writer) -cfg.WriteToIndent(writer, "\t") -``` - -默认情况下,空格将被用于对齐键值之间的等号以美化输出结果,以下代码可以禁用该功能: - -```go -ini.PrettyFormat = false -``` - -## 高级用法 - -### 递归读取键值 - -在获取所有键值的过程中,特殊语法 `%()s` 会被应用,其中 `` 可以是相同分区或者默认分区下的键名。字符串 `%()s` 会被相应的键值所替代,如果指定的键不存在,则会用空字符串替代。您可以最多使用 99 层的递归嵌套。 - -```ini -NAME = ini - -[author] -NAME = Unknwon -GITHUB = https://github.com/%(NAME)s - -[package] -FULL_NAME = github.com/go-ini/%(NAME)s -``` - -```go -cfg.Section("author").Key("GITHUB").String() // https://github.com/Unknwon -cfg.Section("package").Key("FULL_NAME").String() // github.com/go-ini/ini -``` - -### 读取父子分区 - -您可以在分区名称中使用 `.` 来表示两个或多个分区之间的父子关系。如果某个键在子分区中不存在,则会去它的父分区中再次寻找,直到没有父分区为止。 - -```ini -NAME = ini -VERSION = v1 -IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s - -[package] -CLONE_URL = https://%(IMPORT_PATH)s - -[package.sub] -``` - -```go -cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1 -``` - -#### 获取上级父分区下的所有键名 - -```go -cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"] -``` - -### 无法解析的分区 - -如果遇到一些比较特殊的分区,它们不包含常见的键值对,而是没有固定格式的纯文本,则可以使用 `LoadOptions.UnparsableSections` 进行处理: - -```go -cfg, err := LoadSources(LoadOptions{UnparseableSections: []string{"COMMENTS"}}, `[COMMENTS] -<1> This slide has the fuel listed in the wrong units `)) - -body := cfg.Section("COMMENTS").Body() - -/* --- start --- -<1> This slide has the fuel listed in the wrong units ------- end --- */ -``` - -### 读取自增键名 - -如果数据源中的键名为 `-`,则认为该键使用了自增键名的特殊语法。计数器从 1 开始,并且分区之间是相互独立的。 - -```ini -[features] --: Support read/write comments of keys and sections --: Support auto-increment of key names --: Support load multiple files to overwrite key values -``` - -```go -cfg.Section("features").KeyStrings() // []{"#1", "#2", "#3"} -``` - -### 映射到结构 - -想要使用更加面向对象的方式玩转 INI 吗?好主意。 - -```ini -Name = Unknwon -age = 21 -Male = true -Born = 1993-01-01T20:17:05Z - -[Note] -Content = Hi is a good man! -Cities = HangZhou, Boston -``` - -```go -type Note struct { - Content string - Cities []string -} - -type Person struct { - Name string - Age int `ini:"age"` - Male bool - Born time.Time - Note - Created time.Time `ini:"-"` -} - -func main() { - cfg, err := ini.Load("path/to/ini") - // ... - p := new(Person) - err = cfg.MapTo(p) - // ... - - // 一切竟可以如此的简单。 - err = ini.MapTo(p, "path/to/ini") - // ... - - // 嗯哼?只需要映射一个分区吗? - n := new(Note) - err = cfg.Section("Note").MapTo(n) - // ... -} -``` - -结构的字段怎么设置默认值呢?很简单,只要在映射之前对指定字段进行赋值就可以了。如果键未找到或者类型错误,该值不会发生改变。 - -```go -// ... -p := &Person{ - Name: "Joe", -} -// ... -``` - -这样玩 INI 真的好酷啊!然而,如果不能还给我原来的配置文件,有什么卵用? - -### 从结构反射 - -可是,我有说不能吗? - -```go -type Embeded struct { - Dates []time.Time `delim:"|"` - Places []string `ini:"places,omitempty"` - None []int `ini:",omitempty"` -} - -type Author struct { - Name string `ini:"NAME"` - Male bool - Age int - GPA float64 - NeverMind string `ini:"-"` - *Embeded -} - -func main() { - a := &Author{"Unknwon", true, 21, 2.8, "", - &Embeded{ - []time.Time{time.Now(), time.Now()}, - []string{"HangZhou", "Boston"}, - []int{}, - }} - cfg := ini.Empty() - err = ini.ReflectFrom(cfg, a) - // ... -} -``` - -瞧瞧,奇迹发生了。 - -```ini -NAME = Unknwon -Male = true -Age = 21 -GPA = 2.8 - -[Embeded] -Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00 -places = HangZhou,Boston -``` - -#### 名称映射器(Name Mapper) - -为了节省您的时间并简化代码,本库支持类型为 [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) 的名称映射器,该映射器负责结构字段名与分区名和键名之间的映射。 - -目前有 2 款内置的映射器: - -- `AllCapsUnderscore`:该映射器将字段名转换至格式 `ALL_CAPS_UNDERSCORE` 后再去匹配分区名和键名。 -- `TitleUnderscore`:该映射器将字段名转换至格式 `title_underscore` 后再去匹配分区名和键名。 - -使用方法: - -```go -type Info struct{ - PackageName string -} - -func main() { - err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("package_name=ini")) - // ... - - cfg, err := ini.Load([]byte("PACKAGE_NAME=ini")) - // ... - info := new(Info) - cfg.NameMapper = ini.AllCapsUnderscore - err = cfg.MapTo(info) - // ... -} -``` - -使用函数 `ini.ReflectFromWithMapper` 时也可应用相同的规则。 - -#### 值映射器(Value Mapper) - -值映射器允许使用一个自定义函数自动展开值的具体内容,例如:运行时获取环境变量: - -```go -type Env struct { - Foo string `ini:"foo"` -} - -func main() { - cfg, err := ini.Load([]byte("[env]\nfoo = ${MY_VAR}\n") - cfg.ValueMapper = os.ExpandEnv - // ... - env := &Env{} - err = cfg.Section("env").MapTo(env) -} -``` - -本例中,`env.Foo` 将会是运行时所获取到环境变量 `MY_VAR` 的值。 - -#### 映射/反射的其它说明 - -任何嵌入的结构都会被默认认作一个不同的分区,并且不会自动产生所谓的父子分区关联: - -```go -type Child struct { - Age string -} - -type Parent struct { - Name string - Child -} - -type Config struct { - City string - Parent -} -``` - -示例配置文件: - -```ini -City = Boston - -[Parent] -Name = Unknwon - -[Child] -Age = 21 -``` - -很好,但是,我就是要嵌入结构也在同一个分区。好吧,你爹是李刚! - -```go -type Child struct { - Age string -} - -type Parent struct { - Name string - Child `ini:"Parent"` -} - -type Config struct { - City string - Parent -} -``` - -示例配置文件: - -```ini -City = Boston - -[Parent] -Name = Unknwon -Age = 21 -``` - -## 获取帮助 - -- [API 文档](https://gowalker.org/gopkg.in/ini.v1) -- [创建工单](https://github.com/go-ini/ini/issues/new) - -## 常见问题 - -### 字段 `BlockMode` 是什么? - -默认情况下,本库会在您进行读写操作时采用锁机制来确保数据时间。但在某些情况下,您非常确定只进行读操作。此时,您可以通过设置 `cfg.BlockMode = false` 来将读操作提升大约 **50-70%** 的性能。 - -### 为什么要写另一个 INI 解析库? - -许多人都在使用我的 [goconfig](https://github.com/Unknwon/goconfig) 来完成对 INI 文件的操作,但我希望使用更加 Go 风格的代码。并且当您设置 `cfg.BlockMode = false` 时,会有大约 **10-30%** 的性能提升。 - -为了做出这些改变,我必须对 API 进行破坏,所以新开一个仓库是最安全的做法。除此之外,本库直接使用 `gopkg.in` 来进行版本化发布。(其实真相是导入路径更短了) diff --git a/vendor/gopkg.in/ini.v1/error.go b/vendor/gopkg.in/ini.v1/error.go deleted file mode 100644 index 80afe74..0000000 --- a/vendor/gopkg.in/ini.v1/error.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2016 Unknwon -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package ini - -import ( - "fmt" -) - -type ErrDelimiterNotFound struct { - Line string -} - -func IsErrDelimiterNotFound(err error) bool { - _, ok := err.(ErrDelimiterNotFound) - return ok -} - -func (err ErrDelimiterNotFound) Error() string { - return fmt.Sprintf("key-value delimiter not found: %s", err.Line) -} diff --git a/vendor/gopkg.in/ini.v1/ini.go b/vendor/gopkg.in/ini.v1/ini.go deleted file mode 100644 index 0bd6c50..0000000 --- a/vendor/gopkg.in/ini.v1/ini.go +++ /dev/null @@ -1,561 +0,0 @@ -// Copyright 2014 Unknwon -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -// Package ini provides INI file read and write functionality in Go. -package ini - -import ( - "bytes" - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "regexp" - "runtime" - "strconv" - "strings" - "sync" - "time" -) - -const ( - // Name for default section. You can use this constant or the string literal. - // In most of cases, an empty string is all you need to access the section. - DEFAULT_SECTION = "DEFAULT" - - // Maximum allowed depth when recursively substituing variable names. - _DEPTH_VALUES = 99 - _VERSION = "1.28.0" -) - -// Version returns current package version literal. -func Version() string { - return _VERSION -} - -var ( - // Delimiter to determine or compose a new line. - // This variable will be changed to "\r\n" automatically on Windows - // at package init time. - LineBreak = "\n" - - // Variable regexp pattern: %(variable)s - varPattern = regexp.MustCompile(`%\(([^\)]+)\)s`) - - // Indicate whether to align "=" sign with spaces to produce pretty output - // or reduce all possible spaces for compact format. - PrettyFormat = true - - // Explicitly write DEFAULT section header - DefaultHeader = false - - // Indicate whether to put a line between sections - PrettySection = true -) - -func init() { - if runtime.GOOS == "windows" { - LineBreak = "\r\n" - } -} - -func inSlice(str string, s []string) bool { - for _, v := range s { - if str == v { - return true - } - } - return false -} - -// dataSource is an interface that returns object which can be read and closed. -type dataSource interface { - ReadCloser() (io.ReadCloser, error) -} - -// sourceFile represents an object that contains content on the local file system. -type sourceFile struct { - name string -} - -func (s sourceFile) ReadCloser() (_ io.ReadCloser, err error) { - return os.Open(s.name) -} - -type bytesReadCloser struct { - reader io.Reader -} - -func (rc *bytesReadCloser) Read(p []byte) (n int, err error) { - return rc.reader.Read(p) -} - -func (rc *bytesReadCloser) Close() error { - return nil -} - -// sourceData represents an object that contains content in memory. -type sourceData struct { - data []byte -} - -func (s *sourceData) ReadCloser() (io.ReadCloser, error) { - return ioutil.NopCloser(bytes.NewReader(s.data)), nil -} - -// sourceReadCloser represents an input stream with Close method. -type sourceReadCloser struct { - reader io.ReadCloser -} - -func (s *sourceReadCloser) ReadCloser() (io.ReadCloser, error) { - return s.reader, nil -} - -// File represents a combination of a or more INI file(s) in memory. -type File struct { - // Should make things safe, but sometimes doesn't matter. - BlockMode bool - // Make sure data is safe in multiple goroutines. - lock sync.RWMutex - - // Allow combination of multiple data sources. - dataSources []dataSource - // Actual data is stored here. - sections map[string]*Section - - // To keep data in order. - sectionList []string - - options LoadOptions - - NameMapper - ValueMapper -} - -// newFile initializes File object with given data sources. -func newFile(dataSources []dataSource, opts LoadOptions) *File { - return &File{ - BlockMode: true, - dataSources: dataSources, - sections: make(map[string]*Section), - sectionList: make([]string, 0, 10), - options: opts, - } -} - -func parseDataSource(source interface{}) (dataSource, error) { - switch s := source.(type) { - case string: - return sourceFile{s}, nil - case []byte: - return &sourceData{s}, nil - case io.ReadCloser: - return &sourceReadCloser{s}, nil - default: - return nil, fmt.Errorf("error parsing data source: unknown type '%s'", s) - } -} - -type LoadOptions struct { - // Loose indicates whether the parser should ignore nonexistent files or return error. - Loose bool - // Insensitive indicates whether the parser forces all section and key names to lowercase. - Insensitive bool - // IgnoreContinuation indicates whether to ignore continuation lines while parsing. - IgnoreContinuation bool - // IgnoreInlineComment indicates whether to ignore comments at the end of value and treat it as part of value. - IgnoreInlineComment bool - // AllowBooleanKeys indicates whether to allow boolean type keys or treat as value is missing. - // This type of keys are mostly used in my.cnf. - AllowBooleanKeys bool - // AllowShadows indicates whether to keep track of keys with same name under same section. - AllowShadows bool - // Some INI formats allow group blocks that store a block of raw content that doesn't otherwise - // conform to key/value pairs. Specify the names of those blocks here. - UnparseableSections []string -} - -func LoadSources(opts LoadOptions, source interface{}, others ...interface{}) (_ *File, err error) { - sources := make([]dataSource, len(others)+1) - sources[0], err = parseDataSource(source) - if err != nil { - return nil, err - } - for i := range others { - sources[i+1], err = parseDataSource(others[i]) - if err != nil { - return nil, err - } - } - f := newFile(sources, opts) - if err = f.Reload(); err != nil { - return nil, err - } - return f, nil -} - -// Load loads and parses from INI data sources. -// Arguments can be mixed of file name with string type, or raw data in []byte. -// It will return error if list contains nonexistent files. -func Load(source interface{}, others ...interface{}) (*File, error) { - return LoadSources(LoadOptions{}, source, others...) -} - -// LooseLoad has exactly same functionality as Load function -// except it ignores nonexistent files instead of returning error. -func LooseLoad(source interface{}, others ...interface{}) (*File, error) { - return LoadSources(LoadOptions{Loose: true}, source, others...) -} - -// InsensitiveLoad has exactly same functionality as Load function -// except it forces all section and key names to be lowercased. -func InsensitiveLoad(source interface{}, others ...interface{}) (*File, error) { - return LoadSources(LoadOptions{Insensitive: true}, source, others...) -} - -// InsensitiveLoad has exactly same functionality as Load function -// except it allows have shadow keys. -func ShadowLoad(source interface{}, others ...interface{}) (*File, error) { - return LoadSources(LoadOptions{AllowShadows: true}, source, others...) -} - -// Empty returns an empty file object. -func Empty() *File { - // Ignore error here, we sure our data is good. - f, _ := Load([]byte("")) - return f -} - -// NewSection creates a new section. -func (f *File) NewSection(name string) (*Section, error) { - if len(name) == 0 { - return nil, errors.New("error creating new section: empty section name") - } else if f.options.Insensitive && name != DEFAULT_SECTION { - name = strings.ToLower(name) - } - - if f.BlockMode { - f.lock.Lock() - defer f.lock.Unlock() - } - - if inSlice(name, f.sectionList) { - return f.sections[name], nil - } - - f.sectionList = append(f.sectionList, name) - f.sections[name] = newSection(f, name) - return f.sections[name], nil -} - -// NewRawSection creates a new section with an unparseable body. -func (f *File) NewRawSection(name, body string) (*Section, error) { - section, err := f.NewSection(name) - if err != nil { - return nil, err - } - - section.isRawSection = true - section.rawBody = body - return section, nil -} - -// NewSections creates a list of sections. -func (f *File) NewSections(names ...string) (err error) { - for _, name := range names { - if _, err = f.NewSection(name); err != nil { - return err - } - } - return nil -} - -// GetSection returns section by given name. -func (f *File) GetSection(name string) (*Section, error) { - if len(name) == 0 { - name = DEFAULT_SECTION - } else if f.options.Insensitive { - name = strings.ToLower(name) - } - - if f.BlockMode { - f.lock.RLock() - defer f.lock.RUnlock() - } - - sec := f.sections[name] - if sec == nil { - return nil, fmt.Errorf("section '%s' does not exist", name) - } - return sec, nil -} - -// Section assumes named section exists and returns a zero-value when not. -func (f *File) Section(name string) *Section { - sec, err := f.GetSection(name) - if err != nil { - // Note: It's OK here because the only possible error is empty section name, - // but if it's empty, this piece of code won't be executed. - sec, _ = f.NewSection(name) - return sec - } - return sec -} - -// Section returns list of Section. -func (f *File) Sections() []*Section { - sections := make([]*Section, len(f.sectionList)) - for i := range f.sectionList { - sections[i] = f.Section(f.sectionList[i]) - } - return sections -} - -// ChildSections returns a list of child sections of given section name. -func (f *File) ChildSections(name string) []*Section { - return f.Section(name).ChildSections() -} - -// SectionStrings returns list of section names. -func (f *File) SectionStrings() []string { - list := make([]string, len(f.sectionList)) - copy(list, f.sectionList) - return list -} - -// DeleteSection deletes a section. -func (f *File) DeleteSection(name string) { - if f.BlockMode { - f.lock.Lock() - defer f.lock.Unlock() - } - - if len(name) == 0 { - name = DEFAULT_SECTION - } - - for i, s := range f.sectionList { - if s == name { - f.sectionList = append(f.sectionList[:i], f.sectionList[i+1:]...) - delete(f.sections, name) - return - } - } -} - -func (f *File) reload(s dataSource) error { - r, err := s.ReadCloser() - if err != nil { - return err - } - defer r.Close() - - return f.parse(r) -} - -// Reload reloads and parses all data sources. -func (f *File) Reload() (err error) { - for _, s := range f.dataSources { - if err = f.reload(s); err != nil { - // In loose mode, we create an empty default section for nonexistent files. - if os.IsNotExist(err) && f.options.Loose { - f.parse(bytes.NewBuffer(nil)) - continue - } - return err - } - } - return nil -} - -// Append appends one or more data sources and reloads automatically. -func (f *File) Append(source interface{}, others ...interface{}) error { - ds, err := parseDataSource(source) - if err != nil { - return err - } - f.dataSources = append(f.dataSources, ds) - for _, s := range others { - ds, err = parseDataSource(s) - if err != nil { - return err - } - f.dataSources = append(f.dataSources, ds) - } - return f.Reload() -} - -// WriteToIndent writes content into io.Writer with given indention. -// If PrettyFormat has been set to be true, -// it will align "=" sign with spaces under each section. -func (f *File) WriteToIndent(w io.Writer, indent string) (n int64, err error) { - equalSign := "=" - if PrettyFormat { - equalSign = " = " - } - - // Use buffer to make sure target is safe until finish encoding. - buf := bytes.NewBuffer(nil) - for i, sname := range f.sectionList { - sec := f.Section(sname) - if len(sec.Comment) > 0 { - if sec.Comment[0] != '#' && sec.Comment[0] != ';' { - sec.Comment = "; " + sec.Comment - } - if _, err = buf.WriteString(sec.Comment + LineBreak); err != nil { - return 0, err - } - } - - if i > 0 || DefaultHeader { - if _, err = buf.WriteString("[" + sname + "]" + LineBreak); err != nil { - return 0, err - } - } else { - // Write nothing if default section is empty - if len(sec.keyList) == 0 { - continue - } - } - - if sec.isRawSection { - if _, err = buf.WriteString(sec.rawBody); err != nil { - return 0, err - } - continue - } - - // Count and generate alignment length and buffer spaces using the - // longest key. Keys may be modifed if they contain certain characters so - // we need to take that into account in our calculation. - alignLength := 0 - if PrettyFormat { - for _, kname := range sec.keyList { - keyLength := len(kname) - // First case will surround key by ` and second by """ - if strings.ContainsAny(kname, "\"=:") { - keyLength += 2 - } else if strings.Contains(kname, "`") { - keyLength += 6 - } - - if keyLength > alignLength { - alignLength = keyLength - } - } - } - alignSpaces := bytes.Repeat([]byte(" "), alignLength) - - KEY_LIST: - for _, kname := range sec.keyList { - key := sec.Key(kname) - if len(key.Comment) > 0 { - if len(indent) > 0 && sname != DEFAULT_SECTION { - buf.WriteString(indent) - } - if key.Comment[0] != '#' && key.Comment[0] != ';' { - key.Comment = "; " + key.Comment - } - if _, err = buf.WriteString(key.Comment + LineBreak); err != nil { - return 0, err - } - } - - if len(indent) > 0 && sname != DEFAULT_SECTION { - buf.WriteString(indent) - } - - switch { - case key.isAutoIncrement: - kname = "-" - case strings.ContainsAny(kname, "\"=:"): - kname = "`" + kname + "`" - case strings.Contains(kname, "`"): - kname = `"""` + kname + `"""` - } - - for _, val := range key.ValueWithShadows() { - if _, err = buf.WriteString(kname); err != nil { - return 0, err - } - - if key.isBooleanType { - if kname != sec.keyList[len(sec.keyList)-1] { - buf.WriteString(LineBreak) - } - continue KEY_LIST - } - - // Write out alignment spaces before "=" sign - if PrettyFormat { - buf.Write(alignSpaces[:alignLength-len(kname)]) - } - - // In case key value contains "\n", "`", "\"", "#" or ";" - if strings.ContainsAny(val, "\n`") { - val = `"""` + val + `"""` - } else if !f.options.IgnoreInlineComment && strings.ContainsAny(val, "#;") { - val = "`" + val + "`" - } - if _, err = buf.WriteString(equalSign + val + LineBreak); err != nil { - return 0, err - } - } - } - - if PrettySection { - // Put a line between sections - if _, err = buf.WriteString(LineBreak); err != nil { - return 0, err - } - } - } - - return buf.WriteTo(w) -} - -// WriteTo writes file content into io.Writer. -func (f *File) WriteTo(w io.Writer) (int64, error) { - return f.WriteToIndent(w, "") -} - -// SaveToIndent writes content to file system with given value indention. -func (f *File) SaveToIndent(filename, indent string) error { - // Note: Because we are truncating with os.Create, - // so it's safer to save to a temporary file location and rename afte done. - tmpPath := filename + "." + strconv.Itoa(time.Now().Nanosecond()) + ".tmp" - defer os.Remove(tmpPath) - - fw, err := os.Create(tmpPath) - if err != nil { - return err - } - - if _, err = f.WriteToIndent(fw, indent); err != nil { - fw.Close() - return err - } - fw.Close() - - // Remove old file and rename the new one. - os.Remove(filename) - return os.Rename(tmpPath, filename) -} - -// SaveTo writes content to file system. -func (f *File) SaveTo(filename string) error { - return f.SaveToIndent(filename, "") -} diff --git a/vendor/gopkg.in/ini.v1/key.go b/vendor/gopkg.in/ini.v1/key.go deleted file mode 100644 index 838356a..0000000 --- a/vendor/gopkg.in/ini.v1/key.go +++ /dev/null @@ -1,699 +0,0 @@ -// Copyright 2014 Unknwon -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package ini - -import ( - "errors" - "fmt" - "strconv" - "strings" - "time" -) - -// Key represents a key under a section. -type Key struct { - s *Section - name string - value string - isAutoIncrement bool - isBooleanType bool - - isShadow bool - shadows []*Key - - Comment string -} - -// newKey simply return a key object with given values. -func newKey(s *Section, name, val string) *Key { - return &Key{ - s: s, - name: name, - value: val, - } -} - -func (k *Key) addShadow(val string) error { - if k.isShadow { - return errors.New("cannot add shadow to another shadow key") - } else if k.isAutoIncrement || k.isBooleanType { - return errors.New("cannot add shadow to auto-increment or boolean key") - } - - shadow := newKey(k.s, k.name, val) - shadow.isShadow = true - k.shadows = append(k.shadows, shadow) - return nil -} - -// AddShadow adds a new shadow key to itself. -func (k *Key) AddShadow(val string) error { - if !k.s.f.options.AllowShadows { - return errors.New("shadow key is not allowed") - } - return k.addShadow(val) -} - -// ValueMapper represents a mapping function for values, e.g. os.ExpandEnv -type ValueMapper func(string) string - -// Name returns name of key. -func (k *Key) Name() string { - return k.name -} - -// Value returns raw value of key for performance purpose. -func (k *Key) Value() string { - return k.value -} - -// ValueWithShadows returns raw values of key and its shadows if any. -func (k *Key) ValueWithShadows() []string { - if len(k.shadows) == 0 { - return []string{k.value} - } - vals := make([]string, len(k.shadows)+1) - vals[0] = k.value - for i := range k.shadows { - vals[i+1] = k.shadows[i].value - } - return vals -} - -// transformValue takes a raw value and transforms to its final string. -func (k *Key) transformValue(val string) string { - if k.s.f.ValueMapper != nil { - val = k.s.f.ValueMapper(val) - } - - // Fail-fast if no indicate char found for recursive value - if !strings.Contains(val, "%") { - return val - } - for i := 0; i < _DEPTH_VALUES; i++ { - vr := varPattern.FindString(val) - if len(vr) == 0 { - break - } - - // Take off leading '%(' and trailing ')s'. - noption := strings.TrimLeft(vr, "%(") - noption = strings.TrimRight(noption, ")s") - - // Search in the same section. - nk, err := k.s.GetKey(noption) - if err != nil { - // Search again in default section. - nk, _ = k.s.f.Section("").GetKey(noption) - } - - // Substitute by new value and take off leading '%(' and trailing ')s'. - val = strings.Replace(val, vr, nk.value, -1) - } - return val -} - -// String returns string representation of value. -func (k *Key) String() string { - return k.transformValue(k.value) -} - -// Validate accepts a validate function which can -// return modifed result as key value. -func (k *Key) Validate(fn func(string) string) string { - return fn(k.String()) -} - -// parseBool returns the boolean value represented by the string. -// -// It accepts 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On, -// 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off. -// Any other value returns an error. -func parseBool(str string) (value bool, err error) { - switch str { - case "1", "t", "T", "true", "TRUE", "True", "YES", "yes", "Yes", "y", "ON", "on", "On": - return true, nil - case "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "n", "OFF", "off", "Off": - return false, nil - } - return false, fmt.Errorf("parsing \"%s\": invalid syntax", str) -} - -// Bool returns bool type value. -func (k *Key) Bool() (bool, error) { - return parseBool(k.String()) -} - -// Float64 returns float64 type value. -func (k *Key) Float64() (float64, error) { - return strconv.ParseFloat(k.String(), 64) -} - -// Int returns int type value. -func (k *Key) Int() (int, error) { - return strconv.Atoi(k.String()) -} - -// Int64 returns int64 type value. -func (k *Key) Int64() (int64, error) { - return strconv.ParseInt(k.String(), 10, 64) -} - -// Uint returns uint type valued. -func (k *Key) Uint() (uint, error) { - u, e := strconv.ParseUint(k.String(), 10, 64) - return uint(u), e -} - -// Uint64 returns uint64 type value. -func (k *Key) Uint64() (uint64, error) { - return strconv.ParseUint(k.String(), 10, 64) -} - -// Duration returns time.Duration type value. -func (k *Key) Duration() (time.Duration, error) { - return time.ParseDuration(k.String()) -} - -// TimeFormat parses with given format and returns time.Time type value. -func (k *Key) TimeFormat(format string) (time.Time, error) { - return time.Parse(format, k.String()) -} - -// Time parses with RFC3339 format and returns time.Time type value. -func (k *Key) Time() (time.Time, error) { - return k.TimeFormat(time.RFC3339) -} - -// MustString returns default value if key value is empty. -func (k *Key) MustString(defaultVal string) string { - val := k.String() - if len(val) == 0 { - k.value = defaultVal - return defaultVal - } - return val -} - -// MustBool always returns value without error, -// it returns false if error occurs. -func (k *Key) MustBool(defaultVal ...bool) bool { - val, err := k.Bool() - if len(defaultVal) > 0 && err != nil { - k.value = strconv.FormatBool(defaultVal[0]) - return defaultVal[0] - } - return val -} - -// MustFloat64 always returns value without error, -// it returns 0.0 if error occurs. -func (k *Key) MustFloat64(defaultVal ...float64) float64 { - val, err := k.Float64() - if len(defaultVal) > 0 && err != nil { - k.value = strconv.FormatFloat(defaultVal[0], 'f', -1, 64) - return defaultVal[0] - } - return val -} - -// MustInt always returns value without error, -// it returns 0 if error occurs. -func (k *Key) MustInt(defaultVal ...int) int { - val, err := k.Int() - if len(defaultVal) > 0 && err != nil { - k.value = strconv.FormatInt(int64(defaultVal[0]), 10) - return defaultVal[0] - } - return val -} - -// MustInt64 always returns value without error, -// it returns 0 if error occurs. -func (k *Key) MustInt64(defaultVal ...int64) int64 { - val, err := k.Int64() - if len(defaultVal) > 0 && err != nil { - k.value = strconv.FormatInt(defaultVal[0], 10) - return defaultVal[0] - } - return val -} - -// MustUint always returns value without error, -// it returns 0 if error occurs. -func (k *Key) MustUint(defaultVal ...uint) uint { - val, err := k.Uint() - if len(defaultVal) > 0 && err != nil { - k.value = strconv.FormatUint(uint64(defaultVal[0]), 10) - return defaultVal[0] - } - return val -} - -// MustUint64 always returns value without error, -// it returns 0 if error occurs. -func (k *Key) MustUint64(defaultVal ...uint64) uint64 { - val, err := k.Uint64() - if len(defaultVal) > 0 && err != nil { - k.value = strconv.FormatUint(defaultVal[0], 10) - return defaultVal[0] - } - return val -} - -// MustDuration always returns value without error, -// it returns zero value if error occurs. -func (k *Key) MustDuration(defaultVal ...time.Duration) time.Duration { - val, err := k.Duration() - if len(defaultVal) > 0 && err != nil { - k.value = defaultVal[0].String() - return defaultVal[0] - } - return val -} - -// MustTimeFormat always parses with given format and returns value without error, -// it returns zero value if error occurs. -func (k *Key) MustTimeFormat(format string, defaultVal ...time.Time) time.Time { - val, err := k.TimeFormat(format) - if len(defaultVal) > 0 && err != nil { - k.value = defaultVal[0].Format(format) - return defaultVal[0] - } - return val -} - -// MustTime always parses with RFC3339 format and returns value without error, -// it returns zero value if error occurs. -func (k *Key) MustTime(defaultVal ...time.Time) time.Time { - return k.MustTimeFormat(time.RFC3339, defaultVal...) -} - -// In always returns value without error, -// it returns default value if error occurs or doesn't fit into candidates. -func (k *Key) In(defaultVal string, candidates []string) string { - val := k.String() - for _, cand := range candidates { - if val == cand { - return val - } - } - return defaultVal -} - -// InFloat64 always returns value without error, -// it returns default value if error occurs or doesn't fit into candidates. -func (k *Key) InFloat64(defaultVal float64, candidates []float64) float64 { - val := k.MustFloat64() - for _, cand := range candidates { - if val == cand { - return val - } - } - return defaultVal -} - -// InInt always returns value without error, -// it returns default value if error occurs or doesn't fit into candidates. -func (k *Key) InInt(defaultVal int, candidates []int) int { - val := k.MustInt() - for _, cand := range candidates { - if val == cand { - return val - } - } - return defaultVal -} - -// InInt64 always returns value without error, -// it returns default value if error occurs or doesn't fit into candidates. -func (k *Key) InInt64(defaultVal int64, candidates []int64) int64 { - val := k.MustInt64() - for _, cand := range candidates { - if val == cand { - return val - } - } - return defaultVal -} - -// InUint always returns value without error, -// it returns default value if error occurs or doesn't fit into candidates. -func (k *Key) InUint(defaultVal uint, candidates []uint) uint { - val := k.MustUint() - for _, cand := range candidates { - if val == cand { - return val - } - } - return defaultVal -} - -// InUint64 always returns value without error, -// it returns default value if error occurs or doesn't fit into candidates. -func (k *Key) InUint64(defaultVal uint64, candidates []uint64) uint64 { - val := k.MustUint64() - for _, cand := range candidates { - if val == cand { - return val - } - } - return defaultVal -} - -// InTimeFormat always parses with given format and returns value without error, -// it returns default value if error occurs or doesn't fit into candidates. -func (k *Key) InTimeFormat(format string, defaultVal time.Time, candidates []time.Time) time.Time { - val := k.MustTimeFormat(format) - for _, cand := range candidates { - if val == cand { - return val - } - } - return defaultVal -} - -// InTime always parses with RFC3339 format and returns value without error, -// it returns default value if error occurs or doesn't fit into candidates. -func (k *Key) InTime(defaultVal time.Time, candidates []time.Time) time.Time { - return k.InTimeFormat(time.RFC3339, defaultVal, candidates) -} - -// RangeFloat64 checks if value is in given range inclusively, -// and returns default value if it's not. -func (k *Key) RangeFloat64(defaultVal, min, max float64) float64 { - val := k.MustFloat64() - if val < min || val > max { - return defaultVal - } - return val -} - -// RangeInt checks if value is in given range inclusively, -// and returns default value if it's not. -func (k *Key) RangeInt(defaultVal, min, max int) int { - val := k.MustInt() - if val < min || val > max { - return defaultVal - } - return val -} - -// RangeInt64 checks if value is in given range inclusively, -// and returns default value if it's not. -func (k *Key) RangeInt64(defaultVal, min, max int64) int64 { - val := k.MustInt64() - if val < min || val > max { - return defaultVal - } - return val -} - -// RangeTimeFormat checks if value with given format is in given range inclusively, -// and returns default value if it's not. -func (k *Key) RangeTimeFormat(format string, defaultVal, min, max time.Time) time.Time { - val := k.MustTimeFormat(format) - if val.Unix() < min.Unix() || val.Unix() > max.Unix() { - return defaultVal - } - return val -} - -// RangeTime checks if value with RFC3339 format is in given range inclusively, -// and returns default value if it's not. -func (k *Key) RangeTime(defaultVal, min, max time.Time) time.Time { - return k.RangeTimeFormat(time.RFC3339, defaultVal, min, max) -} - -// Strings returns list of string divided by given delimiter. -func (k *Key) Strings(delim string) []string { - str := k.String() - if len(str) == 0 { - return []string{} - } - - vals := strings.Split(str, delim) - for i := range vals { - // vals[i] = k.transformValue(strings.TrimSpace(vals[i])) - vals[i] = strings.TrimSpace(vals[i]) - } - return vals -} - -// StringsWithShadows returns list of string divided by given delimiter. -// Shadows will also be appended if any. -func (k *Key) StringsWithShadows(delim string) []string { - vals := k.ValueWithShadows() - results := make([]string, 0, len(vals)*2) - for i := range vals { - if len(vals) == 0 { - continue - } - - results = append(results, strings.Split(vals[i], delim)...) - } - - for i := range results { - results[i] = k.transformValue(strings.TrimSpace(results[i])) - } - return results -} - -// Float64s returns list of float64 divided by given delimiter. Any invalid input will be treated as zero value. -func (k *Key) Float64s(delim string) []float64 { - vals, _ := k.parseFloat64s(k.Strings(delim), true, false) - return vals -} - -// Ints returns list of int divided by given delimiter. Any invalid input will be treated as zero value. -func (k *Key) Ints(delim string) []int { - vals, _ := k.parseInts(k.Strings(delim), true, false) - return vals -} - -// Int64s returns list of int64 divided by given delimiter. Any invalid input will be treated as zero value. -func (k *Key) Int64s(delim string) []int64 { - vals, _ := k.parseInt64s(k.Strings(delim), true, false) - return vals -} - -// Uints returns list of uint divided by given delimiter. Any invalid input will be treated as zero value. -func (k *Key) Uints(delim string) []uint { - vals, _ := k.parseUints(k.Strings(delim), true, false) - return vals -} - -// Uint64s returns list of uint64 divided by given delimiter. Any invalid input will be treated as zero value. -func (k *Key) Uint64s(delim string) []uint64 { - vals, _ := k.parseUint64s(k.Strings(delim), true, false) - return vals -} - -// TimesFormat parses with given format and returns list of time.Time divided by given delimiter. -// Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC). -func (k *Key) TimesFormat(format, delim string) []time.Time { - vals, _ := k.parseTimesFormat(format, k.Strings(delim), true, false) - return vals -} - -// Times parses with RFC3339 format and returns list of time.Time divided by given delimiter. -// Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC). -func (k *Key) Times(delim string) []time.Time { - return k.TimesFormat(time.RFC3339, delim) -} - -// ValidFloat64s returns list of float64 divided by given delimiter. If some value is not float, then -// it will not be included to result list. -func (k *Key) ValidFloat64s(delim string) []float64 { - vals, _ := k.parseFloat64s(k.Strings(delim), false, false) - return vals -} - -// ValidInts returns list of int divided by given delimiter. If some value is not integer, then it will -// not be included to result list. -func (k *Key) ValidInts(delim string) []int { - vals, _ := k.parseInts(k.Strings(delim), false, false) - return vals -} - -// ValidInt64s returns list of int64 divided by given delimiter. If some value is not 64-bit integer, -// then it will not be included to result list. -func (k *Key) ValidInt64s(delim string) []int64 { - vals, _ := k.parseInt64s(k.Strings(delim), false, false) - return vals -} - -// ValidUints returns list of uint divided by given delimiter. If some value is not unsigned integer, -// then it will not be included to result list. -func (k *Key) ValidUints(delim string) []uint { - vals, _ := k.parseUints(k.Strings(delim), false, false) - return vals -} - -// ValidUint64s returns list of uint64 divided by given delimiter. If some value is not 64-bit unsigned -// integer, then it will not be included to result list. -func (k *Key) ValidUint64s(delim string) []uint64 { - vals, _ := k.parseUint64s(k.Strings(delim), false, false) - return vals -} - -// ValidTimesFormat parses with given format and returns list of time.Time divided by given delimiter. -func (k *Key) ValidTimesFormat(format, delim string) []time.Time { - vals, _ := k.parseTimesFormat(format, k.Strings(delim), false, false) - return vals -} - -// ValidTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter. -func (k *Key) ValidTimes(delim string) []time.Time { - return k.ValidTimesFormat(time.RFC3339, delim) -} - -// StrictFloat64s returns list of float64 divided by given delimiter or error on first invalid input. -func (k *Key) StrictFloat64s(delim string) ([]float64, error) { - return k.parseFloat64s(k.Strings(delim), false, true) -} - -// StrictInts returns list of int divided by given delimiter or error on first invalid input. -func (k *Key) StrictInts(delim string) ([]int, error) { - return k.parseInts(k.Strings(delim), false, true) -} - -// StrictInt64s returns list of int64 divided by given delimiter or error on first invalid input. -func (k *Key) StrictInt64s(delim string) ([]int64, error) { - return k.parseInt64s(k.Strings(delim), false, true) -} - -// StrictUints returns list of uint divided by given delimiter or error on first invalid input. -func (k *Key) StrictUints(delim string) ([]uint, error) { - return k.parseUints(k.Strings(delim), false, true) -} - -// StrictUint64s returns list of uint64 divided by given delimiter or error on first invalid input. -func (k *Key) StrictUint64s(delim string) ([]uint64, error) { - return k.parseUint64s(k.Strings(delim), false, true) -} - -// StrictTimesFormat parses with given format and returns list of time.Time divided by given delimiter -// or error on first invalid input. -func (k *Key) StrictTimesFormat(format, delim string) ([]time.Time, error) { - return k.parseTimesFormat(format, k.Strings(delim), false, true) -} - -// StrictTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter -// or error on first invalid input. -func (k *Key) StrictTimes(delim string) ([]time.Time, error) { - return k.StrictTimesFormat(time.RFC3339, delim) -} - -// parseFloat64s transforms strings to float64s. -func (k *Key) parseFloat64s(strs []string, addInvalid, returnOnInvalid bool) ([]float64, error) { - vals := make([]float64, 0, len(strs)) - for _, str := range strs { - val, err := strconv.ParseFloat(str, 64) - if err != nil && returnOnInvalid { - return nil, err - } - if err == nil || addInvalid { - vals = append(vals, val) - } - } - return vals, nil -} - -// parseInts transforms strings to ints. -func (k *Key) parseInts(strs []string, addInvalid, returnOnInvalid bool) ([]int, error) { - vals := make([]int, 0, len(strs)) - for _, str := range strs { - val, err := strconv.Atoi(str) - if err != nil && returnOnInvalid { - return nil, err - } - if err == nil || addInvalid { - vals = append(vals, val) - } - } - return vals, nil -} - -// parseInt64s transforms strings to int64s. -func (k *Key) parseInt64s(strs []string, addInvalid, returnOnInvalid bool) ([]int64, error) { - vals := make([]int64, 0, len(strs)) - for _, str := range strs { - val, err := strconv.ParseInt(str, 10, 64) - if err != nil && returnOnInvalid { - return nil, err - } - if err == nil || addInvalid { - vals = append(vals, val) - } - } - return vals, nil -} - -// parseUints transforms strings to uints. -func (k *Key) parseUints(strs []string, addInvalid, returnOnInvalid bool) ([]uint, error) { - vals := make([]uint, 0, len(strs)) - for _, str := range strs { - val, err := strconv.ParseUint(str, 10, 0) - if err != nil && returnOnInvalid { - return nil, err - } - if err == nil || addInvalid { - vals = append(vals, uint(val)) - } - } - return vals, nil -} - -// parseUint64s transforms strings to uint64s. -func (k *Key) parseUint64s(strs []string, addInvalid, returnOnInvalid bool) ([]uint64, error) { - vals := make([]uint64, 0, len(strs)) - for _, str := range strs { - val, err := strconv.ParseUint(str, 10, 64) - if err != nil && returnOnInvalid { - return nil, err - } - if err == nil || addInvalid { - vals = append(vals, val) - } - } - return vals, nil -} - -// parseTimesFormat transforms strings to times in given format. -func (k *Key) parseTimesFormat(format string, strs []string, addInvalid, returnOnInvalid bool) ([]time.Time, error) { - vals := make([]time.Time, 0, len(strs)) - for _, str := range strs { - val, err := time.Parse(format, str) - if err != nil && returnOnInvalid { - return nil, err - } - if err == nil || addInvalid { - vals = append(vals, val) - } - } - return vals, nil -} - -// SetValue changes key value. -func (k *Key) SetValue(v string) { - if k.s.f.BlockMode { - k.s.f.lock.Lock() - defer k.s.f.lock.Unlock() - } - - k.value = v - k.s.keysHash[k.name] = v -} diff --git a/vendor/gopkg.in/ini.v1/parser.go b/vendor/gopkg.in/ini.v1/parser.go deleted file mode 100644 index 69d5476..0000000 --- a/vendor/gopkg.in/ini.v1/parser.go +++ /dev/null @@ -1,361 +0,0 @@ -// Copyright 2015 Unknwon -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package ini - -import ( - "bufio" - "bytes" - "fmt" - "io" - "strconv" - "strings" - "unicode" -) - -type tokenType int - -const ( - _TOKEN_INVALID tokenType = iota - _TOKEN_COMMENT - _TOKEN_SECTION - _TOKEN_KEY -) - -type parser struct { - buf *bufio.Reader - isEOF bool - count int - comment *bytes.Buffer -} - -func newParser(r io.Reader) *parser { - return &parser{ - buf: bufio.NewReader(r), - count: 1, - comment: &bytes.Buffer{}, - } -} - -// BOM handles header of UTF-8, UTF-16 LE and UTF-16 BE's BOM format. -// http://en.wikipedia.org/wiki/Byte_order_mark#Representations_of_byte_order_marks_by_encoding -func (p *parser) BOM() error { - mask, err := p.buf.Peek(2) - if err != nil && err != io.EOF { - return err - } else if len(mask) < 2 { - return nil - } - - switch { - case mask[0] == 254 && mask[1] == 255: - fallthrough - case mask[0] == 255 && mask[1] == 254: - p.buf.Read(mask) - case mask[0] == 239 && mask[1] == 187: - mask, err := p.buf.Peek(3) - if err != nil && err != io.EOF { - return err - } else if len(mask) < 3 { - return nil - } - if mask[2] == 191 { - p.buf.Read(mask) - } - } - return nil -} - -func (p *parser) readUntil(delim byte) ([]byte, error) { - data, err := p.buf.ReadBytes(delim) - if err != nil { - if err == io.EOF { - p.isEOF = true - } else { - return nil, err - } - } - return data, nil -} - -func cleanComment(in []byte) ([]byte, bool) { - i := bytes.IndexAny(in, "#;") - if i == -1 { - return nil, false - } - return in[i:], true -} - -func readKeyName(in []byte) (string, int, error) { - line := string(in) - - // Check if key name surrounded by quotes. - var keyQuote string - if line[0] == '"' { - if len(line) > 6 && string(line[0:3]) == `"""` { - keyQuote = `"""` - } else { - keyQuote = `"` - } - } else if line[0] == '`' { - keyQuote = "`" - } - - // Get out key name - endIdx := -1 - if len(keyQuote) > 0 { - startIdx := len(keyQuote) - // FIXME: fail case -> """"""name"""=value - pos := strings.Index(line[startIdx:], keyQuote) - if pos == -1 { - return "", -1, fmt.Errorf("missing closing key quote: %s", line) - } - pos += startIdx - - // Find key-value delimiter - i := strings.IndexAny(line[pos+startIdx:], "=:") - if i < 0 { - return "", -1, ErrDelimiterNotFound{line} - } - endIdx = pos + i - return strings.TrimSpace(line[startIdx:pos]), endIdx + startIdx + 1, nil - } - - endIdx = strings.IndexAny(line, "=:") - if endIdx < 0 { - return "", -1, ErrDelimiterNotFound{line} - } - return strings.TrimSpace(line[0:endIdx]), endIdx + 1, nil -} - -func (p *parser) readMultilines(line, val, valQuote string) (string, error) { - for { - data, err := p.readUntil('\n') - if err != nil { - return "", err - } - next := string(data) - - pos := strings.LastIndex(next, valQuote) - if pos > -1 { - val += next[:pos] - - comment, has := cleanComment([]byte(next[pos:])) - if has { - p.comment.Write(bytes.TrimSpace(comment)) - } - break - } - val += next - if p.isEOF { - return "", fmt.Errorf("missing closing key quote from '%s' to '%s'", line, next) - } - } - return val, nil -} - -func (p *parser) readContinuationLines(val string) (string, error) { - for { - data, err := p.readUntil('\n') - if err != nil { - return "", err - } - next := strings.TrimSpace(string(data)) - - if len(next) == 0 { - break - } - val += next - if val[len(val)-1] != '\\' { - break - } - val = val[:len(val)-1] - } - return val, nil -} - -// hasSurroundedQuote check if and only if the first and last characters -// are quotes \" or \'. -// It returns false if any other parts also contain same kind of quotes. -func hasSurroundedQuote(in string, quote byte) bool { - return len(in) >= 2 && in[0] == quote && in[len(in)-1] == quote && - strings.IndexByte(in[1:], quote) == len(in)-2 -} - -func (p *parser) readValue(in []byte, ignoreContinuation, ignoreInlineComment bool) (string, error) { - line := strings.TrimLeftFunc(string(in), unicode.IsSpace) - if len(line) == 0 { - return "", nil - } - - var valQuote string - if len(line) > 3 && string(line[0:3]) == `"""` { - valQuote = `"""` - } else if line[0] == '`' { - valQuote = "`" - } - - if len(valQuote) > 0 { - startIdx := len(valQuote) - pos := strings.LastIndex(line[startIdx:], valQuote) - // Check for multi-line value - if pos == -1 { - return p.readMultilines(line, line[startIdx:], valQuote) - } - - return line[startIdx : pos+startIdx], nil - } - - // Won't be able to reach here if value only contains whitespace - line = strings.TrimSpace(line) - - // Check continuation lines when desired - if !ignoreContinuation && line[len(line)-1] == '\\' { - return p.readContinuationLines(line[:len(line)-1]) - } - - // Check if ignore inline comment - if !ignoreInlineComment { - i := strings.IndexAny(line, "#;") - if i > -1 { - p.comment.WriteString(line[i:]) - line = strings.TrimSpace(line[:i]) - } - } - - // Trim single quotes - if hasSurroundedQuote(line, '\'') || - hasSurroundedQuote(line, '"') { - line = line[1 : len(line)-1] - } - return line, nil -} - -// parse parses data through an io.Reader. -func (f *File) parse(reader io.Reader) (err error) { - p := newParser(reader) - if err = p.BOM(); err != nil { - return fmt.Errorf("BOM: %v", err) - } - - // Ignore error because default section name is never empty string. - section, _ := f.NewSection(DEFAULT_SECTION) - - var line []byte - var inUnparseableSection bool - for !p.isEOF { - line, err = p.readUntil('\n') - if err != nil { - return err - } - - line = bytes.TrimLeftFunc(line, unicode.IsSpace) - if len(line) == 0 { - continue - } - - // Comments - if line[0] == '#' || line[0] == ';' { - // Note: we do not care ending line break, - // it is needed for adding second line, - // so just clean it once at the end when set to value. - p.comment.Write(line) - continue - } - - // Section - if line[0] == '[' { - // Read to the next ']' (TODO: support quoted strings) - // TODO(unknwon): use LastIndexByte when stop supporting Go1.4 - closeIdx := bytes.LastIndex(line, []byte("]")) - if closeIdx == -1 { - return fmt.Errorf("unclosed section: %s", line) - } - - name := string(line[1:closeIdx]) - section, err = f.NewSection(name) - if err != nil { - return err - } - - comment, has := cleanComment(line[closeIdx+1:]) - if has { - p.comment.Write(comment) - } - - section.Comment = strings.TrimSpace(p.comment.String()) - - // Reset aotu-counter and comments - p.comment.Reset() - p.count = 1 - - inUnparseableSection = false - for i := range f.options.UnparseableSections { - if f.options.UnparseableSections[i] == name || - (f.options.Insensitive && strings.ToLower(f.options.UnparseableSections[i]) == strings.ToLower(name)) { - inUnparseableSection = true - continue - } - } - continue - } - - if inUnparseableSection { - section.isRawSection = true - section.rawBody += string(line) - continue - } - - kname, offset, err := readKeyName(line) - if err != nil { - // Treat as boolean key when desired, and whole line is key name. - if IsErrDelimiterNotFound(err) && f.options.AllowBooleanKeys { - kname, err := p.readValue(line, f.options.IgnoreContinuation, f.options.IgnoreInlineComment) - if err != nil { - return err - } - key, err := section.NewBooleanKey(kname) - if err != nil { - return err - } - key.Comment = strings.TrimSpace(p.comment.String()) - p.comment.Reset() - continue - } - return err - } - - // Auto increment. - isAutoIncr := false - if kname == "-" { - isAutoIncr = true - kname = "#" + strconv.Itoa(p.count) - p.count++ - } - - value, err := p.readValue(line[offset:], f.options.IgnoreContinuation, f.options.IgnoreInlineComment) - if err != nil { - return err - } - - key, err := section.NewKey(kname, value) - if err != nil { - return err - } - key.isAutoIncrement = isAutoIncr - key.Comment = strings.TrimSpace(p.comment.String()) - p.comment.Reset() - } - return nil -} diff --git a/vendor/gopkg.in/ini.v1/section.go b/vendor/gopkg.in/ini.v1/section.go deleted file mode 100644 index 94f7375..0000000 --- a/vendor/gopkg.in/ini.v1/section.go +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright 2014 Unknwon -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package ini - -import ( - "errors" - "fmt" - "strings" -) - -// Section represents a config section. -type Section struct { - f *File - Comment string - name string - keys map[string]*Key - keyList []string - keysHash map[string]string - - isRawSection bool - rawBody string -} - -func newSection(f *File, name string) *Section { - return &Section{ - f: f, - name: name, - keys: make(map[string]*Key), - keyList: make([]string, 0, 10), - keysHash: make(map[string]string), - } -} - -// Name returns name of Section. -func (s *Section) Name() string { - return s.name -} - -// Body returns rawBody of Section if the section was marked as unparseable. -// It still follows the other rules of the INI format surrounding leading/trailing whitespace. -func (s *Section) Body() string { - return strings.TrimSpace(s.rawBody) -} - -// NewKey creates a new key to given section. -func (s *Section) NewKey(name, val string) (*Key, error) { - if len(name) == 0 { - return nil, errors.New("error creating new key: empty key name") - } else if s.f.options.Insensitive { - name = strings.ToLower(name) - } - - if s.f.BlockMode { - s.f.lock.Lock() - defer s.f.lock.Unlock() - } - - if inSlice(name, s.keyList) { - if s.f.options.AllowShadows { - if err := s.keys[name].addShadow(val); err != nil { - return nil, err - } - } else { - s.keys[name].value = val - } - return s.keys[name], nil - } - - s.keyList = append(s.keyList, name) - s.keys[name] = newKey(s, name, val) - s.keysHash[name] = val - return s.keys[name], nil -} - -// NewBooleanKey creates a new boolean type key to given section. -func (s *Section) NewBooleanKey(name string) (*Key, error) { - key, err := s.NewKey(name, "true") - if err != nil { - return nil, err - } - - key.isBooleanType = true - return key, nil -} - -// GetKey returns key in section by given name. -func (s *Section) GetKey(name string) (*Key, error) { - // FIXME: change to section level lock? - if s.f.BlockMode { - s.f.lock.RLock() - } - if s.f.options.Insensitive { - name = strings.ToLower(name) - } - key := s.keys[name] - if s.f.BlockMode { - s.f.lock.RUnlock() - } - - if key == nil { - // Check if it is a child-section. - sname := s.name - for { - if i := strings.LastIndex(sname, "."); i > -1 { - sname = sname[:i] - sec, err := s.f.GetSection(sname) - if err != nil { - continue - } - return sec.GetKey(name) - } else { - break - } - } - return nil, fmt.Errorf("error when getting key of section '%s': key '%s' not exists", s.name, name) - } - return key, nil -} - -// HasKey returns true if section contains a key with given name. -func (s *Section) HasKey(name string) bool { - key, _ := s.GetKey(name) - return key != nil -} - -// Haskey is a backwards-compatible name for HasKey. -func (s *Section) Haskey(name string) bool { - return s.HasKey(name) -} - -// HasValue returns true if section contains given raw value. -func (s *Section) HasValue(value string) bool { - if s.f.BlockMode { - s.f.lock.RLock() - defer s.f.lock.RUnlock() - } - - for _, k := range s.keys { - if value == k.value { - return true - } - } - return false -} - -// Key assumes named Key exists in section and returns a zero-value when not. -func (s *Section) Key(name string) *Key { - key, err := s.GetKey(name) - if err != nil { - // It's OK here because the only possible error is empty key name, - // but if it's empty, this piece of code won't be executed. - key, _ = s.NewKey(name, "") - return key - } - return key -} - -// Keys returns list of keys of section. -func (s *Section) Keys() []*Key { - keys := make([]*Key, len(s.keyList)) - for i := range s.keyList { - keys[i] = s.Key(s.keyList[i]) - } - return keys -} - -// ParentKeys returns list of keys of parent section. -func (s *Section) ParentKeys() []*Key { - var parentKeys []*Key - sname := s.name - for { - if i := strings.LastIndex(sname, "."); i > -1 { - sname = sname[:i] - sec, err := s.f.GetSection(sname) - if err != nil { - continue - } - parentKeys = append(parentKeys, sec.Keys()...) - } else { - break - } - - } - return parentKeys -} - -// KeyStrings returns list of key names of section. -func (s *Section) KeyStrings() []string { - list := make([]string, len(s.keyList)) - copy(list, s.keyList) - return list -} - -// KeysHash returns keys hash consisting of names and values. -func (s *Section) KeysHash() map[string]string { - if s.f.BlockMode { - s.f.lock.RLock() - defer s.f.lock.RUnlock() - } - - hash := map[string]string{} - for key, value := range s.keysHash { - hash[key] = value - } - return hash -} - -// DeleteKey deletes a key from section. -func (s *Section) DeleteKey(name string) { - if s.f.BlockMode { - s.f.lock.Lock() - defer s.f.lock.Unlock() - } - - for i, k := range s.keyList { - if k == name { - s.keyList = append(s.keyList[:i], s.keyList[i+1:]...) - delete(s.keys, name) - return - } - } -} - -// ChildSections returns a list of child sections of current section. -// For example, "[parent.child1]" and "[parent.child12]" are child sections -// of section "[parent]". -func (s *Section) ChildSections() []*Section { - prefix := s.name + "." - children := make([]*Section, 0, 3) - for _, name := range s.f.sectionList { - if strings.HasPrefix(name, prefix) { - children = append(children, s.f.sections[name]) - } - } - return children -} diff --git a/vendor/gopkg.in/ini.v1/struct.go b/vendor/gopkg.in/ini.v1/struct.go deleted file mode 100644 index d522e00..0000000 --- a/vendor/gopkg.in/ini.v1/struct.go +++ /dev/null @@ -1,499 +0,0 @@ -// Copyright 2014 Unknwon -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package ini - -import ( - "bytes" - "errors" - "fmt" - "reflect" - "strings" - "time" - "unicode" -) - -// NameMapper represents a ini tag name mapper. -type NameMapper func(string) string - -// Built-in name getters. -var ( - // AllCapsUnderscore converts to format ALL_CAPS_UNDERSCORE. - AllCapsUnderscore NameMapper = func(raw string) string { - newstr := make([]rune, 0, len(raw)) - for i, chr := range raw { - if isUpper := 'A' <= chr && chr <= 'Z'; isUpper { - if i > 0 { - newstr = append(newstr, '_') - } - } - newstr = append(newstr, unicode.ToUpper(chr)) - } - return string(newstr) - } - // TitleUnderscore converts to format title_underscore. - TitleUnderscore NameMapper = func(raw string) string { - newstr := make([]rune, 0, len(raw)) - for i, chr := range raw { - if isUpper := 'A' <= chr && chr <= 'Z'; isUpper { - if i > 0 { - newstr = append(newstr, '_') - } - chr -= ('A' - 'a') - } - newstr = append(newstr, chr) - } - return string(newstr) - } -) - -func (s *Section) parseFieldName(raw, actual string) string { - if len(actual) > 0 { - return actual - } - if s.f.NameMapper != nil { - return s.f.NameMapper(raw) - } - return raw -} - -func parseDelim(actual string) string { - if len(actual) > 0 { - return actual - } - return "," -} - -var reflectTime = reflect.TypeOf(time.Now()).Kind() - -// setSliceWithProperType sets proper values to slice based on its type. -func setSliceWithProperType(key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error { - var strs []string - if allowShadow { - strs = key.StringsWithShadows(delim) - } else { - strs = key.Strings(delim) - } - - numVals := len(strs) - if numVals == 0 { - return nil - } - - var vals interface{} - var err error - - sliceOf := field.Type().Elem().Kind() - switch sliceOf { - case reflect.String: - vals = strs - case reflect.Int: - vals, err = key.parseInts(strs, true, false) - case reflect.Int64: - vals, err = key.parseInt64s(strs, true, false) - case reflect.Uint: - vals, err = key.parseUints(strs, true, false) - case reflect.Uint64: - vals, err = key.parseUint64s(strs, true, false) - case reflect.Float64: - vals, err = key.parseFloat64s(strs, true, false) - case reflectTime: - vals, err = key.parseTimesFormat(time.RFC3339, strs, true, false) - default: - return fmt.Errorf("unsupported type '[]%s'", sliceOf) - } - if isStrict { - return err - } - - slice := reflect.MakeSlice(field.Type(), numVals, numVals) - for i := 0; i < numVals; i++ { - switch sliceOf { - case reflect.String: - slice.Index(i).Set(reflect.ValueOf(vals.([]string)[i])) - case reflect.Int: - slice.Index(i).Set(reflect.ValueOf(vals.([]int)[i])) - case reflect.Int64: - slice.Index(i).Set(reflect.ValueOf(vals.([]int64)[i])) - case reflect.Uint: - slice.Index(i).Set(reflect.ValueOf(vals.([]uint)[i])) - case reflect.Uint64: - slice.Index(i).Set(reflect.ValueOf(vals.([]uint64)[i])) - case reflect.Float64: - slice.Index(i).Set(reflect.ValueOf(vals.([]float64)[i])) - case reflectTime: - slice.Index(i).Set(reflect.ValueOf(vals.([]time.Time)[i])) - } - } - field.Set(slice) - return nil -} - -func wrapStrictError(err error, isStrict bool) error { - if isStrict { - return err - } - return nil -} - -// setWithProperType sets proper value to field based on its type, -// but it does not return error for failing parsing, -// because we want to use default value that is already assigned to strcut. -func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error { - switch t.Kind() { - case reflect.String: - if len(key.String()) == 0 { - return nil - } - field.SetString(key.String()) - case reflect.Bool: - boolVal, err := key.Bool() - if err != nil { - return wrapStrictError(err, isStrict) - } - field.SetBool(boolVal) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - durationVal, err := key.Duration() - // Skip zero value - if err == nil && int(durationVal) > 0 { - field.Set(reflect.ValueOf(durationVal)) - return nil - } - - intVal, err := key.Int64() - if err != nil { - return wrapStrictError(err, isStrict) - } - field.SetInt(intVal) - // byte is an alias for uint8, so supporting uint8 breaks support for byte - case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: - durationVal, err := key.Duration() - // Skip zero value - if err == nil && int(durationVal) > 0 { - field.Set(reflect.ValueOf(durationVal)) - return nil - } - - uintVal, err := key.Uint64() - if err != nil { - return wrapStrictError(err, isStrict) - } - field.SetUint(uintVal) - - case reflect.Float32, reflect.Float64: - floatVal, err := key.Float64() - if err != nil { - return wrapStrictError(err, isStrict) - } - field.SetFloat(floatVal) - case reflectTime: - timeVal, err := key.Time() - if err != nil { - return wrapStrictError(err, isStrict) - } - field.Set(reflect.ValueOf(timeVal)) - case reflect.Slice: - return setSliceWithProperType(key, field, delim, allowShadow, isStrict) - default: - return fmt.Errorf("unsupported type '%s'", t) - } - return nil -} - -func parseTagOptions(tag string) (rawName string, omitEmpty bool, allowShadow bool) { - opts := strings.SplitN(tag, ",", 3) - rawName = opts[0] - if len(opts) > 1 { - omitEmpty = opts[1] == "omitempty" - } - if len(opts) > 2 { - allowShadow = opts[2] == "allowshadow" - } - return rawName, omitEmpty, allowShadow -} - -func (s *Section) mapTo(val reflect.Value, isStrict bool) error { - if val.Kind() == reflect.Ptr { - val = val.Elem() - } - typ := val.Type() - - for i := 0; i < typ.NumField(); i++ { - field := val.Field(i) - tpField := typ.Field(i) - - tag := tpField.Tag.Get("ini") - if tag == "-" { - continue - } - - rawName, _, allowShadow := parseTagOptions(tag) - fieldName := s.parseFieldName(tpField.Name, rawName) - if len(fieldName) == 0 || !field.CanSet() { - continue - } - - isAnonymous := tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous - isStruct := tpField.Type.Kind() == reflect.Struct - if isAnonymous { - field.Set(reflect.New(tpField.Type.Elem())) - } - - if isAnonymous || isStruct { - if sec, err := s.f.GetSection(fieldName); err == nil { - if err = sec.mapTo(field, isStrict); err != nil { - return fmt.Errorf("error mapping field(%s): %v", fieldName, err) - } - continue - } - } - - if key, err := s.GetKey(fieldName); err == nil { - delim := parseDelim(tpField.Tag.Get("delim")) - if err = setWithProperType(tpField.Type, key, field, delim, allowShadow, isStrict); err != nil { - return fmt.Errorf("error mapping field(%s): %v", fieldName, err) - } - } - } - return nil -} - -// MapTo maps section to given struct. -func (s *Section) MapTo(v interface{}) error { - typ := reflect.TypeOf(v) - val := reflect.ValueOf(v) - if typ.Kind() == reflect.Ptr { - typ = typ.Elem() - val = val.Elem() - } else { - return errors.New("cannot map to non-pointer struct") - } - - return s.mapTo(val, false) -} - -// MapTo maps section to given struct in strict mode, -// which returns all possible error including value parsing error. -func (s *Section) StrictMapTo(v interface{}) error { - typ := reflect.TypeOf(v) - val := reflect.ValueOf(v) - if typ.Kind() == reflect.Ptr { - typ = typ.Elem() - val = val.Elem() - } else { - return errors.New("cannot map to non-pointer struct") - } - - return s.mapTo(val, true) -} - -// MapTo maps file to given struct. -func (f *File) MapTo(v interface{}) error { - return f.Section("").MapTo(v) -} - -// MapTo maps file to given struct in strict mode, -// which returns all possible error including value parsing error. -func (f *File) StrictMapTo(v interface{}) error { - return f.Section("").StrictMapTo(v) -} - -// MapTo maps data sources to given struct with name mapper. -func MapToWithMapper(v interface{}, mapper NameMapper, source interface{}, others ...interface{}) error { - cfg, err := Load(source, others...) - if err != nil { - return err - } - cfg.NameMapper = mapper - return cfg.MapTo(v) -} - -// StrictMapToWithMapper maps data sources to given struct with name mapper in strict mode, -// which returns all possible error including value parsing error. -func StrictMapToWithMapper(v interface{}, mapper NameMapper, source interface{}, others ...interface{}) error { - cfg, err := Load(source, others...) - if err != nil { - return err - } - cfg.NameMapper = mapper - return cfg.StrictMapTo(v) -} - -// MapTo maps data sources to given struct. -func MapTo(v, source interface{}, others ...interface{}) error { - return MapToWithMapper(v, nil, source, others...) -} - -// StrictMapTo maps data sources to given struct in strict mode, -// which returns all possible error including value parsing error. -func StrictMapTo(v, source interface{}, others ...interface{}) error { - return StrictMapToWithMapper(v, nil, source, others...) -} - -// reflectSliceWithProperType does the opposite thing as setSliceWithProperType. -func reflectSliceWithProperType(key *Key, field reflect.Value, delim string) error { - slice := field.Slice(0, field.Len()) - if field.Len() == 0 { - return nil - } - - var buf bytes.Buffer - sliceOf := field.Type().Elem().Kind() - for i := 0; i < field.Len(); i++ { - switch sliceOf { - case reflect.String: - buf.WriteString(slice.Index(i).String()) - case reflect.Int, reflect.Int64: - buf.WriteString(fmt.Sprint(slice.Index(i).Int())) - case reflect.Uint, reflect.Uint64: - buf.WriteString(fmt.Sprint(slice.Index(i).Uint())) - case reflect.Float64: - buf.WriteString(fmt.Sprint(slice.Index(i).Float())) - case reflectTime: - buf.WriteString(slice.Index(i).Interface().(time.Time).Format(time.RFC3339)) - default: - return fmt.Errorf("unsupported type '[]%s'", sliceOf) - } - buf.WriteString(delim) - } - key.SetValue(buf.String()[:buf.Len()-1]) - return nil -} - -// reflectWithProperType does the opposite thing as setWithProperType. -func reflectWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string) error { - switch t.Kind() { - case reflect.String: - key.SetValue(field.String()) - case reflect.Bool: - key.SetValue(fmt.Sprint(field.Bool())) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - key.SetValue(fmt.Sprint(field.Int())) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - key.SetValue(fmt.Sprint(field.Uint())) - case reflect.Float32, reflect.Float64: - key.SetValue(fmt.Sprint(field.Float())) - case reflectTime: - key.SetValue(fmt.Sprint(field.Interface().(time.Time).Format(time.RFC3339))) - case reflect.Slice: - return reflectSliceWithProperType(key, field, delim) - default: - return fmt.Errorf("unsupported type '%s'", t) - } - return nil -} - -// CR: copied from encoding/json/encode.go with modifications of time.Time support. -// TODO: add more test coverage. -func isEmptyValue(v reflect.Value) bool { - switch v.Kind() { - case reflect.Array, reflect.Map, reflect.Slice, reflect.String: - return v.Len() == 0 - case reflect.Bool: - return !v.Bool() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return v.Int() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return v.Uint() == 0 - case reflect.Float32, reflect.Float64: - return v.Float() == 0 - case reflectTime: - return v.Interface().(time.Time).IsZero() - case reflect.Interface, reflect.Ptr: - return v.IsNil() - } - return false -} - -func (s *Section) reflectFrom(val reflect.Value) error { - if val.Kind() == reflect.Ptr { - val = val.Elem() - } - typ := val.Type() - - for i := 0; i < typ.NumField(); i++ { - field := val.Field(i) - tpField := typ.Field(i) - - tag := tpField.Tag.Get("ini") - if tag == "-" { - continue - } - - opts := strings.SplitN(tag, ",", 2) - if len(opts) == 2 && opts[1] == "omitempty" && isEmptyValue(field) { - continue - } - - fieldName := s.parseFieldName(tpField.Name, opts[0]) - if len(fieldName) == 0 || !field.CanSet() { - continue - } - - if (tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous) || - (tpField.Type.Kind() == reflect.Struct && tpField.Type.Name() != "Time") { - // Note: The only error here is section doesn't exist. - sec, err := s.f.GetSection(fieldName) - if err != nil { - // Note: fieldName can never be empty here, ignore error. - sec, _ = s.f.NewSection(fieldName) - } - if err = sec.reflectFrom(field); err != nil { - return fmt.Errorf("error reflecting field (%s): %v", fieldName, err) - } - continue - } - - // Note: Same reason as secion. - key, err := s.GetKey(fieldName) - if err != nil { - key, _ = s.NewKey(fieldName, "") - } - if err = reflectWithProperType(tpField.Type, key, field, parseDelim(tpField.Tag.Get("delim"))); err != nil { - return fmt.Errorf("error reflecting field (%s): %v", fieldName, err) - } - - } - return nil -} - -// ReflectFrom reflects secion from given struct. -func (s *Section) ReflectFrom(v interface{}) error { - typ := reflect.TypeOf(v) - val := reflect.ValueOf(v) - if typ.Kind() == reflect.Ptr { - typ = typ.Elem() - val = val.Elem() - } else { - return errors.New("cannot reflect from non-pointer struct") - } - - return s.reflectFrom(val) -} - -// ReflectFrom reflects file from given struct. -func (f *File) ReflectFrom(v interface{}) error { - return f.Section("").ReflectFrom(v) -} - -// ReflectFrom reflects data sources from given struct with name mapper. -func ReflectFromWithMapper(cfg *File, v interface{}, mapper NameMapper) error { - cfg.NameMapper = mapper - return cfg.ReflectFrom(v) -} - -// ReflectFrom reflects data sources from given struct. -func ReflectFrom(cfg *File, v interface{}) error { - return ReflectFromWithMapper(cfg, v, nil) -} diff --git a/vendor/vendor.json b/vendor/vendor.json deleted file mode 100644 index ada01b2..0000000 --- a/vendor/vendor.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "comment": "", - "ignore": "test", - "package": [ - { - "checksumSHA1": "DB+/DpKJUO7dR+MyQVchJ+yyHoI=", - "path": "github.com/go-gomail/gomail", - "revision": "81ebce5c23dfd25c6c67194b37d3dd3f338c98b1", - "revisionTime": "2016-04-11T21:29:32Z" - }, - { - "checksumSHA1": "6IzzHO9p32aHJhMYMwijccDUIVA=", - "path": "gopkg.in/alexcesaro/quotedprintable.v3", - "revision": "2caba252f4dc53eaf6b553000885530023f54623", - "revisionTime": "2015-07-16T17:19:45Z" - }, - { - "checksumSHA1": "WNjUTH01NMhPWjpwnsaL3SW52Gw=", - "path": "gopkg.in/ini.v1", - "revision": "d3de07a94d22b4a0972deb4b96d790c2c0ce8333", - "revisionTime": "2017-06-02T20:46:24Z" - } - ], - "rootPath": "github.com/ouqiang/supervisor-event-listener" -}