From 91f456530ca3893f2c35c6731496d887c1d407a3 Mon Sep 17 00:00:00 2001 From: ouqiang Date: Sun, 4 Jun 2017 17:35:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=82=AE=E4=BB=B6=E3=80=81Sl?= =?UTF-8?q?ack=E3=80=81WebHook=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 80 +- build.sh | 96 +++ config/config.go | 142 ++++ event/event.go | 107 +++ listener/listener.go | 98 +++ listener/notify/mail.go | 34 + listener/notify/notify.go | 66 ++ listener/notify/slack.go | 35 + listener/notify/webhook.go | 26 + supervisor-event-listener.go | 11 + supervisor-event-listener.ini | 19 + upload_package_to_qiniu.sh | 36 + utils/httpclient/http_client.go | 83 ++ utils/utils.go | 58 ++ .../github.com/go-gomail/gomail/CHANGELOG.md | 20 + .../go-gomail/gomail/CONTRIBUTING.md | 20 + vendor/github.com/go-gomail/gomail/LICENSE | 20 + vendor/github.com/go-gomail/gomail/README.md | 92 +++ vendor/github.com/go-gomail/gomail/auth.go | 49 ++ vendor/github.com/go-gomail/gomail/doc.go | 5 + vendor/github.com/go-gomail/gomail/message.go | 322 ++++++++ vendor/github.com/go-gomail/gomail/mime.go | 21 + .../github.com/go-gomail/gomail/mime_go14.go | 25 + vendor/github.com/go-gomail/gomail/send.go | 116 +++ vendor/github.com/go-gomail/gomail/smtp.go | 202 +++++ vendor/github.com/go-gomail/gomail/writeto.go | 306 +++++++ .../alexcesaro/quotedprintable.v3/LICENSE | 20 + .../alexcesaro/quotedprintable.v3/README.md | 16 + .../quotedprintable.v3/encodedword.go | 279 +++++++ .../alexcesaro/quotedprintable.v3/pool.go | 26 + .../quotedprintable.v3/pool_go12.go | 24 + .../alexcesaro/quotedprintable.v3/reader.go | 121 +++ .../alexcesaro/quotedprintable.v3/writer.go | 166 ++++ vendor/gopkg.in/ini.v1/LICENSE | 191 +++++ vendor/gopkg.in/ini.v1/Makefile | 12 + vendor/gopkg.in/ini.v1/README.md | 746 ++++++++++++++++++ vendor/gopkg.in/ini.v1/README_ZH.md | 733 +++++++++++++++++ vendor/gopkg.in/ini.v1/error.go | 32 + vendor/gopkg.in/ini.v1/ini.go | 561 +++++++++++++ vendor/gopkg.in/ini.v1/key.go | 699 ++++++++++++++++ vendor/gopkg.in/ini.v1/parser.go | 361 +++++++++ vendor/gopkg.in/ini.v1/section.go | 248 ++++++ vendor/gopkg.in/ini.v1/struct.go | 499 ++++++++++++ vendor/vendor.json | 25 + 45 files changed, 6848 insertions(+), 1 deletion(-) create mode 100644 build.sh create mode 100644 config/config.go create mode 100644 event/event.go create mode 100644 listener/listener.go create mode 100644 listener/notify/mail.go create mode 100644 listener/notify/notify.go create mode 100644 listener/notify/slack.go create mode 100644 listener/notify/webhook.go create mode 100644 supervisor-event-listener.go create mode 100644 supervisor-event-listener.ini create mode 100644 upload_package_to_qiniu.sh create mode 100644 utils/httpclient/http_client.go create mode 100644 utils/utils.go create mode 100644 vendor/github.com/go-gomail/gomail/CHANGELOG.md create mode 100644 vendor/github.com/go-gomail/gomail/CONTRIBUTING.md create mode 100644 vendor/github.com/go-gomail/gomail/LICENSE create mode 100644 vendor/github.com/go-gomail/gomail/README.md create mode 100644 vendor/github.com/go-gomail/gomail/auth.go create mode 100644 vendor/github.com/go-gomail/gomail/doc.go create mode 100644 vendor/github.com/go-gomail/gomail/message.go create mode 100644 vendor/github.com/go-gomail/gomail/mime.go create mode 100644 vendor/github.com/go-gomail/gomail/mime_go14.go create mode 100644 vendor/github.com/go-gomail/gomail/send.go create mode 100644 vendor/github.com/go-gomail/gomail/smtp.go create mode 100644 vendor/github.com/go-gomail/gomail/writeto.go create mode 100644 vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE create mode 100644 vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md create mode 100644 vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go create mode 100644 vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go create mode 100644 vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go create mode 100644 vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go create mode 100644 vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go create mode 100644 vendor/gopkg.in/ini.v1/LICENSE create mode 100644 vendor/gopkg.in/ini.v1/Makefile create mode 100644 vendor/gopkg.in/ini.v1/README.md create mode 100644 vendor/gopkg.in/ini.v1/README_ZH.md create mode 100644 vendor/gopkg.in/ini.v1/error.go create mode 100644 vendor/gopkg.in/ini.v1/ini.go create mode 100644 vendor/gopkg.in/ini.v1/key.go create mode 100644 vendor/gopkg.in/ini.v1/parser.go create mode 100644 vendor/gopkg.in/ini.v1/section.go create mode 100644 vendor/gopkg.in/ini.v1/struct.go create mode 100644 vendor/vendor.json diff --git a/.gitignore b/.gitignore index a1338d6..adac6a6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ +.idear diff --git a/README.md b/README.md index 2cf55a3..dc35562 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,80 @@ -# supervisor-listener +# supervisor-event-listener Supervisor事件通知, 支持邮件, Slack, WebHook + +## 简介 +Supervisor是*nix环境下的进程管理工具, 可以把前台进程转换为守护进程, 当进程异常退出时自动重启. +supervisor-event-listener监听进程异常退出事件, 并发送通知. + +## 下载 +* [Linux-64位](http://opns468ov.bkt.clouddn.com/supervisor/supervisor-event-listener-linux-amd64.tar.gz) +* [Mac OS-64位](http://opns468ov.bkt.clouddn.com/supervisor/supervisor-event-listener-darwin-amd64.tar.gz) + +### 源码安装 +* `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 + } +} +``` \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..8062b5c --- /dev/null +++ b/build.sh @@ -0,0 +1,96 @@ +#!/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/config/config.go b/config/config.go new file mode 100644 index 0000000..41eb201 --- /dev/null +++ b/config/config.go @@ -0,0 +1,142 @@ +package config + +import ( + "flag" + "strings" + "fmt" + "os" + "gopkg.in/ini.v1" + "github.com/ouqiang/supervisor-event-listener/utils" +) + +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) +} \ No newline at end of file diff --git a/event/event.go b/event/event.go new file mode 100644 index 0000000..191ca04 --- /dev/null +++ b/event/event.go @@ -0,0 +1,107 @@ +package event + +import ( + "strings" + "errors" + "strconv" + "github.com/ouqiang/supervisor-event-listener/utils" + "fmt" + "os" +) + +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) + +} + +// Supervisord触发事件时会先发送Header,根据Header中len字段去读取Payload +type Header struct { + Ver string + Server string + Serial int + Pool string + PoolSerial int + EventName string // 事件名称 + Len int // Payload长度 +} + +type Payload struct { + Ip string + ProcessName string // 进程名称 + GroupName string // 进程组名称 + FromState string + Expected int + Pid int +} + +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 +} \ No newline at end of file diff --git a/listener/listener.go b/listener/listener.go new file mode 100644 index 0000000..13eba04 --- /dev/null +++ b/listener/listener.go @@ -0,0 +1,98 @@ +package listener + +import ( + "log" + "os" + "bufio" + "fmt" + "github.com/ouqiang/supervisor-event-listener/event" + "errors" + "github.com/ouqiang/supervisor-event-listener/listener/notify" +) + +var ( + ErrPayloadLength = errors.New("Header中len长度与实际读取长度不一致") +) + +func Start() { + defer func() { + if err := recover(); err != nil { + log.Print("panic", err) + } + } () + listen() +} + +// 监听事件, 从标准输入获取事件内容 +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) + if err != nil { + failure(err) + continue + } + // 只处理进程异常退出事件 + if header.EventName == "PROCESS_STATE_EXITED" { + notify.Push(header, payload) + } + success() + } +} + +// 读取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") +} + +func success() { + fmt.Fprint(os.Stdout, "RESULT 2\nOK") +} + +func failure(err error) { + fmt.Fprintln(os.Stderr, err) + fmt.Fprint(os.Stdout, "Result 2\nFAIL") +} + + diff --git a/listener/notify/mail.go b/listener/notify/mail.go new file mode 100644 index 0000000..c8b7506 --- /dev/null +++ b/listener/notify/mail.go @@ -0,0 +1,34 @@ +package notify + +import ( + "github.com/ouqiang/supervisor-event-listener/event" + "github.com/go-gomail/gomail" + "strings" + "fmt" + "errors" +) + +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) +} \ No newline at end of file diff --git a/listener/notify/notify.go b/listener/notify/notify.go new file mode 100644 index 0000000..dfe5638 --- /dev/null +++ b/listener/notify/notify.go @@ -0,0 +1,66 @@ +package notify + +import ( + "github.com/ouqiang/supervisor-event-listener/event" + "github.com/ouqiang/supervisor-event-listener/config" + + "time" + "fmt" + "os" +) + + +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++ + } +} \ No newline at end of file diff --git a/listener/notify/slack.go b/listener/notify/slack.go new file mode 100644 index 0000000..45da68f --- /dev/null +++ b/listener/notify/slack.go @@ -0,0 +1,35 @@ +package notify + +import ( + "github.com/ouqiang/supervisor-event-listener/event" + "github.com/ouqiang/supervisor-event-listener/utils" + "fmt" + "github.com/ouqiang/gocron/modules/httpclient" + "errors" +) + +type Slack struct {} + +func (slack *Slack) Send(message event.Message) error { + body := slack.format(message.String(), Conf.Slack.Channel) + timeout := 120 + response := httpclient.PostJson(Conf.Slack.WebHookUrl, body, timeout) + if response.StatusCode == 200 { + return nil + } + + errorMessage := fmt.Sprintf("发送Slack消息失败#HTTP状态码-%d#HTTP-Body-%s", + response.StatusCode, response.Body) + + return errors.New(errorMessage) +} + +// 格式化消息内容 +func (slack *Slack) format(content string, channel string) string { + content = utils.EscapeJson(content) + specialChars := []string{"&", "<", ">"} + replaceChars := []string{"&", "<", ">"} + content = utils.ReplaceStrings(content, specialChars, replaceChars) + + return fmt.Sprintf(`{"text":"%s","username":"Supervisor事件通知", "channel":"%s"}`, content, channel) +} \ No newline at end of file diff --git a/listener/notify/webhook.go b/listener/notify/webhook.go new file mode 100644 index 0000000..08f36e4 --- /dev/null +++ b/listener/notify/webhook.go @@ -0,0 +1,26 @@ +package notify + +import ( + "github.com/ouqiang/supervisor-event-listener/event" + "encoding/json" + "github.com/ouqiang/supervisor-event-listener/utils/httpclient" + "errors" + "fmt" +) + +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) +} \ No newline at end of file diff --git a/supervisor-event-listener.go b/supervisor-event-listener.go new file mode 100644 index 0000000..1f16b43 --- /dev/null +++ b/supervisor-event-listener.go @@ -0,0 +1,11 @@ +package main + +import ( + "github.com/ouqiang/supervisor-event-listener/listener" +) + +func main() { + for { + listener.Start() + } +} \ No newline at end of file diff --git a/supervisor-event-listener.ini b/supervisor-event-listener.ini new file mode 100644 index 0000000..dbe00ed --- /dev/null +++ b/supervisor-event-listener.ini @@ -0,0 +1,19 @@ +[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/upload_package_to_qiniu.sh b/upload_package_to_qiniu.sh new file mode 100644 index 0000000..da71b9d --- /dev/null +++ b/upload_package_to_qiniu.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# set -x -u +# 上传二进制包到七牛 + +if [[ -z $QINIU_ACCESS_KEY || -z $QINIU_SECRET_KEY || -z $QINIU_URL ]];then + echo 'QINIU_ACCESS_KEY | QINIU_SECRET_KEY | QINIU_URL is need' + exit 1 +fi + +# 打包 +for i in linux darwin +do + ./build.sh -p $i + if [[ $? != 0 ]];then + break + fi +done + +# 身份认证 +qrsctl login $QINIU_ACCESS_KEY $QINIU_SECRET_KEY + +# 上传 +for i in `ls supervisor*.gz` +do + # 上传文件 qrsctl put bucket key srcFile + KEY=supervisor/$i + qrsctl put github $KEY $i + if [[ $? != 0 ]];then + break + fi + echo "刷新七牛CDN-" $QINIU_URL/$KEY + qrsctl cdn/refresh $QINIU_URL/$KEY + rm $i +done + +echo '打包并上传成功' \ No newline at end of file diff --git a/utils/httpclient/http_client.go b/utils/httpclient/http_client.go new file mode 100644 index 0000000..36d5c19 --- /dev/null +++ b/utils/httpclient/http_client.go @@ -0,0 +1,83 @@ +package httpclient + +// http-client + +import ( + "io/ioutil" + "net/http" + "time" + "fmt" + "bytes" +) + +type ResponseWrapper struct { + StatusCode int + Body string + Header http.Header +} + +func Get(url string, timeout int) ResponseWrapper { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return createRequestError(err) + } + + return request(req, timeout) +} + +func PostParams(url string,params string, timeout int) ResponseWrapper { + buf := bytes.NewBufferString(params) + req, err := http.NewRequest("POST", url, buf) + if err != nil { + return createRequestError(err) + } + req.Header.Set("Content-type", "application/x-www-form-urlencoded") + + return request(req, timeout) +} + +func PostJson(url string, body string, timeout int) ResponseWrapper { + buf := bytes.NewBufferString(body) + req, err := http.NewRequest("POST", url, buf) + if err != nil { + return createRequestError(err) + } + req.Header.Set("Content-type", "application/json") + + return request(req, timeout) +} + +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 + } + setRequestHeader(req) + resp, err := client.Do(req) + if err != nil { + wrapper.Body = fmt.Sprintf("执行HTTP请求错误-%s", err.Error()) + 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 + } + wrapper.StatusCode = resp.StatusCode + wrapper.Body = string(body) + wrapper.Header = resp.Header + + return wrapper +} + +func setRequestHeader(req *http.Request) { + req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8,en;q=0.6") + 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 { + errorMessage := fmt.Sprintf("创建HTTP请求错误-%s", err.Error()) + return ResponseWrapper{0, errorMessage, make(http.Header)} +} \ No newline at end of file diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..e7b346e --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,58 @@ +package utils + +import ( + "strings" + "net" +) + +// 批量替换字符串 +func ReplaceStrings(s string, old []string, replace []string) string { + if s == "" { + return s + } + if len(old) != len(replace) { + return s + } + + for i, v := range old { + s = strings.Replace(s, v, replace[i], 1000) + } + + return s +} + +func InStringSlice(slice []string, element string) bool { + element = strings.TrimSpace(element) + for _, v := range slice { + if strings.TrimSpace(v) == element{ + return true + } + } + + return false +} + +// 转义json特殊字符 +func EscapeJson(s string) string { + specialChars := []string{"\\", "\b","\f", "\n", "\r", "\t", "\"",} + replaceChars := []string{ "\\\\", "\\b", "\\f", "\\n", "\\r", "\\t", "\\\"",} + + return ReplaceStrings(s, specialChars, replaceChars) +} + +func GetLocalIp() string { + addrs, err := net.InterfaceAddrs() + if err != nil { + return "" + } + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + return ipnet.IP.String() + } + + } + } + + return "" +} \ No newline at end of file diff --git a/vendor/github.com/go-gomail/gomail/CHANGELOG.md b/vendor/github.com/go-gomail/gomail/CHANGELOG.md new file mode 100644 index 0000000..a797ab4 --- /dev/null +++ b/vendor/github.com/go-gomail/gomail/CHANGELOG.md @@ -0,0 +1,20 @@ +# 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 new file mode 100644 index 0000000..d5601c2 --- /dev/null +++ b/vendor/github.com/go-gomail/gomail/CONTRIBUTING.md @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000..5f5c12a --- /dev/null +++ b/vendor/github.com/go-gomail/gomail/LICENSE @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000..b3be9e1 --- /dev/null +++ b/vendor/github.com/go-gomail/gomail/README.md @@ -0,0 +1,92 @@ +# 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 new file mode 100644 index 0000000..d28b83a --- /dev/null +++ b/vendor/github.com/go-gomail/gomail/auth.go @@ -0,0 +1,49 @@ +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 new file mode 100644 index 0000000..a8f5091 --- /dev/null +++ b/vendor/github.com/go-gomail/gomail/doc.go @@ -0,0 +1,5 @@ +// 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 new file mode 100644 index 0000000..4bffb1e --- /dev/null +++ b/vendor/github.com/go-gomail/gomail/message.go @@ -0,0 +1,322 @@ +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 new file mode 100644 index 0000000..194d4a7 --- /dev/null +++ b/vendor/github.com/go-gomail/gomail/mime.go @@ -0,0 +1,21 @@ +// +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 new file mode 100644 index 0000000..3dc26aa --- /dev/null +++ b/vendor/github.com/go-gomail/gomail/mime_go14.go @@ -0,0 +1,25 @@ +// +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 new file mode 100644 index 0000000..9115ebe --- /dev/null +++ b/vendor/github.com/go-gomail/gomail/send.go @@ -0,0 +1,116 @@ +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 new file mode 100644 index 0000000..2aa49c8 --- /dev/null +++ b/vendor/github.com/go-gomail/gomail/smtp.go @@ -0,0 +1,202 @@ +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 new file mode 100644 index 0000000..9fb6b86 --- /dev/null +++ b/vendor/github.com/go-gomail/gomail/writeto.go @@ -0,0 +1,306 @@ +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 new file mode 100644 index 0000000..5f5c12a --- /dev/null +++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000..98ddf82 --- /dev/null +++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md @@ -0,0 +1,16 @@ +# 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 new file mode 100644 index 0000000..cfd0261 --- /dev/null +++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go @@ -0,0 +1,279 @@ +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 new file mode 100644 index 0000000..24283c5 --- /dev/null +++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go @@ -0,0 +1,26 @@ +// +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 new file mode 100644 index 0000000..d335b4a --- /dev/null +++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go @@ -0,0 +1,24 @@ +// +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 new file mode 100644 index 0000000..955edca --- /dev/null +++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go @@ -0,0 +1,121 @@ +// 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 new file mode 100644 index 0000000..43359d5 --- /dev/null +++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go @@ -0,0 +1,166 @@ +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 new file mode 100644 index 0000000..37ec93a --- /dev/null +++ b/vendor/gopkg.in/ini.v1/LICENSE @@ -0,0 +1,191 @@ +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 new file mode 100644 index 0000000..ac034e5 --- /dev/null +++ b/vendor/gopkg.in/ini.v1/Makefile @@ -0,0 +1,12 @@ +.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 new file mode 100644 index 0000000..e67d51f --- /dev/null +++ b/vendor/gopkg.in/ini.v1/README.md @@ -0,0 +1,746 @@ +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 new file mode 100644 index 0000000..0cf4194 --- /dev/null +++ b/vendor/gopkg.in/ini.v1/README_ZH.md @@ -0,0 +1,733 @@ +本包提供了 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 new file mode 100644 index 0000000..80afe74 --- /dev/null +++ b/vendor/gopkg.in/ini.v1/error.go @@ -0,0 +1,32 @@ +// 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 new file mode 100644 index 0000000..0bd6c50 --- /dev/null +++ b/vendor/gopkg.in/ini.v1/ini.go @@ -0,0 +1,561 @@ +// 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 new file mode 100644 index 0000000..838356a --- /dev/null +++ b/vendor/gopkg.in/ini.v1/key.go @@ -0,0 +1,699 @@ +// 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 new file mode 100644 index 0000000..69d5476 --- /dev/null +++ b/vendor/gopkg.in/ini.v1/parser.go @@ -0,0 +1,361 @@ +// 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 new file mode 100644 index 0000000..94f7375 --- /dev/null +++ b/vendor/gopkg.in/ini.v1/section.go @@ -0,0 +1,248 @@ +// 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 new file mode 100644 index 0000000..d522e00 --- /dev/null +++ b/vendor/gopkg.in/ini.v1/struct.go @@ -0,0 +1,499 @@ +// 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 new file mode 100644 index 0000000..ada01b2 --- /dev/null +++ b/vendor/vendor.json @@ -0,0 +1,25 @@ +{ + "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" +}