├── supervisor-event-listener.go ├── vendor ├── github.com │ └── go-gomail │ │ └── gomail │ │ ├── doc.go │ │ ├── mime.go │ │ ├── mime_go14.go │ │ ├── CONTRIBUTING.md │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── auth.go │ │ ├── README.md │ │ ├── send.go │ │ ├── smtp.go │ │ ├── writeto.go │ │ └── message.go ├── gopkg.in │ ├── ini.v1 │ │ ├── Makefile │ │ ├── error.go │ │ ├── section.go │ │ ├── parser.go │ │ ├── LICENSE │ │ ├── struct.go │ │ ├── ini.go │ │ ├── README_ZH.md │ │ ├── README.md │ │ └── key.go │ └── alexcesaro │ │ └── quotedprintable.v3 │ │ ├── pool_go12.go │ │ ├── pool.go │ │ ├── README.md │ │ ├── LICENSE │ │ ├── reader.go │ │ ├── writer.go │ │ └── encodedword.go └── vendor.json ├── .gitignore ├── supervisor-event-listener.ini ├── listener ├── notify │ ├── webhook.go │ ├── mail.go │ ├── slack.go │ └── notify.go └── listener.go ├── upload_package_to_qiniu.sh ├── LICENSE ├── utils ├── utils.go └── httpclient │ └── http_client.go ├── README.md ├── event └── event.go └── config └── config.go /supervisor-event-listener.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/ouqiang/supervisor-event-listener/listener" 5 | ) 6 | 7 | func main() { 8 | for { 9 | listener.Start() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /vendor/github.com/go-gomail/gomail/doc.go: -------------------------------------------------------------------------------- 1 | // Package gomail provides a simple interface to compose emails and to mail them 2 | // efficiently. 3 | // 4 | // More info on Github: https://github.com/go-gomail/gomail 5 | package gomail 6 | -------------------------------------------------------------------------------- /vendor/gopkg.in/ini.v1/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build test bench vet 2 | 3 | build: vet bench 4 | 5 | test: 6 | go test -v -cover -race 7 | 8 | bench: 9 | go test -v -cover -race -test.bench=. -test.benchmem 10 | 11 | vet: 12 | go vet 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | .idear 16 | -------------------------------------------------------------------------------- /vendor/github.com/go-gomail/gomail/mime.go: -------------------------------------------------------------------------------- 1 | // +build go1.5 2 | 3 | package gomail 4 | 5 | import ( 6 | "mime" 7 | "mime/quotedprintable" 8 | "strings" 9 | ) 10 | 11 | var newQPWriter = quotedprintable.NewWriter 12 | 13 | type mimeEncoder struct { 14 | mime.WordEncoder 15 | } 16 | 17 | var ( 18 | bEncoding = mimeEncoder{mime.BEncoding} 19 | qEncoding = mimeEncoder{mime.QEncoding} 20 | lastIndexByte = strings.LastIndexByte 21 | ) 22 | -------------------------------------------------------------------------------- /vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go: -------------------------------------------------------------------------------- 1 | // +build !go1.3 2 | 3 | package quotedprintable 4 | 5 | import "bytes" 6 | 7 | var ch = make(chan *bytes.Buffer, 32) 8 | 9 | func getBuffer() *bytes.Buffer { 10 | select { 11 | case buf := <-ch: 12 | return buf 13 | default: 14 | } 15 | return new(bytes.Buffer) 16 | } 17 | 18 | func putBuffer(buf *bytes.Buffer) { 19 | buf.Reset() 20 | select { 21 | case ch <- buf: 22 | default: 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /supervisor-event-listener.ini: -------------------------------------------------------------------------------- 1 | [default] 2 | # 通知类型 mail,slack,webhook 只能选择一种 3 | notify_type = mail 4 | 5 | # 邮件服务器配置 6 | mail.server.user = test@163.com 7 | mail.server.password = 123456 8 | mail.server.host = smtp.163.com 9 | mail.server.port = 25 10 | 11 | # 邮件收件人配置, 多个收件人, 逗号分隔 12 | mail.user = hello@163.com 13 | 14 | # Slack配置 15 | slack.webhook_url = https://hooks.slack.com/services/xxxx/xxx/xxxx 16 | slack.channel = exception 17 | 18 | # WebHook通知URL配置 19 | webhook_url = http://my.webhook.com -------------------------------------------------------------------------------- /vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go: -------------------------------------------------------------------------------- 1 | // +build go1.3 2 | 3 | package quotedprintable 4 | 5 | import ( 6 | "bytes" 7 | "sync" 8 | ) 9 | 10 | var bufPool = sync.Pool{ 11 | New: func() interface{} { 12 | return new(bytes.Buffer) 13 | }, 14 | } 15 | 16 | func getBuffer() *bytes.Buffer { 17 | return bufPool.Get().(*bytes.Buffer) 18 | } 19 | 20 | func putBuffer(buf *bytes.Buffer) { 21 | if buf.Len() > 1024 { 22 | return 23 | } 24 | buf.Reset() 25 | bufPool.Put(buf) 26 | } 27 | -------------------------------------------------------------------------------- /vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md: -------------------------------------------------------------------------------- 1 | # quotedprintable 2 | 3 | ## Introduction 4 | 5 | Package quotedprintable implements quoted-printable and message header encoding 6 | as specified by RFC 2045 and RFC 2047. 7 | 8 | It is a copy of the Go 1.5 package `mime/quotedprintable`. It also includes 9 | the new functions of package `mime` concerning RFC 2047. 10 | 11 | This code has minor changes with the standard library code in order to work 12 | with Go 1.0 and newer. 13 | 14 | ## Documentation 15 | 16 | https://godoc.org/gopkg.in/alexcesaro/quotedprintable.v3 17 | -------------------------------------------------------------------------------- /vendor/github.com/go-gomail/gomail/mime_go14.go: -------------------------------------------------------------------------------- 1 | // +build !go1.5 2 | 3 | package gomail 4 | 5 | import "gopkg.in/alexcesaro/quotedprintable.v3" 6 | 7 | var newQPWriter = quotedprintable.NewWriter 8 | 9 | type mimeEncoder struct { 10 | quotedprintable.WordEncoder 11 | } 12 | 13 | var ( 14 | bEncoding = mimeEncoder{quotedprintable.BEncoding} 15 | qEncoding = mimeEncoder{quotedprintable.QEncoding} 16 | lastIndexByte = func(s string, c byte) int { 17 | for i := len(s) - 1; i >= 0; i-- { 18 | 19 | if s[i] == c { 20 | return i 21 | } 22 | } 23 | return -1 24 | } 25 | ) 26 | -------------------------------------------------------------------------------- /vendor/github.com/go-gomail/gomail/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thank you for contributing to Gomail! Here are a few guidelines: 2 | 3 | ## Bugs 4 | 5 | If you think you found a bug, create an issue and supply the minimum amount 6 | of code triggering the bug so it can be reproduced. 7 | 8 | 9 | ## Fixing a bug 10 | 11 | If you want to fix a bug, you can send a pull request. It should contains a 12 | new test or update an existing one to cover that bug. 13 | 14 | 15 | ## New feature proposal 16 | 17 | If you think Gomail lacks a feature, you can open an issue or send a pull 18 | request. I want to keep Gomail code and API as simple as possible so please 19 | describe your needs so we can discuss whether this feature should be added to 20 | Gomail or not. 21 | -------------------------------------------------------------------------------- /listener/notify/webhook.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/ouqiang/supervisor-event-listener/event" 8 | "github.com/ouqiang/supervisor-event-listener/utils/httpclient" 9 | ) 10 | 11 | type WebHook struct{} 12 | 13 | func (hook *WebHook) Send(message event.Message) error { 14 | encodeMessage, err := json.Marshal(message) 15 | if err != nil { 16 | return err 17 | } 18 | timeout := 60 19 | response := httpclient.PostJson(Conf.WebHook.Url, string(encodeMessage), timeout) 20 | 21 | if response.StatusCode == 200 { 22 | return nil 23 | } 24 | errorMessage := fmt.Sprintf("webhook执行失败#HTTP状态码-%d#HTTP-BODY-%s", response.StatusCode, response.Body) 25 | return errors.New(errorMessage) 26 | } 27 | -------------------------------------------------------------------------------- /upload_package_to_qiniu.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # set -x -u 3 | # 上传二进制包到七牛 4 | 5 | if [[ -z $QINIU_ACCESS_KEY || -z $QINIU_SECRET_KEY || -z $QINIU_URL ]];then 6 | echo 'QINIU_ACCESS_KEY | QINIU_SECRET_KEY | QINIU_URL is need' 7 | exit 1 8 | fi 9 | 10 | # 打包 11 | for i in linux darwin 12 | do 13 | ./build.sh -p $i 14 | if [[ $? != 0 ]];then 15 | break 16 | fi 17 | done 18 | 19 | # 身份认证 20 | qrsctl login $QINIU_ACCESS_KEY $QINIU_SECRET_KEY 21 | 22 | # 上传 23 | for i in `ls supervisor*.gz` 24 | do 25 | # 上传文件 qrsctl put bucket key srcFile 26 | KEY=supervisor/$i 27 | qrsctl put github $KEY $i 28 | if [[ $? != 0 ]];then 29 | break 30 | fi 31 | echo "刷新七牛CDN-" $QINIU_URL/$KEY 32 | qrsctl cdn/refresh $QINIU_URL/$KEY 33 | rm $i 34 | done 35 | 36 | echo '打包并上传成功' -------------------------------------------------------------------------------- /vendor/vendor.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "", 3 | "ignore": "test", 4 | "package": [ 5 | { 6 | "checksumSHA1": "DB+/DpKJUO7dR+MyQVchJ+yyHoI=", 7 | "path": "github.com/go-gomail/gomail", 8 | "revision": "81ebce5c23dfd25c6c67194b37d3dd3f338c98b1", 9 | "revisionTime": "2016-04-11T21:29:32Z" 10 | }, 11 | { 12 | "checksumSHA1": "6IzzHO9p32aHJhMYMwijccDUIVA=", 13 | "path": "gopkg.in/alexcesaro/quotedprintable.v3", 14 | "revision": "2caba252f4dc53eaf6b553000885530023f54623", 15 | "revisionTime": "2015-07-16T17:19:45Z" 16 | }, 17 | { 18 | "checksumSHA1": "WNjUTH01NMhPWjpwnsaL3SW52Gw=", 19 | "path": "gopkg.in/ini.v1", 20 | "revision": "d3de07a94d22b4a0972deb4b96d790c2c0ce8333", 21 | "revisionTime": "2017-06-02T20:46:24Z" 22 | } 23 | ], 24 | "rootPath": "github.com/ouqiang/supervisor-event-listener" 25 | } 26 | -------------------------------------------------------------------------------- /listener/notify/mail.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/go-gomail/gomail" 7 | "github.com/ouqiang/supervisor-event-listener/event" 8 | "strings" 9 | ) 10 | 11 | type Mail struct{} 12 | 13 | func (mail *Mail) Send(message event.Message) error { 14 | body := message.String() 15 | body = strings.Replace(body, "\n", "
", -1) 16 | gomailMessage := gomail.NewMessage() 17 | gomailMessage.SetHeader("From", Conf.MailServer.User) 18 | gomailMessage.SetHeader("To", Conf.MailUser.Email...) 19 | gomailMessage.SetHeader("Subject", "Supervisor事件通知") 20 | gomailMessage.SetBody("text/html", body) 21 | mailer := gomail.NewPlainDialer( 22 | Conf.MailServer.Host, 23 | Conf.MailServer.Port, 24 | Conf.MailServer.User, 25 | Conf.MailServer.Password, 26 | ) 27 | err := mailer.DialAndSend(gomailMessage) 28 | if err == nil { 29 | return nil 30 | } 31 | errorMessage := fmt.Sprintf("邮件发送失败#%s", err.Error()) 32 | 33 | return errors.New(errorMessage) 34 | } 35 | -------------------------------------------------------------------------------- /vendor/gopkg.in/ini.v1/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package ini 16 | 17 | import ( 18 | "fmt" 19 | ) 20 | 21 | type ErrDelimiterNotFound struct { 22 | Line string 23 | } 24 | 25 | func IsErrDelimiterNotFound(err error) bool { 26 | _, ok := err.(ErrDelimiterNotFound) 27 | return ok 28 | } 29 | 30 | func (err ErrDelimiterNotFound) Error() string { 31 | return fmt.Sprintf("key-value delimiter not found: %s", err.Line) 32 | } 33 | -------------------------------------------------------------------------------- /vendor/github.com/go-gomail/gomail/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [2.0.0] - 2015-09-02 6 | 7 | - Mailer has been removed. It has been replaced by Dialer and Sender. 8 | - `File` type and the `CreateFile` and `OpenFile` functions have been removed. 9 | - `Message.Attach` and `Message.Embed` have a new signature. 10 | - `Message.GetBodyWriter` has been removed. Use `Message.AddAlternativeWriter` 11 | instead. 12 | - `Message.Export` has been removed. `Message.WriteTo` can be used instead. 13 | - `Message.DelHeader` has been removed. 14 | - The `Bcc` header field is no longer sent. It is far more simpler and 15 | efficient: the same message is sent to all recipients instead of sending a 16 | different email to each Bcc address. 17 | - LoginAuth has been removed. `NewPlainDialer` now implements the LOGIN 18 | authentication mechanism when needed. 19 | - Go 1.2 is now required instead of Go 1.3. No external dependency are used when 20 | using Go 1.5. 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 qiang.ou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /listener/notify/slack.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/ouqiang/supervisor-event-listener/utils/httpclient" 7 | "github.com/ouqiang/supervisor-event-listener/event" 8 | "github.com/ouqiang/supervisor-event-listener/utils" 9 | ) 10 | 11 | type Slack struct{} 12 | 13 | func (slack *Slack) Send(message event.Message) error { 14 | body := slack.format(message.String(), Conf.Slack.Channel) 15 | timeout := 120 16 | response := httpclient.PostJson(Conf.Slack.WebHookUrl, body, timeout) 17 | if response.StatusCode == 200 { 18 | return nil 19 | } 20 | 21 | errorMessage := fmt.Sprintf("发送Slack消息失败#HTTP状态码-%d#HTTP-Body-%s", 22 | response.StatusCode, response.Body) 23 | 24 | return errors.New(errorMessage) 25 | } 26 | 27 | // 格式化消息内容 28 | func (slack *Slack) format(content string, channel string) string { 29 | content = utils.EscapeJson(content) 30 | specialChars := []string{"&", "<", ">"} 31 | replaceChars := []string{"&", "<", ">"} 32 | content = utils.ReplaceStrings(content, specialChars, replaceChars) 33 | 34 | return fmt.Sprintf(`{"text":"%s","username":"Supervisor事件通知", "channel":"%s"}`, content, channel) 35 | } 36 | -------------------------------------------------------------------------------- /vendor/github.com/go-gomail/gomail/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Alexandre Cesaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Alexandre Cesaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | ) 7 | 8 | // 批量替换字符串 9 | func ReplaceStrings(s string, old []string, replace []string) string { 10 | if s == "" { 11 | return s 12 | } 13 | if len(old) != len(replace) { 14 | return s 15 | } 16 | 17 | for i, v := range old { 18 | s = strings.Replace(s, v, replace[i], 1000) 19 | } 20 | 21 | return s 22 | } 23 | 24 | func InStringSlice(slice []string, element string) bool { 25 | element = strings.TrimSpace(element) 26 | for _, v := range slice { 27 | if strings.TrimSpace(v) == element { 28 | return true 29 | } 30 | } 31 | 32 | return false 33 | } 34 | 35 | // 转义json特殊字符 36 | func EscapeJson(s string) string { 37 | specialChars := []string{"\\", "\b", "\f", "\n", "\r", "\t", "\""} 38 | replaceChars := []string{"\\\\", "\\b", "\\f", "\\n", "\\r", "\\t", "\\\""} 39 | 40 | return ReplaceStrings(s, specialChars, replaceChars) 41 | } 42 | 43 | func GetLocalIp() string { 44 | addrs, err := net.InterfaceAddrs() 45 | if err != nil { 46 | return "" 47 | } 48 | for _, addr := range addrs { 49 | if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 50 | if ipnet.IP.To4() != nil { 51 | return ipnet.IP.String() 52 | } 53 | 54 | } 55 | } 56 | 57 | return "" 58 | } 59 | -------------------------------------------------------------------------------- /vendor/github.com/go-gomail/gomail/auth.go: -------------------------------------------------------------------------------- 1 | package gomail 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "net/smtp" 8 | ) 9 | 10 | // loginAuth is an smtp.Auth that implements the LOGIN authentication mechanism. 11 | type loginAuth struct { 12 | username string 13 | password string 14 | host string 15 | } 16 | 17 | func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { 18 | if !server.TLS { 19 | advertised := false 20 | for _, mechanism := range server.Auth { 21 | if mechanism == "LOGIN" { 22 | advertised = true 23 | break 24 | } 25 | } 26 | if !advertised { 27 | return "", nil, errors.New("gomail: unencrypted connection") 28 | } 29 | } 30 | if server.Name != a.host { 31 | return "", nil, errors.New("gomail: wrong host name") 32 | } 33 | return "LOGIN", nil, nil 34 | } 35 | 36 | func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { 37 | if !more { 38 | return nil, nil 39 | } 40 | 41 | switch { 42 | case bytes.Equal(fromServer, []byte("Username:")): 43 | return []byte(a.username), nil 44 | case bytes.Equal(fromServer, []byte("Password:")): 45 | return []byte(a.password), nil 46 | default: 47 | return nil, fmt.Errorf("gomail: unexpected server challenge: %s", fromServer) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /listener/notify/notify.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "github.com/ouqiang/supervisor-event-listener/config" 5 | "github.com/ouqiang/supervisor-event-listener/event" 6 | 7 | "fmt" 8 | "os" 9 | "time" 10 | ) 11 | 12 | var ( 13 | Conf *config.Config 14 | queue chan event.Message 15 | ) 16 | 17 | func init() { 18 | Conf = config.ParseConfig() 19 | queue = make(chan event.Message, 10) 20 | go start() 21 | } 22 | 23 | type Notifiable interface { 24 | Send(event.Message) error 25 | } 26 | 27 | func Push(header *event.Header, payload *event.Payload) { 28 | queue <- event.Message{header, payload} 29 | } 30 | 31 | func start() { 32 | var message event.Message 33 | var notifyHandler Notifiable 34 | for { 35 | message = <-queue 36 | switch Conf.NotifyType { 37 | case "mail": 38 | notifyHandler = &Mail{} 39 | case "slack": 40 | notifyHandler = &Slack{} 41 | case "webhook": 42 | notifyHandler = &WebHook{} 43 | } 44 | if notifyHandler == nil { 45 | continue 46 | } 47 | go send(notifyHandler, message) 48 | time.Sleep(1 * time.Second) 49 | } 50 | } 51 | 52 | func send(notifyHandler Notifiable, message event.Message) { 53 | // 最多重试3次 54 | tryTimes := 3 55 | i := 0 56 | for i < tryTimes { 57 | err := notifyHandler.Send(message) 58 | if err == nil { 59 | break 60 | } 61 | fmt.Fprintln(os.Stderr, err) 62 | time.Sleep(30 * time.Second) 63 | i++ 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # supervisor-event-listener 2 | Supervisor事件通知, 支持邮件, Slack, WebHook 3 | 4 | ## 简介 5 | Supervisor是*nix环境下的进程管理工具, 可以把前台进程转换为守护进程, 当进程异常退出时自动重启. 6 | supervisor-event-listener监听进程异常退出事件, 并发送通知. 7 | 8 | ## 下载 9 | [v1.0](https://github.com/ouqiang/supervisor-event-listener/releases) 10 | 11 | ### 源码安装 12 | * `go get -u github.com/ouqiang/supervisor-event-listener` 13 | 14 | ## Supervisor配置 15 | ```ini 16 | [eventlistener:supervisor-event-listener] 17 | ; 默认读取配置文件/etc/supervisor-event-listener.ini 18 | command=/path/to/supervisor-event-listener 19 | ; 指定配置文件路径 20 | ;command=/path/to/supervisor-event-listener -c /path/to/supervisor-event-listener.ini 21 | events=PROCESS_STATE_EXITED 22 | ...... 23 | ``` 24 | 25 | ## 配置文件, 默认读取`/etc/supervisor-event-listener.ini` 26 | 27 | ```ini 28 | [default] 29 | # 通知类型 mail,slack,webhook 只能选择一种 30 | notify_type = mail 31 | 32 | # 邮件服务器配置 33 | mail.server.user = test@163.com 34 | mail.server.password = 123456 35 | mail.server.host = smtp.163.com 36 | mail.server.port = 25 37 | 38 | # 邮件收件人配置, 多个收件人, 逗号分隔 39 | mail.user = hello@163.com 40 | 41 | # Slack配置 42 | slack.webhook_url = https://hooks.slack.com/services/xxxx/xxx/xxxx 43 | slack.channel = exception 44 | 45 | # WebHook通知URL配置 46 | webhook_url = http://my.webhook.com 47 | 48 | ``` 49 | 50 | ## 通知内容 51 | 邮件、Slack 52 | ```shell 53 | Host: ip(hostname) 54 | Process: process-name 55 | PID: 6152 56 | EXITED FROM state: RUNNING 57 | ``` 58 | WebHook, Post请求, 字段含义查看Supervisor文档 59 | ```json 60 | { 61 | "Header": { 62 | "Ver": "3.0", 63 | "Server": "supervisor", 64 | "Serial": 11, 65 | "Pool": "supervisor-listener", 66 | "PoolSerial": 11, 67 | "EventName": "PROCESS_STATE_EXITED", 68 | "Len": 84 69 | }, 70 | "Payload": { 71 | "Ip": "ip(hostname)", 72 | "ProcessName": "process-name", 73 | "GroupName": "group-name", 74 | "FromState": "RUNNING", 75 | "Expected": 0, 76 | "Pid": 6371 77 | } 78 | } 79 | ``` 80 | -------------------------------------------------------------------------------- /listener/listener.go: -------------------------------------------------------------------------------- 1 | package listener 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "github.com/ouqiang/supervisor-event-listener/event" 8 | "github.com/ouqiang/supervisor-event-listener/listener/notify" 9 | "log" 10 | "os" 11 | ) 12 | 13 | var ( 14 | ErrPayloadLength = errors.New("Header中len长度与实际读取长度不一致") 15 | ) 16 | 17 | func Start() { 18 | defer func() { 19 | if err := recover(); err != nil { 20 | log.Print("panic", err) 21 | } 22 | }() 23 | listen() 24 | } 25 | 26 | // 监听事件, 从标准输入获取事件内容 27 | func listen() { 28 | reader := bufio.NewReader(os.Stdin) 29 | for { 30 | ready() 31 | header, err := readHeader(reader) 32 | if err != nil { 33 | failure(err) 34 | continue 35 | } 36 | payload, err := readPayload(reader, header.Len) 37 | if err != nil { 38 | failure(err) 39 | continue 40 | } 41 | // 只处理进程异常退出事件 42 | if header.EventName == "PROCESS_STATE_EXITED" { 43 | notify.Push(header, payload) 44 | } 45 | success() 46 | } 47 | } 48 | 49 | // 读取header 50 | func readHeader(reader *bufio.Reader) (*event.Header, error) { 51 | // 读取Header 52 | data, err := reader.ReadString('\n') 53 | if err != nil { 54 | return nil, err 55 | } 56 | // 解析Header 57 | header, err := event.ParseHeader(data) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | return header, nil 63 | } 64 | 65 | // 读取payload 66 | func readPayload(reader *bufio.Reader, payloadLen int) (*event.Payload, error) { 67 | // 读取payload 68 | buf := make([]byte, payloadLen) 69 | length, err := reader.Read(buf) 70 | if err != nil { 71 | return nil, err 72 | } 73 | if payloadLen != length { 74 | return nil, ErrPayloadLength 75 | } 76 | // 解析payload 77 | payload, err := event.ParsePayload(string(buf)) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | return payload, nil 83 | } 84 | 85 | func ready() { 86 | fmt.Fprint(os.Stdout, "READY\n") 87 | } 88 | 89 | func success() { 90 | fmt.Fprint(os.Stdout, "RESULT 2\nOK") 91 | } 92 | 93 | func failure(err error) { 94 | fmt.Fprintln(os.Stderr, err) 95 | fmt.Fprint(os.Stdout, "Result 2\nFAIL") 96 | } 97 | -------------------------------------------------------------------------------- /utils/httpclient/http_client.go: -------------------------------------------------------------------------------- 1 | package httpclient 2 | 3 | // http-client 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | type ResponseWrapper struct { 14 | StatusCode int 15 | Body string 16 | Header http.Header 17 | } 18 | 19 | func Get(url string, timeout int) ResponseWrapper { 20 | req, err := http.NewRequest("GET", url, nil) 21 | if err != nil { 22 | return createRequestError(err) 23 | } 24 | 25 | return request(req, timeout) 26 | } 27 | 28 | func PostParams(url string, params string, timeout int) ResponseWrapper { 29 | buf := bytes.NewBufferString(params) 30 | req, err := http.NewRequest("POST", url, buf) 31 | if err != nil { 32 | return createRequestError(err) 33 | } 34 | req.Header.Set("Content-type", "application/x-www-form-urlencoded") 35 | 36 | return request(req, timeout) 37 | } 38 | 39 | func PostJson(url string, body string, timeout int) ResponseWrapper { 40 | buf := bytes.NewBufferString(body) 41 | req, err := http.NewRequest("POST", url, buf) 42 | if err != nil { 43 | return createRequestError(err) 44 | } 45 | req.Header.Set("Content-type", "application/json") 46 | 47 | return request(req, timeout) 48 | } 49 | 50 | func request(req *http.Request, timeout int) ResponseWrapper { 51 | wrapper := ResponseWrapper{StatusCode: 0, Body: "", Header: make(http.Header)} 52 | client := &http.Client{} 53 | if timeout > 0 { 54 | client.Timeout = time.Duration(timeout) * time.Second 55 | } 56 | setRequestHeader(req) 57 | resp, err := client.Do(req) 58 | if err != nil { 59 | wrapper.Body = fmt.Sprintf("执行HTTP请求错误-%s", err.Error()) 60 | return wrapper 61 | } 62 | defer resp.Body.Close() 63 | body, err := ioutil.ReadAll(resp.Body) 64 | if err != nil { 65 | wrapper.Body = fmt.Sprintf("读取HTTP请求返回值失败-%s", err.Error()) 66 | return wrapper 67 | } 68 | wrapper.StatusCode = resp.StatusCode 69 | wrapper.Body = string(body) 70 | wrapper.Header = resp.Header 71 | 72 | return wrapper 73 | } 74 | 75 | func setRequestHeader(req *http.Request) { 76 | req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8,en;q=0.6") 77 | 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") 78 | } 79 | 80 | func createRequestError(err error) ResponseWrapper { 81 | errorMessage := fmt.Sprintf("创建HTTP请求错误-%s", err.Error()) 82 | return ResponseWrapper{0, errorMessage, make(http.Header)} 83 | } 84 | -------------------------------------------------------------------------------- /vendor/github.com/go-gomail/gomail/README.md: -------------------------------------------------------------------------------- 1 | # Gomail 2 | [![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) 3 | 4 | ## Introduction 5 | 6 | Gomail is a simple and efficient package to send emails. It is well tested and 7 | documented. 8 | 9 | Gomail can only send emails using an SMTP server. But the API is flexible and it 10 | is easy to implement other methods for sending emails using a local Postfix, an 11 | API, etc. 12 | 13 | It is versioned using [gopkg.in](https://gopkg.in) so I promise 14 | there will never be backward incompatible changes within each version. 15 | 16 | It requires Go 1.2 or newer. With Go 1.5, no external dependencies are used. 17 | 18 | 19 | ## Features 20 | 21 | Gomail supports: 22 | - Attachments 23 | - Embedded images 24 | - HTML and text templates 25 | - Automatic encoding of special characters 26 | - SSL and TLS 27 | - Sending multiple emails with the same SMTP connection 28 | 29 | 30 | ## Documentation 31 | 32 | https://godoc.org/gopkg.in/gomail.v2 33 | 34 | 35 | ## Download 36 | 37 | go get gopkg.in/gomail.v2 38 | 39 | 40 | ## Examples 41 | 42 | See the [examples in the documentation](https://godoc.org/gopkg.in/gomail.v2#example-package). 43 | 44 | 45 | ## FAQ 46 | 47 | ### x509: certificate signed by unknown authority 48 | 49 | If you get this error it means the certificate used by the SMTP server is not 50 | considered valid by the client running Gomail. As a quick workaround you can 51 | bypass the verification of the server's certificate chain and host name by using 52 | `SetTLSConfig`: 53 | 54 | package main 55 | 56 | import ( 57 | "crypto/tls" 58 | 59 | "gopkg.in/gomail.v2" 60 | ) 61 | 62 | func main() { 63 | d := gomail.NewDialer("smtp.example.com", 587, "user", "123456") 64 | d.TLSConfig = &tls.Config{InsecureSkipVerify: true} 65 | 66 | // Send emails using d. 67 | } 68 | 69 | Note, however, that this is insecure and should not be used in production. 70 | 71 | 72 | ## Contribute 73 | 74 | Contributions are more than welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for 75 | more info. 76 | 77 | 78 | ## Change log 79 | 80 | See [CHANGELOG.md](CHANGELOG.md). 81 | 82 | 83 | ## License 84 | 85 | [MIT](LICENSE) 86 | 87 | 88 | ## Contact 89 | 90 | You can ask questions on the [Gomail 91 | thread](https://groups.google.com/d/topic/golang-nuts/jMxZHzvvEVg/discussion) 92 | in the Go mailing-list. 93 | -------------------------------------------------------------------------------- /event/event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/ouqiang/supervisor-event-listener/utils" 7 | "os" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // Message 消息格式 13 | type Message struct { 14 | Header *Header 15 | Payload *Payload 16 | } 17 | 18 | func (msg *Message) String() string { 19 | 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) 20 | 21 | } 22 | 23 | // Header Supervisord触发事件时会先发送Header,根据Header中len字段去读取Payload 24 | type Header struct { 25 | Ver string 26 | Server string 27 | Serial int 28 | Pool string 29 | PoolSerial int 30 | EventName string // 事件名称 31 | Len int // Payload长度 32 | } 33 | 34 | // Payload 35 | type Payload struct { 36 | Ip string 37 | ProcessName string // 进程名称 38 | GroupName string // 进程组名称 39 | FromState string 40 | Expected int 41 | Pid int 42 | } 43 | 44 | // Fields 45 | type Fields map[string]string 46 | 47 | var ( 48 | ErrParseHeader = errors.New("解析Header失败") 49 | ErrParsePayload = errors.New("解析Payload失败") 50 | ) 51 | 52 | func ParseHeader(header string) (*Header, error) { 53 | h := &Header{} 54 | fields := parseFields(header) 55 | if len(fields) == 0 { 56 | return h, ErrParseHeader 57 | } 58 | 59 | h.Ver = fields["ver"] 60 | h.Server = fields["server"] 61 | h.Serial, _ = strconv.Atoi(fields["serial"]) 62 | h.Pool = fields["pool"] 63 | h.PoolSerial, _ = strconv.Atoi(fields["poolserial"]) 64 | h.EventName = fields["eventname"] 65 | h.Len, _ = strconv.Atoi(fields["len"]) 66 | 67 | return h, nil 68 | } 69 | 70 | func ParsePayload(payload string) (*Payload, error) { 71 | p := &Payload{} 72 | fields := parseFields(payload) 73 | if len(fields) == 0 { 74 | return p, ErrParsePayload 75 | } 76 | hostname, _ := os.Hostname() 77 | p.Ip = fmt.Sprintf("%s(%s)", utils.GetLocalIp(), hostname) 78 | p.ProcessName = fields["processname"] 79 | p.GroupName = fields["groupname"] 80 | p.FromState = fields["from_state"] 81 | p.Expected, _ = strconv.Atoi(fields["expected"]) 82 | p.Pid, _ = strconv.Atoi(fields["pid"]) 83 | 84 | return p, nil 85 | } 86 | 87 | func parseFields(data string) Fields { 88 | fields := make(Fields) 89 | data = strings.TrimSpace(data) 90 | if data == "" { 91 | return fields 92 | } 93 | // 格式如下 94 | // ver:3.0 server:supervisor serial:5 95 | slice := strings.Split(data, " ") 96 | if len(slice) == 0 { 97 | return fields 98 | } 99 | for _, item := range slice { 100 | group := strings.Split(item, ":") 101 | if len(group) < 2 { 102 | continue 103 | } 104 | key := strings.TrimSpace(group[0]) 105 | value := strings.TrimSpace(group[1]) 106 | fields[key] = value 107 | } 108 | 109 | return fields 110 | } 111 | -------------------------------------------------------------------------------- /vendor/github.com/go-gomail/gomail/send.go: -------------------------------------------------------------------------------- 1 | package gomail 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/mail" 8 | ) 9 | 10 | // Sender is the interface that wraps the Send method. 11 | // 12 | // Send sends an email to the given addresses. 13 | type Sender interface { 14 | Send(from string, to []string, msg io.WriterTo) error 15 | } 16 | 17 | // SendCloser is the interface that groups the Send and Close methods. 18 | type SendCloser interface { 19 | Sender 20 | Close() error 21 | } 22 | 23 | // A SendFunc is a function that sends emails to the given addresses. 24 | // 25 | // The SendFunc type is an adapter to allow the use of ordinary functions as 26 | // email senders. If f is a function with the appropriate signature, SendFunc(f) 27 | // is a Sender object that calls f. 28 | type SendFunc func(from string, to []string, msg io.WriterTo) error 29 | 30 | // Send calls f(from, to, msg). 31 | func (f SendFunc) Send(from string, to []string, msg io.WriterTo) error { 32 | return f(from, to, msg) 33 | } 34 | 35 | // Send sends emails using the given Sender. 36 | func Send(s Sender, msg ...*Message) error { 37 | for i, m := range msg { 38 | if err := send(s, m); err != nil { 39 | return fmt.Errorf("gomail: could not send email %d: %v", i+1, err) 40 | } 41 | } 42 | 43 | return nil 44 | } 45 | 46 | func send(s Sender, m *Message) error { 47 | from, err := m.getFrom() 48 | if err != nil { 49 | return err 50 | } 51 | 52 | to, err := m.getRecipients() 53 | if err != nil { 54 | return err 55 | } 56 | 57 | if err := s.Send(from, to, m); err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (m *Message) getFrom() (string, error) { 65 | from := m.header["Sender"] 66 | if len(from) == 0 { 67 | from = m.header["From"] 68 | if len(from) == 0 { 69 | return "", errors.New(`gomail: invalid message, "From" field is absent`) 70 | } 71 | } 72 | 73 | return parseAddress(from[0]) 74 | } 75 | 76 | func (m *Message) getRecipients() ([]string, error) { 77 | n := 0 78 | for _, field := range []string{"To", "Cc", "Bcc"} { 79 | if addresses, ok := m.header[field]; ok { 80 | n += len(addresses) 81 | } 82 | } 83 | list := make([]string, 0, n) 84 | 85 | for _, field := range []string{"To", "Cc", "Bcc"} { 86 | if addresses, ok := m.header[field]; ok { 87 | for _, a := range addresses { 88 | addr, err := parseAddress(a) 89 | if err != nil { 90 | return nil, err 91 | } 92 | list = addAddress(list, addr) 93 | } 94 | } 95 | } 96 | 97 | return list, nil 98 | } 99 | 100 | func addAddress(list []string, addr string) []string { 101 | for _, a := range list { 102 | if addr == a { 103 | return list 104 | } 105 | } 106 | 107 | return append(list, addr) 108 | } 109 | 110 | func parseAddress(field string) (string, error) { 111 | addr, err := mail.ParseAddress(field) 112 | if err != nil { 113 | return "", fmt.Errorf("gomail: invalid address %q: %v", field, err) 114 | } 115 | return addr.Address, nil 116 | } 117 | -------------------------------------------------------------------------------- /vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go: -------------------------------------------------------------------------------- 1 | // Package quotedprintable implements quoted-printable encoding as specified by 2 | // RFC 2045. 3 | package quotedprintable 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "fmt" 9 | "io" 10 | ) 11 | 12 | // Reader is a quoted-printable decoder. 13 | type Reader struct { 14 | br *bufio.Reader 15 | rerr error // last read error 16 | line []byte // to be consumed before more of br 17 | } 18 | 19 | // NewReader returns a quoted-printable reader, decoding from r. 20 | func NewReader(r io.Reader) *Reader { 21 | return &Reader{ 22 | br: bufio.NewReader(r), 23 | } 24 | } 25 | 26 | func fromHex(b byte) (byte, error) { 27 | switch { 28 | case b >= '0' && b <= '9': 29 | return b - '0', nil 30 | case b >= 'A' && b <= 'F': 31 | return b - 'A' + 10, nil 32 | // Accept badly encoded bytes. 33 | case b >= 'a' && b <= 'f': 34 | return b - 'a' + 10, nil 35 | } 36 | return 0, fmt.Errorf("quotedprintable: invalid hex byte 0x%02x", b) 37 | } 38 | 39 | func readHexByte(a, b byte) (byte, error) { 40 | var hb, lb byte 41 | var err error 42 | if hb, err = fromHex(a); err != nil { 43 | return 0, err 44 | } 45 | if lb, err = fromHex(b); err != nil { 46 | return 0, err 47 | } 48 | return hb<<4 | lb, nil 49 | } 50 | 51 | func isQPDiscardWhitespace(r rune) bool { 52 | switch r { 53 | case '\n', '\r', ' ', '\t': 54 | return true 55 | } 56 | return false 57 | } 58 | 59 | var ( 60 | crlf = []byte("\r\n") 61 | lf = []byte("\n") 62 | softSuffix = []byte("=") 63 | ) 64 | 65 | // Read reads and decodes quoted-printable data from the underlying reader. 66 | func (r *Reader) Read(p []byte) (n int, err error) { 67 | // Deviations from RFC 2045: 68 | // 1. in addition to "=\r\n", "=\n" is also treated as soft line break. 69 | // 2. it will pass through a '\r' or '\n' not preceded by '=', consistent 70 | // with other broken QP encoders & decoders. 71 | for len(p) > 0 { 72 | if len(r.line) == 0 { 73 | if r.rerr != nil { 74 | return n, r.rerr 75 | } 76 | r.line, r.rerr = r.br.ReadSlice('\n') 77 | 78 | // Does the line end in CRLF instead of just LF? 79 | hasLF := bytes.HasSuffix(r.line, lf) 80 | hasCR := bytes.HasSuffix(r.line, crlf) 81 | wholeLine := r.line 82 | r.line = bytes.TrimRightFunc(wholeLine, isQPDiscardWhitespace) 83 | if bytes.HasSuffix(r.line, softSuffix) { 84 | rightStripped := wholeLine[len(r.line):] 85 | r.line = r.line[:len(r.line)-1] 86 | if !bytes.HasPrefix(rightStripped, lf) && !bytes.HasPrefix(rightStripped, crlf) { 87 | r.rerr = fmt.Errorf("quotedprintable: invalid bytes after =: %q", rightStripped) 88 | } 89 | } else if hasLF { 90 | if hasCR { 91 | r.line = append(r.line, '\r', '\n') 92 | } else { 93 | r.line = append(r.line, '\n') 94 | } 95 | } 96 | continue 97 | } 98 | b := r.line[0] 99 | 100 | switch { 101 | case b == '=': 102 | if len(r.line[1:]) < 2 { 103 | return n, io.ErrUnexpectedEOF 104 | } 105 | b, err = readHexByte(r.line[1], r.line[2]) 106 | if err != nil { 107 | return n, err 108 | } 109 | r.line = r.line[2:] // 2 of the 3; other 1 is done below 110 | case b == '\t' || b == '\r' || b == '\n': 111 | break 112 | case b < ' ' || b > '~': 113 | return n, fmt.Errorf("quotedprintable: invalid unescaped byte 0x%02x in body", b) 114 | } 115 | p[0] = b 116 | p = p[1:] 117 | r.line = r.line[1:] 118 | n++ 119 | } 120 | return n, nil 121 | } 122 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/ouqiang/supervisor-event-listener/utils" 7 | "gopkg.in/ini.v1" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | type Config struct { 13 | NotifyType string 14 | WebHook WebHook 15 | MailServer MailServer 16 | MailUser MailUser 17 | Slack Slack 18 | } 19 | 20 | type WebHook struct { 21 | Url string 22 | } 23 | 24 | type Slack struct { 25 | WebHookUrl string 26 | Channel string 27 | } 28 | 29 | // 邮件服务器 30 | type MailServer struct { 31 | User string 32 | Password string 33 | Host string 34 | Port int 35 | } 36 | 37 | // 接收邮件的用户 38 | type MailUser struct { 39 | Email []string 40 | } 41 | 42 | func ParseConfig() *Config { 43 | var configFile string 44 | flag.StringVar(&configFile, "c", "/etc/supervisor-event-listener.ini", "config file") 45 | flag.Parse() 46 | configFile = strings.TrimSpace(configFile) 47 | if configFile == "" { 48 | Exit("请指定配置文件路径") 49 | } 50 | file, err := ini.Load(configFile) 51 | if err != nil { 52 | Exit("读取配置文件失败#" + err.Error()) 53 | } 54 | section := file.Section("default") 55 | notifyType := section.Key("notify_type").String() 56 | notifyType = strings.TrimSpace(notifyType) 57 | if !utils.InStringSlice([]string{"mail", "slack", "webhook"}, notifyType) { 58 | Exit("不支持的通知类型-" + notifyType) 59 | } 60 | 61 | config := &Config{} 62 | config.NotifyType = notifyType 63 | switch notifyType { 64 | case "mail": 65 | config.MailServer = parseMailServer(section) 66 | config.MailUser = parseMailUser(section) 67 | case "slack": 68 | config.Slack = parseSlack(section) 69 | case "webhook": 70 | config.WebHook = parseWebHook(section) 71 | } 72 | 73 | return config 74 | } 75 | 76 | func parseMailServer(section *ini.Section) MailServer { 77 | user := section.Key("mail.server.user").String() 78 | user = strings.TrimSpace(user) 79 | password := section.Key("mail.server.password").String() 80 | password = strings.TrimSpace(password) 81 | host := section.Key("mail.server.host").String() 82 | host = strings.TrimSpace(host) 83 | port, portErr := section.Key("mail.server.port").Int() 84 | if user == "" || password == "" || host == "" || portErr != nil { 85 | Exit("邮件服务器配置错误") 86 | } 87 | 88 | mailServer := MailServer{} 89 | mailServer.User = user 90 | mailServer.Password = password 91 | mailServer.Host = host 92 | mailServer.Port = port 93 | 94 | return mailServer 95 | } 96 | 97 | func parseMailUser(section *ini.Section) MailUser { 98 | user := section.Key("mail.user").String() 99 | user = strings.TrimSpace(user) 100 | if user == "" { 101 | Exit("邮件收件人配置错误") 102 | } 103 | mailUser := MailUser{} 104 | mailUser.Email = strings.Split(user, ",") 105 | 106 | return mailUser 107 | } 108 | 109 | func parseSlack(section *ini.Section) Slack { 110 | webHookUrl := section.Key("slack.webhook_url").String() 111 | webHookUrl = strings.TrimSpace(webHookUrl) 112 | channel := section.Key("slack.channel").String() 113 | channel = strings.TrimSpace(channel) 114 | if webHookUrl == "" || channel == "" { 115 | Exit("Slack配置错误") 116 | } 117 | 118 | slack := Slack{} 119 | slack.WebHookUrl = webHookUrl 120 | slack.Channel = channel 121 | 122 | return slack 123 | } 124 | 125 | func parseWebHook(section *ini.Section) WebHook { 126 | url := section.Key("webhook_url").String() 127 | url = strings.TrimSpace(url) 128 | if url == "" { 129 | Exit("WebHookUrl配置错误") 130 | } 131 | webHook := WebHook{} 132 | webHook.Url = url 133 | 134 | return webHook 135 | } 136 | 137 | func Exit(msg string) { 138 | fmt.Fprintln(os.Stderr, msg) 139 | os.Exit(1) 140 | } 141 | -------------------------------------------------------------------------------- /vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go: -------------------------------------------------------------------------------- 1 | package quotedprintable 2 | 3 | import "io" 4 | 5 | const lineMaxLen = 76 6 | 7 | // A Writer is a quoted-printable writer that implements io.WriteCloser. 8 | type Writer struct { 9 | // Binary mode treats the writer's input as pure binary and processes end of 10 | // line bytes as binary data. 11 | Binary bool 12 | 13 | w io.Writer 14 | i int 15 | line [78]byte 16 | cr bool 17 | } 18 | 19 | // NewWriter returns a new Writer that writes to w. 20 | func NewWriter(w io.Writer) *Writer { 21 | return &Writer{w: w} 22 | } 23 | 24 | // Write encodes p using quoted-printable encoding and writes it to the 25 | // underlying io.Writer. It limits line length to 76 characters. The encoded 26 | // bytes are not necessarily flushed until the Writer is closed. 27 | func (w *Writer) Write(p []byte) (n int, err error) { 28 | for i, b := range p { 29 | switch { 30 | // Simple writes are done in batch. 31 | case b >= '!' && b <= '~' && b != '=': 32 | continue 33 | case isWhitespace(b) || !w.Binary && (b == '\n' || b == '\r'): 34 | continue 35 | } 36 | 37 | if i > n { 38 | if err := w.write(p[n:i]); err != nil { 39 | return n, err 40 | } 41 | n = i 42 | } 43 | 44 | if err := w.encode(b); err != nil { 45 | return n, err 46 | } 47 | n++ 48 | } 49 | 50 | if n == len(p) { 51 | return n, nil 52 | } 53 | 54 | if err := w.write(p[n:]); err != nil { 55 | return n, err 56 | } 57 | 58 | return len(p), nil 59 | } 60 | 61 | // Close closes the Writer, flushing any unwritten data to the underlying 62 | // io.Writer, but does not close the underlying io.Writer. 63 | func (w *Writer) Close() error { 64 | if err := w.checkLastByte(); err != nil { 65 | return err 66 | } 67 | 68 | return w.flush() 69 | } 70 | 71 | // write limits text encoded in quoted-printable to 76 characters per line. 72 | func (w *Writer) write(p []byte) error { 73 | for _, b := range p { 74 | if b == '\n' || b == '\r' { 75 | // If the previous byte was \r, the CRLF has already been inserted. 76 | if w.cr && b == '\n' { 77 | w.cr = false 78 | continue 79 | } 80 | 81 | if b == '\r' { 82 | w.cr = true 83 | } 84 | 85 | if err := w.checkLastByte(); err != nil { 86 | return err 87 | } 88 | if err := w.insertCRLF(); err != nil { 89 | return err 90 | } 91 | continue 92 | } 93 | 94 | if w.i == lineMaxLen-1 { 95 | if err := w.insertSoftLineBreak(); err != nil { 96 | return err 97 | } 98 | } 99 | 100 | w.line[w.i] = b 101 | w.i++ 102 | w.cr = false 103 | } 104 | 105 | return nil 106 | } 107 | 108 | func (w *Writer) encode(b byte) error { 109 | if lineMaxLen-1-w.i < 3 { 110 | if err := w.insertSoftLineBreak(); err != nil { 111 | return err 112 | } 113 | } 114 | 115 | w.line[w.i] = '=' 116 | w.line[w.i+1] = upperhex[b>>4] 117 | w.line[w.i+2] = upperhex[b&0x0f] 118 | w.i += 3 119 | 120 | return nil 121 | } 122 | 123 | // checkLastByte encodes the last buffered byte if it is a space or a tab. 124 | func (w *Writer) checkLastByte() error { 125 | if w.i == 0 { 126 | return nil 127 | } 128 | 129 | b := w.line[w.i-1] 130 | if isWhitespace(b) { 131 | w.i-- 132 | if err := w.encode(b); err != nil { 133 | return err 134 | } 135 | } 136 | 137 | return nil 138 | } 139 | 140 | func (w *Writer) insertSoftLineBreak() error { 141 | w.line[w.i] = '=' 142 | w.i++ 143 | 144 | return w.insertCRLF() 145 | } 146 | 147 | func (w *Writer) insertCRLF() error { 148 | w.line[w.i] = '\r' 149 | w.line[w.i+1] = '\n' 150 | w.i += 2 151 | 152 | return w.flush() 153 | } 154 | 155 | func (w *Writer) flush() error { 156 | if _, err := w.w.Write(w.line[:w.i]); err != nil { 157 | return err 158 | } 159 | 160 | w.i = 0 161 | return nil 162 | } 163 | 164 | func isWhitespace(b byte) bool { 165 | return b == ' ' || b == '\t' 166 | } 167 | -------------------------------------------------------------------------------- /vendor/github.com/go-gomail/gomail/smtp.go: -------------------------------------------------------------------------------- 1 | package gomail 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "io" 7 | "net" 8 | "net/smtp" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // A Dialer is a dialer to an SMTP server. 14 | type Dialer struct { 15 | // Host represents the host of the SMTP server. 16 | Host string 17 | // Port represents the port of the SMTP server. 18 | Port int 19 | // Username is the username to use to authenticate to the SMTP server. 20 | Username string 21 | // Password is the password to use to authenticate to the SMTP server. 22 | Password string 23 | // Auth represents the authentication mechanism used to authenticate to the 24 | // SMTP server. 25 | Auth smtp.Auth 26 | // SSL defines whether an SSL connection is used. It should be false in 27 | // most cases since the authentication mechanism should use the STARTTLS 28 | // extension instead. 29 | SSL bool 30 | // TSLConfig represents the TLS configuration used for the TLS (when the 31 | // STARTTLS extension is used) or SSL connection. 32 | TLSConfig *tls.Config 33 | // LocalName is the hostname sent to the SMTP server with the HELO command. 34 | // By default, "localhost" is sent. 35 | LocalName string 36 | } 37 | 38 | // NewDialer returns a new SMTP Dialer. The given parameters are used to connect 39 | // to the SMTP server. 40 | func NewDialer(host string, port int, username, password string) *Dialer { 41 | return &Dialer{ 42 | Host: host, 43 | Port: port, 44 | Username: username, 45 | Password: password, 46 | SSL: port == 465, 47 | } 48 | } 49 | 50 | // NewPlainDialer returns a new SMTP Dialer. The given parameters are used to 51 | // connect to the SMTP server. 52 | // 53 | // Deprecated: Use NewDialer instead. 54 | func NewPlainDialer(host string, port int, username, password string) *Dialer { 55 | return NewDialer(host, port, username, password) 56 | } 57 | 58 | // Dial dials and authenticates to an SMTP server. The returned SendCloser 59 | // should be closed when done using it. 60 | func (d *Dialer) Dial() (SendCloser, error) { 61 | conn, err := netDialTimeout("tcp", addr(d.Host, d.Port), 10*time.Second) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | if d.SSL { 67 | conn = tlsClient(conn, d.tlsConfig()) 68 | } 69 | 70 | c, err := smtpNewClient(conn, d.Host) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | if d.LocalName != "" { 76 | if err := c.Hello(d.LocalName); err != nil { 77 | return nil, err 78 | } 79 | } 80 | 81 | if !d.SSL { 82 | if ok, _ := c.Extension("STARTTLS"); ok { 83 | if err := c.StartTLS(d.tlsConfig()); err != nil { 84 | c.Close() 85 | return nil, err 86 | } 87 | } 88 | } 89 | 90 | if d.Auth == nil && d.Username != "" { 91 | if ok, auths := c.Extension("AUTH"); ok { 92 | if strings.Contains(auths, "CRAM-MD5") { 93 | d.Auth = smtp.CRAMMD5Auth(d.Username, d.Password) 94 | } else if strings.Contains(auths, "LOGIN") && 95 | !strings.Contains(auths, "PLAIN") { 96 | d.Auth = &loginAuth{ 97 | username: d.Username, 98 | password: d.Password, 99 | host: d.Host, 100 | } 101 | } else { 102 | d.Auth = smtp.PlainAuth("", d.Username, d.Password, d.Host) 103 | } 104 | } 105 | } 106 | 107 | if d.Auth != nil { 108 | if err = c.Auth(d.Auth); err != nil { 109 | c.Close() 110 | return nil, err 111 | } 112 | } 113 | 114 | return &smtpSender{c, d}, nil 115 | } 116 | 117 | func (d *Dialer) tlsConfig() *tls.Config { 118 | if d.TLSConfig == nil { 119 | return &tls.Config{ServerName: d.Host} 120 | } 121 | return d.TLSConfig 122 | } 123 | 124 | func addr(host string, port int) string { 125 | return fmt.Sprintf("%s:%d", host, port) 126 | } 127 | 128 | // DialAndSend opens a connection to the SMTP server, sends the given emails and 129 | // closes the connection. 130 | func (d *Dialer) DialAndSend(m ...*Message) error { 131 | s, err := d.Dial() 132 | if err != nil { 133 | return err 134 | } 135 | defer s.Close() 136 | 137 | return Send(s, m...) 138 | } 139 | 140 | type smtpSender struct { 141 | smtpClient 142 | d *Dialer 143 | } 144 | 145 | func (c *smtpSender) Send(from string, to []string, msg io.WriterTo) error { 146 | if err := c.Mail(from); err != nil { 147 | if err == io.EOF { 148 | // This is probably due to a timeout, so reconnect and try again. 149 | sc, derr := c.d.Dial() 150 | if derr == nil { 151 | if s, ok := sc.(*smtpSender); ok { 152 | *c = *s 153 | return c.Send(from, to, msg) 154 | } 155 | } 156 | } 157 | return err 158 | } 159 | 160 | for _, addr := range to { 161 | if err := c.Rcpt(addr); err != nil { 162 | return err 163 | } 164 | } 165 | 166 | w, err := c.Data() 167 | if err != nil { 168 | return err 169 | } 170 | 171 | if _, err = msg.WriteTo(w); err != nil { 172 | w.Close() 173 | return err 174 | } 175 | 176 | return w.Close() 177 | } 178 | 179 | func (c *smtpSender) Close() error { 180 | return c.Quit() 181 | } 182 | 183 | // Stubbed out for tests. 184 | var ( 185 | netDialTimeout = net.DialTimeout 186 | tlsClient = tls.Client 187 | smtpNewClient = func(conn net.Conn, host string) (smtpClient, error) { 188 | return smtp.NewClient(conn, host) 189 | } 190 | ) 191 | 192 | type smtpClient interface { 193 | Hello(string) error 194 | Extension(string) (bool, string) 195 | StartTLS(*tls.Config) error 196 | Auth(smtp.Auth) error 197 | Mail(string) error 198 | Rcpt(string) error 199 | Data() (io.WriteCloser, error) 200 | Quit() error 201 | Close() error 202 | } 203 | -------------------------------------------------------------------------------- /vendor/gopkg.in/ini.v1/section.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package ini 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "strings" 21 | ) 22 | 23 | // Section represents a config section. 24 | type Section struct { 25 | f *File 26 | Comment string 27 | name string 28 | keys map[string]*Key 29 | keyList []string 30 | keysHash map[string]string 31 | 32 | isRawSection bool 33 | rawBody string 34 | } 35 | 36 | func newSection(f *File, name string) *Section { 37 | return &Section{ 38 | f: f, 39 | name: name, 40 | keys: make(map[string]*Key), 41 | keyList: make([]string, 0, 10), 42 | keysHash: make(map[string]string), 43 | } 44 | } 45 | 46 | // Name returns name of Section. 47 | func (s *Section) Name() string { 48 | return s.name 49 | } 50 | 51 | // Body returns rawBody of Section if the section was marked as unparseable. 52 | // It still follows the other rules of the INI format surrounding leading/trailing whitespace. 53 | func (s *Section) Body() string { 54 | return strings.TrimSpace(s.rawBody) 55 | } 56 | 57 | // NewKey creates a new key to given section. 58 | func (s *Section) NewKey(name, val string) (*Key, error) { 59 | if len(name) == 0 { 60 | return nil, errors.New("error creating new key: empty key name") 61 | } else if s.f.options.Insensitive { 62 | name = strings.ToLower(name) 63 | } 64 | 65 | if s.f.BlockMode { 66 | s.f.lock.Lock() 67 | defer s.f.lock.Unlock() 68 | } 69 | 70 | if inSlice(name, s.keyList) { 71 | if s.f.options.AllowShadows { 72 | if err := s.keys[name].addShadow(val); err != nil { 73 | return nil, err 74 | } 75 | } else { 76 | s.keys[name].value = val 77 | } 78 | return s.keys[name], nil 79 | } 80 | 81 | s.keyList = append(s.keyList, name) 82 | s.keys[name] = newKey(s, name, val) 83 | s.keysHash[name] = val 84 | return s.keys[name], nil 85 | } 86 | 87 | // NewBooleanKey creates a new boolean type key to given section. 88 | func (s *Section) NewBooleanKey(name string) (*Key, error) { 89 | key, err := s.NewKey(name, "true") 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | key.isBooleanType = true 95 | return key, nil 96 | } 97 | 98 | // GetKey returns key in section by given name. 99 | func (s *Section) GetKey(name string) (*Key, error) { 100 | // FIXME: change to section level lock? 101 | if s.f.BlockMode { 102 | s.f.lock.RLock() 103 | } 104 | if s.f.options.Insensitive { 105 | name = strings.ToLower(name) 106 | } 107 | key := s.keys[name] 108 | if s.f.BlockMode { 109 | s.f.lock.RUnlock() 110 | } 111 | 112 | if key == nil { 113 | // Check if it is a child-section. 114 | sname := s.name 115 | for { 116 | if i := strings.LastIndex(sname, "."); i > -1 { 117 | sname = sname[:i] 118 | sec, err := s.f.GetSection(sname) 119 | if err != nil { 120 | continue 121 | } 122 | return sec.GetKey(name) 123 | } else { 124 | break 125 | } 126 | } 127 | return nil, fmt.Errorf("error when getting key of section '%s': key '%s' not exists", s.name, name) 128 | } 129 | return key, nil 130 | } 131 | 132 | // HasKey returns true if section contains a key with given name. 133 | func (s *Section) HasKey(name string) bool { 134 | key, _ := s.GetKey(name) 135 | return key != nil 136 | } 137 | 138 | // Haskey is a backwards-compatible name for HasKey. 139 | func (s *Section) Haskey(name string) bool { 140 | return s.HasKey(name) 141 | } 142 | 143 | // HasValue returns true if section contains given raw value. 144 | func (s *Section) HasValue(value string) bool { 145 | if s.f.BlockMode { 146 | s.f.lock.RLock() 147 | defer s.f.lock.RUnlock() 148 | } 149 | 150 | for _, k := range s.keys { 151 | if value == k.value { 152 | return true 153 | } 154 | } 155 | return false 156 | } 157 | 158 | // Key assumes named Key exists in section and returns a zero-value when not. 159 | func (s *Section) Key(name string) *Key { 160 | key, err := s.GetKey(name) 161 | if err != nil { 162 | // It's OK here because the only possible error is empty key name, 163 | // but if it's empty, this piece of code won't be executed. 164 | key, _ = s.NewKey(name, "") 165 | return key 166 | } 167 | return key 168 | } 169 | 170 | // Keys returns list of keys of section. 171 | func (s *Section) Keys() []*Key { 172 | keys := make([]*Key, len(s.keyList)) 173 | for i := range s.keyList { 174 | keys[i] = s.Key(s.keyList[i]) 175 | } 176 | return keys 177 | } 178 | 179 | // ParentKeys returns list of keys of parent section. 180 | func (s *Section) ParentKeys() []*Key { 181 | var parentKeys []*Key 182 | sname := s.name 183 | for { 184 | if i := strings.LastIndex(sname, "."); i > -1 { 185 | sname = sname[:i] 186 | sec, err := s.f.GetSection(sname) 187 | if err != nil { 188 | continue 189 | } 190 | parentKeys = append(parentKeys, sec.Keys()...) 191 | } else { 192 | break 193 | } 194 | 195 | } 196 | return parentKeys 197 | } 198 | 199 | // KeyStrings returns list of key names of section. 200 | func (s *Section) KeyStrings() []string { 201 | list := make([]string, len(s.keyList)) 202 | copy(list, s.keyList) 203 | return list 204 | } 205 | 206 | // KeysHash returns keys hash consisting of names and values. 207 | func (s *Section) KeysHash() map[string]string { 208 | if s.f.BlockMode { 209 | s.f.lock.RLock() 210 | defer s.f.lock.RUnlock() 211 | } 212 | 213 | hash := map[string]string{} 214 | for key, value := range s.keysHash { 215 | hash[key] = value 216 | } 217 | return hash 218 | } 219 | 220 | // DeleteKey deletes a key from section. 221 | func (s *Section) DeleteKey(name string) { 222 | if s.f.BlockMode { 223 | s.f.lock.Lock() 224 | defer s.f.lock.Unlock() 225 | } 226 | 227 | for i, k := range s.keyList { 228 | if k == name { 229 | s.keyList = append(s.keyList[:i], s.keyList[i+1:]...) 230 | delete(s.keys, name) 231 | return 232 | } 233 | } 234 | } 235 | 236 | // ChildSections returns a list of child sections of current section. 237 | // For example, "[parent.child1]" and "[parent.child12]" are child sections 238 | // of section "[parent]". 239 | func (s *Section) ChildSections() []*Section { 240 | prefix := s.name + "." 241 | children := make([]*Section, 0, 3) 242 | for _, name := range s.f.sectionList { 243 | if strings.HasPrefix(name, prefix) { 244 | children = append(children, s.f.sections[name]) 245 | } 246 | } 247 | return children 248 | } 249 | -------------------------------------------------------------------------------- /vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go: -------------------------------------------------------------------------------- 1 | package quotedprintable 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "strings" 10 | "unicode" 11 | "unicode/utf8" 12 | ) 13 | 14 | // A WordEncoder is a RFC 2047 encoded-word encoder. 15 | type WordEncoder byte 16 | 17 | const ( 18 | // BEncoding represents Base64 encoding scheme as defined by RFC 2045. 19 | BEncoding = WordEncoder('b') 20 | // QEncoding represents the Q-encoding scheme as defined by RFC 2047. 21 | QEncoding = WordEncoder('q') 22 | ) 23 | 24 | var ( 25 | errInvalidWord = errors.New("mime: invalid RFC 2047 encoded-word") 26 | ) 27 | 28 | // Encode returns the encoded-word form of s. If s is ASCII without special 29 | // characters, it is returned unchanged. The provided charset is the IANA 30 | // charset name of s. It is case insensitive. 31 | func (e WordEncoder) Encode(charset, s string) string { 32 | if !needsEncoding(s) { 33 | return s 34 | } 35 | return e.encodeWord(charset, s) 36 | } 37 | 38 | func needsEncoding(s string) bool { 39 | for _, b := range s { 40 | if (b < ' ' || b > '~') && b != '\t' { 41 | return true 42 | } 43 | } 44 | return false 45 | } 46 | 47 | // encodeWord encodes a string into an encoded-word. 48 | func (e WordEncoder) encodeWord(charset, s string) string { 49 | buf := getBuffer() 50 | defer putBuffer(buf) 51 | 52 | buf.WriteString("=?") 53 | buf.WriteString(charset) 54 | buf.WriteByte('?') 55 | buf.WriteByte(byte(e)) 56 | buf.WriteByte('?') 57 | 58 | if e == BEncoding { 59 | w := base64.NewEncoder(base64.StdEncoding, buf) 60 | io.WriteString(w, s) 61 | w.Close() 62 | } else { 63 | enc := make([]byte, 3) 64 | for i := 0; i < len(s); i++ { 65 | b := s[i] 66 | switch { 67 | case b == ' ': 68 | buf.WriteByte('_') 69 | case b <= '~' && b >= '!' && b != '=' && b != '?' && b != '_': 70 | buf.WriteByte(b) 71 | default: 72 | enc[0] = '=' 73 | enc[1] = upperhex[b>>4] 74 | enc[2] = upperhex[b&0x0f] 75 | buf.Write(enc) 76 | } 77 | } 78 | } 79 | buf.WriteString("?=") 80 | return buf.String() 81 | } 82 | 83 | const upperhex = "0123456789ABCDEF" 84 | 85 | // A WordDecoder decodes MIME headers containing RFC 2047 encoded-words. 86 | type WordDecoder struct { 87 | // CharsetReader, if non-nil, defines a function to generate 88 | // charset-conversion readers, converting from the provided 89 | // charset into UTF-8. 90 | // Charsets are always lower-case. utf-8, iso-8859-1 and us-ascii charsets 91 | // are handled by default. 92 | // One of the the CharsetReader's result values must be non-nil. 93 | CharsetReader func(charset string, input io.Reader) (io.Reader, error) 94 | } 95 | 96 | // Decode decodes an encoded-word. If word is not a valid RFC 2047 encoded-word, 97 | // word is returned unchanged. 98 | func (d *WordDecoder) Decode(word string) (string, error) { 99 | fields := strings.Split(word, "?") // TODO: remove allocation? 100 | if len(fields) != 5 || fields[0] != "=" || fields[4] != "=" || len(fields[2]) != 1 { 101 | return "", errInvalidWord 102 | } 103 | 104 | content, err := decode(fields[2][0], fields[3]) 105 | if err != nil { 106 | return "", err 107 | } 108 | 109 | buf := getBuffer() 110 | defer putBuffer(buf) 111 | 112 | if err := d.convert(buf, fields[1], content); err != nil { 113 | return "", err 114 | } 115 | 116 | return buf.String(), nil 117 | } 118 | 119 | // DecodeHeader decodes all encoded-words of the given string. It returns an 120 | // error if and only if CharsetReader of d returns an error. 121 | func (d *WordDecoder) DecodeHeader(header string) (string, error) { 122 | // If there is no encoded-word, returns before creating a buffer. 123 | i := strings.Index(header, "=?") 124 | if i == -1 { 125 | return header, nil 126 | } 127 | 128 | buf := getBuffer() 129 | defer putBuffer(buf) 130 | 131 | buf.WriteString(header[:i]) 132 | header = header[i:] 133 | 134 | betweenWords := false 135 | for { 136 | start := strings.Index(header, "=?") 137 | if start == -1 { 138 | break 139 | } 140 | cur := start + len("=?") 141 | 142 | i := strings.Index(header[cur:], "?") 143 | if i == -1 { 144 | break 145 | } 146 | charset := header[cur : cur+i] 147 | cur += i + len("?") 148 | 149 | if len(header) < cur+len("Q??=") { 150 | break 151 | } 152 | encoding := header[cur] 153 | cur++ 154 | 155 | if header[cur] != '?' { 156 | break 157 | } 158 | cur++ 159 | 160 | j := strings.Index(header[cur:], "?=") 161 | if j == -1 { 162 | break 163 | } 164 | text := header[cur : cur+j] 165 | end := cur + j + len("?=") 166 | 167 | content, err := decode(encoding, text) 168 | if err != nil { 169 | betweenWords = false 170 | buf.WriteString(header[:start+2]) 171 | header = header[start+2:] 172 | continue 173 | } 174 | 175 | // Write characters before the encoded-word. White-space and newline 176 | // characters separating two encoded-words must be deleted. 177 | if start > 0 && (!betweenWords || hasNonWhitespace(header[:start])) { 178 | buf.WriteString(header[:start]) 179 | } 180 | 181 | if err := d.convert(buf, charset, content); err != nil { 182 | return "", err 183 | } 184 | 185 | header = header[end:] 186 | betweenWords = true 187 | } 188 | 189 | if len(header) > 0 { 190 | buf.WriteString(header) 191 | } 192 | 193 | return buf.String(), nil 194 | } 195 | 196 | func decode(encoding byte, text string) ([]byte, error) { 197 | switch encoding { 198 | case 'B', 'b': 199 | return base64.StdEncoding.DecodeString(text) 200 | case 'Q', 'q': 201 | return qDecode(text) 202 | } 203 | return nil, errInvalidWord 204 | } 205 | 206 | func (d *WordDecoder) convert(buf *bytes.Buffer, charset string, content []byte) error { 207 | switch { 208 | case strings.EqualFold("utf-8", charset): 209 | buf.Write(content) 210 | case strings.EqualFold("iso-8859-1", charset): 211 | for _, c := range content { 212 | buf.WriteRune(rune(c)) 213 | } 214 | case strings.EqualFold("us-ascii", charset): 215 | for _, c := range content { 216 | if c >= utf8.RuneSelf { 217 | buf.WriteRune(unicode.ReplacementChar) 218 | } else { 219 | buf.WriteByte(c) 220 | } 221 | } 222 | default: 223 | if d.CharsetReader == nil { 224 | return fmt.Errorf("mime: unhandled charset %q", charset) 225 | } 226 | r, err := d.CharsetReader(strings.ToLower(charset), bytes.NewReader(content)) 227 | if err != nil { 228 | return err 229 | } 230 | if _, err = buf.ReadFrom(r); err != nil { 231 | return err 232 | } 233 | } 234 | return nil 235 | } 236 | 237 | // hasNonWhitespace reports whether s (assumed to be ASCII) contains at least 238 | // one byte of non-whitespace. 239 | func hasNonWhitespace(s string) bool { 240 | for _, b := range s { 241 | switch b { 242 | // Encoded-words can only be separated by linear white spaces which does 243 | // not include vertical tabs (\v). 244 | case ' ', '\t', '\n', '\r': 245 | default: 246 | return true 247 | } 248 | } 249 | return false 250 | } 251 | 252 | // qDecode decodes a Q encoded string. 253 | func qDecode(s string) ([]byte, error) { 254 | dec := make([]byte, len(s)) 255 | n := 0 256 | for i := 0; i < len(s); i++ { 257 | switch c := s[i]; { 258 | case c == '_': 259 | dec[n] = ' ' 260 | case c == '=': 261 | if i+2 >= len(s) { 262 | return nil, errInvalidWord 263 | } 264 | b, err := readHexByte(s[i+1], s[i+2]) 265 | if err != nil { 266 | return nil, err 267 | } 268 | dec[n] = b 269 | i += 2 270 | case (c <= '~' && c >= ' ') || c == '\n' || c == '\r' || c == '\t': 271 | dec[n] = c 272 | default: 273 | return nil, errInvalidWord 274 | } 275 | n++ 276 | } 277 | 278 | return dec[:n], nil 279 | } 280 | -------------------------------------------------------------------------------- /vendor/github.com/go-gomail/gomail/writeto.go: -------------------------------------------------------------------------------- 1 | package gomail 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "io" 7 | "mime" 8 | "mime/multipart" 9 | "path/filepath" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // WriteTo implements io.WriterTo. It dumps the whole message into w. 15 | func (m *Message) WriteTo(w io.Writer) (int64, error) { 16 | mw := &messageWriter{w: w} 17 | mw.writeMessage(m) 18 | return mw.n, mw.err 19 | } 20 | 21 | func (w *messageWriter) writeMessage(m *Message) { 22 | if _, ok := m.header["Mime-Version"]; !ok { 23 | w.writeString("Mime-Version: 1.0\r\n") 24 | } 25 | if _, ok := m.header["Date"]; !ok { 26 | w.writeHeader("Date", m.FormatDate(now())) 27 | } 28 | w.writeHeaders(m.header) 29 | 30 | if m.hasMixedPart() { 31 | w.openMultipart("mixed") 32 | } 33 | 34 | if m.hasRelatedPart() { 35 | w.openMultipart("related") 36 | } 37 | 38 | if m.hasAlternativePart() { 39 | w.openMultipart("alternative") 40 | } 41 | for _, part := range m.parts { 42 | w.writePart(part, m.charset) 43 | } 44 | if m.hasAlternativePart() { 45 | w.closeMultipart() 46 | } 47 | 48 | w.addFiles(m.embedded, false) 49 | if m.hasRelatedPart() { 50 | w.closeMultipart() 51 | } 52 | 53 | w.addFiles(m.attachments, true) 54 | if m.hasMixedPart() { 55 | w.closeMultipart() 56 | } 57 | } 58 | 59 | func (m *Message) hasMixedPart() bool { 60 | return (len(m.parts) > 0 && len(m.attachments) > 0) || len(m.attachments) > 1 61 | } 62 | 63 | func (m *Message) hasRelatedPart() bool { 64 | return (len(m.parts) > 0 && len(m.embedded) > 0) || len(m.embedded) > 1 65 | } 66 | 67 | func (m *Message) hasAlternativePart() bool { 68 | return len(m.parts) > 1 69 | } 70 | 71 | type messageWriter struct { 72 | w io.Writer 73 | n int64 74 | writers [3]*multipart.Writer 75 | partWriter io.Writer 76 | depth uint8 77 | err error 78 | } 79 | 80 | func (w *messageWriter) openMultipart(mimeType string) { 81 | mw := multipart.NewWriter(w) 82 | contentType := "multipart/" + mimeType + ";\r\n boundary=" + mw.Boundary() 83 | w.writers[w.depth] = mw 84 | 85 | if w.depth == 0 { 86 | w.writeHeader("Content-Type", contentType) 87 | w.writeString("\r\n") 88 | } else { 89 | w.createPart(map[string][]string{ 90 | "Content-Type": {contentType}, 91 | }) 92 | } 93 | w.depth++ 94 | } 95 | 96 | func (w *messageWriter) createPart(h map[string][]string) { 97 | w.partWriter, w.err = w.writers[w.depth-1].CreatePart(h) 98 | } 99 | 100 | func (w *messageWriter) closeMultipart() { 101 | if w.depth > 0 { 102 | w.writers[w.depth-1].Close() 103 | w.depth-- 104 | } 105 | } 106 | 107 | func (w *messageWriter) writePart(p *part, charset string) { 108 | w.writeHeaders(map[string][]string{ 109 | "Content-Type": {p.contentType + "; charset=" + charset}, 110 | "Content-Transfer-Encoding": {string(p.encoding)}, 111 | }) 112 | w.writeBody(p.copier, p.encoding) 113 | } 114 | 115 | func (w *messageWriter) addFiles(files []*file, isAttachment bool) { 116 | for _, f := range files { 117 | if _, ok := f.Header["Content-Type"]; !ok { 118 | mediaType := mime.TypeByExtension(filepath.Ext(f.Name)) 119 | if mediaType == "" { 120 | mediaType = "application/octet-stream" 121 | } 122 | f.setHeader("Content-Type", mediaType+`; name="`+f.Name+`"`) 123 | } 124 | 125 | if _, ok := f.Header["Content-Transfer-Encoding"]; !ok { 126 | f.setHeader("Content-Transfer-Encoding", string(Base64)) 127 | } 128 | 129 | if _, ok := f.Header["Content-Disposition"]; !ok { 130 | var disp string 131 | if isAttachment { 132 | disp = "attachment" 133 | } else { 134 | disp = "inline" 135 | } 136 | f.setHeader("Content-Disposition", disp+`; filename="`+f.Name+`"`) 137 | } 138 | 139 | if !isAttachment { 140 | if _, ok := f.Header["Content-ID"]; !ok { 141 | f.setHeader("Content-ID", "<"+f.Name+">") 142 | } 143 | } 144 | w.writeHeaders(f.Header) 145 | w.writeBody(f.CopyFunc, Base64) 146 | } 147 | } 148 | 149 | func (w *messageWriter) Write(p []byte) (int, error) { 150 | if w.err != nil { 151 | return 0, errors.New("gomail: cannot write as writer is in error") 152 | } 153 | 154 | var n int 155 | n, w.err = w.w.Write(p) 156 | w.n += int64(n) 157 | return n, w.err 158 | } 159 | 160 | func (w *messageWriter) writeString(s string) { 161 | n, _ := io.WriteString(w.w, s) 162 | w.n += int64(n) 163 | } 164 | 165 | func (w *messageWriter) writeHeader(k string, v ...string) { 166 | w.writeString(k) 167 | if len(v) == 0 { 168 | w.writeString(":\r\n") 169 | return 170 | } 171 | w.writeString(": ") 172 | 173 | // Max header line length is 78 characters in RFC 5322 and 76 characters 174 | // in RFC 2047. So for the sake of simplicity we use the 76 characters 175 | // limit. 176 | charsLeft := 76 - len(k) - len(": ") 177 | 178 | for i, s := range v { 179 | // If the line is already too long, insert a newline right away. 180 | if charsLeft < 1 { 181 | if i == 0 { 182 | w.writeString("\r\n ") 183 | } else { 184 | w.writeString(",\r\n ") 185 | } 186 | charsLeft = 75 187 | } else if i != 0 { 188 | w.writeString(", ") 189 | charsLeft -= 2 190 | } 191 | 192 | // While the header content is too long, fold it by inserting a newline. 193 | for len(s) > charsLeft { 194 | s = w.writeLine(s, charsLeft) 195 | charsLeft = 75 196 | } 197 | w.writeString(s) 198 | if i := lastIndexByte(s, '\n'); i != -1 { 199 | charsLeft = 75 - (len(s) - i - 1) 200 | } else { 201 | charsLeft -= len(s) 202 | } 203 | } 204 | w.writeString("\r\n") 205 | } 206 | 207 | func (w *messageWriter) writeLine(s string, charsLeft int) string { 208 | // If there is already a newline before the limit. Write the line. 209 | if i := strings.IndexByte(s, '\n'); i != -1 && i < charsLeft { 210 | w.writeString(s[:i+1]) 211 | return s[i+1:] 212 | } 213 | 214 | for i := charsLeft - 1; i >= 0; i-- { 215 | if s[i] == ' ' { 216 | w.writeString(s[:i]) 217 | w.writeString("\r\n ") 218 | return s[i+1:] 219 | } 220 | } 221 | 222 | // We could not insert a newline cleanly so look for a space or a newline 223 | // even if it is after the limit. 224 | for i := 75; i < len(s); i++ { 225 | if s[i] == ' ' { 226 | w.writeString(s[:i]) 227 | w.writeString("\r\n ") 228 | return s[i+1:] 229 | } 230 | if s[i] == '\n' { 231 | w.writeString(s[:i+1]) 232 | return s[i+1:] 233 | } 234 | } 235 | 236 | // Too bad, no space or newline in the whole string. Just write everything. 237 | w.writeString(s) 238 | return "" 239 | } 240 | 241 | func (w *messageWriter) writeHeaders(h map[string][]string) { 242 | if w.depth == 0 { 243 | for k, v := range h { 244 | if k != "Bcc" { 245 | w.writeHeader(k, v...) 246 | } 247 | } 248 | } else { 249 | w.createPart(h) 250 | } 251 | } 252 | 253 | func (w *messageWriter) writeBody(f func(io.Writer) error, enc Encoding) { 254 | var subWriter io.Writer 255 | if w.depth == 0 { 256 | w.writeString("\r\n") 257 | subWriter = w.w 258 | } else { 259 | subWriter = w.partWriter 260 | } 261 | 262 | if enc == Base64 { 263 | wc := base64.NewEncoder(base64.StdEncoding, newBase64LineWriter(subWriter)) 264 | w.err = f(wc) 265 | wc.Close() 266 | } else if enc == Unencoded { 267 | w.err = f(subWriter) 268 | } else { 269 | wc := newQPWriter(subWriter) 270 | w.err = f(wc) 271 | wc.Close() 272 | } 273 | } 274 | 275 | // As required by RFC 2045, 6.7. (page 21) for quoted-printable, and 276 | // RFC 2045, 6.8. (page 25) for base64. 277 | const maxLineLen = 76 278 | 279 | // base64LineWriter limits text encoded in base64 to 76 characters per line 280 | type base64LineWriter struct { 281 | w io.Writer 282 | lineLen int 283 | } 284 | 285 | func newBase64LineWriter(w io.Writer) *base64LineWriter { 286 | return &base64LineWriter{w: w} 287 | } 288 | 289 | func (w *base64LineWriter) Write(p []byte) (int, error) { 290 | n := 0 291 | for len(p)+w.lineLen > maxLineLen { 292 | w.w.Write(p[:maxLineLen-w.lineLen]) 293 | w.w.Write([]byte("\r\n")) 294 | p = p[maxLineLen-w.lineLen:] 295 | n += maxLineLen - w.lineLen 296 | w.lineLen = 0 297 | } 298 | 299 | w.w.Write(p) 300 | w.lineLen += len(p) 301 | 302 | return n + len(p), nil 303 | } 304 | 305 | // Stubbed out for testing. 306 | var now = time.Now 307 | -------------------------------------------------------------------------------- /vendor/github.com/go-gomail/gomail/message.go: -------------------------------------------------------------------------------- 1 | package gomail 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | ) 10 | 11 | // Message represents an email. 12 | type Message struct { 13 | header header 14 | parts []*part 15 | attachments []*file 16 | embedded []*file 17 | charset string 18 | encoding Encoding 19 | hEncoder mimeEncoder 20 | buf bytes.Buffer 21 | } 22 | 23 | type header map[string][]string 24 | 25 | type part struct { 26 | contentType string 27 | copier func(io.Writer) error 28 | encoding Encoding 29 | } 30 | 31 | // NewMessage creates a new message. It uses UTF-8 and quoted-printable encoding 32 | // by default. 33 | func NewMessage(settings ...MessageSetting) *Message { 34 | m := &Message{ 35 | header: make(header), 36 | charset: "UTF-8", 37 | encoding: QuotedPrintable, 38 | } 39 | 40 | m.applySettings(settings) 41 | 42 | if m.encoding == Base64 { 43 | m.hEncoder = bEncoding 44 | } else { 45 | m.hEncoder = qEncoding 46 | } 47 | 48 | return m 49 | } 50 | 51 | // Reset resets the message so it can be reused. The message keeps its previous 52 | // settings so it is in the same state that after a call to NewMessage. 53 | func (m *Message) Reset() { 54 | for k := range m.header { 55 | delete(m.header, k) 56 | } 57 | m.parts = nil 58 | m.attachments = nil 59 | m.embedded = nil 60 | } 61 | 62 | func (m *Message) applySettings(settings []MessageSetting) { 63 | for _, s := range settings { 64 | s(m) 65 | } 66 | } 67 | 68 | // A MessageSetting can be used as an argument in NewMessage to configure an 69 | // email. 70 | type MessageSetting func(m *Message) 71 | 72 | // SetCharset is a message setting to set the charset of the email. 73 | func SetCharset(charset string) MessageSetting { 74 | return func(m *Message) { 75 | m.charset = charset 76 | } 77 | } 78 | 79 | // SetEncoding is a message setting to set the encoding of the email. 80 | func SetEncoding(enc Encoding) MessageSetting { 81 | return func(m *Message) { 82 | m.encoding = enc 83 | } 84 | } 85 | 86 | // Encoding represents a MIME encoding scheme like quoted-printable or base64. 87 | type Encoding string 88 | 89 | const ( 90 | // QuotedPrintable represents the quoted-printable encoding as defined in 91 | // RFC 2045. 92 | QuotedPrintable Encoding = "quoted-printable" 93 | // Base64 represents the base64 encoding as defined in RFC 2045. 94 | Base64 Encoding = "base64" 95 | // Unencoded can be used to avoid encoding the body of an email. The headers 96 | // will still be encoded using quoted-printable encoding. 97 | Unencoded Encoding = "8bit" 98 | ) 99 | 100 | // SetHeader sets a value to the given header field. 101 | func (m *Message) SetHeader(field string, value ...string) { 102 | m.encodeHeader(value) 103 | m.header[field] = value 104 | } 105 | 106 | func (m *Message) encodeHeader(values []string) { 107 | for i := range values { 108 | values[i] = m.encodeString(values[i]) 109 | } 110 | } 111 | 112 | func (m *Message) encodeString(value string) string { 113 | return m.hEncoder.Encode(m.charset, value) 114 | } 115 | 116 | // SetHeaders sets the message headers. 117 | func (m *Message) SetHeaders(h map[string][]string) { 118 | for k, v := range h { 119 | m.SetHeader(k, v...) 120 | } 121 | } 122 | 123 | // SetAddressHeader sets an address to the given header field. 124 | func (m *Message) SetAddressHeader(field, address, name string) { 125 | m.header[field] = []string{m.FormatAddress(address, name)} 126 | } 127 | 128 | // FormatAddress formats an address and a name as a valid RFC 5322 address. 129 | func (m *Message) FormatAddress(address, name string) string { 130 | if name == "" { 131 | return address 132 | } 133 | 134 | enc := m.encodeString(name) 135 | if enc == name { 136 | m.buf.WriteByte('"') 137 | for i := 0; i < len(name); i++ { 138 | b := name[i] 139 | if b == '\\' || b == '"' { 140 | m.buf.WriteByte('\\') 141 | } 142 | m.buf.WriteByte(b) 143 | } 144 | m.buf.WriteByte('"') 145 | } else if hasSpecials(name) { 146 | m.buf.WriteString(bEncoding.Encode(m.charset, name)) 147 | } else { 148 | m.buf.WriteString(enc) 149 | } 150 | m.buf.WriteString(" <") 151 | m.buf.WriteString(address) 152 | m.buf.WriteByte('>') 153 | 154 | addr := m.buf.String() 155 | m.buf.Reset() 156 | return addr 157 | } 158 | 159 | func hasSpecials(text string) bool { 160 | for i := 0; i < len(text); i++ { 161 | switch c := text[i]; c { 162 | case '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '.', '"': 163 | return true 164 | } 165 | } 166 | 167 | return false 168 | } 169 | 170 | // SetDateHeader sets a date to the given header field. 171 | func (m *Message) SetDateHeader(field string, date time.Time) { 172 | m.header[field] = []string{m.FormatDate(date)} 173 | } 174 | 175 | // FormatDate formats a date as a valid RFC 5322 date. 176 | func (m *Message) FormatDate(date time.Time) string { 177 | return date.Format(time.RFC1123Z) 178 | } 179 | 180 | // GetHeader gets a header field. 181 | func (m *Message) GetHeader(field string) []string { 182 | return m.header[field] 183 | } 184 | 185 | // SetBody sets the body of the message. It replaces any content previously set 186 | // by SetBody, AddAlternative or AddAlternativeWriter. 187 | func (m *Message) SetBody(contentType, body string, settings ...PartSetting) { 188 | m.parts = []*part{m.newPart(contentType, newCopier(body), settings)} 189 | } 190 | 191 | // AddAlternative adds an alternative part to the message. 192 | // 193 | // It is commonly used to send HTML emails that default to the plain text 194 | // version for backward compatibility. AddAlternative appends the new part to 195 | // the end of the message. So the plain text part should be added before the 196 | // HTML part. See http://en.wikipedia.org/wiki/MIME#Alternative 197 | func (m *Message) AddAlternative(contentType, body string, settings ...PartSetting) { 198 | m.AddAlternativeWriter(contentType, newCopier(body), settings...) 199 | } 200 | 201 | func newCopier(s string) func(io.Writer) error { 202 | return func(w io.Writer) error { 203 | _, err := io.WriteString(w, s) 204 | return err 205 | } 206 | } 207 | 208 | // AddAlternativeWriter adds an alternative part to the message. It can be 209 | // useful with the text/template or html/template packages. 210 | func (m *Message) AddAlternativeWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) { 211 | m.parts = append(m.parts, m.newPart(contentType, f, settings)) 212 | } 213 | 214 | func (m *Message) newPart(contentType string, f func(io.Writer) error, settings []PartSetting) *part { 215 | p := &part{ 216 | contentType: contentType, 217 | copier: f, 218 | encoding: m.encoding, 219 | } 220 | 221 | for _, s := range settings { 222 | s(p) 223 | } 224 | 225 | return p 226 | } 227 | 228 | // A PartSetting can be used as an argument in Message.SetBody, 229 | // Message.AddAlternative or Message.AddAlternativeWriter to configure the part 230 | // added to a message. 231 | type PartSetting func(*part) 232 | 233 | // SetPartEncoding sets the encoding of the part added to the message. By 234 | // default, parts use the same encoding than the message. 235 | func SetPartEncoding(e Encoding) PartSetting { 236 | return PartSetting(func(p *part) { 237 | p.encoding = e 238 | }) 239 | } 240 | 241 | type file struct { 242 | Name string 243 | Header map[string][]string 244 | CopyFunc func(w io.Writer) error 245 | } 246 | 247 | func (f *file) setHeader(field, value string) { 248 | f.Header[field] = []string{value} 249 | } 250 | 251 | // A FileSetting can be used as an argument in Message.Attach or Message.Embed. 252 | type FileSetting func(*file) 253 | 254 | // SetHeader is a file setting to set the MIME header of the message part that 255 | // contains the file content. 256 | // 257 | // Mandatory headers are automatically added if they are not set when sending 258 | // the email. 259 | func SetHeader(h map[string][]string) FileSetting { 260 | return func(f *file) { 261 | for k, v := range h { 262 | f.Header[k] = v 263 | } 264 | } 265 | } 266 | 267 | // Rename is a file setting to set the name of the attachment if the name is 268 | // different than the filename on disk. 269 | func Rename(name string) FileSetting { 270 | return func(f *file) { 271 | f.Name = name 272 | } 273 | } 274 | 275 | // SetCopyFunc is a file setting to replace the function that runs when the 276 | // message is sent. It should copy the content of the file to the io.Writer. 277 | // 278 | // The default copy function opens the file with the given filename, and copy 279 | // its content to the io.Writer. 280 | func SetCopyFunc(f func(io.Writer) error) FileSetting { 281 | return func(fi *file) { 282 | fi.CopyFunc = f 283 | } 284 | } 285 | 286 | func (m *Message) appendFile(list []*file, name string, settings []FileSetting) []*file { 287 | f := &file{ 288 | Name: filepath.Base(name), 289 | Header: make(map[string][]string), 290 | CopyFunc: func(w io.Writer) error { 291 | h, err := os.Open(name) 292 | if err != nil { 293 | return err 294 | } 295 | if _, err := io.Copy(w, h); err != nil { 296 | h.Close() 297 | return err 298 | } 299 | return h.Close() 300 | }, 301 | } 302 | 303 | for _, s := range settings { 304 | s(f) 305 | } 306 | 307 | if list == nil { 308 | return []*file{f} 309 | } 310 | 311 | return append(list, f) 312 | } 313 | 314 | // Attach attaches the files to the email. 315 | func (m *Message) Attach(filename string, settings ...FileSetting) { 316 | m.attachments = m.appendFile(m.attachments, filename, settings) 317 | } 318 | 319 | // Embed embeds the images to the email. 320 | func (m *Message) Embed(filename string, settings ...FileSetting) { 321 | m.embedded = m.appendFile(m.embedded, filename, settings) 322 | } 323 | -------------------------------------------------------------------------------- /vendor/gopkg.in/ini.v1/parser.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package ini 16 | 17 | import ( 18 | "bufio" 19 | "bytes" 20 | "fmt" 21 | "io" 22 | "strconv" 23 | "strings" 24 | "unicode" 25 | ) 26 | 27 | type tokenType int 28 | 29 | const ( 30 | _TOKEN_INVALID tokenType = iota 31 | _TOKEN_COMMENT 32 | _TOKEN_SECTION 33 | _TOKEN_KEY 34 | ) 35 | 36 | type parser struct { 37 | buf *bufio.Reader 38 | isEOF bool 39 | count int 40 | comment *bytes.Buffer 41 | } 42 | 43 | func newParser(r io.Reader) *parser { 44 | return &parser{ 45 | buf: bufio.NewReader(r), 46 | count: 1, 47 | comment: &bytes.Buffer{}, 48 | } 49 | } 50 | 51 | // BOM handles header of UTF-8, UTF-16 LE and UTF-16 BE's BOM format. 52 | // http://en.wikipedia.org/wiki/Byte_order_mark#Representations_of_byte_order_marks_by_encoding 53 | func (p *parser) BOM() error { 54 | mask, err := p.buf.Peek(2) 55 | if err != nil && err != io.EOF { 56 | return err 57 | } else if len(mask) < 2 { 58 | return nil 59 | } 60 | 61 | switch { 62 | case mask[0] == 254 && mask[1] == 255: 63 | fallthrough 64 | case mask[0] == 255 && mask[1] == 254: 65 | p.buf.Read(mask) 66 | case mask[0] == 239 && mask[1] == 187: 67 | mask, err := p.buf.Peek(3) 68 | if err != nil && err != io.EOF { 69 | return err 70 | } else if len(mask) < 3 { 71 | return nil 72 | } 73 | if mask[2] == 191 { 74 | p.buf.Read(mask) 75 | } 76 | } 77 | return nil 78 | } 79 | 80 | func (p *parser) readUntil(delim byte) ([]byte, error) { 81 | data, err := p.buf.ReadBytes(delim) 82 | if err != nil { 83 | if err == io.EOF { 84 | p.isEOF = true 85 | } else { 86 | return nil, err 87 | } 88 | } 89 | return data, nil 90 | } 91 | 92 | func cleanComment(in []byte) ([]byte, bool) { 93 | i := bytes.IndexAny(in, "#;") 94 | if i == -1 { 95 | return nil, false 96 | } 97 | return in[i:], true 98 | } 99 | 100 | func readKeyName(in []byte) (string, int, error) { 101 | line := string(in) 102 | 103 | // Check if key name surrounded by quotes. 104 | var keyQuote string 105 | if line[0] == '"' { 106 | if len(line) > 6 && string(line[0:3]) == `"""` { 107 | keyQuote = `"""` 108 | } else { 109 | keyQuote = `"` 110 | } 111 | } else if line[0] == '`' { 112 | keyQuote = "`" 113 | } 114 | 115 | // Get out key name 116 | endIdx := -1 117 | if len(keyQuote) > 0 { 118 | startIdx := len(keyQuote) 119 | // FIXME: fail case -> """"""name"""=value 120 | pos := strings.Index(line[startIdx:], keyQuote) 121 | if pos == -1 { 122 | return "", -1, fmt.Errorf("missing closing key quote: %s", line) 123 | } 124 | pos += startIdx 125 | 126 | // Find key-value delimiter 127 | i := strings.IndexAny(line[pos+startIdx:], "=:") 128 | if i < 0 { 129 | return "", -1, ErrDelimiterNotFound{line} 130 | } 131 | endIdx = pos + i 132 | return strings.TrimSpace(line[startIdx:pos]), endIdx + startIdx + 1, nil 133 | } 134 | 135 | endIdx = strings.IndexAny(line, "=:") 136 | if endIdx < 0 { 137 | return "", -1, ErrDelimiterNotFound{line} 138 | } 139 | return strings.TrimSpace(line[0:endIdx]), endIdx + 1, nil 140 | } 141 | 142 | func (p *parser) readMultilines(line, val, valQuote string) (string, error) { 143 | for { 144 | data, err := p.readUntil('\n') 145 | if err != nil { 146 | return "", err 147 | } 148 | next := string(data) 149 | 150 | pos := strings.LastIndex(next, valQuote) 151 | if pos > -1 { 152 | val += next[:pos] 153 | 154 | comment, has := cleanComment([]byte(next[pos:])) 155 | if has { 156 | p.comment.Write(bytes.TrimSpace(comment)) 157 | } 158 | break 159 | } 160 | val += next 161 | if p.isEOF { 162 | return "", fmt.Errorf("missing closing key quote from '%s' to '%s'", line, next) 163 | } 164 | } 165 | return val, nil 166 | } 167 | 168 | func (p *parser) readContinuationLines(val string) (string, error) { 169 | for { 170 | data, err := p.readUntil('\n') 171 | if err != nil { 172 | return "", err 173 | } 174 | next := strings.TrimSpace(string(data)) 175 | 176 | if len(next) == 0 { 177 | break 178 | } 179 | val += next 180 | if val[len(val)-1] != '\\' { 181 | break 182 | } 183 | val = val[:len(val)-1] 184 | } 185 | return val, nil 186 | } 187 | 188 | // hasSurroundedQuote check if and only if the first and last characters 189 | // are quotes \" or \'. 190 | // It returns false if any other parts also contain same kind of quotes. 191 | func hasSurroundedQuote(in string, quote byte) bool { 192 | return len(in) >= 2 && in[0] == quote && in[len(in)-1] == quote && 193 | strings.IndexByte(in[1:], quote) == len(in)-2 194 | } 195 | 196 | func (p *parser) readValue(in []byte, ignoreContinuation, ignoreInlineComment bool) (string, error) { 197 | line := strings.TrimLeftFunc(string(in), unicode.IsSpace) 198 | if len(line) == 0 { 199 | return "", nil 200 | } 201 | 202 | var valQuote string 203 | if len(line) > 3 && string(line[0:3]) == `"""` { 204 | valQuote = `"""` 205 | } else if line[0] == '`' { 206 | valQuote = "`" 207 | } 208 | 209 | if len(valQuote) > 0 { 210 | startIdx := len(valQuote) 211 | pos := strings.LastIndex(line[startIdx:], valQuote) 212 | // Check for multi-line value 213 | if pos == -1 { 214 | return p.readMultilines(line, line[startIdx:], valQuote) 215 | } 216 | 217 | return line[startIdx : pos+startIdx], nil 218 | } 219 | 220 | // Won't be able to reach here if value only contains whitespace 221 | line = strings.TrimSpace(line) 222 | 223 | // Check continuation lines when desired 224 | if !ignoreContinuation && line[len(line)-1] == '\\' { 225 | return p.readContinuationLines(line[:len(line)-1]) 226 | } 227 | 228 | // Check if ignore inline comment 229 | if !ignoreInlineComment { 230 | i := strings.IndexAny(line, "#;") 231 | if i > -1 { 232 | p.comment.WriteString(line[i:]) 233 | line = strings.TrimSpace(line[:i]) 234 | } 235 | } 236 | 237 | // Trim single quotes 238 | if hasSurroundedQuote(line, '\'') || 239 | hasSurroundedQuote(line, '"') { 240 | line = line[1 : len(line)-1] 241 | } 242 | return line, nil 243 | } 244 | 245 | // parse parses data through an io.Reader. 246 | func (f *File) parse(reader io.Reader) (err error) { 247 | p := newParser(reader) 248 | if err = p.BOM(); err != nil { 249 | return fmt.Errorf("BOM: %v", err) 250 | } 251 | 252 | // Ignore error because default section name is never empty string. 253 | section, _ := f.NewSection(DEFAULT_SECTION) 254 | 255 | var line []byte 256 | var inUnparseableSection bool 257 | for !p.isEOF { 258 | line, err = p.readUntil('\n') 259 | if err != nil { 260 | return err 261 | } 262 | 263 | line = bytes.TrimLeftFunc(line, unicode.IsSpace) 264 | if len(line) == 0 { 265 | continue 266 | } 267 | 268 | // Comments 269 | if line[0] == '#' || line[0] == ';' { 270 | // Note: we do not care ending line break, 271 | // it is needed for adding second line, 272 | // so just clean it once at the end when set to value. 273 | p.comment.Write(line) 274 | continue 275 | } 276 | 277 | // Section 278 | if line[0] == '[' { 279 | // Read to the next ']' (TODO: support quoted strings) 280 | // TODO(unknwon): use LastIndexByte when stop supporting Go1.4 281 | closeIdx := bytes.LastIndex(line, []byte("]")) 282 | if closeIdx == -1 { 283 | return fmt.Errorf("unclosed section: %s", line) 284 | } 285 | 286 | name := string(line[1:closeIdx]) 287 | section, err = f.NewSection(name) 288 | if err != nil { 289 | return err 290 | } 291 | 292 | comment, has := cleanComment(line[closeIdx+1:]) 293 | if has { 294 | p.comment.Write(comment) 295 | } 296 | 297 | section.Comment = strings.TrimSpace(p.comment.String()) 298 | 299 | // Reset aotu-counter and comments 300 | p.comment.Reset() 301 | p.count = 1 302 | 303 | inUnparseableSection = false 304 | for i := range f.options.UnparseableSections { 305 | if f.options.UnparseableSections[i] == name || 306 | (f.options.Insensitive && strings.ToLower(f.options.UnparseableSections[i]) == strings.ToLower(name)) { 307 | inUnparseableSection = true 308 | continue 309 | } 310 | } 311 | continue 312 | } 313 | 314 | if inUnparseableSection { 315 | section.isRawSection = true 316 | section.rawBody += string(line) 317 | continue 318 | } 319 | 320 | kname, offset, err := readKeyName(line) 321 | if err != nil { 322 | // Treat as boolean key when desired, and whole line is key name. 323 | if IsErrDelimiterNotFound(err) && f.options.AllowBooleanKeys { 324 | kname, err := p.readValue(line, f.options.IgnoreContinuation, f.options.IgnoreInlineComment) 325 | if err != nil { 326 | return err 327 | } 328 | key, err := section.NewBooleanKey(kname) 329 | if err != nil { 330 | return err 331 | } 332 | key.Comment = strings.TrimSpace(p.comment.String()) 333 | p.comment.Reset() 334 | continue 335 | } 336 | return err 337 | } 338 | 339 | // Auto increment. 340 | isAutoIncr := false 341 | if kname == "-" { 342 | isAutoIncr = true 343 | kname = "#" + strconv.Itoa(p.count) 344 | p.count++ 345 | } 346 | 347 | value, err := p.readValue(line[offset:], f.options.IgnoreContinuation, f.options.IgnoreInlineComment) 348 | if err != nil { 349 | return err 350 | } 351 | 352 | key, err := section.NewKey(kname, value) 353 | if err != nil { 354 | return err 355 | } 356 | key.isAutoIncrement = isAutoIncr 357 | key.Comment = strings.TrimSpace(p.comment.String()) 358 | p.comment.Reset() 359 | } 360 | return nil 361 | } 362 | -------------------------------------------------------------------------------- /vendor/gopkg.in/ini.v1/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /vendor/gopkg.in/ini.v1/struct.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package ini 16 | 17 | import ( 18 | "bytes" 19 | "errors" 20 | "fmt" 21 | "reflect" 22 | "strings" 23 | "time" 24 | "unicode" 25 | ) 26 | 27 | // NameMapper represents a ini tag name mapper. 28 | type NameMapper func(string) string 29 | 30 | // Built-in name getters. 31 | var ( 32 | // AllCapsUnderscore converts to format ALL_CAPS_UNDERSCORE. 33 | AllCapsUnderscore NameMapper = func(raw string) string { 34 | newstr := make([]rune, 0, len(raw)) 35 | for i, chr := range raw { 36 | if isUpper := 'A' <= chr && chr <= 'Z'; isUpper { 37 | if i > 0 { 38 | newstr = append(newstr, '_') 39 | } 40 | } 41 | newstr = append(newstr, unicode.ToUpper(chr)) 42 | } 43 | return string(newstr) 44 | } 45 | // TitleUnderscore converts to format title_underscore. 46 | TitleUnderscore NameMapper = func(raw string) string { 47 | newstr := make([]rune, 0, len(raw)) 48 | for i, chr := range raw { 49 | if isUpper := 'A' <= chr && chr <= 'Z'; isUpper { 50 | if i > 0 { 51 | newstr = append(newstr, '_') 52 | } 53 | chr -= ('A' - 'a') 54 | } 55 | newstr = append(newstr, chr) 56 | } 57 | return string(newstr) 58 | } 59 | ) 60 | 61 | func (s *Section) parseFieldName(raw, actual string) string { 62 | if len(actual) > 0 { 63 | return actual 64 | } 65 | if s.f.NameMapper != nil { 66 | return s.f.NameMapper(raw) 67 | } 68 | return raw 69 | } 70 | 71 | func parseDelim(actual string) string { 72 | if len(actual) > 0 { 73 | return actual 74 | } 75 | return "," 76 | } 77 | 78 | var reflectTime = reflect.TypeOf(time.Now()).Kind() 79 | 80 | // setSliceWithProperType sets proper values to slice based on its type. 81 | func setSliceWithProperType(key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error { 82 | var strs []string 83 | if allowShadow { 84 | strs = key.StringsWithShadows(delim) 85 | } else { 86 | strs = key.Strings(delim) 87 | } 88 | 89 | numVals := len(strs) 90 | if numVals == 0 { 91 | return nil 92 | } 93 | 94 | var vals interface{} 95 | var err error 96 | 97 | sliceOf := field.Type().Elem().Kind() 98 | switch sliceOf { 99 | case reflect.String: 100 | vals = strs 101 | case reflect.Int: 102 | vals, err = key.parseInts(strs, true, false) 103 | case reflect.Int64: 104 | vals, err = key.parseInt64s(strs, true, false) 105 | case reflect.Uint: 106 | vals, err = key.parseUints(strs, true, false) 107 | case reflect.Uint64: 108 | vals, err = key.parseUint64s(strs, true, false) 109 | case reflect.Float64: 110 | vals, err = key.parseFloat64s(strs, true, false) 111 | case reflectTime: 112 | vals, err = key.parseTimesFormat(time.RFC3339, strs, true, false) 113 | default: 114 | return fmt.Errorf("unsupported type '[]%s'", sliceOf) 115 | } 116 | if isStrict { 117 | return err 118 | } 119 | 120 | slice := reflect.MakeSlice(field.Type(), numVals, numVals) 121 | for i := 0; i < numVals; i++ { 122 | switch sliceOf { 123 | case reflect.String: 124 | slice.Index(i).Set(reflect.ValueOf(vals.([]string)[i])) 125 | case reflect.Int: 126 | slice.Index(i).Set(reflect.ValueOf(vals.([]int)[i])) 127 | case reflect.Int64: 128 | slice.Index(i).Set(reflect.ValueOf(vals.([]int64)[i])) 129 | case reflect.Uint: 130 | slice.Index(i).Set(reflect.ValueOf(vals.([]uint)[i])) 131 | case reflect.Uint64: 132 | slice.Index(i).Set(reflect.ValueOf(vals.([]uint64)[i])) 133 | case reflect.Float64: 134 | slice.Index(i).Set(reflect.ValueOf(vals.([]float64)[i])) 135 | case reflectTime: 136 | slice.Index(i).Set(reflect.ValueOf(vals.([]time.Time)[i])) 137 | } 138 | } 139 | field.Set(slice) 140 | return nil 141 | } 142 | 143 | func wrapStrictError(err error, isStrict bool) error { 144 | if isStrict { 145 | return err 146 | } 147 | return nil 148 | } 149 | 150 | // setWithProperType sets proper value to field based on its type, 151 | // but it does not return error for failing parsing, 152 | // because we want to use default value that is already assigned to strcut. 153 | func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error { 154 | switch t.Kind() { 155 | case reflect.String: 156 | if len(key.String()) == 0 { 157 | return nil 158 | } 159 | field.SetString(key.String()) 160 | case reflect.Bool: 161 | boolVal, err := key.Bool() 162 | if err != nil { 163 | return wrapStrictError(err, isStrict) 164 | } 165 | field.SetBool(boolVal) 166 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 167 | durationVal, err := key.Duration() 168 | // Skip zero value 169 | if err == nil && int(durationVal) > 0 { 170 | field.Set(reflect.ValueOf(durationVal)) 171 | return nil 172 | } 173 | 174 | intVal, err := key.Int64() 175 | if err != nil { 176 | return wrapStrictError(err, isStrict) 177 | } 178 | field.SetInt(intVal) 179 | // byte is an alias for uint8, so supporting uint8 breaks support for byte 180 | case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: 181 | durationVal, err := key.Duration() 182 | // Skip zero value 183 | if err == nil && int(durationVal) > 0 { 184 | field.Set(reflect.ValueOf(durationVal)) 185 | return nil 186 | } 187 | 188 | uintVal, err := key.Uint64() 189 | if err != nil { 190 | return wrapStrictError(err, isStrict) 191 | } 192 | field.SetUint(uintVal) 193 | 194 | case reflect.Float32, reflect.Float64: 195 | floatVal, err := key.Float64() 196 | if err != nil { 197 | return wrapStrictError(err, isStrict) 198 | } 199 | field.SetFloat(floatVal) 200 | case reflectTime: 201 | timeVal, err := key.Time() 202 | if err != nil { 203 | return wrapStrictError(err, isStrict) 204 | } 205 | field.Set(reflect.ValueOf(timeVal)) 206 | case reflect.Slice: 207 | return setSliceWithProperType(key, field, delim, allowShadow, isStrict) 208 | default: 209 | return fmt.Errorf("unsupported type '%s'", t) 210 | } 211 | return nil 212 | } 213 | 214 | func parseTagOptions(tag string) (rawName string, omitEmpty bool, allowShadow bool) { 215 | opts := strings.SplitN(tag, ",", 3) 216 | rawName = opts[0] 217 | if len(opts) > 1 { 218 | omitEmpty = opts[1] == "omitempty" 219 | } 220 | if len(opts) > 2 { 221 | allowShadow = opts[2] == "allowshadow" 222 | } 223 | return rawName, omitEmpty, allowShadow 224 | } 225 | 226 | func (s *Section) mapTo(val reflect.Value, isStrict bool) error { 227 | if val.Kind() == reflect.Ptr { 228 | val = val.Elem() 229 | } 230 | typ := val.Type() 231 | 232 | for i := 0; i < typ.NumField(); i++ { 233 | field := val.Field(i) 234 | tpField := typ.Field(i) 235 | 236 | tag := tpField.Tag.Get("ini") 237 | if tag == "-" { 238 | continue 239 | } 240 | 241 | rawName, _, allowShadow := parseTagOptions(tag) 242 | fieldName := s.parseFieldName(tpField.Name, rawName) 243 | if len(fieldName) == 0 || !field.CanSet() { 244 | continue 245 | } 246 | 247 | isAnonymous := tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous 248 | isStruct := tpField.Type.Kind() == reflect.Struct 249 | if isAnonymous { 250 | field.Set(reflect.New(tpField.Type.Elem())) 251 | } 252 | 253 | if isAnonymous || isStruct { 254 | if sec, err := s.f.GetSection(fieldName); err == nil { 255 | if err = sec.mapTo(field, isStrict); err != nil { 256 | return fmt.Errorf("error mapping field(%s): %v", fieldName, err) 257 | } 258 | continue 259 | } 260 | } 261 | 262 | if key, err := s.GetKey(fieldName); err == nil { 263 | delim := parseDelim(tpField.Tag.Get("delim")) 264 | if err = setWithProperType(tpField.Type, key, field, delim, allowShadow, isStrict); err != nil { 265 | return fmt.Errorf("error mapping field(%s): %v", fieldName, err) 266 | } 267 | } 268 | } 269 | return nil 270 | } 271 | 272 | // MapTo maps section to given struct. 273 | func (s *Section) MapTo(v interface{}) error { 274 | typ := reflect.TypeOf(v) 275 | val := reflect.ValueOf(v) 276 | if typ.Kind() == reflect.Ptr { 277 | typ = typ.Elem() 278 | val = val.Elem() 279 | } else { 280 | return errors.New("cannot map to non-pointer struct") 281 | } 282 | 283 | return s.mapTo(val, false) 284 | } 285 | 286 | // MapTo maps section to given struct in strict mode, 287 | // which returns all possible error including value parsing error. 288 | func (s *Section) StrictMapTo(v interface{}) error { 289 | typ := reflect.TypeOf(v) 290 | val := reflect.ValueOf(v) 291 | if typ.Kind() == reflect.Ptr { 292 | typ = typ.Elem() 293 | val = val.Elem() 294 | } else { 295 | return errors.New("cannot map to non-pointer struct") 296 | } 297 | 298 | return s.mapTo(val, true) 299 | } 300 | 301 | // MapTo maps file to given struct. 302 | func (f *File) MapTo(v interface{}) error { 303 | return f.Section("").MapTo(v) 304 | } 305 | 306 | // MapTo maps file to given struct in strict mode, 307 | // which returns all possible error including value parsing error. 308 | func (f *File) StrictMapTo(v interface{}) error { 309 | return f.Section("").StrictMapTo(v) 310 | } 311 | 312 | // MapTo maps data sources to given struct with name mapper. 313 | func MapToWithMapper(v interface{}, mapper NameMapper, source interface{}, others ...interface{}) error { 314 | cfg, err := Load(source, others...) 315 | if err != nil { 316 | return err 317 | } 318 | cfg.NameMapper = mapper 319 | return cfg.MapTo(v) 320 | } 321 | 322 | // StrictMapToWithMapper maps data sources to given struct with name mapper in strict mode, 323 | // which returns all possible error including value parsing error. 324 | func StrictMapToWithMapper(v interface{}, mapper NameMapper, source interface{}, others ...interface{}) error { 325 | cfg, err := Load(source, others...) 326 | if err != nil { 327 | return err 328 | } 329 | cfg.NameMapper = mapper 330 | return cfg.StrictMapTo(v) 331 | } 332 | 333 | // MapTo maps data sources to given struct. 334 | func MapTo(v, source interface{}, others ...interface{}) error { 335 | return MapToWithMapper(v, nil, source, others...) 336 | } 337 | 338 | // StrictMapTo maps data sources to given struct in strict mode, 339 | // which returns all possible error including value parsing error. 340 | func StrictMapTo(v, source interface{}, others ...interface{}) error { 341 | return StrictMapToWithMapper(v, nil, source, others...) 342 | } 343 | 344 | // reflectSliceWithProperType does the opposite thing as setSliceWithProperType. 345 | func reflectSliceWithProperType(key *Key, field reflect.Value, delim string) error { 346 | slice := field.Slice(0, field.Len()) 347 | if field.Len() == 0 { 348 | return nil 349 | } 350 | 351 | var buf bytes.Buffer 352 | sliceOf := field.Type().Elem().Kind() 353 | for i := 0; i < field.Len(); i++ { 354 | switch sliceOf { 355 | case reflect.String: 356 | buf.WriteString(slice.Index(i).String()) 357 | case reflect.Int, reflect.Int64: 358 | buf.WriteString(fmt.Sprint(slice.Index(i).Int())) 359 | case reflect.Uint, reflect.Uint64: 360 | buf.WriteString(fmt.Sprint(slice.Index(i).Uint())) 361 | case reflect.Float64: 362 | buf.WriteString(fmt.Sprint(slice.Index(i).Float())) 363 | case reflectTime: 364 | buf.WriteString(slice.Index(i).Interface().(time.Time).Format(time.RFC3339)) 365 | default: 366 | return fmt.Errorf("unsupported type '[]%s'", sliceOf) 367 | } 368 | buf.WriteString(delim) 369 | } 370 | key.SetValue(buf.String()[:buf.Len()-1]) 371 | return nil 372 | } 373 | 374 | // reflectWithProperType does the opposite thing as setWithProperType. 375 | func reflectWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string) error { 376 | switch t.Kind() { 377 | case reflect.String: 378 | key.SetValue(field.String()) 379 | case reflect.Bool: 380 | key.SetValue(fmt.Sprint(field.Bool())) 381 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 382 | key.SetValue(fmt.Sprint(field.Int())) 383 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 384 | key.SetValue(fmt.Sprint(field.Uint())) 385 | case reflect.Float32, reflect.Float64: 386 | key.SetValue(fmt.Sprint(field.Float())) 387 | case reflectTime: 388 | key.SetValue(fmt.Sprint(field.Interface().(time.Time).Format(time.RFC3339))) 389 | case reflect.Slice: 390 | return reflectSliceWithProperType(key, field, delim) 391 | default: 392 | return fmt.Errorf("unsupported type '%s'", t) 393 | } 394 | return nil 395 | } 396 | 397 | // CR: copied from encoding/json/encode.go with modifications of time.Time support. 398 | // TODO: add more test coverage. 399 | func isEmptyValue(v reflect.Value) bool { 400 | switch v.Kind() { 401 | case reflect.Array, reflect.Map, reflect.Slice, reflect.String: 402 | return v.Len() == 0 403 | case reflect.Bool: 404 | return !v.Bool() 405 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 406 | return v.Int() == 0 407 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 408 | return v.Uint() == 0 409 | case reflect.Float32, reflect.Float64: 410 | return v.Float() == 0 411 | case reflectTime: 412 | return v.Interface().(time.Time).IsZero() 413 | case reflect.Interface, reflect.Ptr: 414 | return v.IsNil() 415 | } 416 | return false 417 | } 418 | 419 | func (s *Section) reflectFrom(val reflect.Value) error { 420 | if val.Kind() == reflect.Ptr { 421 | val = val.Elem() 422 | } 423 | typ := val.Type() 424 | 425 | for i := 0; i < typ.NumField(); i++ { 426 | field := val.Field(i) 427 | tpField := typ.Field(i) 428 | 429 | tag := tpField.Tag.Get("ini") 430 | if tag == "-" { 431 | continue 432 | } 433 | 434 | opts := strings.SplitN(tag, ",", 2) 435 | if len(opts) == 2 && opts[1] == "omitempty" && isEmptyValue(field) { 436 | continue 437 | } 438 | 439 | fieldName := s.parseFieldName(tpField.Name, opts[0]) 440 | if len(fieldName) == 0 || !field.CanSet() { 441 | continue 442 | } 443 | 444 | if (tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous) || 445 | (tpField.Type.Kind() == reflect.Struct && tpField.Type.Name() != "Time") { 446 | // Note: The only error here is section doesn't exist. 447 | sec, err := s.f.GetSection(fieldName) 448 | if err != nil { 449 | // Note: fieldName can never be empty here, ignore error. 450 | sec, _ = s.f.NewSection(fieldName) 451 | } 452 | if err = sec.reflectFrom(field); err != nil { 453 | return fmt.Errorf("error reflecting field (%s): %v", fieldName, err) 454 | } 455 | continue 456 | } 457 | 458 | // Note: Same reason as secion. 459 | key, err := s.GetKey(fieldName) 460 | if err != nil { 461 | key, _ = s.NewKey(fieldName, "") 462 | } 463 | if err = reflectWithProperType(tpField.Type, key, field, parseDelim(tpField.Tag.Get("delim"))); err != nil { 464 | return fmt.Errorf("error reflecting field (%s): %v", fieldName, err) 465 | } 466 | 467 | } 468 | return nil 469 | } 470 | 471 | // ReflectFrom reflects secion from given struct. 472 | func (s *Section) ReflectFrom(v interface{}) error { 473 | typ := reflect.TypeOf(v) 474 | val := reflect.ValueOf(v) 475 | if typ.Kind() == reflect.Ptr { 476 | typ = typ.Elem() 477 | val = val.Elem() 478 | } else { 479 | return errors.New("cannot reflect from non-pointer struct") 480 | } 481 | 482 | return s.reflectFrom(val) 483 | } 484 | 485 | // ReflectFrom reflects file from given struct. 486 | func (f *File) ReflectFrom(v interface{}) error { 487 | return f.Section("").ReflectFrom(v) 488 | } 489 | 490 | // ReflectFrom reflects data sources from given struct with name mapper. 491 | func ReflectFromWithMapper(cfg *File, v interface{}, mapper NameMapper) error { 492 | cfg.NameMapper = mapper 493 | return cfg.ReflectFrom(v) 494 | } 495 | 496 | // ReflectFrom reflects data sources from given struct. 497 | func ReflectFrom(cfg *File, v interface{}) error { 498 | return ReflectFromWithMapper(cfg, v, nil) 499 | } 500 | -------------------------------------------------------------------------------- /vendor/gopkg.in/ini.v1/ini.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | // Package ini provides INI file read and write functionality in Go. 16 | package ini 17 | 18 | import ( 19 | "bytes" 20 | "errors" 21 | "fmt" 22 | "io" 23 | "io/ioutil" 24 | "os" 25 | "regexp" 26 | "runtime" 27 | "strconv" 28 | "strings" 29 | "sync" 30 | "time" 31 | ) 32 | 33 | const ( 34 | // Name for default section. You can use this constant or the string literal. 35 | // In most of cases, an empty string is all you need to access the section. 36 | DEFAULT_SECTION = "DEFAULT" 37 | 38 | // Maximum allowed depth when recursively substituing variable names. 39 | _DEPTH_VALUES = 99 40 | _VERSION = "1.28.0" 41 | ) 42 | 43 | // Version returns current package version literal. 44 | func Version() string { 45 | return _VERSION 46 | } 47 | 48 | var ( 49 | // Delimiter to determine or compose a new line. 50 | // This variable will be changed to "\r\n" automatically on Windows 51 | // at package init time. 52 | LineBreak = "\n" 53 | 54 | // Variable regexp pattern: %(variable)s 55 | varPattern = regexp.MustCompile(`%\(([^\)]+)\)s`) 56 | 57 | // Indicate whether to align "=" sign with spaces to produce pretty output 58 | // or reduce all possible spaces for compact format. 59 | PrettyFormat = true 60 | 61 | // Explicitly write DEFAULT section header 62 | DefaultHeader = false 63 | 64 | // Indicate whether to put a line between sections 65 | PrettySection = true 66 | ) 67 | 68 | func init() { 69 | if runtime.GOOS == "windows" { 70 | LineBreak = "\r\n" 71 | } 72 | } 73 | 74 | func inSlice(str string, s []string) bool { 75 | for _, v := range s { 76 | if str == v { 77 | return true 78 | } 79 | } 80 | return false 81 | } 82 | 83 | // dataSource is an interface that returns object which can be read and closed. 84 | type dataSource interface { 85 | ReadCloser() (io.ReadCloser, error) 86 | } 87 | 88 | // sourceFile represents an object that contains content on the local file system. 89 | type sourceFile struct { 90 | name string 91 | } 92 | 93 | func (s sourceFile) ReadCloser() (_ io.ReadCloser, err error) { 94 | return os.Open(s.name) 95 | } 96 | 97 | type bytesReadCloser struct { 98 | reader io.Reader 99 | } 100 | 101 | func (rc *bytesReadCloser) Read(p []byte) (n int, err error) { 102 | return rc.reader.Read(p) 103 | } 104 | 105 | func (rc *bytesReadCloser) Close() error { 106 | return nil 107 | } 108 | 109 | // sourceData represents an object that contains content in memory. 110 | type sourceData struct { 111 | data []byte 112 | } 113 | 114 | func (s *sourceData) ReadCloser() (io.ReadCloser, error) { 115 | return ioutil.NopCloser(bytes.NewReader(s.data)), nil 116 | } 117 | 118 | // sourceReadCloser represents an input stream with Close method. 119 | type sourceReadCloser struct { 120 | reader io.ReadCloser 121 | } 122 | 123 | func (s *sourceReadCloser) ReadCloser() (io.ReadCloser, error) { 124 | return s.reader, nil 125 | } 126 | 127 | // File represents a combination of a or more INI file(s) in memory. 128 | type File struct { 129 | // Should make things safe, but sometimes doesn't matter. 130 | BlockMode bool 131 | // Make sure data is safe in multiple goroutines. 132 | lock sync.RWMutex 133 | 134 | // Allow combination of multiple data sources. 135 | dataSources []dataSource 136 | // Actual data is stored here. 137 | sections map[string]*Section 138 | 139 | // To keep data in order. 140 | sectionList []string 141 | 142 | options LoadOptions 143 | 144 | NameMapper 145 | ValueMapper 146 | } 147 | 148 | // newFile initializes File object with given data sources. 149 | func newFile(dataSources []dataSource, opts LoadOptions) *File { 150 | return &File{ 151 | BlockMode: true, 152 | dataSources: dataSources, 153 | sections: make(map[string]*Section), 154 | sectionList: make([]string, 0, 10), 155 | options: opts, 156 | } 157 | } 158 | 159 | func parseDataSource(source interface{}) (dataSource, error) { 160 | switch s := source.(type) { 161 | case string: 162 | return sourceFile{s}, nil 163 | case []byte: 164 | return &sourceData{s}, nil 165 | case io.ReadCloser: 166 | return &sourceReadCloser{s}, nil 167 | default: 168 | return nil, fmt.Errorf("error parsing data source: unknown type '%s'", s) 169 | } 170 | } 171 | 172 | type LoadOptions struct { 173 | // Loose indicates whether the parser should ignore nonexistent files or return error. 174 | Loose bool 175 | // Insensitive indicates whether the parser forces all section and key names to lowercase. 176 | Insensitive bool 177 | // IgnoreContinuation indicates whether to ignore continuation lines while parsing. 178 | IgnoreContinuation bool 179 | // IgnoreInlineComment indicates whether to ignore comments at the end of value and treat it as part of value. 180 | IgnoreInlineComment bool 181 | // AllowBooleanKeys indicates whether to allow boolean type keys or treat as value is missing. 182 | // This type of keys are mostly used in my.cnf. 183 | AllowBooleanKeys bool 184 | // AllowShadows indicates whether to keep track of keys with same name under same section. 185 | AllowShadows bool 186 | // Some INI formats allow group blocks that store a block of raw content that doesn't otherwise 187 | // conform to key/value pairs. Specify the names of those blocks here. 188 | UnparseableSections []string 189 | } 190 | 191 | func LoadSources(opts LoadOptions, source interface{}, others ...interface{}) (_ *File, err error) { 192 | sources := make([]dataSource, len(others)+1) 193 | sources[0], err = parseDataSource(source) 194 | if err != nil { 195 | return nil, err 196 | } 197 | for i := range others { 198 | sources[i+1], err = parseDataSource(others[i]) 199 | if err != nil { 200 | return nil, err 201 | } 202 | } 203 | f := newFile(sources, opts) 204 | if err = f.Reload(); err != nil { 205 | return nil, err 206 | } 207 | return f, nil 208 | } 209 | 210 | // Load loads and parses from INI data sources. 211 | // Arguments can be mixed of file name with string type, or raw data in []byte. 212 | // It will return error if list contains nonexistent files. 213 | func Load(source interface{}, others ...interface{}) (*File, error) { 214 | return LoadSources(LoadOptions{}, source, others...) 215 | } 216 | 217 | // LooseLoad has exactly same functionality as Load function 218 | // except it ignores nonexistent files instead of returning error. 219 | func LooseLoad(source interface{}, others ...interface{}) (*File, error) { 220 | return LoadSources(LoadOptions{Loose: true}, source, others...) 221 | } 222 | 223 | // InsensitiveLoad has exactly same functionality as Load function 224 | // except it forces all section and key names to be lowercased. 225 | func InsensitiveLoad(source interface{}, others ...interface{}) (*File, error) { 226 | return LoadSources(LoadOptions{Insensitive: true}, source, others...) 227 | } 228 | 229 | // InsensitiveLoad has exactly same functionality as Load function 230 | // except it allows have shadow keys. 231 | func ShadowLoad(source interface{}, others ...interface{}) (*File, error) { 232 | return LoadSources(LoadOptions{AllowShadows: true}, source, others...) 233 | } 234 | 235 | // Empty returns an empty file object. 236 | func Empty() *File { 237 | // Ignore error here, we sure our data is good. 238 | f, _ := Load([]byte("")) 239 | return f 240 | } 241 | 242 | // NewSection creates a new section. 243 | func (f *File) NewSection(name string) (*Section, error) { 244 | if len(name) == 0 { 245 | return nil, errors.New("error creating new section: empty section name") 246 | } else if f.options.Insensitive && name != DEFAULT_SECTION { 247 | name = strings.ToLower(name) 248 | } 249 | 250 | if f.BlockMode { 251 | f.lock.Lock() 252 | defer f.lock.Unlock() 253 | } 254 | 255 | if inSlice(name, f.sectionList) { 256 | return f.sections[name], nil 257 | } 258 | 259 | f.sectionList = append(f.sectionList, name) 260 | f.sections[name] = newSection(f, name) 261 | return f.sections[name], nil 262 | } 263 | 264 | // NewRawSection creates a new section with an unparseable body. 265 | func (f *File) NewRawSection(name, body string) (*Section, error) { 266 | section, err := f.NewSection(name) 267 | if err != nil { 268 | return nil, err 269 | } 270 | 271 | section.isRawSection = true 272 | section.rawBody = body 273 | return section, nil 274 | } 275 | 276 | // NewSections creates a list of sections. 277 | func (f *File) NewSections(names ...string) (err error) { 278 | for _, name := range names { 279 | if _, err = f.NewSection(name); err != nil { 280 | return err 281 | } 282 | } 283 | return nil 284 | } 285 | 286 | // GetSection returns section by given name. 287 | func (f *File) GetSection(name string) (*Section, error) { 288 | if len(name) == 0 { 289 | name = DEFAULT_SECTION 290 | } else if f.options.Insensitive { 291 | name = strings.ToLower(name) 292 | } 293 | 294 | if f.BlockMode { 295 | f.lock.RLock() 296 | defer f.lock.RUnlock() 297 | } 298 | 299 | sec := f.sections[name] 300 | if sec == nil { 301 | return nil, fmt.Errorf("section '%s' does not exist", name) 302 | } 303 | return sec, nil 304 | } 305 | 306 | // Section assumes named section exists and returns a zero-value when not. 307 | func (f *File) Section(name string) *Section { 308 | sec, err := f.GetSection(name) 309 | if err != nil { 310 | // Note: It's OK here because the only possible error is empty section name, 311 | // but if it's empty, this piece of code won't be executed. 312 | sec, _ = f.NewSection(name) 313 | return sec 314 | } 315 | return sec 316 | } 317 | 318 | // Section returns list of Section. 319 | func (f *File) Sections() []*Section { 320 | sections := make([]*Section, len(f.sectionList)) 321 | for i := range f.sectionList { 322 | sections[i] = f.Section(f.sectionList[i]) 323 | } 324 | return sections 325 | } 326 | 327 | // ChildSections returns a list of child sections of given section name. 328 | func (f *File) ChildSections(name string) []*Section { 329 | return f.Section(name).ChildSections() 330 | } 331 | 332 | // SectionStrings returns list of section names. 333 | func (f *File) SectionStrings() []string { 334 | list := make([]string, len(f.sectionList)) 335 | copy(list, f.sectionList) 336 | return list 337 | } 338 | 339 | // DeleteSection deletes a section. 340 | func (f *File) DeleteSection(name string) { 341 | if f.BlockMode { 342 | f.lock.Lock() 343 | defer f.lock.Unlock() 344 | } 345 | 346 | if len(name) == 0 { 347 | name = DEFAULT_SECTION 348 | } 349 | 350 | for i, s := range f.sectionList { 351 | if s == name { 352 | f.sectionList = append(f.sectionList[:i], f.sectionList[i+1:]...) 353 | delete(f.sections, name) 354 | return 355 | } 356 | } 357 | } 358 | 359 | func (f *File) reload(s dataSource) error { 360 | r, err := s.ReadCloser() 361 | if err != nil { 362 | return err 363 | } 364 | defer r.Close() 365 | 366 | return f.parse(r) 367 | } 368 | 369 | // Reload reloads and parses all data sources. 370 | func (f *File) Reload() (err error) { 371 | for _, s := range f.dataSources { 372 | if err = f.reload(s); err != nil { 373 | // In loose mode, we create an empty default section for nonexistent files. 374 | if os.IsNotExist(err) && f.options.Loose { 375 | f.parse(bytes.NewBuffer(nil)) 376 | continue 377 | } 378 | return err 379 | } 380 | } 381 | return nil 382 | } 383 | 384 | // Append appends one or more data sources and reloads automatically. 385 | func (f *File) Append(source interface{}, others ...interface{}) error { 386 | ds, err := parseDataSource(source) 387 | if err != nil { 388 | return err 389 | } 390 | f.dataSources = append(f.dataSources, ds) 391 | for _, s := range others { 392 | ds, err = parseDataSource(s) 393 | if err != nil { 394 | return err 395 | } 396 | f.dataSources = append(f.dataSources, ds) 397 | } 398 | return f.Reload() 399 | } 400 | 401 | // WriteToIndent writes content into io.Writer with given indention. 402 | // If PrettyFormat has been set to be true, 403 | // it will align "=" sign with spaces under each section. 404 | func (f *File) WriteToIndent(w io.Writer, indent string) (n int64, err error) { 405 | equalSign := "=" 406 | if PrettyFormat { 407 | equalSign = " = " 408 | } 409 | 410 | // Use buffer to make sure target is safe until finish encoding. 411 | buf := bytes.NewBuffer(nil) 412 | for i, sname := range f.sectionList { 413 | sec := f.Section(sname) 414 | if len(sec.Comment) > 0 { 415 | if sec.Comment[0] != '#' && sec.Comment[0] != ';' { 416 | sec.Comment = "; " + sec.Comment 417 | } 418 | if _, err = buf.WriteString(sec.Comment + LineBreak); err != nil { 419 | return 0, err 420 | } 421 | } 422 | 423 | if i > 0 || DefaultHeader { 424 | if _, err = buf.WriteString("[" + sname + "]" + LineBreak); err != nil { 425 | return 0, err 426 | } 427 | } else { 428 | // Write nothing if default section is empty 429 | if len(sec.keyList) == 0 { 430 | continue 431 | } 432 | } 433 | 434 | if sec.isRawSection { 435 | if _, err = buf.WriteString(sec.rawBody); err != nil { 436 | return 0, err 437 | } 438 | continue 439 | } 440 | 441 | // Count and generate alignment length and buffer spaces using the 442 | // longest key. Keys may be modifed if they contain certain characters so 443 | // we need to take that into account in our calculation. 444 | alignLength := 0 445 | if PrettyFormat { 446 | for _, kname := range sec.keyList { 447 | keyLength := len(kname) 448 | // First case will surround key by ` and second by """ 449 | if strings.ContainsAny(kname, "\"=:") { 450 | keyLength += 2 451 | } else if strings.Contains(kname, "`") { 452 | keyLength += 6 453 | } 454 | 455 | if keyLength > alignLength { 456 | alignLength = keyLength 457 | } 458 | } 459 | } 460 | alignSpaces := bytes.Repeat([]byte(" "), alignLength) 461 | 462 | KEY_LIST: 463 | for _, kname := range sec.keyList { 464 | key := sec.Key(kname) 465 | if len(key.Comment) > 0 { 466 | if len(indent) > 0 && sname != DEFAULT_SECTION { 467 | buf.WriteString(indent) 468 | } 469 | if key.Comment[0] != '#' && key.Comment[0] != ';' { 470 | key.Comment = "; " + key.Comment 471 | } 472 | if _, err = buf.WriteString(key.Comment + LineBreak); err != nil { 473 | return 0, err 474 | } 475 | } 476 | 477 | if len(indent) > 0 && sname != DEFAULT_SECTION { 478 | buf.WriteString(indent) 479 | } 480 | 481 | switch { 482 | case key.isAutoIncrement: 483 | kname = "-" 484 | case strings.ContainsAny(kname, "\"=:"): 485 | kname = "`" + kname + "`" 486 | case strings.Contains(kname, "`"): 487 | kname = `"""` + kname + `"""` 488 | } 489 | 490 | for _, val := range key.ValueWithShadows() { 491 | if _, err = buf.WriteString(kname); err != nil { 492 | return 0, err 493 | } 494 | 495 | if key.isBooleanType { 496 | if kname != sec.keyList[len(sec.keyList)-1] { 497 | buf.WriteString(LineBreak) 498 | } 499 | continue KEY_LIST 500 | } 501 | 502 | // Write out alignment spaces before "=" sign 503 | if PrettyFormat { 504 | buf.Write(alignSpaces[:alignLength-len(kname)]) 505 | } 506 | 507 | // In case key value contains "\n", "`", "\"", "#" or ";" 508 | if strings.ContainsAny(val, "\n`") { 509 | val = `"""` + val + `"""` 510 | } else if !f.options.IgnoreInlineComment && strings.ContainsAny(val, "#;") { 511 | val = "`" + val + "`" 512 | } 513 | if _, err = buf.WriteString(equalSign + val + LineBreak); err != nil { 514 | return 0, err 515 | } 516 | } 517 | } 518 | 519 | if PrettySection { 520 | // Put a line between sections 521 | if _, err = buf.WriteString(LineBreak); err != nil { 522 | return 0, err 523 | } 524 | } 525 | } 526 | 527 | return buf.WriteTo(w) 528 | } 529 | 530 | // WriteTo writes file content into io.Writer. 531 | func (f *File) WriteTo(w io.Writer) (int64, error) { 532 | return f.WriteToIndent(w, "") 533 | } 534 | 535 | // SaveToIndent writes content to file system with given value indention. 536 | func (f *File) SaveToIndent(filename, indent string) error { 537 | // Note: Because we are truncating with os.Create, 538 | // so it's safer to save to a temporary file location and rename afte done. 539 | tmpPath := filename + "." + strconv.Itoa(time.Now().Nanosecond()) + ".tmp" 540 | defer os.Remove(tmpPath) 541 | 542 | fw, err := os.Create(tmpPath) 543 | if err != nil { 544 | return err 545 | } 546 | 547 | if _, err = f.WriteToIndent(fw, indent); err != nil { 548 | fw.Close() 549 | return err 550 | } 551 | fw.Close() 552 | 553 | // Remove old file and rename the new one. 554 | os.Remove(filename) 555 | return os.Rename(tmpPath, filename) 556 | } 557 | 558 | // SaveTo writes content to file system. 559 | func (f *File) SaveTo(filename string) error { 560 | return f.SaveToIndent(filename, "") 561 | } 562 | -------------------------------------------------------------------------------- /vendor/gopkg.in/ini.v1/README_ZH.md: -------------------------------------------------------------------------------- 1 | 本包提供了 Go 语言中读写 INI 文件的功能。 2 | 3 | ## 功能特性 4 | 5 | - 支持覆盖加载多个数据源(`[]byte`、文件和 `io.ReadCloser`) 6 | - 支持递归读取键值 7 | - 支持读取父子分区 8 | - 支持读取自增键名 9 | - 支持读取多行的键值 10 | - 支持大量辅助方法 11 | - 支持在读取时直接转换为 Go 语言类型 12 | - 支持读取和 **写入** 分区和键的注释 13 | - 轻松操作分区、键值和注释 14 | - 在保存文件时分区和键值会保持原有的顺序 15 | 16 | ## 下载安装 17 | 18 | 使用一个特定版本: 19 | 20 | go get gopkg.in/ini.v1 21 | 22 | 使用最新版: 23 | 24 | go get github.com/go-ini/ini 25 | 26 | 如需更新请添加 `-u` 选项。 27 | 28 | ### 测试安装 29 | 30 | 如果您想要在自己的机器上运行测试,请使用 `-t` 标记: 31 | 32 | go get -t gopkg.in/ini.v1 33 | 34 | 如需更新请添加 `-u` 选项。 35 | 36 | ## 开始使用 37 | 38 | ### 从数据源加载 39 | 40 | 一个 **数据源** 可以是 `[]byte` 类型的原始数据,`string` 类型的文件路径或 `io.ReadCloser`。您可以加载 **任意多个** 数据源。如果您传递其它类型的数据源,则会直接返回错误。 41 | 42 | ```go 43 | cfg, err := ini.Load([]byte("raw data"), "filename", ioutil.NopCloser(bytes.NewReader([]byte("some other data")))) 44 | ``` 45 | 46 | 或者从一个空白的文件开始: 47 | 48 | ```go 49 | cfg := ini.Empty() 50 | ``` 51 | 52 | 当您在一开始无法决定需要加载哪些数据源时,仍可以使用 **Append()** 在需要的时候加载它们。 53 | 54 | ```go 55 | err := cfg.Append("other file", []byte("other raw data")) 56 | ``` 57 | 58 | 当您想要加载一系列文件,但是不能够确定其中哪些文件是不存在的,可以通过调用函数 `LooseLoad` 来忽略它们(`Load` 会因为文件不存在而返回错误): 59 | 60 | ```go 61 | cfg, err := ini.LooseLoad("filename", "filename_404") 62 | ``` 63 | 64 | 更牛逼的是,当那些之前不存在的文件在重新调用 `Reload` 方法的时候突然出现了,那么它们会被正常加载。 65 | 66 | #### 忽略键名的大小写 67 | 68 | 有时候分区和键的名称大小写混合非常烦人,这个时候就可以通过 `InsensitiveLoad` 将所有分区和键名在读取里强制转换为小写: 69 | 70 | ```go 71 | cfg, err := ini.InsensitiveLoad("filename") 72 | //... 73 | 74 | // sec1 和 sec2 指向同一个分区对象 75 | sec1, err := cfg.GetSection("Section") 76 | sec2, err := cfg.GetSection("SecTIOn") 77 | 78 | // key1 和 key2 指向同一个键对象 79 | key1, err := sec1.GetKey("Key") 80 | key2, err := sec2.GetKey("KeY") 81 | ``` 82 | 83 | #### 类似 MySQL 配置中的布尔值键 84 | 85 | MySQL 的配置文件中会出现没有具体值的布尔类型的键: 86 | 87 | ```ini 88 | [mysqld] 89 | ... 90 | skip-host-cache 91 | skip-name-resolve 92 | ``` 93 | 94 | 默认情况下这被认为是缺失值而无法完成解析,但可以通过高级的加载选项对它们进行处理: 95 | 96 | ```go 97 | cfg, err := LoadSources(LoadOptions{AllowBooleanKeys: true}, "my.cnf")) 98 | ``` 99 | 100 | 这些键的值永远为 `true`,且在保存到文件时也只会输出键名。 101 | 102 | 如果您想要通过程序来生成此类键,则可以使用 `NewBooleanKey`: 103 | 104 | ```go 105 | key, err := sec.NewBooleanKey("skip-host-cache") 106 | ``` 107 | 108 | #### 关于注释 109 | 110 | 下述几种情况的内容将被视为注释: 111 | 112 | 1. 所有以 `#` 或 `;` 开头的行 113 | 2. 所有在 `#` 或 `;` 之后的内容 114 | 3. 分区标签后的文字 (即 `[分区名]` 之后的内容) 115 | 116 | 如果你希望使用包含 `#` 或 `;` 的值,请使用 ``` ` ``` 或 ``` """ ``` 进行包覆。 117 | 118 | 除此之外,您还可以通过 `LoadOptions` 完全忽略行内注释: 119 | 120 | ```go 121 | cfg, err := LoadSources(LoadOptions{IgnoreInlineComment: true}, "app.ini")) 122 | ``` 123 | 124 | ### 操作分区(Section) 125 | 126 | 获取指定分区: 127 | 128 | ```go 129 | section, err := cfg.GetSection("section name") 130 | ``` 131 | 132 | 如果您想要获取默认分区,则可以用空字符串代替分区名: 133 | 134 | ```go 135 | section, err := cfg.GetSection("") 136 | ``` 137 | 138 | 当您非常确定某个分区是存在的,可以使用以下简便方法: 139 | 140 | ```go 141 | section := cfg.Section("section name") 142 | ``` 143 | 144 | 如果不小心判断错了,要获取的分区其实是不存在的,那会发生什么呢?没事的,它会自动创建并返回一个对应的分区对象给您。 145 | 146 | 创建一个分区: 147 | 148 | ```go 149 | err := cfg.NewSection("new section") 150 | ``` 151 | 152 | 获取所有分区对象或名称: 153 | 154 | ```go 155 | sections := cfg.Sections() 156 | names := cfg.SectionStrings() 157 | ``` 158 | 159 | ### 操作键(Key) 160 | 161 | 获取某个分区下的键: 162 | 163 | ```go 164 | key, err := cfg.Section("").GetKey("key name") 165 | ``` 166 | 167 | 和分区一样,您也可以直接获取键而忽略错误处理: 168 | 169 | ```go 170 | key := cfg.Section("").Key("key name") 171 | ``` 172 | 173 | 判断某个键是否存在: 174 | 175 | ```go 176 | yes := cfg.Section("").HasKey("key name") 177 | ``` 178 | 179 | 创建一个新的键: 180 | 181 | ```go 182 | err := cfg.Section("").NewKey("name", "value") 183 | ``` 184 | 185 | 获取分区下的所有键或键名: 186 | 187 | ```go 188 | keys := cfg.Section("").Keys() 189 | names := cfg.Section("").KeyStrings() 190 | ``` 191 | 192 | 获取分区下的所有键值对的克隆: 193 | 194 | ```go 195 | hash := cfg.Section("").KeysHash() 196 | ``` 197 | 198 | ### 操作键值(Value) 199 | 200 | 获取一个类型为字符串(string)的值: 201 | 202 | ```go 203 | val := cfg.Section("").Key("key name").String() 204 | ``` 205 | 206 | 获取值的同时通过自定义函数进行处理验证: 207 | 208 | ```go 209 | val := cfg.Section("").Key("key name").Validate(func(in string) string { 210 | if len(in) == 0 { 211 | return "default" 212 | } 213 | return in 214 | }) 215 | ``` 216 | 217 | 如果您不需要任何对值的自动转变功能(例如递归读取),可以直接获取原值(这种方式性能最佳): 218 | 219 | ```go 220 | val := cfg.Section("").Key("key name").Value() 221 | ``` 222 | 223 | 判断某个原值是否存在: 224 | 225 | ```go 226 | yes := cfg.Section("").HasValue("test value") 227 | ``` 228 | 229 | 获取其它类型的值: 230 | 231 | ```go 232 | // 布尔值的规则: 233 | // true 当值为:1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On 234 | // false 当值为:0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off 235 | v, err = cfg.Section("").Key("BOOL").Bool() 236 | v, err = cfg.Section("").Key("FLOAT64").Float64() 237 | v, err = cfg.Section("").Key("INT").Int() 238 | v, err = cfg.Section("").Key("INT64").Int64() 239 | v, err = cfg.Section("").Key("UINT").Uint() 240 | v, err = cfg.Section("").Key("UINT64").Uint64() 241 | v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339) 242 | v, err = cfg.Section("").Key("TIME").Time() // RFC3339 243 | 244 | v = cfg.Section("").Key("BOOL").MustBool() 245 | v = cfg.Section("").Key("FLOAT64").MustFloat64() 246 | v = cfg.Section("").Key("INT").MustInt() 247 | v = cfg.Section("").Key("INT64").MustInt64() 248 | v = cfg.Section("").Key("UINT").MustUint() 249 | v = cfg.Section("").Key("UINT64").MustUint64() 250 | v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339) 251 | v = cfg.Section("").Key("TIME").MustTime() // RFC3339 252 | 253 | // 由 Must 开头的方法名允许接收一个相同类型的参数来作为默认值, 254 | // 当键不存在或者转换失败时,则会直接返回该默认值。 255 | // 但是,MustString 方法必须传递一个默认值。 256 | 257 | v = cfg.Seciont("").Key("String").MustString("default") 258 | v = cfg.Section("").Key("BOOL").MustBool(true) 259 | v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25) 260 | v = cfg.Section("").Key("INT").MustInt(10) 261 | v = cfg.Section("").Key("INT64").MustInt64(99) 262 | v = cfg.Section("").Key("UINT").MustUint(3) 263 | v = cfg.Section("").Key("UINT64").MustUint64(6) 264 | v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now()) 265 | v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339 266 | ``` 267 | 268 | 如果我的值有好多行怎么办? 269 | 270 | ```ini 271 | [advance] 272 | ADDRESS = """404 road, 273 | NotFound, State, 5000 274 | Earth""" 275 | ``` 276 | 277 | 嗯哼?小 case! 278 | 279 | ```go 280 | cfg.Section("advance").Key("ADDRESS").String() 281 | 282 | /* --- start --- 283 | 404 road, 284 | NotFound, State, 5000 285 | Earth 286 | ------ end --- */ 287 | ``` 288 | 289 | 赞爆了!那要是我属于一行的内容写不下想要写到第二行怎么办? 290 | 291 | ```ini 292 | [advance] 293 | two_lines = how about \ 294 | continuation lines? 295 | lots_of_lines = 1 \ 296 | 2 \ 297 | 3 \ 298 | 4 299 | ``` 300 | 301 | 简直是小菜一碟! 302 | 303 | ```go 304 | cfg.Section("advance").Key("two_lines").String() // how about continuation lines? 305 | cfg.Section("advance").Key("lots_of_lines").String() // 1 2 3 4 306 | ``` 307 | 308 | 可是我有时候觉得两行连在一起特别没劲,怎么才能不自动连接两行呢? 309 | 310 | ```go 311 | cfg, err := ini.LoadSources(ini.LoadOptions{ 312 | IgnoreContinuation: true, 313 | }, "filename") 314 | ``` 315 | 316 | 哇靠给力啊! 317 | 318 | 需要注意的是,值两侧的单引号会被自动剔除: 319 | 320 | ```ini 321 | foo = "some value" // foo: some value 322 | bar = 'some value' // bar: some value 323 | ``` 324 | 325 | 这就是全部了?哈哈,当然不是。 326 | 327 | #### 操作键值的辅助方法 328 | 329 | 获取键值时设定候选值: 330 | 331 | ```go 332 | v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"}) 333 | v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75}) 334 | v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30}) 335 | v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30}) 336 | v = cfg.Section("").Key("UINT").InUint(4, []int{3, 6, 9}) 337 | v = cfg.Section("").Key("UINT64").InUint64(8, []int64{3, 6, 9}) 338 | v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3}) 339 | v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339 340 | ``` 341 | 342 | 如果获取到的值不是候选值的任意一个,则会返回默认值,而默认值不需要是候选值中的一员。 343 | 344 | 验证获取的值是否在指定范围内: 345 | 346 | ```go 347 | vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2) 348 | vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20) 349 | vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20) 350 | vals = cfg.Section("").Key("UINT").RangeUint(0, 3, 9) 351 | vals = cfg.Section("").Key("UINT64").RangeUint64(0, 3, 9) 352 | vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime) 353 | vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339 354 | ``` 355 | 356 | ##### 自动分割键值到切片(slice) 357 | 358 | 当存在无效输入时,使用零值代替: 359 | 360 | ```go 361 | // Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] 362 | // Input: how, 2.2, are, you -> [0.0 2.2 0.0 0.0] 363 | vals = cfg.Section("").Key("STRINGS").Strings(",") 364 | vals = cfg.Section("").Key("FLOAT64S").Float64s(",") 365 | vals = cfg.Section("").Key("INTS").Ints(",") 366 | vals = cfg.Section("").Key("INT64S").Int64s(",") 367 | vals = cfg.Section("").Key("UINTS").Uints(",") 368 | vals = cfg.Section("").Key("UINT64S").Uint64s(",") 369 | vals = cfg.Section("").Key("TIMES").Times(",") 370 | ``` 371 | 372 | 从结果切片中剔除无效输入: 373 | 374 | ```go 375 | // Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] 376 | // Input: how, 2.2, are, you -> [2.2] 377 | vals = cfg.Section("").Key("FLOAT64S").ValidFloat64s(",") 378 | vals = cfg.Section("").Key("INTS").ValidInts(",") 379 | vals = cfg.Section("").Key("INT64S").ValidInt64s(",") 380 | vals = cfg.Section("").Key("UINTS").ValidUints(",") 381 | vals = cfg.Section("").Key("UINT64S").ValidUint64s(",") 382 | vals = cfg.Section("").Key("TIMES").ValidTimes(",") 383 | ``` 384 | 385 | 当存在无效输入时,直接返回错误: 386 | 387 | ```go 388 | // Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] 389 | // Input: how, 2.2, are, you -> error 390 | vals = cfg.Section("").Key("FLOAT64S").StrictFloat64s(",") 391 | vals = cfg.Section("").Key("INTS").StrictInts(",") 392 | vals = cfg.Section("").Key("INT64S").StrictInt64s(",") 393 | vals = cfg.Section("").Key("UINTS").StrictUints(",") 394 | vals = cfg.Section("").Key("UINT64S").StrictUint64s(",") 395 | vals = cfg.Section("").Key("TIMES").StrictTimes(",") 396 | ``` 397 | 398 | ### 保存配置 399 | 400 | 终于到了这个时刻,是时候保存一下配置了。 401 | 402 | 比较原始的做法是输出配置到某个文件: 403 | 404 | ```go 405 | // ... 406 | err = cfg.SaveTo("my.ini") 407 | err = cfg.SaveToIndent("my.ini", "\t") 408 | ``` 409 | 410 | 另一个比较高级的做法是写入到任何实现 `io.Writer` 接口的对象中: 411 | 412 | ```go 413 | // ... 414 | cfg.WriteTo(writer) 415 | cfg.WriteToIndent(writer, "\t") 416 | ``` 417 | 418 | 默认情况下,空格将被用于对齐键值之间的等号以美化输出结果,以下代码可以禁用该功能: 419 | 420 | ```go 421 | ini.PrettyFormat = false 422 | ``` 423 | 424 | ## 高级用法 425 | 426 | ### 递归读取键值 427 | 428 | 在获取所有键值的过程中,特殊语法 `%()s` 会被应用,其中 `` 可以是相同分区或者默认分区下的键名。字符串 `%()s` 会被相应的键值所替代,如果指定的键不存在,则会用空字符串替代。您可以最多使用 99 层的递归嵌套。 429 | 430 | ```ini 431 | NAME = ini 432 | 433 | [author] 434 | NAME = Unknwon 435 | GITHUB = https://github.com/%(NAME)s 436 | 437 | [package] 438 | FULL_NAME = github.com/go-ini/%(NAME)s 439 | ``` 440 | 441 | ```go 442 | cfg.Section("author").Key("GITHUB").String() // https://github.com/Unknwon 443 | cfg.Section("package").Key("FULL_NAME").String() // github.com/go-ini/ini 444 | ``` 445 | 446 | ### 读取父子分区 447 | 448 | 您可以在分区名称中使用 `.` 来表示两个或多个分区之间的父子关系。如果某个键在子分区中不存在,则会去它的父分区中再次寻找,直到没有父分区为止。 449 | 450 | ```ini 451 | NAME = ini 452 | VERSION = v1 453 | IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s 454 | 455 | [package] 456 | CLONE_URL = https://%(IMPORT_PATH)s 457 | 458 | [package.sub] 459 | ``` 460 | 461 | ```go 462 | cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1 463 | ``` 464 | 465 | #### 获取上级父分区下的所有键名 466 | 467 | ```go 468 | cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"] 469 | ``` 470 | 471 | ### 无法解析的分区 472 | 473 | 如果遇到一些比较特殊的分区,它们不包含常见的键值对,而是没有固定格式的纯文本,则可以使用 `LoadOptions.UnparsableSections` 进行处理: 474 | 475 | ```go 476 | cfg, err := LoadSources(LoadOptions{UnparseableSections: []string{"COMMENTS"}}, `[COMMENTS] 477 | <1> This slide has the fuel listed in the wrong units `)) 478 | 479 | body := cfg.Section("COMMENTS").Body() 480 | 481 | /* --- start --- 482 | <1> This slide has the fuel listed in the wrong units 483 | ------ end --- */ 484 | ``` 485 | 486 | ### 读取自增键名 487 | 488 | 如果数据源中的键名为 `-`,则认为该键使用了自增键名的特殊语法。计数器从 1 开始,并且分区之间是相互独立的。 489 | 490 | ```ini 491 | [features] 492 | -: Support read/write comments of keys and sections 493 | -: Support auto-increment of key names 494 | -: Support load multiple files to overwrite key values 495 | ``` 496 | 497 | ```go 498 | cfg.Section("features").KeyStrings() // []{"#1", "#2", "#3"} 499 | ``` 500 | 501 | ### 映射到结构 502 | 503 | 想要使用更加面向对象的方式玩转 INI 吗?好主意。 504 | 505 | ```ini 506 | Name = Unknwon 507 | age = 21 508 | Male = true 509 | Born = 1993-01-01T20:17:05Z 510 | 511 | [Note] 512 | Content = Hi is a good man! 513 | Cities = HangZhou, Boston 514 | ``` 515 | 516 | ```go 517 | type Note struct { 518 | Content string 519 | Cities []string 520 | } 521 | 522 | type Person struct { 523 | Name string 524 | Age int `ini:"age"` 525 | Male bool 526 | Born time.Time 527 | Note 528 | Created time.Time `ini:"-"` 529 | } 530 | 531 | func main() { 532 | cfg, err := ini.Load("path/to/ini") 533 | // ... 534 | p := new(Person) 535 | err = cfg.MapTo(p) 536 | // ... 537 | 538 | // 一切竟可以如此的简单。 539 | err = ini.MapTo(p, "path/to/ini") 540 | // ... 541 | 542 | // 嗯哼?只需要映射一个分区吗? 543 | n := new(Note) 544 | err = cfg.Section("Note").MapTo(n) 545 | // ... 546 | } 547 | ``` 548 | 549 | 结构的字段怎么设置默认值呢?很简单,只要在映射之前对指定字段进行赋值就可以了。如果键未找到或者类型错误,该值不会发生改变。 550 | 551 | ```go 552 | // ... 553 | p := &Person{ 554 | Name: "Joe", 555 | } 556 | // ... 557 | ``` 558 | 559 | 这样玩 INI 真的好酷啊!然而,如果不能还给我原来的配置文件,有什么卵用? 560 | 561 | ### 从结构反射 562 | 563 | 可是,我有说不能吗? 564 | 565 | ```go 566 | type Embeded struct { 567 | Dates []time.Time `delim:"|"` 568 | Places []string `ini:"places,omitempty"` 569 | None []int `ini:",omitempty"` 570 | } 571 | 572 | type Author struct { 573 | Name string `ini:"NAME"` 574 | Male bool 575 | Age int 576 | GPA float64 577 | NeverMind string `ini:"-"` 578 | *Embeded 579 | } 580 | 581 | func main() { 582 | a := &Author{"Unknwon", true, 21, 2.8, "", 583 | &Embeded{ 584 | []time.Time{time.Now(), time.Now()}, 585 | []string{"HangZhou", "Boston"}, 586 | []int{}, 587 | }} 588 | cfg := ini.Empty() 589 | err = ini.ReflectFrom(cfg, a) 590 | // ... 591 | } 592 | ``` 593 | 594 | 瞧瞧,奇迹发生了。 595 | 596 | ```ini 597 | NAME = Unknwon 598 | Male = true 599 | Age = 21 600 | GPA = 2.8 601 | 602 | [Embeded] 603 | Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00 604 | places = HangZhou,Boston 605 | ``` 606 | 607 | #### 名称映射器(Name Mapper) 608 | 609 | 为了节省您的时间并简化代码,本库支持类型为 [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) 的名称映射器,该映射器负责结构字段名与分区名和键名之间的映射。 610 | 611 | 目前有 2 款内置的映射器: 612 | 613 | - `AllCapsUnderscore`:该映射器将字段名转换至格式 `ALL_CAPS_UNDERSCORE` 后再去匹配分区名和键名。 614 | - `TitleUnderscore`:该映射器将字段名转换至格式 `title_underscore` 后再去匹配分区名和键名。 615 | 616 | 使用方法: 617 | 618 | ```go 619 | type Info struct{ 620 | PackageName string 621 | } 622 | 623 | func main() { 624 | err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("package_name=ini")) 625 | // ... 626 | 627 | cfg, err := ini.Load([]byte("PACKAGE_NAME=ini")) 628 | // ... 629 | info := new(Info) 630 | cfg.NameMapper = ini.AllCapsUnderscore 631 | err = cfg.MapTo(info) 632 | // ... 633 | } 634 | ``` 635 | 636 | 使用函数 `ini.ReflectFromWithMapper` 时也可应用相同的规则。 637 | 638 | #### 值映射器(Value Mapper) 639 | 640 | 值映射器允许使用一个自定义函数自动展开值的具体内容,例如:运行时获取环境变量: 641 | 642 | ```go 643 | type Env struct { 644 | Foo string `ini:"foo"` 645 | } 646 | 647 | func main() { 648 | cfg, err := ini.Load([]byte("[env]\nfoo = ${MY_VAR}\n") 649 | cfg.ValueMapper = os.ExpandEnv 650 | // ... 651 | env := &Env{} 652 | err = cfg.Section("env").MapTo(env) 653 | } 654 | ``` 655 | 656 | 本例中,`env.Foo` 将会是运行时所获取到环境变量 `MY_VAR` 的值。 657 | 658 | #### 映射/反射的其它说明 659 | 660 | 任何嵌入的结构都会被默认认作一个不同的分区,并且不会自动产生所谓的父子分区关联: 661 | 662 | ```go 663 | type Child struct { 664 | Age string 665 | } 666 | 667 | type Parent struct { 668 | Name string 669 | Child 670 | } 671 | 672 | type Config struct { 673 | City string 674 | Parent 675 | } 676 | ``` 677 | 678 | 示例配置文件: 679 | 680 | ```ini 681 | City = Boston 682 | 683 | [Parent] 684 | Name = Unknwon 685 | 686 | [Child] 687 | Age = 21 688 | ``` 689 | 690 | 很好,但是,我就是要嵌入结构也在同一个分区。好吧,你爹是李刚! 691 | 692 | ```go 693 | type Child struct { 694 | Age string 695 | } 696 | 697 | type Parent struct { 698 | Name string 699 | Child `ini:"Parent"` 700 | } 701 | 702 | type Config struct { 703 | City string 704 | Parent 705 | } 706 | ``` 707 | 708 | 示例配置文件: 709 | 710 | ```ini 711 | City = Boston 712 | 713 | [Parent] 714 | Name = Unknwon 715 | Age = 21 716 | ``` 717 | 718 | ## 获取帮助 719 | 720 | - [API 文档](https://gowalker.org/gopkg.in/ini.v1) 721 | - [创建工单](https://github.com/go-ini/ini/issues/new) 722 | 723 | ## 常见问题 724 | 725 | ### 字段 `BlockMode` 是什么? 726 | 727 | 默认情况下,本库会在您进行读写操作时采用锁机制来确保数据时间。但在某些情况下,您非常确定只进行读操作。此时,您可以通过设置 `cfg.BlockMode = false` 来将读操作提升大约 **50-70%** 的性能。 728 | 729 | ### 为什么要写另一个 INI 解析库? 730 | 731 | 许多人都在使用我的 [goconfig](https://github.com/Unknwon/goconfig) 来完成对 INI 文件的操作,但我希望使用更加 Go 风格的代码。并且当您设置 `cfg.BlockMode = false` 时,会有大约 **10-30%** 的性能提升。 732 | 733 | 为了做出这些改变,我必须对 API 进行破坏,所以新开一个仓库是最安全的做法。除此之外,本库直接使用 `gopkg.in` 来进行版本化发布。(其实真相是导入路径更短了) 734 | -------------------------------------------------------------------------------- /vendor/gopkg.in/ini.v1/README.md: -------------------------------------------------------------------------------- 1 | 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) 2 | === 3 | 4 | ![](https://avatars0.githubusercontent.com/u/10216035?v=3&s=200) 5 | 6 | Package ini provides INI file read and write functionality in Go. 7 | 8 | [简体中文](README_ZH.md) 9 | 10 | ## Feature 11 | 12 | - Load multiple data sources(`[]byte`, file and `io.ReadCloser`) with overwrites. 13 | - Read with recursion values. 14 | - Read with parent-child sections. 15 | - Read with auto-increment key names. 16 | - Read with multiple-line values. 17 | - Read with tons of helper methods. 18 | - Read and convert values to Go types. 19 | - Read and **WRITE** comments of sections and keys. 20 | - Manipulate sections, keys and comments with ease. 21 | - Keep sections and keys in order as you parse and save. 22 | 23 | ## Installation 24 | 25 | To use a tagged revision: 26 | 27 | go get gopkg.in/ini.v1 28 | 29 | To use with latest changes: 30 | 31 | go get github.com/go-ini/ini 32 | 33 | Please add `-u` flag to update in the future. 34 | 35 | ### Testing 36 | 37 | If you want to test on your machine, please apply `-t` flag: 38 | 39 | go get -t gopkg.in/ini.v1 40 | 41 | Please add `-u` flag to update in the future. 42 | 43 | ## Getting Started 44 | 45 | ### Loading from data sources 46 | 47 | 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. 48 | 49 | ```go 50 | cfg, err := ini.Load([]byte("raw data"), "filename", ioutil.NopCloser(bytes.NewReader([]byte("some other data")))) 51 | ``` 52 | 53 | Or start with an empty object: 54 | 55 | ```go 56 | cfg := ini.Empty() 57 | ``` 58 | 59 | When you cannot decide how many data sources to load at the beginning, you will still be able to **Append()** them later. 60 | 61 | ```go 62 | err := cfg.Append("other file", []byte("other raw data")) 63 | ``` 64 | 65 | 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. 66 | 67 | ```go 68 | cfg, err := ini.LooseLoad("filename", "filename_404") 69 | ``` 70 | 71 | The cool thing is, whenever the file is available to load while you're calling `Reload` method, it will be counted as usual. 72 | 73 | #### Ignore cases of key name 74 | 75 | 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. 76 | 77 | ```go 78 | cfg, err := ini.InsensitiveLoad("filename") 79 | //... 80 | 81 | // sec1 and sec2 are the exactly same section object 82 | sec1, err := cfg.GetSection("Section") 83 | sec2, err := cfg.GetSection("SecTIOn") 84 | 85 | // key1 and key2 are the exactly same key object 86 | key1, err := sec1.GetKey("Key") 87 | key2, err := sec2.GetKey("KeY") 88 | ``` 89 | 90 | #### MySQL-like boolean key 91 | 92 | MySQL's configuration allows a key without value as follows: 93 | 94 | ```ini 95 | [mysqld] 96 | ... 97 | skip-host-cache 98 | skip-name-resolve 99 | ``` 100 | 101 | 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: 102 | 103 | ```go 104 | cfg, err := LoadSources(LoadOptions{AllowBooleanKeys: true}, "my.cnf")) 105 | ``` 106 | 107 | 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. 108 | 109 | To generate such keys in your program, you could use `NewBooleanKey`: 110 | 111 | ```go 112 | key, err := sec.NewBooleanKey("skip-host-cache") 113 | ``` 114 | 115 | #### Comment 116 | 117 | Take care that following format will be treated as comment: 118 | 119 | 1. Line begins with `#` or `;` 120 | 2. Words after `#` or `;` 121 | 3. Words after section name (i.e words after `[some section name]`) 122 | 123 | If you want to save a value with `#` or `;`, please quote them with ``` ` ``` or ``` """ ```. 124 | 125 | Alternatively, you can use following `LoadOptions` to completely ignore inline comments: 126 | 127 | ```go 128 | cfg, err := LoadSources(LoadOptions{IgnoreInlineComment: true}, "app.ini")) 129 | ``` 130 | 131 | ### Working with sections 132 | 133 | To get a section, you would need to: 134 | 135 | ```go 136 | section, err := cfg.GetSection("section name") 137 | ``` 138 | 139 | For a shortcut for default section, just give an empty string as name: 140 | 141 | ```go 142 | section, err := cfg.GetSection("") 143 | ``` 144 | 145 | When you're pretty sure the section exists, following code could make your life easier: 146 | 147 | ```go 148 | section := cfg.Section("section name") 149 | ``` 150 | 151 | What happens when the section somehow does not exist? Don't panic, it automatically creates and returns a new section to you. 152 | 153 | To create a new section: 154 | 155 | ```go 156 | err := cfg.NewSection("new section") 157 | ``` 158 | 159 | To get a list of sections or section names: 160 | 161 | ```go 162 | sections := cfg.Sections() 163 | names := cfg.SectionStrings() 164 | ``` 165 | 166 | ### Working with keys 167 | 168 | To get a key under a section: 169 | 170 | ```go 171 | key, err := cfg.Section("").GetKey("key name") 172 | ``` 173 | 174 | Same rule applies to key operations: 175 | 176 | ```go 177 | key := cfg.Section("").Key("key name") 178 | ``` 179 | 180 | To check if a key exists: 181 | 182 | ```go 183 | yes := cfg.Section("").HasKey("key name") 184 | ``` 185 | 186 | To create a new key: 187 | 188 | ```go 189 | err := cfg.Section("").NewKey("name", "value") 190 | ``` 191 | 192 | To get a list of keys or key names: 193 | 194 | ```go 195 | keys := cfg.Section("").Keys() 196 | names := cfg.Section("").KeyStrings() 197 | ``` 198 | 199 | To get a clone hash of keys and corresponding values: 200 | 201 | ```go 202 | hash := cfg.Section("").KeysHash() 203 | ``` 204 | 205 | ### Working with values 206 | 207 | To get a string value: 208 | 209 | ```go 210 | val := cfg.Section("").Key("key name").String() 211 | ``` 212 | 213 | To validate key value on the fly: 214 | 215 | ```go 216 | val := cfg.Section("").Key("key name").Validate(func(in string) string { 217 | if len(in) == 0 { 218 | return "default" 219 | } 220 | return in 221 | }) 222 | ``` 223 | 224 | 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): 225 | 226 | ```go 227 | val := cfg.Section("").Key("key name").Value() 228 | ``` 229 | 230 | To check if raw value exists: 231 | 232 | ```go 233 | yes := cfg.Section("").HasValue("test value") 234 | ``` 235 | 236 | To get value with types: 237 | 238 | ```go 239 | // For boolean values: 240 | // true when value is: 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On 241 | // false when value is: 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off 242 | v, err = cfg.Section("").Key("BOOL").Bool() 243 | v, err = cfg.Section("").Key("FLOAT64").Float64() 244 | v, err = cfg.Section("").Key("INT").Int() 245 | v, err = cfg.Section("").Key("INT64").Int64() 246 | v, err = cfg.Section("").Key("UINT").Uint() 247 | v, err = cfg.Section("").Key("UINT64").Uint64() 248 | v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339) 249 | v, err = cfg.Section("").Key("TIME").Time() // RFC3339 250 | 251 | v = cfg.Section("").Key("BOOL").MustBool() 252 | v = cfg.Section("").Key("FLOAT64").MustFloat64() 253 | v = cfg.Section("").Key("INT").MustInt() 254 | v = cfg.Section("").Key("INT64").MustInt64() 255 | v = cfg.Section("").Key("UINT").MustUint() 256 | v = cfg.Section("").Key("UINT64").MustUint64() 257 | v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339) 258 | v = cfg.Section("").Key("TIME").MustTime() // RFC3339 259 | 260 | // Methods start with Must also accept one argument for default value 261 | // when key not found or fail to parse value to given type. 262 | // Except method MustString, which you have to pass a default value. 263 | 264 | v = cfg.Section("").Key("String").MustString("default") 265 | v = cfg.Section("").Key("BOOL").MustBool(true) 266 | v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25) 267 | v = cfg.Section("").Key("INT").MustInt(10) 268 | v = cfg.Section("").Key("INT64").MustInt64(99) 269 | v = cfg.Section("").Key("UINT").MustUint(3) 270 | v = cfg.Section("").Key("UINT64").MustUint64(6) 271 | v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now()) 272 | v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339 273 | ``` 274 | 275 | What if my value is three-line long? 276 | 277 | ```ini 278 | [advance] 279 | ADDRESS = """404 road, 280 | NotFound, State, 5000 281 | Earth""" 282 | ``` 283 | 284 | Not a problem! 285 | 286 | ```go 287 | cfg.Section("advance").Key("ADDRESS").String() 288 | 289 | /* --- start --- 290 | 404 road, 291 | NotFound, State, 5000 292 | Earth 293 | ------ end --- */ 294 | ``` 295 | 296 | That's cool, how about continuation lines? 297 | 298 | ```ini 299 | [advance] 300 | two_lines = how about \ 301 | continuation lines? 302 | lots_of_lines = 1 \ 303 | 2 \ 304 | 3 \ 305 | 4 306 | ``` 307 | 308 | Piece of cake! 309 | 310 | ```go 311 | cfg.Section("advance").Key("two_lines").String() // how about continuation lines? 312 | cfg.Section("advance").Key("lots_of_lines").String() // 1 2 3 4 313 | ``` 314 | 315 | Well, I hate continuation lines, how do I disable that? 316 | 317 | ```go 318 | cfg, err := ini.LoadSources(ini.LoadOptions{ 319 | IgnoreContinuation: true, 320 | }, "filename") 321 | ``` 322 | 323 | Holy crap! 324 | 325 | Note that single quotes around values will be stripped: 326 | 327 | ```ini 328 | foo = "some value" // foo: some value 329 | bar = 'some value' // bar: some value 330 | ``` 331 | 332 | That's all? Hmm, no. 333 | 334 | #### Helper methods of working with values 335 | 336 | To get value with given candidates: 337 | 338 | ```go 339 | v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"}) 340 | v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75}) 341 | v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30}) 342 | v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30}) 343 | v = cfg.Section("").Key("UINT").InUint(4, []int{3, 6, 9}) 344 | v = cfg.Section("").Key("UINT64").InUint64(8, []int64{3, 6, 9}) 345 | v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3}) 346 | v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339 347 | ``` 348 | 349 | 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. 350 | 351 | To validate value in a given range: 352 | 353 | ```go 354 | vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2) 355 | vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20) 356 | vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20) 357 | vals = cfg.Section("").Key("UINT").RangeUint(0, 3, 9) 358 | vals = cfg.Section("").Key("UINT64").RangeUint64(0, 3, 9) 359 | vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime) 360 | vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339 361 | ``` 362 | 363 | ##### Auto-split values into a slice 364 | 365 | To use zero value of type for invalid inputs: 366 | 367 | ```go 368 | // Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] 369 | // Input: how, 2.2, are, you -> [0.0 2.2 0.0 0.0] 370 | vals = cfg.Section("").Key("STRINGS").Strings(",") 371 | vals = cfg.Section("").Key("FLOAT64S").Float64s(",") 372 | vals = cfg.Section("").Key("INTS").Ints(",") 373 | vals = cfg.Section("").Key("INT64S").Int64s(",") 374 | vals = cfg.Section("").Key("UINTS").Uints(",") 375 | vals = cfg.Section("").Key("UINT64S").Uint64s(",") 376 | vals = cfg.Section("").Key("TIMES").Times(",") 377 | ``` 378 | 379 | To exclude invalid values out of result slice: 380 | 381 | ```go 382 | // Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] 383 | // Input: how, 2.2, are, you -> [2.2] 384 | vals = cfg.Section("").Key("FLOAT64S").ValidFloat64s(",") 385 | vals = cfg.Section("").Key("INTS").ValidInts(",") 386 | vals = cfg.Section("").Key("INT64S").ValidInt64s(",") 387 | vals = cfg.Section("").Key("UINTS").ValidUints(",") 388 | vals = cfg.Section("").Key("UINT64S").ValidUint64s(",") 389 | vals = cfg.Section("").Key("TIMES").ValidTimes(",") 390 | ``` 391 | 392 | Or to return nothing but error when have invalid inputs: 393 | 394 | ```go 395 | // Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4] 396 | // Input: how, 2.2, are, you -> error 397 | vals = cfg.Section("").Key("FLOAT64S").StrictFloat64s(",") 398 | vals = cfg.Section("").Key("INTS").StrictInts(",") 399 | vals = cfg.Section("").Key("INT64S").StrictInt64s(",") 400 | vals = cfg.Section("").Key("UINTS").StrictUints(",") 401 | vals = cfg.Section("").Key("UINT64S").StrictUint64s(",") 402 | vals = cfg.Section("").Key("TIMES").StrictTimes(",") 403 | ``` 404 | 405 | ### Save your configuration 406 | 407 | Finally, it's time to save your configuration to somewhere. 408 | 409 | A typical way to save configuration is writing it to a file: 410 | 411 | ```go 412 | // ... 413 | err = cfg.SaveTo("my.ini") 414 | err = cfg.SaveToIndent("my.ini", "\t") 415 | ``` 416 | 417 | Another way to save is writing to a `io.Writer` interface: 418 | 419 | ```go 420 | // ... 421 | cfg.WriteTo(writer) 422 | cfg.WriteToIndent(writer, "\t") 423 | ``` 424 | 425 | By default, spaces are used to align "=" sign between key and values, to disable that: 426 | 427 | ```go 428 | ini.PrettyFormat = false 429 | ``` 430 | 431 | ## Advanced Usage 432 | 433 | ### Recursive Values 434 | 435 | 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. 436 | 437 | ```ini 438 | NAME = ini 439 | 440 | [author] 441 | NAME = Unknwon 442 | GITHUB = https://github.com/%(NAME)s 443 | 444 | [package] 445 | FULL_NAME = github.com/go-ini/%(NAME)s 446 | ``` 447 | 448 | ```go 449 | cfg.Section("author").Key("GITHUB").String() // https://github.com/Unknwon 450 | cfg.Section("package").Key("FULL_NAME").String() // github.com/go-ini/ini 451 | ``` 452 | 453 | ### Parent-child Sections 454 | 455 | 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. 456 | 457 | ```ini 458 | NAME = ini 459 | VERSION = v1 460 | IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s 461 | 462 | [package] 463 | CLONE_URL = https://%(IMPORT_PATH)s 464 | 465 | [package.sub] 466 | ``` 467 | 468 | ```go 469 | cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1 470 | ``` 471 | 472 | #### Retrieve parent keys available to a child section 473 | 474 | ```go 475 | cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"] 476 | ``` 477 | 478 | ### Unparseable Sections 479 | 480 | Sometimes, you have sections that do not contain key-value pairs but raw content, to handle such case, you can use `LoadOptions.UnparsableSections`: 481 | 482 | ```go 483 | cfg, err := LoadSources(LoadOptions{UnparseableSections: []string{"COMMENTS"}}, `[COMMENTS] 484 | <1> This slide has the fuel listed in the wrong units `)) 485 | 486 | body := cfg.Section("COMMENTS").Body() 487 | 488 | /* --- start --- 489 | <1> This slide has the fuel listed in the wrong units 490 | ------ end --- */ 491 | ``` 492 | 493 | ### Auto-increment Key Names 494 | 495 | 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. 496 | 497 | ```ini 498 | [features] 499 | -: Support read/write comments of keys and sections 500 | -: Support auto-increment of key names 501 | -: Support load multiple files to overwrite key values 502 | ``` 503 | 504 | ```go 505 | cfg.Section("features").KeyStrings() // []{"#1", "#2", "#3"} 506 | ``` 507 | 508 | ### Map To Struct 509 | 510 | Want more objective way to play with INI? Cool. 511 | 512 | ```ini 513 | Name = Unknwon 514 | age = 21 515 | Male = true 516 | Born = 1993-01-01T20:17:05Z 517 | 518 | [Note] 519 | Content = Hi is a good man! 520 | Cities = HangZhou, Boston 521 | ``` 522 | 523 | ```go 524 | type Note struct { 525 | Content string 526 | Cities []string 527 | } 528 | 529 | type Person struct { 530 | Name string 531 | Age int `ini:"age"` 532 | Male bool 533 | Born time.Time 534 | Note 535 | Created time.Time `ini:"-"` 536 | } 537 | 538 | func main() { 539 | cfg, err := ini.Load("path/to/ini") 540 | // ... 541 | p := new(Person) 542 | err = cfg.MapTo(p) 543 | // ... 544 | 545 | // Things can be simpler. 546 | err = ini.MapTo(p, "path/to/ini") 547 | // ... 548 | 549 | // Just map a section? Fine. 550 | n := new(Note) 551 | err = cfg.Section("Note").MapTo(n) 552 | // ... 553 | } 554 | ``` 555 | 556 | Can I have default value for field? Absolutely. 557 | 558 | 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. 559 | 560 | ```go 561 | // ... 562 | p := &Person{ 563 | Name: "Joe", 564 | } 565 | // ... 566 | ``` 567 | 568 | It's really cool, but what's the point if you can't give me my file back from struct? 569 | 570 | ### Reflect From Struct 571 | 572 | Why not? 573 | 574 | ```go 575 | type Embeded struct { 576 | Dates []time.Time `delim:"|"` 577 | Places []string `ini:"places,omitempty"` 578 | None []int `ini:",omitempty"` 579 | } 580 | 581 | type Author struct { 582 | Name string `ini:"NAME"` 583 | Male bool 584 | Age int 585 | GPA float64 586 | NeverMind string `ini:"-"` 587 | *Embeded 588 | } 589 | 590 | func main() { 591 | a := &Author{"Unknwon", true, 21, 2.8, "", 592 | &Embeded{ 593 | []time.Time{time.Now(), time.Now()}, 594 | []string{"HangZhou", "Boston"}, 595 | []int{}, 596 | }} 597 | cfg := ini.Empty() 598 | err = ini.ReflectFrom(cfg, a) 599 | // ... 600 | } 601 | ``` 602 | 603 | So, what do I get? 604 | 605 | ```ini 606 | NAME = Unknwon 607 | Male = true 608 | Age = 21 609 | GPA = 2.8 610 | 611 | [Embeded] 612 | Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00 613 | places = HangZhou,Boston 614 | ``` 615 | 616 | #### Name Mapper 617 | 618 | 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. 619 | 620 | There are 2 built-in name mappers: 621 | 622 | - `AllCapsUnderscore`: it converts to format `ALL_CAPS_UNDERSCORE` then match section or key. 623 | - `TitleUnderscore`: it converts to format `title_underscore` then match section or key. 624 | 625 | To use them: 626 | 627 | ```go 628 | type Info struct { 629 | PackageName string 630 | } 631 | 632 | func main() { 633 | err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("package_name=ini")) 634 | // ... 635 | 636 | cfg, err := ini.Load([]byte("PACKAGE_NAME=ini")) 637 | // ... 638 | info := new(Info) 639 | cfg.NameMapper = ini.AllCapsUnderscore 640 | err = cfg.MapTo(info) 641 | // ... 642 | } 643 | ``` 644 | 645 | Same rules of name mapper apply to `ini.ReflectFromWithMapper` function. 646 | 647 | #### Value Mapper 648 | 649 | To expand values (e.g. from environment variables), you can use the `ValueMapper` to transform values: 650 | 651 | ```go 652 | type Env struct { 653 | Foo string `ini:"foo"` 654 | } 655 | 656 | func main() { 657 | cfg, err := ini.Load([]byte("[env]\nfoo = ${MY_VAR}\n") 658 | cfg.ValueMapper = os.ExpandEnv 659 | // ... 660 | env := &Env{} 661 | err = cfg.Section("env").MapTo(env) 662 | } 663 | ``` 664 | 665 | This would set the value of `env.Foo` to the value of the environment variable `MY_VAR`. 666 | 667 | #### Other Notes On Map/Reflect 668 | 669 | Any embedded struct is treated as a section by default, and there is no automatic parent-child relations in map/reflect feature: 670 | 671 | ```go 672 | type Child struct { 673 | Age string 674 | } 675 | 676 | type Parent struct { 677 | Name string 678 | Child 679 | } 680 | 681 | type Config struct { 682 | City string 683 | Parent 684 | } 685 | ``` 686 | 687 | Example configuration: 688 | 689 | ```ini 690 | City = Boston 691 | 692 | [Parent] 693 | Name = Unknwon 694 | 695 | [Child] 696 | Age = 21 697 | ``` 698 | 699 | What if, yes, I'm paranoid, I want embedded struct to be in the same section. Well, all roads lead to Rome. 700 | 701 | ```go 702 | type Child struct { 703 | Age string 704 | } 705 | 706 | type Parent struct { 707 | Name string 708 | Child `ini:"Parent"` 709 | } 710 | 711 | type Config struct { 712 | City string 713 | Parent 714 | } 715 | ``` 716 | 717 | Example configuration: 718 | 719 | ```ini 720 | City = Boston 721 | 722 | [Parent] 723 | Name = Unknwon 724 | Age = 21 725 | ``` 726 | 727 | ## Getting Help 728 | 729 | - [API Documentation](https://gowalker.org/gopkg.in/ini.v1) 730 | - [File An Issue](https://github.com/go-ini/ini/issues/new) 731 | 732 | ## FAQs 733 | 734 | ### What does `BlockMode` field do? 735 | 736 | 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. 737 | 738 | ### Why another INI library? 739 | 740 | 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. 741 | 742 | 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) 743 | 744 | ## License 745 | 746 | This project is under Apache v2 License. See the [LICENSE](LICENSE) file for the full license text. 747 | -------------------------------------------------------------------------------- /vendor/gopkg.in/ini.v1/key.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package ini 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "strconv" 21 | "strings" 22 | "time" 23 | ) 24 | 25 | // Key represents a key under a section. 26 | type Key struct { 27 | s *Section 28 | name string 29 | value string 30 | isAutoIncrement bool 31 | isBooleanType bool 32 | 33 | isShadow bool 34 | shadows []*Key 35 | 36 | Comment string 37 | } 38 | 39 | // newKey simply return a key object with given values. 40 | func newKey(s *Section, name, val string) *Key { 41 | return &Key{ 42 | s: s, 43 | name: name, 44 | value: val, 45 | } 46 | } 47 | 48 | func (k *Key) addShadow(val string) error { 49 | if k.isShadow { 50 | return errors.New("cannot add shadow to another shadow key") 51 | } else if k.isAutoIncrement || k.isBooleanType { 52 | return errors.New("cannot add shadow to auto-increment or boolean key") 53 | } 54 | 55 | shadow := newKey(k.s, k.name, val) 56 | shadow.isShadow = true 57 | k.shadows = append(k.shadows, shadow) 58 | return nil 59 | } 60 | 61 | // AddShadow adds a new shadow key to itself. 62 | func (k *Key) AddShadow(val string) error { 63 | if !k.s.f.options.AllowShadows { 64 | return errors.New("shadow key is not allowed") 65 | } 66 | return k.addShadow(val) 67 | } 68 | 69 | // ValueMapper represents a mapping function for values, e.g. os.ExpandEnv 70 | type ValueMapper func(string) string 71 | 72 | // Name returns name of key. 73 | func (k *Key) Name() string { 74 | return k.name 75 | } 76 | 77 | // Value returns raw value of key for performance purpose. 78 | func (k *Key) Value() string { 79 | return k.value 80 | } 81 | 82 | // ValueWithShadows returns raw values of key and its shadows if any. 83 | func (k *Key) ValueWithShadows() []string { 84 | if len(k.shadows) == 0 { 85 | return []string{k.value} 86 | } 87 | vals := make([]string, len(k.shadows)+1) 88 | vals[0] = k.value 89 | for i := range k.shadows { 90 | vals[i+1] = k.shadows[i].value 91 | } 92 | return vals 93 | } 94 | 95 | // transformValue takes a raw value and transforms to its final string. 96 | func (k *Key) transformValue(val string) string { 97 | if k.s.f.ValueMapper != nil { 98 | val = k.s.f.ValueMapper(val) 99 | } 100 | 101 | // Fail-fast if no indicate char found for recursive value 102 | if !strings.Contains(val, "%") { 103 | return val 104 | } 105 | for i := 0; i < _DEPTH_VALUES; i++ { 106 | vr := varPattern.FindString(val) 107 | if len(vr) == 0 { 108 | break 109 | } 110 | 111 | // Take off leading '%(' and trailing ')s'. 112 | noption := strings.TrimLeft(vr, "%(") 113 | noption = strings.TrimRight(noption, ")s") 114 | 115 | // Search in the same section. 116 | nk, err := k.s.GetKey(noption) 117 | if err != nil { 118 | // Search again in default section. 119 | nk, _ = k.s.f.Section("").GetKey(noption) 120 | } 121 | 122 | // Substitute by new value and take off leading '%(' and trailing ')s'. 123 | val = strings.Replace(val, vr, nk.value, -1) 124 | } 125 | return val 126 | } 127 | 128 | // String returns string representation of value. 129 | func (k *Key) String() string { 130 | return k.transformValue(k.value) 131 | } 132 | 133 | // Validate accepts a validate function which can 134 | // return modifed result as key value. 135 | func (k *Key) Validate(fn func(string) string) string { 136 | return fn(k.String()) 137 | } 138 | 139 | // parseBool returns the boolean value represented by the string. 140 | // 141 | // It accepts 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On, 142 | // 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off. 143 | // Any other value returns an error. 144 | func parseBool(str string) (value bool, err error) { 145 | switch str { 146 | case "1", "t", "T", "true", "TRUE", "True", "YES", "yes", "Yes", "y", "ON", "on", "On": 147 | return true, nil 148 | case "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "n", "OFF", "off", "Off": 149 | return false, nil 150 | } 151 | return false, fmt.Errorf("parsing \"%s\": invalid syntax", str) 152 | } 153 | 154 | // Bool returns bool type value. 155 | func (k *Key) Bool() (bool, error) { 156 | return parseBool(k.String()) 157 | } 158 | 159 | // Float64 returns float64 type value. 160 | func (k *Key) Float64() (float64, error) { 161 | return strconv.ParseFloat(k.String(), 64) 162 | } 163 | 164 | // Int returns int type value. 165 | func (k *Key) Int() (int, error) { 166 | return strconv.Atoi(k.String()) 167 | } 168 | 169 | // Int64 returns int64 type value. 170 | func (k *Key) Int64() (int64, error) { 171 | return strconv.ParseInt(k.String(), 10, 64) 172 | } 173 | 174 | // Uint returns uint type valued. 175 | func (k *Key) Uint() (uint, error) { 176 | u, e := strconv.ParseUint(k.String(), 10, 64) 177 | return uint(u), e 178 | } 179 | 180 | // Uint64 returns uint64 type value. 181 | func (k *Key) Uint64() (uint64, error) { 182 | return strconv.ParseUint(k.String(), 10, 64) 183 | } 184 | 185 | // Duration returns time.Duration type value. 186 | func (k *Key) Duration() (time.Duration, error) { 187 | return time.ParseDuration(k.String()) 188 | } 189 | 190 | // TimeFormat parses with given format and returns time.Time type value. 191 | func (k *Key) TimeFormat(format string) (time.Time, error) { 192 | return time.Parse(format, k.String()) 193 | } 194 | 195 | // Time parses with RFC3339 format and returns time.Time type value. 196 | func (k *Key) Time() (time.Time, error) { 197 | return k.TimeFormat(time.RFC3339) 198 | } 199 | 200 | // MustString returns default value if key value is empty. 201 | func (k *Key) MustString(defaultVal string) string { 202 | val := k.String() 203 | if len(val) == 0 { 204 | k.value = defaultVal 205 | return defaultVal 206 | } 207 | return val 208 | } 209 | 210 | // MustBool always returns value without error, 211 | // it returns false if error occurs. 212 | func (k *Key) MustBool(defaultVal ...bool) bool { 213 | val, err := k.Bool() 214 | if len(defaultVal) > 0 && err != nil { 215 | k.value = strconv.FormatBool(defaultVal[0]) 216 | return defaultVal[0] 217 | } 218 | return val 219 | } 220 | 221 | // MustFloat64 always returns value without error, 222 | // it returns 0.0 if error occurs. 223 | func (k *Key) MustFloat64(defaultVal ...float64) float64 { 224 | val, err := k.Float64() 225 | if len(defaultVal) > 0 && err != nil { 226 | k.value = strconv.FormatFloat(defaultVal[0], 'f', -1, 64) 227 | return defaultVal[0] 228 | } 229 | return val 230 | } 231 | 232 | // MustInt always returns value without error, 233 | // it returns 0 if error occurs. 234 | func (k *Key) MustInt(defaultVal ...int) int { 235 | val, err := k.Int() 236 | if len(defaultVal) > 0 && err != nil { 237 | k.value = strconv.FormatInt(int64(defaultVal[0]), 10) 238 | return defaultVal[0] 239 | } 240 | return val 241 | } 242 | 243 | // MustInt64 always returns value without error, 244 | // it returns 0 if error occurs. 245 | func (k *Key) MustInt64(defaultVal ...int64) int64 { 246 | val, err := k.Int64() 247 | if len(defaultVal) > 0 && err != nil { 248 | k.value = strconv.FormatInt(defaultVal[0], 10) 249 | return defaultVal[0] 250 | } 251 | return val 252 | } 253 | 254 | // MustUint always returns value without error, 255 | // it returns 0 if error occurs. 256 | func (k *Key) MustUint(defaultVal ...uint) uint { 257 | val, err := k.Uint() 258 | if len(defaultVal) > 0 && err != nil { 259 | k.value = strconv.FormatUint(uint64(defaultVal[0]), 10) 260 | return defaultVal[0] 261 | } 262 | return val 263 | } 264 | 265 | // MustUint64 always returns value without error, 266 | // it returns 0 if error occurs. 267 | func (k *Key) MustUint64(defaultVal ...uint64) uint64 { 268 | val, err := k.Uint64() 269 | if len(defaultVal) > 0 && err != nil { 270 | k.value = strconv.FormatUint(defaultVal[0], 10) 271 | return defaultVal[0] 272 | } 273 | return val 274 | } 275 | 276 | // MustDuration always returns value without error, 277 | // it returns zero value if error occurs. 278 | func (k *Key) MustDuration(defaultVal ...time.Duration) time.Duration { 279 | val, err := k.Duration() 280 | if len(defaultVal) > 0 && err != nil { 281 | k.value = defaultVal[0].String() 282 | return defaultVal[0] 283 | } 284 | return val 285 | } 286 | 287 | // MustTimeFormat always parses with given format and returns value without error, 288 | // it returns zero value if error occurs. 289 | func (k *Key) MustTimeFormat(format string, defaultVal ...time.Time) time.Time { 290 | val, err := k.TimeFormat(format) 291 | if len(defaultVal) > 0 && err != nil { 292 | k.value = defaultVal[0].Format(format) 293 | return defaultVal[0] 294 | } 295 | return val 296 | } 297 | 298 | // MustTime always parses with RFC3339 format and returns value without error, 299 | // it returns zero value if error occurs. 300 | func (k *Key) MustTime(defaultVal ...time.Time) time.Time { 301 | return k.MustTimeFormat(time.RFC3339, defaultVal...) 302 | } 303 | 304 | // In always returns value without error, 305 | // it returns default value if error occurs or doesn't fit into candidates. 306 | func (k *Key) In(defaultVal string, candidates []string) string { 307 | val := k.String() 308 | for _, cand := range candidates { 309 | if val == cand { 310 | return val 311 | } 312 | } 313 | return defaultVal 314 | } 315 | 316 | // InFloat64 always returns value without error, 317 | // it returns default value if error occurs or doesn't fit into candidates. 318 | func (k *Key) InFloat64(defaultVal float64, candidates []float64) float64 { 319 | val := k.MustFloat64() 320 | for _, cand := range candidates { 321 | if val == cand { 322 | return val 323 | } 324 | } 325 | return defaultVal 326 | } 327 | 328 | // InInt always returns value without error, 329 | // it returns default value if error occurs or doesn't fit into candidates. 330 | func (k *Key) InInt(defaultVal int, candidates []int) int { 331 | val := k.MustInt() 332 | for _, cand := range candidates { 333 | if val == cand { 334 | return val 335 | } 336 | } 337 | return defaultVal 338 | } 339 | 340 | // InInt64 always returns value without error, 341 | // it returns default value if error occurs or doesn't fit into candidates. 342 | func (k *Key) InInt64(defaultVal int64, candidates []int64) int64 { 343 | val := k.MustInt64() 344 | for _, cand := range candidates { 345 | if val == cand { 346 | return val 347 | } 348 | } 349 | return defaultVal 350 | } 351 | 352 | // InUint always returns value without error, 353 | // it returns default value if error occurs or doesn't fit into candidates. 354 | func (k *Key) InUint(defaultVal uint, candidates []uint) uint { 355 | val := k.MustUint() 356 | for _, cand := range candidates { 357 | if val == cand { 358 | return val 359 | } 360 | } 361 | return defaultVal 362 | } 363 | 364 | // InUint64 always returns value without error, 365 | // it returns default value if error occurs or doesn't fit into candidates. 366 | func (k *Key) InUint64(defaultVal uint64, candidates []uint64) uint64 { 367 | val := k.MustUint64() 368 | for _, cand := range candidates { 369 | if val == cand { 370 | return val 371 | } 372 | } 373 | return defaultVal 374 | } 375 | 376 | // InTimeFormat always parses with given format and returns value without error, 377 | // it returns default value if error occurs or doesn't fit into candidates. 378 | func (k *Key) InTimeFormat(format string, defaultVal time.Time, candidates []time.Time) time.Time { 379 | val := k.MustTimeFormat(format) 380 | for _, cand := range candidates { 381 | if val == cand { 382 | return val 383 | } 384 | } 385 | return defaultVal 386 | } 387 | 388 | // InTime always parses with RFC3339 format and returns value without error, 389 | // it returns default value if error occurs or doesn't fit into candidates. 390 | func (k *Key) InTime(defaultVal time.Time, candidates []time.Time) time.Time { 391 | return k.InTimeFormat(time.RFC3339, defaultVal, candidates) 392 | } 393 | 394 | // RangeFloat64 checks if value is in given range inclusively, 395 | // and returns default value if it's not. 396 | func (k *Key) RangeFloat64(defaultVal, min, max float64) float64 { 397 | val := k.MustFloat64() 398 | if val < min || val > max { 399 | return defaultVal 400 | } 401 | return val 402 | } 403 | 404 | // RangeInt checks if value is in given range inclusively, 405 | // and returns default value if it's not. 406 | func (k *Key) RangeInt(defaultVal, min, max int) int { 407 | val := k.MustInt() 408 | if val < min || val > max { 409 | return defaultVal 410 | } 411 | return val 412 | } 413 | 414 | // RangeInt64 checks if value is in given range inclusively, 415 | // and returns default value if it's not. 416 | func (k *Key) RangeInt64(defaultVal, min, max int64) int64 { 417 | val := k.MustInt64() 418 | if val < min || val > max { 419 | return defaultVal 420 | } 421 | return val 422 | } 423 | 424 | // RangeTimeFormat checks if value with given format is in given range inclusively, 425 | // and returns default value if it's not. 426 | func (k *Key) RangeTimeFormat(format string, defaultVal, min, max time.Time) time.Time { 427 | val := k.MustTimeFormat(format) 428 | if val.Unix() < min.Unix() || val.Unix() > max.Unix() { 429 | return defaultVal 430 | } 431 | return val 432 | } 433 | 434 | // RangeTime checks if value with RFC3339 format is in given range inclusively, 435 | // and returns default value if it's not. 436 | func (k *Key) RangeTime(defaultVal, min, max time.Time) time.Time { 437 | return k.RangeTimeFormat(time.RFC3339, defaultVal, min, max) 438 | } 439 | 440 | // Strings returns list of string divided by given delimiter. 441 | func (k *Key) Strings(delim string) []string { 442 | str := k.String() 443 | if len(str) == 0 { 444 | return []string{} 445 | } 446 | 447 | vals := strings.Split(str, delim) 448 | for i := range vals { 449 | // vals[i] = k.transformValue(strings.TrimSpace(vals[i])) 450 | vals[i] = strings.TrimSpace(vals[i]) 451 | } 452 | return vals 453 | } 454 | 455 | // StringsWithShadows returns list of string divided by given delimiter. 456 | // Shadows will also be appended if any. 457 | func (k *Key) StringsWithShadows(delim string) []string { 458 | vals := k.ValueWithShadows() 459 | results := make([]string, 0, len(vals)*2) 460 | for i := range vals { 461 | if len(vals) == 0 { 462 | continue 463 | } 464 | 465 | results = append(results, strings.Split(vals[i], delim)...) 466 | } 467 | 468 | for i := range results { 469 | results[i] = k.transformValue(strings.TrimSpace(results[i])) 470 | } 471 | return results 472 | } 473 | 474 | // Float64s returns list of float64 divided by given delimiter. Any invalid input will be treated as zero value. 475 | func (k *Key) Float64s(delim string) []float64 { 476 | vals, _ := k.parseFloat64s(k.Strings(delim), true, false) 477 | return vals 478 | } 479 | 480 | // Ints returns list of int divided by given delimiter. Any invalid input will be treated as zero value. 481 | func (k *Key) Ints(delim string) []int { 482 | vals, _ := k.parseInts(k.Strings(delim), true, false) 483 | return vals 484 | } 485 | 486 | // Int64s returns list of int64 divided by given delimiter. Any invalid input will be treated as zero value. 487 | func (k *Key) Int64s(delim string) []int64 { 488 | vals, _ := k.parseInt64s(k.Strings(delim), true, false) 489 | return vals 490 | } 491 | 492 | // Uints returns list of uint divided by given delimiter. Any invalid input will be treated as zero value. 493 | func (k *Key) Uints(delim string) []uint { 494 | vals, _ := k.parseUints(k.Strings(delim), true, false) 495 | return vals 496 | } 497 | 498 | // Uint64s returns list of uint64 divided by given delimiter. Any invalid input will be treated as zero value. 499 | func (k *Key) Uint64s(delim string) []uint64 { 500 | vals, _ := k.parseUint64s(k.Strings(delim), true, false) 501 | return vals 502 | } 503 | 504 | // TimesFormat parses with given format and returns list of time.Time divided by given delimiter. 505 | // Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC). 506 | func (k *Key) TimesFormat(format, delim string) []time.Time { 507 | vals, _ := k.parseTimesFormat(format, k.Strings(delim), true, false) 508 | return vals 509 | } 510 | 511 | // Times parses with RFC3339 format and returns list of time.Time divided by given delimiter. 512 | // Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC). 513 | func (k *Key) Times(delim string) []time.Time { 514 | return k.TimesFormat(time.RFC3339, delim) 515 | } 516 | 517 | // ValidFloat64s returns list of float64 divided by given delimiter. If some value is not float, then 518 | // it will not be included to result list. 519 | func (k *Key) ValidFloat64s(delim string) []float64 { 520 | vals, _ := k.parseFloat64s(k.Strings(delim), false, false) 521 | return vals 522 | } 523 | 524 | // ValidInts returns list of int divided by given delimiter. If some value is not integer, then it will 525 | // not be included to result list. 526 | func (k *Key) ValidInts(delim string) []int { 527 | vals, _ := k.parseInts(k.Strings(delim), false, false) 528 | return vals 529 | } 530 | 531 | // ValidInt64s returns list of int64 divided by given delimiter. If some value is not 64-bit integer, 532 | // then it will not be included to result list. 533 | func (k *Key) ValidInt64s(delim string) []int64 { 534 | vals, _ := k.parseInt64s(k.Strings(delim), false, false) 535 | return vals 536 | } 537 | 538 | // ValidUints returns list of uint divided by given delimiter. If some value is not unsigned integer, 539 | // then it will not be included to result list. 540 | func (k *Key) ValidUints(delim string) []uint { 541 | vals, _ := k.parseUints(k.Strings(delim), false, false) 542 | return vals 543 | } 544 | 545 | // ValidUint64s returns list of uint64 divided by given delimiter. If some value is not 64-bit unsigned 546 | // integer, then it will not be included to result list. 547 | func (k *Key) ValidUint64s(delim string) []uint64 { 548 | vals, _ := k.parseUint64s(k.Strings(delim), false, false) 549 | return vals 550 | } 551 | 552 | // ValidTimesFormat parses with given format and returns list of time.Time divided by given delimiter. 553 | func (k *Key) ValidTimesFormat(format, delim string) []time.Time { 554 | vals, _ := k.parseTimesFormat(format, k.Strings(delim), false, false) 555 | return vals 556 | } 557 | 558 | // ValidTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter. 559 | func (k *Key) ValidTimes(delim string) []time.Time { 560 | return k.ValidTimesFormat(time.RFC3339, delim) 561 | } 562 | 563 | // StrictFloat64s returns list of float64 divided by given delimiter or error on first invalid input. 564 | func (k *Key) StrictFloat64s(delim string) ([]float64, error) { 565 | return k.parseFloat64s(k.Strings(delim), false, true) 566 | } 567 | 568 | // StrictInts returns list of int divided by given delimiter or error on first invalid input. 569 | func (k *Key) StrictInts(delim string) ([]int, error) { 570 | return k.parseInts(k.Strings(delim), false, true) 571 | } 572 | 573 | // StrictInt64s returns list of int64 divided by given delimiter or error on first invalid input. 574 | func (k *Key) StrictInt64s(delim string) ([]int64, error) { 575 | return k.parseInt64s(k.Strings(delim), false, true) 576 | } 577 | 578 | // StrictUints returns list of uint divided by given delimiter or error on first invalid input. 579 | func (k *Key) StrictUints(delim string) ([]uint, error) { 580 | return k.parseUints(k.Strings(delim), false, true) 581 | } 582 | 583 | // StrictUint64s returns list of uint64 divided by given delimiter or error on first invalid input. 584 | func (k *Key) StrictUint64s(delim string) ([]uint64, error) { 585 | return k.parseUint64s(k.Strings(delim), false, true) 586 | } 587 | 588 | // StrictTimesFormat parses with given format and returns list of time.Time divided by given delimiter 589 | // or error on first invalid input. 590 | func (k *Key) StrictTimesFormat(format, delim string) ([]time.Time, error) { 591 | return k.parseTimesFormat(format, k.Strings(delim), false, true) 592 | } 593 | 594 | // StrictTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter 595 | // or error on first invalid input. 596 | func (k *Key) StrictTimes(delim string) ([]time.Time, error) { 597 | return k.StrictTimesFormat(time.RFC3339, delim) 598 | } 599 | 600 | // parseFloat64s transforms strings to float64s. 601 | func (k *Key) parseFloat64s(strs []string, addInvalid, returnOnInvalid bool) ([]float64, error) { 602 | vals := make([]float64, 0, len(strs)) 603 | for _, str := range strs { 604 | val, err := strconv.ParseFloat(str, 64) 605 | if err != nil && returnOnInvalid { 606 | return nil, err 607 | } 608 | if err == nil || addInvalid { 609 | vals = append(vals, val) 610 | } 611 | } 612 | return vals, nil 613 | } 614 | 615 | // parseInts transforms strings to ints. 616 | func (k *Key) parseInts(strs []string, addInvalid, returnOnInvalid bool) ([]int, error) { 617 | vals := make([]int, 0, len(strs)) 618 | for _, str := range strs { 619 | val, err := strconv.Atoi(str) 620 | if err != nil && returnOnInvalid { 621 | return nil, err 622 | } 623 | if err == nil || addInvalid { 624 | vals = append(vals, val) 625 | } 626 | } 627 | return vals, nil 628 | } 629 | 630 | // parseInt64s transforms strings to int64s. 631 | func (k *Key) parseInt64s(strs []string, addInvalid, returnOnInvalid bool) ([]int64, error) { 632 | vals := make([]int64, 0, len(strs)) 633 | for _, str := range strs { 634 | val, err := strconv.ParseInt(str, 10, 64) 635 | if err != nil && returnOnInvalid { 636 | return nil, err 637 | } 638 | if err == nil || addInvalid { 639 | vals = append(vals, val) 640 | } 641 | } 642 | return vals, nil 643 | } 644 | 645 | // parseUints transforms strings to uints. 646 | func (k *Key) parseUints(strs []string, addInvalid, returnOnInvalid bool) ([]uint, error) { 647 | vals := make([]uint, 0, len(strs)) 648 | for _, str := range strs { 649 | val, err := strconv.ParseUint(str, 10, 0) 650 | if err != nil && returnOnInvalid { 651 | return nil, err 652 | } 653 | if err == nil || addInvalid { 654 | vals = append(vals, uint(val)) 655 | } 656 | } 657 | return vals, nil 658 | } 659 | 660 | // parseUint64s transforms strings to uint64s. 661 | func (k *Key) parseUint64s(strs []string, addInvalid, returnOnInvalid bool) ([]uint64, error) { 662 | vals := make([]uint64, 0, len(strs)) 663 | for _, str := range strs { 664 | val, err := strconv.ParseUint(str, 10, 64) 665 | if err != nil && returnOnInvalid { 666 | return nil, err 667 | } 668 | if err == nil || addInvalid { 669 | vals = append(vals, val) 670 | } 671 | } 672 | return vals, nil 673 | } 674 | 675 | // parseTimesFormat transforms strings to times in given format. 676 | func (k *Key) parseTimesFormat(format string, strs []string, addInvalid, returnOnInvalid bool) ([]time.Time, error) { 677 | vals := make([]time.Time, 0, len(strs)) 678 | for _, str := range strs { 679 | val, err := time.Parse(format, str) 680 | if err != nil && returnOnInvalid { 681 | return nil, err 682 | } 683 | if err == nil || addInvalid { 684 | vals = append(vals, val) 685 | } 686 | } 687 | return vals, nil 688 | } 689 | 690 | // SetValue changes key value. 691 | func (k *Key) SetValue(v string) { 692 | if k.s.f.BlockMode { 693 | k.s.f.lock.Lock() 694 | defer k.s.f.lock.Unlock() 695 | } 696 | 697 | k.value = v 698 | k.s.keysHash[k.name] = v 699 | } 700 | --------------------------------------------------------------------------------