From f828428344be7f9b3d9a517dca7b7685882ed074 Mon Sep 17 00:00:00 2001 From: Yu Jing Date: Sun, 8 Oct 2023 00:39:03 +0800 Subject: [PATCH] init project --- .gitignore | 18 ++ .travis.yml | 20 ++ LICENSE | 21 ++ README.md | 11 + assert/README.md | 3 + assert/assert.go | 364 ++++++++++++++++++++++++ assert/assert_test.go | 102 +++++++ assert/compare.go | 309 ++++++++++++++++++++ cli/README.md | 2 + cli/color.go | 194 +++++++++++++ cli/color_test.go | 46 +++ cntr/README.md | 2 + cntr/cntr.go | 3 + cntr/distinct.go | 32 +++ cntr/distinct_test.go | 21 ++ cntr/edit_distance.go | 84 ++++++ cntr/edit_distance_test.go | 83 ++++++ cntr/m.go | 134 +++++++++ cntr/m_test.go | 34 +++ cntr/rand_str.go | 28 ++ cntr/rand_str_test.go | 25 ++ compr/README.md | 2 + compr/compr.go | 2 + compr/gzip.go | 30 ++ config/README.md | 2 + config/config.go | 71 +++++ config/key_builder.go | 107 +++++++ config/keys.go | 35 +++ config/loader.go | 163 +++++++++++ config/mail.go | 62 ++++ config/mongo.go | 59 ++++ config/redis.go | 28 ++ debug/README.md | 0 debug/stopwatch.go | 213 ++++++++++++++ debug/stopwatch_test.go | 21 ++ go.mod | 36 +++ go.sum | 506 +++++++++++++++++++++++++++++++++ log/README.md | 2 + log/logger.go | 157 ++++++++++ log/logger_test.go | 41 +++ mail/closer.go | 118 ++++++++ mail/closer_test.go | 77 +++++ mail/mail.go | 5 + mail/mail_test.go | 1 + mail/smtp.go | 227 +++++++++++++++ mail/smtp_test.go | 7 + mongo/ac.go | 408 ++++++++++++++++++++++++++ mongo/mongo.go | 300 +++++++++++++++++++ mongo/utils.go | 89 ++++++ mtx/critical_section.go | 179 ++++++++++++ mtx/critical_section_test.go | 160 +++++++++++ mtx/mtx.go | 2 + mtx/singleton.go | 29 ++ mtx/singleton_test.go | 34 +++ mtx/wg_state.go | 57 ++++ mtx/wg_state_test.go | 23 ++ pb/struct.go | 218 ++++++++++++++ pb/struct_test.go | 64 +++++ schd/fifo_queue.go | 27 ++ schd/fifo_queue_test.go | 27 ++ schd/multi_task_ticker.go | 178 ++++++++++++ schd/multi_task_ticker_test.go | 40 +++ schd/schd.go | 26 ++ schd/task_queue.go | 152 ++++++++++ schd/task_queue_test.go | 101 +++++++ sigr/README.md | 79 +++++ sigr/doc.go | 1 + sigr/sigr.go | 99 +++++++ sigr/sigr_test.go | 85 ++++++ stork.go | 1 + web/cookie.go | 55 ++++ web/request.go | 23 ++ 72 files changed, 5965 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assert/README.md create mode 100644 assert/assert.go create mode 100644 assert/assert_test.go create mode 100644 assert/compare.go create mode 100644 cli/README.md create mode 100644 cli/color.go create mode 100644 cli/color_test.go create mode 100644 cntr/README.md create mode 100644 cntr/cntr.go create mode 100644 cntr/distinct.go create mode 100644 cntr/distinct_test.go create mode 100644 cntr/edit_distance.go create mode 100644 cntr/edit_distance_test.go create mode 100644 cntr/m.go create mode 100644 cntr/m_test.go create mode 100644 cntr/rand_str.go create mode 100644 cntr/rand_str_test.go create mode 100644 compr/README.md create mode 100644 compr/compr.go create mode 100644 compr/gzip.go create mode 100644 config/README.md create mode 100644 config/config.go create mode 100644 config/key_builder.go create mode 100644 config/keys.go create mode 100644 config/loader.go create mode 100644 config/mail.go create mode 100644 config/mongo.go create mode 100644 config/redis.go create mode 100644 debug/README.md create mode 100644 debug/stopwatch.go create mode 100644 debug/stopwatch_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 log/README.md create mode 100644 log/logger.go create mode 100644 log/logger_test.go create mode 100644 mail/closer.go create mode 100644 mail/closer_test.go create mode 100644 mail/mail.go create mode 100644 mail/mail_test.go create mode 100644 mail/smtp.go create mode 100644 mail/smtp_test.go create mode 100644 mongo/ac.go create mode 100644 mongo/mongo.go create mode 100644 mongo/utils.go create mode 100644 mtx/critical_section.go create mode 100644 mtx/critical_section_test.go create mode 100644 mtx/mtx.go create mode 100644 mtx/singleton.go create mode 100644 mtx/singleton_test.go create mode 100644 mtx/wg_state.go create mode 100644 mtx/wg_state_test.go create mode 100644 pb/struct.go create mode 100644 pb/struct_test.go create mode 100644 schd/fifo_queue.go create mode 100644 schd/fifo_queue_test.go create mode 100644 schd/multi_task_ticker.go create mode 100644 schd/multi_task_ticker_test.go create mode 100644 schd/schd.go create mode 100644 schd/task_queue.go create mode 100644 schd/task_queue_test.go create mode 100644 sigr/README.md create mode 100644 sigr/doc.go create mode 100644 sigr/sigr.go create mode 100644 sigr/sigr_test.go create mode 100644 stork.go create mode 100644 web/cookie.go create mode 100644 web/request.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5bec7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +/coverage.txt +/.idea + +/vendor/ + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6fde95e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: go + +go: + - "1.10" + - 1.11 + - 1.12 + - 1.13 + - master + +install: + - go get -t ./... + +script: + - go build + - go test -v ./... + - go test -v -bench=. + - go test -race -coverprofile=coverage.txt -covermode=atomic ./... + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f5a7487 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 ArgCV + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..37593f3 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# STORK + +Simple Tools and Operations Resource Kit + +[![Build Status][badge-travis]][link-travis] +[![codecov][badge-codecov]][link-codecov] + +[badge-travis]: https://travis-ci.org/argcv/stork.svg?branch=master +[link-travis]: https://travis-ci.org/argcv/stork +[badge-codecov]: https://codecov.io/gh/argcv/stork/branch/master/graph/badge.svg +[link-codecov]: https://codecov.io/gh/argcv/stork \ No newline at end of file diff --git a/assert/README.md b/assert/README.md new file mode 100644 index 0000000..142b5c2 --- /dev/null +++ b/assert/README.md @@ -0,0 +1,3 @@ +# Assert + +A simple assertion library for Go. Implements [this doc](https://github.com/google/googletest/blob/main/docs/primer.md) \ No newline at end of file diff --git a/assert/assert.go b/assert/assert.go new file mode 100644 index 0000000..1190f98 --- /dev/null +++ b/assert/assert.go @@ -0,0 +1,364 @@ +package assert + +import ( + "bytes" + "fmt" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" +) + +const ( + defaultSkip = 0 +) + +var ( + testMode = false + testBuff *bytes.Buffer +) + +// Note: Don't Use the test out of assert +func TestFunc(f func()) (ret string) { + testMode = true + testBuff = &bytes.Buffer{} + defer func() { + recover() + testMode = false + }() + f() + ret = testBuff.String() + testBuff = nil + return ret +} + +func ExpectEQ(t testing.TB, expected, actual interface{}, msg ...string) { + if ret := equal(defaultSkip+1, expected, actual, msg...); ret != "" { + if testMode { + testBuff.Write([]byte(ret)) + } else { + fmt.Println(ret) + t.Fail() + } + } +} + +func CheckIsNil(i interface{}) bool { + if i == nil { + return true + } + switch reflect.TypeOf(i).Kind() { + case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: + return reflect.ValueOf(i).IsNil() + } + return false +} + +func CheckIsNotNil(i interface{}) bool { + return !CheckIsNil(i) +} + +func ExpectNil(t testing.TB, actual interface{}, msg ...string) { + if ret := test(defaultSkip+1, + fmt.Sprintf("Expected: nil, actual: '%v'", actual), + func() bool { + //fmt.Fprintf(os.Stderr, "actual: %v | is nil? %v", actual, CheckIsNil(actual)) + return CheckIsNil(actual) + }, + msg...); ret != "" { + if testMode { + testBuff.Write([]byte(ret)) + } else { + fmt.Println(ret) + t.Fail() + } + } +} + +func ExpectNotNil(t testing.TB, actual interface{}, msg ...string) { + if ret := test(defaultSkip+1, + fmt.Sprintf("Expected: not nil, actual: '%v'", actual), + func() bool { + return CheckIsNotNil(actual) + }, + msg...); ret != "" { + if testMode { + testBuff.Write([]byte(ret)) + } else { + fmt.Println(ret) + t.Fail() + } + } +} + +// 期望长度 +// TODO(yu): test cases +func ExpectLen(t testing.TB, length int, actual interface{}, msg ...string) { + s := reflect.ValueOf(actual) + + for s.Kind() == reflect.Ptr { + s = reflect.Indirect(s) + } + + if !s.IsValid() { + ret := test(defaultSkip+1, + fmt.Sprintf("Expected: length = %v, invalid type: %v", length, s.Kind()), + func() bool { + return false + }, + msg...) + if testMode { + testBuff.Write([]byte(ret)) + } else { + fmt.Println(ret) + t.Fail() + } + } + + switch s.Kind() { + case reflect.Slice, reflect.Array: + if ret := test(defaultSkip+1, + fmt.Sprintf("Expected: length = %v, actual: '%v'", length, s.Len()), + func() bool { + return s.Len() == length + }, + msg...); ret != "" { + if testMode { + testBuff.Write([]byte(ret)) + } else { + fmt.Println(ret) + t.Fail() + } + } + default: + ret := test(defaultSkip+1, + fmt.Sprintf("Expected: length = %v, unexpected type: %v", length, s.Kind()), + func() bool { + return false + }, + msg...) + if testMode { + testBuff.Write([]byte(ret)) + } else { + fmt.Println(ret) + t.Fail() + } + } +} + +// 期望出现 error +func ExpectErr(t testing.TB, err error, msg ...string) { + if ret := test(defaultSkip+1, + fmt.Sprintf("Expected: error, actual: '%v'", err), + func() bool { + return err != nil + }, + msg...); ret != "" { + if testMode { + testBuff.Write([]byte(ret)) + } else { + fmt.Println(ret) + t.Fail() + } + } +} + +func ExpectNoErr(t testing.TB, err error, msg ...string) { + if ret := test(defaultSkip+1, + fmt.Sprintf("Expected: no error, actual: '%v'", err), + func() bool { + return err == nil + }, + msg...); ret != "" { + if testMode { + testBuff.Write([]byte(ret)) + } else { + fmt.Println(ret) + t.Fail() + } + } +} + +func ExpectTrue(t testing.TB, condition bool, msg ...string) { + if ret := equal(defaultSkip+1, true, condition, msg...); ret != "" { + if testMode { + testBuff.Write([]byte(ret)) + } else { + fmt.Println(ret) + t.Fail() + } + } +} + +func ExpectFalse(t testing.TB, condition bool, msg ...string) { + if ret := equal(defaultSkip+1, false, condition, msg...); ret != "" { + if testMode { + testBuff.Write([]byte(ret)) + } else { + fmt.Println(ret) + t.Fail() + } + } +} + +func ExpectNE(t testing.TB, expected, actual interface{}, msg ...string) { + if ret := notEqual(defaultSkip+1, expected, actual, msg...); ret != "" { + if testMode { + testBuff.Write([]byte(ret)) + } else { + fmt.Println(ret) + t.Fail() + } + } +} + +// ExpectLT expects val1 less than val2 +func ExpectLT(t testing.TB, val1, val2 interface{}, msg ...string) { + if ret := compare(defaultSkip+1, val1, val2, "<", func() bool { + if val1 == nil || val2 == nil { + return false + } + // + //s1, ok1 := val1.(sort.Interface) + //s2, ok2 := val2.(sort.Interface) + //if ok1 && ok2 { + // return s1.Less() + //} + // prime type + v1 := reflect.ValueOf(val1) + v2 := reflect.ValueOf(val2) + if v1.Type() != v2.Type() { + return false + } + return compareRealValues(val1, val2, v1.Kind(), compareLess) + }, msg...); ret != "" { + if testMode { + testBuff.Write([]byte(ret)) + } else { + fmt.Println(ret) + t.Fail() + } + } +} + +// ExpectGT expect val1 greater than val2 +func ExpectGT(t testing.TB, val1, val2 interface{}, msg ...string) { + if ret := compare(defaultSkip+1, val1, val2, ">", func() bool { + if val1 == nil || val2 == nil { + return false + } + // prime type + v1 := reflect.ValueOf(val1) + v2 := reflect.ValueOf(val2) + if v1.Type() != v2.Type() { + return false + } + return compareRealValues(val1, val2, v1.Kind(), compareGreater) + }, msg...); ret != "" { + if testMode { + testBuff.Write([]byte(ret)) + } else { + fmt.Println(ret) + t.Fail() + } + } +} + +// ExpectLE +func ExpectLE(t testing.TB, val1, val2 interface{}, msg ...string) { + if ret := compare(defaultSkip+1, val1, val2, "<", func() bool { + if val1 == nil || val2 == nil { + return false + } + // + //s1, ok1 := val1.(sort.Interface) + //s2, ok2 := val2.(sort.Interface) + //if ok1 && ok2 { + // return s1.Less() + //} + // prime type + v1 := reflect.ValueOf(val1) + v2 := reflect.ValueOf(val2) + if v1.Type() != v2.Type() { + return false + } + return compareRealValues(val1, val2, v1.Kind(), compareLess|compareEqual) + }, msg...); ret != "" { + if testMode { + testBuff.Write([]byte(ret)) + } else { + fmt.Println(ret) + t.Fail() + } + } +} + +// ExpectGE expect val1 greater than or equal to val2 +func ExpectGE(t testing.TB, val1, val2 interface{}, msg ...string) { + if ret := compare(defaultSkip+1, val1, val2, ">", func() bool { + if val1 == nil || val2 == nil { + return false + } + // prime type + v1 := reflect.ValueOf(val1) + v2 := reflect.ValueOf(val2) + if v1.Type() != v2.Type() { + return false + } + return compareRealValues(val1, val2, v1.Kind(), compareGreater|compareEqual) + }, msg...); ret != "" { + if testMode { + testBuff.Write([]byte(ret)) + } else { + fmt.Println(ret) + t.Fail() + } + } +} + +func compare(skip int, val1, val2 interface{}, operator string, cmp func() bool, msg ...string) string { + return test(skip+1, + fmt.Sprintf("Expected: '%v' %v '%v', actual: '%v' (%v) vs. '%v' (%v)", + val1, operator, val2, + val1, reflect.TypeOf(val1), val2, reflect.TypeOf(val2)), + cmp, + msg...) +} + +func equal(skip int, expected, actual interface{}, msg ...string) string { + return test(skip+1, + fmt.Sprintf("\tExpected: '%v'\n\tWhich is: %v\nTo be equal to: '%v'\n\tWhich is: %v", + expected, reflect.TypeOf(expected), actual, reflect.TypeOf(actual)), + func() bool { + return reflect.DeepEqual(expected, actual) + }, + msg...) +} + +func notEqual(skip int, expected, actual interface{}, msg ...string) string { + return test(skip+1, + fmt.Sprintf("Expected: '%v' != '%v', actual: '%v' (%v) vs. '%v' (%v)", + expected, actual, + expected, reflect.TypeOf(expected), actual, reflect.TypeOf(actual)), + func() bool { + return !reflect.DeepEqual(expected, actual) + }, + msg...) +} + +func test(skip int, concl string, cmp func() bool, msg ...string) string { + if !cmp() { + return fail(skip+1, "Failure:\n%s\nMessage: %s", concl, strings.Join(msg, " ")) + } + return "" +} + +func fail(skip int, format string, args ...interface{}) string { + _, file, line, _ := runtime.Caller(skip + 1) + return fmt.Sprintf(" %s:%d: %s\n", filepath.Base(file), line, fmt.Sprintf(format, args...)) +} + +func TestWrap(skip int, concl string, cmp func() bool, msg ...string) string { + return test(skip+1, concl, cmp, msg...) +} diff --git a/assert/assert_test.go b/assert/assert_test.go new file mode 100644 index 0000000..224e075 --- /dev/null +++ b/assert/assert_test.go @@ -0,0 +1,102 @@ +package assert_test + +import ( + "errors" + "strings" + "testing" + + "github.com/argcv/stork/assert" +) + +func TestExpectEQ(t *testing.T) { + assert.ExpectEQ(t, "aaa", "aaa", "message for test equal") + buff := assert.TestFunc(func() { + assert.ExpectEQ(t, "aaa", "bbb", "message for test equal") + }) + assert.ExpectTrue(t, strings.Contains(buff, "Failure:")) +} + +func TestExpectNE(t *testing.T) { + assert.ExpectNE(t, "aaa", 1, "message for test not equal") + buff := assert.TestFunc(func() { + assert.ExpectNE(t, "aaa", "aaa", "message for test not equal") + }) + assert.ExpectTrue(t, strings.Contains(buff, "Failure:")) +} + +func TestExpectLE(t *testing.T) { + assert.ExpectLE(t, "aaa", "aab", "message for test less equal") + buff := assert.TestFunc(func() { + assert.ExpectLE(t, "aab", "aaa", "message for test less equal") + }) + assert.ExpectTrue(t, strings.Contains(buff, "Failure:")) +} + +func TestExpectNil(t *testing.T) { + assert.ExpectNil(t, nil, "message for test not nil") + buff := assert.TestFunc(func() { + value := 1 + assert.ExpectNil(t, &value, "message for test not nil") + }) + assert.ExpectTrue(t, strings.Contains(buff, "Failure:")) +} + +func TestExpectNotNil(t *testing.T) { + value := 1 + assert.ExpectNotNil(t, &value, "message for test is nil") + buff := assert.TestFunc(func() { + assert.ExpectNotNil(t, nil, "message for test is nil") + }) + assert.ExpectTrue(t, strings.Contains(buff, "Failure:")) +} + +func TestExpectTrue(t *testing.T) { + assert.ExpectTrue(t, true) + buff := assert.TestFunc(func() { + assert.ExpectTrue(t, false) + }) + assert.ExpectTrue(t, strings.Contains(buff, "Failure:")) +} + +func TestExpectFalse(t *testing.T) { + assert.ExpectFalse(t, false) + buff := assert.TestFunc(func() { + assert.ExpectFalse(t, true) + }) + assert.ExpectTrue(t, strings.Contains(buff, "Failure:")) +} + +func TestExpectLT(t *testing.T) { + assert.ExpectLT(t, "aaa", "aab") + buff := assert.TestFunc(func() { + assert.ExpectLT(t, "aab", "aaa") + }) + assert.ExpectTrue(t, strings.Contains(buff, "Failure:")) +} + +func TestExpectErr(t *testing.T) { + assert.ExpectErr(t, errors.New("dummy error")) + buff := assert.TestFunc(func() { + assert.ExpectErr(t, nil) + }) + assert.ExpectTrue(t, strings.Contains(buff, "Failure:")) +} + +func TestExpectNoErr(t *testing.T) { + assert.ExpectNoErr(t, nil) + buff := assert.TestFunc(func() { + assert.ExpectNoErr(t, errors.New("dummy error")) + }) + assert.ExpectTrue(t, strings.Contains(buff, "Failure:")) +} + +func TestTestWrap(t *testing.T) { + buff := assert.TestWrap(1, "con", func() bool { + return true + }) + t.Logf("buff ok: %v", buff) + assert.ExpectEQ(t, "", buff) + buff = assert.TestWrap(1, "con", func() bool { return false }, "it may failed") + t.Logf("buff error: %v", buff) + assert.ExpectTrue(t, strings.Contains(buff, "Failure:")) +} diff --git a/assert/compare.go b/assert/compare.go new file mode 100644 index 0000000..82459e6 --- /dev/null +++ b/assert/compare.go @@ -0,0 +1,309 @@ +package assert + +import ( + "reflect" +) + +// inspired by https://github.com/stretchr/testify/blob/master/assert/assertion_compare.go + +type CompareType int + +const ( + compareLess CompareType = 1 << (iota + 1) + compareEqual + compareGreater +) + +var ( + intType = reflect.TypeOf(int(1)) + int8Type = reflect.TypeOf(int8(1)) + int16Type = reflect.TypeOf(int16(1)) + int32Type = reflect.TypeOf(int32(1)) + int64Type = reflect.TypeOf(int64(1)) + + uintType = reflect.TypeOf(uint(1)) + uint8Type = reflect.TypeOf(uint8(1)) + uint16Type = reflect.TypeOf(uint16(1)) + uint32Type = reflect.TypeOf(uint32(1)) + uint64Type = reflect.TypeOf(uint64(1)) + + float32Type = reflect.TypeOf(float32(1)) + float64Type = reflect.TypeOf(float64(1)) + + stringType = reflect.TypeOf("") +) + +func compareRealValues(obj1, obj2 interface{}, kind reflect.Kind, expected CompareType) bool { + obj1Value := reflect.ValueOf(obj1) + obj2Value := reflect.ValueOf(obj2) + + testIsExpected := func(value CompareType) bool { + return expected&value > 0 + } + + // throughout this switch we try and avoid calling .Convert() if possible, + // as this has a pretty big performance impact + switch kind { + case reflect.Int: + { + intobj1, ok := obj1.(int) + if !ok { + intobj1 = obj1Value.Convert(intType).Interface().(int) + } + intobj2, ok := obj2.(int) + if !ok { + intobj2 = obj2Value.Convert(intType).Interface().(int) + } + if intobj1 > intobj2 { + return testIsExpected(compareGreater) + } + if intobj1 == intobj2 { + return testIsExpected(compareEqual) + } + if intobj1 < intobj2 { + return testIsExpected(compareLess) + } + } + case reflect.Int8: + { + int8obj1, ok := obj1.(int8) + if !ok { + int8obj1 = obj1Value.Convert(int8Type).Interface().(int8) + } + int8obj2, ok := obj2.(int8) + if !ok { + int8obj2 = obj2Value.Convert(int8Type).Interface().(int8) + } + if int8obj1 > int8obj2 { + return testIsExpected(compareGreater) + } + if int8obj1 == int8obj2 { + return testIsExpected(compareEqual) + } + if int8obj1 < int8obj2 { + return testIsExpected(compareLess) + } + } + case reflect.Int16: + { + int16obj1, ok := obj1.(int16) + if !ok { + int16obj1 = obj1Value.Convert(int16Type).Interface().(int16) + } + int16obj2, ok := obj2.(int16) + if !ok { + int16obj2 = obj2Value.Convert(int16Type).Interface().(int16) + } + if int16obj1 > int16obj2 { + return testIsExpected(compareGreater) + } + if int16obj1 == int16obj2 { + return testIsExpected(compareEqual) + } + if int16obj1 < int16obj2 { + return testIsExpected(compareLess) + } + } + case reflect.Int32: + { + int32obj1, ok := obj1.(int32) + if !ok { + int32obj1 = obj1Value.Convert(int32Type).Interface().(int32) + } + int32obj2, ok := obj2.(int32) + if !ok { + int32obj2 = obj2Value.Convert(int32Type).Interface().(int32) + } + if int32obj1 > int32obj2 { + return testIsExpected(compareGreater) + } + if int32obj1 == int32obj2 { + return testIsExpected(compareEqual) + } + if int32obj1 < int32obj2 { + return testIsExpected(compareLess) + } + } + case reflect.Int64: + { + int64obj1, ok := obj1.(int64) + if !ok { + int64obj1 = obj1Value.Convert(int64Type).Interface().(int64) + } + int64obj2, ok := obj2.(int64) + if !ok { + int64obj2 = obj2Value.Convert(int64Type).Interface().(int64) + } + if int64obj1 > int64obj2 { + return testIsExpected(compareGreater) + } + if int64obj1 == int64obj2 { + return testIsExpected(compareEqual) + } + if int64obj1 < int64obj2 { + return testIsExpected(compareLess) + } + } + case reflect.Uint: + { + uintobj1, ok := obj1.(uint) + if !ok { + uintobj1 = obj1Value.Convert(uintType).Interface().(uint) + } + uintobj2, ok := obj2.(uint) + if !ok { + uintobj2 = obj2Value.Convert(uintType).Interface().(uint) + } + if uintobj1 > uintobj2 { + return testIsExpected(compareGreater) + } + if uintobj1 == uintobj2 { + return testIsExpected(compareEqual) + } + if uintobj1 < uintobj2 { + return testIsExpected(compareLess) + } + } + case reflect.Uint8: + { + uint8obj1, ok := obj1.(uint8) + if !ok { + uint8obj1 = obj1Value.Convert(uint8Type).Interface().(uint8) + } + uint8obj2, ok := obj2.(uint8) + if !ok { + uint8obj2 = obj2Value.Convert(uint8Type).Interface().(uint8) + } + if uint8obj1 > uint8obj2 { + return testIsExpected(compareGreater) + } + if uint8obj1 == uint8obj2 { + return testIsExpected(compareEqual) + } + if uint8obj1 < uint8obj2 { + return testIsExpected(compareLess) + } + } + case reflect.Uint16: + { + uint16obj1, ok := obj1.(uint16) + if !ok { + uint16obj1 = obj1Value.Convert(uint16Type).Interface().(uint16) + } + uint16obj2, ok := obj2.(uint16) + if !ok { + uint16obj2 = obj2Value.Convert(uint16Type).Interface().(uint16) + } + if uint16obj1 > uint16obj2 { + return testIsExpected(compareGreater) + } + if uint16obj1 == uint16obj2 { + return testIsExpected(compareEqual) + } + if uint16obj1 < uint16obj2 { + return testIsExpected(compareLess) + } + } + case reflect.Uint32: + { + uint32obj1, ok := obj1.(uint32) + if !ok { + uint32obj1 = obj1Value.Convert(uint32Type).Interface().(uint32) + } + uint32obj2, ok := obj2.(uint32) + if !ok { + uint32obj2 = obj2Value.Convert(uint32Type).Interface().(uint32) + } + if uint32obj1 > uint32obj2 { + return testIsExpected(compareGreater) + } + if uint32obj1 == uint32obj2 { + return testIsExpected(compareEqual) + } + if uint32obj1 < uint32obj2 { + return testIsExpected(compareLess) + } + } + case reflect.Uint64: + { + uint64obj1, ok := obj1.(uint64) + if !ok { + uint64obj1 = obj1Value.Convert(uint64Type).Interface().(uint64) + } + uint64obj2, ok := obj2.(uint64) + if !ok { + uint64obj2 = obj2Value.Convert(uint64Type).Interface().(uint64) + } + if uint64obj1 > uint64obj2 { + return testIsExpected(compareGreater) + } + if uint64obj1 == uint64obj2 { + return testIsExpected(compareEqual) + } + if uint64obj1 < uint64obj2 { + return testIsExpected(compareLess) + } + } + case reflect.Float32: + { + float32obj1, ok := obj1.(float32) + if !ok { + float32obj1 = obj1Value.Convert(float32Type).Interface().(float32) + } + float32obj2, ok := obj2.(float32) + if !ok { + float32obj2 = obj2Value.Convert(float32Type).Interface().(float32) + } + if float32obj1 > float32obj2 { + return testIsExpected(compareGreater) + } + if float32obj1 == float32obj2 { + return testIsExpected(compareEqual) + } + if float32obj1 < float32obj2 { + return testIsExpected(compareLess) + } + } + case reflect.Float64: + { + float64obj1, ok := obj1.(float64) + if !ok { + float64obj1 = obj1Value.Convert(float64Type).Interface().(float64) + } + float64obj2, ok := obj2.(float64) + if !ok { + float64obj2 = obj2Value.Convert(float64Type).Interface().(float64) + } + if float64obj1 > float64obj2 { + return testIsExpected(compareGreater) + } + if float64obj1 == float64obj2 { + return testIsExpected(compareEqual) + } + if float64obj1 < float64obj2 { + return testIsExpected(compareLess) + } + } + case reflect.String: + { + stringobj1, ok := obj1.(string) + if !ok { + stringobj1 = obj1Value.Convert(stringType).Interface().(string) + } + stringobj2, ok := obj2.(string) + if !ok { + stringobj2 = obj2Value.Convert(stringType).Interface().(string) + } + if stringobj1 > stringobj2 { + return testIsExpected(compareGreater) + } + if stringobj1 == stringobj2 { + return testIsExpected(compareEqual) + } + if stringobj1 < stringobj2 { + return testIsExpected(compareLess) + } + } + } + return false +} diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 0000000..bbd450f --- /dev/null +++ b/cli/README.md @@ -0,0 +1,2 @@ +# Command-Line Interface + diff --git a/cli/color.go b/cli/color.go new file mode 100644 index 0000000..b83fd7d --- /dev/null +++ b/cli/color.go @@ -0,0 +1,194 @@ +package cli + +import ( + "bytes" + "fmt" +) + +type Color int +type Attr int + +const ( + Inherit Color = iota + NoColor + Black + Red + Green + Yellow + Blue + Magenta + Cyan + White + BrightBlack + BrightRed + BrightGreen + BrightYellow + BrightBlue + BrightMagenta + BrightCyan + BrightWhite + Bold Attr = iota + Underscore + Blink + ReverseVideo + Concealed +) + +// ref: https://stackoverflow.com/questions/5947742/how-to-change-the-output-color-of-echo-in-linux +var ( + prefix = "\033[" + fgCode = map[Color]int{ + NoColor: 0, + Black: 30, + Red: 31, + Green: 32, + Yellow: 33, + Blue: 34, + Magenta: 35, + Cyan: 36, + White: 37, + BrightBlack: 90, + BrightRed: 91, + BrightGreen: 92, + BrightYellow: 93, + BrightBlue: 94, + BrightMagenta: 95, + BrightCyan: 96, + BrightWhite: 97, + } + bgCode = map[Color]int{ + NoColor: 0, + Black: 40, + Red: 41, + Green: 42, + Yellow: 43, + Blue: 44, + Magenta: 45, + Cyan: 46, + White: 47, + BrightBlack: 100, + BrightRed: 101, + BrightGreen: 102, + BrightYellow: 103, + BrightBlue: 104, + BrightMagenta: 105, + BrightCyan: 106, + BrightWhite: 107, + } +) + +type ColoredText struct { + Text string + FG Color + BG Color + Attrs map[int]bool + Clear bool +} + +func NewColoredText(text string) *ColoredText { + ct := ColoredText{ + Text: text, + Attrs: map[int]bool{}, + } + return ct.Init() +} + +func (c *ColoredText) Reset() *ColoredText { + c.FG = Inherit + c.BG = Inherit + c.Clear = true + return c +} + +func (c *ColoredText) Init() *ColoredText { + c.FG = Inherit + c.BG = Inherit + c.Clear = false + c.Attrs = map[int]bool{} + return c +} + +func (c *ColoredText) SetFG(color Color) *ColoredText { + c.FG = color + return c +} + +func (c *ColoredText) SetBG(color Color) *ColoredText { + c.BG = color + return c +} + +func (c *ColoredText) SetAttr(attr Attr, on bool) *ColoredText { + op := func(val int) { + if on { + c.Attrs[val] = true + } else { + delete(c.Attrs, val) + } + } + switch attr { + case Bold: + op(1) // bold on/off + case Underscore: + op(4) // Underscore on/off + case Blink: + op(5) // Blink on/off + case ReverseVideo: + op(7) // Reverse Video on/off + case Concealed: + op(8) // Concealed on/off + } + return c +} + +func (c *ColoredText) String() string { + var buff []string + if c.Clear { + // Reset + buff = append(buff, prefix) + buff = append(buff, "0m") + buff = append(buff, c.Text) + } else if c.FG == Inherit && c.BG == Inherit && len(c.Attrs) == 0 { + // Just Pass + buff = append(buff, c.Text) + } else { + buff = append(buff, prefix) + cnt := 0 + checkPrefix := func() { + if cnt == 0 { + cnt++ + } else { + cnt++ + buff = append(buff, ";") + } + } + if c.FG != Inherit { + buff = append(buff, fmt.Sprint(fgCode[c.FG])) + cnt++ + } + + if c.BG != Inherit { + checkPrefix() + buff = append(buff, fmt.Sprint(bgCode[c.BG])) + } + + for key, _ := range c.Attrs { + checkPrefix() + buff = append(buff, fmt.Sprint(key)) + } + + buff = append(buff, "m") + + buff = append(buff, c.Text) + // clear all + buff = append(buff, prefix) + buff = append(buff, "0m") + + } + out := bytes.NewBufferString("") + for _, buf := range buff { + //fmt.Printf("append: %T%v\n\n", buf, buf) + out.WriteString(buf) + } + return out.String() +} diff --git a/cli/color_test.go b/cli/color_test.go new file mode 100644 index 0000000..771ee75 --- /dev/null +++ b/cli/color_test.go @@ -0,0 +1,46 @@ +package cli + +import ( + "testing" +) + +func TestColoredText_Init(t *testing.T) { + ct := NewColoredText("colored") + t.Logf("here is a [%v] string", ct.SetFG(BrightWhite).Init().SetBG(BrightBlue).String()) +} + +func TestColoredText_Reset(t *testing.T) { + ct := NewColoredText("colored") + t.Logf("here is a [%v] string", ct.SetFG(BrightWhite).Reset().SetBG(BrightBlue).String()) +} + +func TestColoredText_SetBG(t *testing.T) { + ct := NewColoredText("colored") + t.Logf("here is a [%v] string", ct.SetBG(BrightBlue).String()) +} + +func TestColoredText_SetFG(t *testing.T) { + ct := NewColoredText("colored") + t.Logf("here is a [%v] string", ct.SetFG(BrightBlue).String()) +} + +func TestColoredText_String(t *testing.T) { + ct := NewColoredText("colored") + t.Logf("here is a [%v] string", ct.SetBG(Red).SetFG(BrightWhite).String()) +} + +func TestNewColoredText(t *testing.T) { + ct := NewColoredText("colored") + t.Logf("here is a [%v] string", ct.SetFG(BrightWhite). + SetBG(BrightBlue). + SetAttr(Bold, true). + SetAttr(Bold, false). + SetAttr(Underscore, true). + SetAttr(Blink, true). + SetAttr(ReverseVideo, true). + SetAttr(Concealed, true). + String()) + t.Logf("here is a [%v] string", NewColoredText("empty"). + String()) + t.Logf("end...") +} diff --git a/cntr/README.md b/cntr/README.md new file mode 100644 index 0000000..34e11f1 --- /dev/null +++ b/cntr/README.md @@ -0,0 +1,2 @@ +# Container + diff --git a/cntr/cntr.go b/cntr/cntr.go new file mode 100644 index 0000000..c3800fe --- /dev/null +++ b/cntr/cntr.go @@ -0,0 +1,3 @@ +// "Containers", +// functions to operate array, vector, map... easier +package cntr diff --git a/cntr/distinct.go b/cntr/distinct.go new file mode 100644 index 0000000..31371a2 --- /dev/null +++ b/cntr/distinct.go @@ -0,0 +1,32 @@ +package cntr + +import ( + "strings" +) + +// DistinctStrings will return distinct strings, case sensitive +func DistinctStrings(elems ...string) []string { + m := map[string]bool{} + var retElems []string + for _, e := range elems { + if _, ok := m[e]; !ok { + m[e] = true + retElems = append(retElems, e) + } + } + return retElems +} + +// DistinctStringsCaseInsensitive will return distinct strings, case insensitive +func DistinctStringsCaseInsensitive(elems ...string) []string { + m := map[string]bool{} + var retElems []string + for _, e := range elems { + le := strings.ToLower(e) + if _, ok := m[le]; !ok { + m[le] = true + retElems = append(retElems, e) + } + } + return retElems +} diff --git a/cntr/distinct_test.go b/cntr/distinct_test.go new file mode 100644 index 0000000..087215f --- /dev/null +++ b/cntr/distinct_test.go @@ -0,0 +1,21 @@ +package cntr + +import ( + "testing" +) + +func TestDistinctStrings(t *testing.T) { + k := []string{"aa", "aa", "bb", "aa"} + k = DistinctStrings(k...) + if len(k) != 2 { + t.Errorf("Distinct Failed!!!") + } +} + +func TestDistinctStringsCaseInsensitive(t *testing.T) { + k := []string{"aa", "AA", "bb", "aa"} + k = DistinctStringsCaseInsensitive(k...) + if len(k) != 2 { + t.Errorf("Distinct Failed!!!") + } +} diff --git a/cntr/edit_distance.go b/cntr/edit_distance.go new file mode 100644 index 0000000..1a1b68a --- /dev/null +++ b/cntr/edit_distance.go @@ -0,0 +1,84 @@ +package cntr + +// GetEditDistanceT returns the edit distance between two strings. +// The edit distance is the number of operations required to transform +// one string into the other, where an operation is defined as an +// insertion, deletion, or substitution of a single character, or a +// swap of two adjacent characters. +// The function will return -1 if the distance is greater than the threshold. +func GetEditDistanceT(r1, r2 string, threshold int) int { + abs := func(a int) int { + if a < 0 { + return -a + } + return a + } + + max := func(a, b int) int { + if a > b { + return a + } + return b + } + + min := func(a, b int) int { + if a < b { + return a + } + return b + } + + sig := func(a, b rune) int { + if a == b { + return 0 + } + return 1 + } + + if threshold <= 0 { + if r1 == r2 { + return 0 + } else { + return -1 + } + } + + s1 := []rune(r1) + s2 := []rune(r2) + + if abs(len(s1)-len(s2)) > threshold { + return -1 + } + + d := make([][]int, len(s1)+1) + for i := range d { + d[i] = make([]int, len(s2)+1) + } + + for i := 1; i <= len(s1); i++ { + st := max(1, i-threshold) + en := min(len(s2), i+threshold) + flag := true + for j := st; j <= en; j++ { + d[i][j] = threshold + 1 + if j-i+1 <= threshold && d[i-1][j]+1 < d[i][j] { + d[i][j] = d[i-1][j] + 1 + } + if i-j+1 <= threshold && d[i][j-1]+1 < d[i][j] { + d[i][j] = d[i][j-1] + 1 + } + d[i][j] = min(d[i][j], d[i-1][j-1]+sig(s1[i-1], s2[j-1])) + if d[i][j] <= threshold { + flag = false + } + } + if flag && i > threshold { + return -1 + } + } + + if d[len(s1)][len(s2)] > threshold { + return -1 + } + return d[len(s1)][len(s2)] +} diff --git a/cntr/edit_distance_test.go b/cntr/edit_distance_test.go new file mode 100644 index 0000000..c1f0d42 --- /dev/null +++ b/cntr/edit_distance_test.go @@ -0,0 +1,83 @@ +package cntr + +import ( + "fmt" + "testing" + + "github.com/argcv/stork/assert" +) + +func TestGetEditDistance(t *testing.T) { + tests := []struct { + name string + s1 string + s2 string + threshold int + want int + }{ + { + name: "empty", + s1: "", + s2: "", + threshold: 0, + want: 0, + }, + { + name: "empty-vs-nonempty", + s1: "", + s2: "x", + threshold: 0, + want: -1, + }, + { + name: "equal", + s1: "abc", + s2: "abc", + threshold: 1, + want: 0, + }, + { + name: "non-equal", + s1: "bbc", + s2: "abc", + threshold: 2, + want: 1, + }, + { + name: "non-equal", + s1: "abc", + s2: "abd", + threshold: 3, + want: 1, + }, + { + name: "too-far", + s1: "hello", + s2: "world", + threshold: 3, + want: -1, + }, + { + name: "not-too-far", + s1: "hello", + s2: "world", + threshold: 5, + want: 4, + }, + { + name: "unicode-distance", + s1: "北京西门", + s2: "北海西", + threshold: 5, + want: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GetEditDistanceT(tt.s1, tt.s2, tt.threshold) + assert.ExpectEQ(t, got, tt.want, + fmt.Sprintf("GetEditDistanceT(%v, %v) = %v, want %v", tt.s1, tt.s2, got, tt.want)) + }) + } +} diff --git a/cntr/m.go b/cntr/m.go new file mode 100644 index 0000000..741cfa0 --- /dev/null +++ b/cntr/m.go @@ -0,0 +1,134 @@ +package cntr + +import ( + "database/sql/driver" + "encoding/json" + "errors" +) + +// M Interface for JSON Field of M Table +type M map[string]interface{} + +// Value Marshal +// +//goland:noinspection GoMixedReceiverTypes +func (a M) Value() (driver.Value, error) { + return json.Marshal(a) +} + +// Scan Unmarshal +// +//goland:noinspection GoMixedReceiverTypes +func (a *M) Scan(value interface{}) error { + b, ok := value.([]byte) + if !ok { + return errors.New("type assertion to []byte failed") + } + return json.Unmarshal(b, &a) +} + +// CleanZeroBytes Clean all string fields with \u0000 +// +//goland:noinspection GoMixedReceiverTypes +func (a M) CleanZeroBytes() { + for k, v := range a { + if s, ok := v.(string); ok { + b := []byte(s) + detected := false + for i, c := range b { + if c == 0 { + b[i] = ' ' + detected = true + } + } + if detected { + (a)[k] = string(b) + } + } + + // try to clean sub map + if m, ok := v.(map[string]interface{}); ok { + if m == nil || len(m) == 0 { + continue + } + subMap := M(m) + subMap.CleanZeroBytes() + } + } +} + +// GetString Get string value from M +// +//goland:noinspection GoMixedReceiverTypes +func (a M) GetString(key string) string { + if v, ok := (a)[key]; ok { + if s, ok := v.(string); ok { + return s + } + } + return "" +} + +// GetInt Get int value from M +// +//goland:noinspection GoMixedReceiverTypes +func (a M) GetInt(key string) int { + if v, ok := (a)[key]; ok { + if s, ok := v.(int); ok { + return s + } + } + return 0 +} + +// GetInt64 Get int64 value from M +// +//goland:noinspection GoMixedReceiverTypes +func (a M) GetInt64(key string) int64 { + if v, ok := (a)[key]; ok { + if s, ok := v.(int64); ok { + return s + } + } + return 0 +} + +// GetFloat64 Get float64 value from M +// +//goland:noinspection GoMixedReceiverTypes +func (a M) GetFloat64(key string) float64 { + if v, ok := (a)[key]; ok { + if s, ok := v.(float64); ok { + return s + } + } + return 0 +} + +// GetBool Get bool value from M +// +//goland:noinspection GoMixedReceiverTypes +func (a M) GetBool(key string) bool { + if v, ok := (a)[key]; ok { + if s, ok := v.(bool); ok { + return s + } + } + return false +} + +// GetM Get M value from M +// +//goland:noinspection GoMixedReceiverTypes +func (a M) GetM(key string) M { + if v, ok := (a)[key]; ok { + if s, ok := v.(M); ok { + return s + } + // try to convert to M + if s, ok := v.(map[string]interface{}); ok { + return M(s) + } + } + return nil +} diff --git a/cntr/m_test.go b/cntr/m_test.go new file mode 100644 index 0000000..f5e0a57 --- /dev/null +++ b/cntr/m_test.go @@ -0,0 +1,34 @@ +package cntr + +import ( + "encoding/json" + "testing" + + "github.com/argcv/stork/assert" +) + +func TestM_CleanZeroBytes(t *testing.T) { + rawStr := `{ + "options": { + "f1": "abc\u0000def", + "f2": { + "f3": "abc\u0000def" + }, + "f4": null, + "f5": {}, + "f6": 1.0 + }, + "k1": "abc\u0000def" +}` + rawM := M{} + err := json.Unmarshal([]byte(rawStr), &rawM) + if err != nil { + t.Fatal(err) + } + t.Logf("rawM: %+v", rawM) + rawM.CleanZeroBytes() + t.Logf("cleanedM: %+v", rawM) + assert.ExpectEQ(t, rawM.GetString("k1"), "abc def") + assert.ExpectEQ(t, rawM.GetM("options").GetString("f1"), "abc def") + assert.ExpectEQ(t, rawM.GetM("options").GetM("f2").GetString("f3"), "abc def") +} diff --git a/cntr/rand_str.go b/cntr/rand_str.go new file mode 100644 index 0000000..94bf6da --- /dev/null +++ b/cntr/rand_str.go @@ -0,0 +1,28 @@ +package cntr + +import ( + "math/rand" +) + +const ( + CharsetUpperCaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + CharsetLowerCaseLetters = "abcdefghijklmnopqrstuvwxyz" + CharsetLetters = CharsetUpperCaseLetters + CharsetLowerCaseLetters + CharsetNumbers = "0123456789" + DefaultCharset = CharsetLetters + CharsetNumbers +) + +func RandomStringWithCharset(length int, charset string) string { + if len(charset) == 0 || length < 1 { + return "" + } + b := make([]byte, length) + for i := range b { + b[i] = charset[rand.Intn(len(charset))] + } + return string(b) +} + +func RandomString(length int) string { + return RandomStringWithCharset(length, DefaultCharset) +} diff --git a/cntr/rand_str_test.go b/cntr/rand_str_test.go new file mode 100644 index 0000000..eb17aee --- /dev/null +++ b/cntr/rand_str_test.go @@ -0,0 +1,25 @@ +package cntr + +import ( + "testing" + + "github.com/argcv/stork/assert" +) + +func TestRandomString(t *testing.T) { + length := 10 + randStr := RandomString(length) + assert.ExpectEQ(t, length, len(randStr)) +} + +func TestRandomStringWithCharset(t *testing.T) { + length := 10 + charset := "a" + randStr := RandomStringWithCharset(length, charset) + assert.ExpectEQ(t, length, len(randStr)) + for i := 0; i < length; i++ { + assert.ExpectEQ(t, charset[0], randStr[i]) + } + assert.ExpectEQ(t, "", RandomStringWithCharset(10, "")) + assert.ExpectEQ(t, "", RandomStringWithCharset(-1, "")) +} diff --git a/compr/README.md b/compr/README.md new file mode 100644 index 0000000..f31761b --- /dev/null +++ b/compr/README.md @@ -0,0 +1,2 @@ +# Compress + diff --git a/compr/compr.go b/compr/compr.go new file mode 100644 index 0000000..20d482b --- /dev/null +++ b/compr/compr.go @@ -0,0 +1,2 @@ +// Compress helpers +package compr diff --git a/compr/gzip.go b/compr/gzip.go new file mode 100644 index 0000000..d4e1f01 --- /dev/null +++ b/compr/gzip.go @@ -0,0 +1,30 @@ +package compr + +import ( + "bytes" + "compress/gzip" + "io" +) + +func GzipDecompress(in []byte) (out []byte, err error) { + gr, err := gzip.NewReader(bytes.NewReader(in)) + if err != nil { + return + } + out, err = io.ReadAll(gr) + if err != nil { + return + } + return +} + +func GzipCompress(in []byte) (out []byte, err error) { + var buf bytes.Buffer + w := gzip.NewWriter(&buf) + _, err = w.Write(in) + if err != nil { + return nil, err + } + err = w.Close() + return buf.Bytes(), err +} diff --git a/config/README.md b/config/README.md new file mode 100644 index 0000000..e7ab557 --- /dev/null +++ b/config/README.md @@ -0,0 +1,2 @@ +# Configure Loader + diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..b3ecd19 --- /dev/null +++ b/config/config.go @@ -0,0 +1,71 @@ +// Configuration loader +package config + +import ( + "github.com/spf13/viper" +) + +func CheckKeyIsExists(key string) bool { + return viper.Get(key) != nil +} + +func GetStringOrDefault(key, or string) string { + if CheckKeyIsExists(key) { + return viper.GetString(key) + } else { + return or + } +} + +func GetStringSliceOrDefault(key string, or []string) []string { + if CheckKeyIsExists(key) { + return viper.GetStringSlice(key) + } else { + return or + } +} + +func GetIntOrDefault(key string, or int) int { + if CheckKeyIsExists(key) { + return viper.GetInt(key) + } else { + return or + } +} + +func GetInt64OrDefault(key string, or int64) int64 { + if CheckKeyIsExists(key) { + return viper.GetInt64(key) + } else { + return or + } +} + +func GetFloat32OrDefault(key string, or float32) float32 { + if CheckKeyIsExists(key) { + return float32(viper.GetFloat64(key)) + } else { + return or + } +} + +func GetFloat64OrDefault(key string, or float64) float64 { + if CheckKeyIsExists(key) { + return viper.GetFloat64(key) + } else { + return or + } +} + +func GetBoolOrDefault(key string, or bool) bool { + if CheckKeyIsExists(key) { + return viper.GetBool(key) + } else { + return or + } +} + +func SetConfig(key string, value interface{}) error { + viper.Set(key, value) + return viper.WriteConfig() +} diff --git a/config/key_builder.go b/config/key_builder.go new file mode 100644 index 0000000..d32423b --- /dev/null +++ b/config/key_builder.go @@ -0,0 +1,107 @@ +package config + +import ( + "github.com/spf13/viper" + "strings" +) + +type KeyBuilder struct { + Base string + Profile string + Class []string + Src *viper.Viper +} + +func (ckb *KeyBuilder) Join(elems ...string) string { + var neElems []string + // remove empty elements + for _, e := range elems { + if len(e) > 0 { + neElems = append(neElems, e) + } + } + return strings.Join(neElems, ".") +} + +func (ckb *KeyBuilder) Current() string { + return ckb.GetKey("") +} + +func (ckb *KeyBuilder) WithProfile(p string) *KeyBuilder { + ckb.Profile = p + return ckb +} + +func (ckb *KeyBuilder) WithClass(c string) *KeyBuilder { + ckb.Class = append(ckb.Class, c) + return ckb +} + +func (ckb *KeyBuilder) Clone() *KeyBuilder { + class := make([]string, len(ckb.Class)) + copy(class, ckb.Class) + newKb := &KeyBuilder{ + Base: ckb.Base, + Profile: ckb.Profile, + Class: class, + Src: ckb.Src, + } + return newKb +} + +func (ckb *KeyBuilder) SubViper(key string) (*viper.Viper) { + return ckb.Src.Sub(ckb.GetKey(key)) +} + +func (ckb *KeyBuilder) GetKey(key string) string { + keys := []string{} + keys = append(keys, ckb.Base, ckb.Profile) + keys = append(keys, ckb.Class...) + keys = append(keys, key) + return ckb.Join(keys...) +} + +func (ckb *KeyBuilder) GetStringOrDefault(key string, or string) string { + return GetStringOrDefault(ckb.GetKey(key), or) +} + +func (ckb *KeyBuilder) CheckKeyIsExists(key string) bool { + return CheckKeyIsExists(ckb.GetKey(key)) +} + +func (ckb *KeyBuilder) GetIntOrDefault(key string, or int) int { + return GetIntOrDefault(ckb.GetKey(key), or) +} + +func (ckb *KeyBuilder) GetInt64OrDefault(key string, or int64) int64 { + return GetInt64OrDefault(ckb.GetKey(key), or) +} + +func (ckb *KeyBuilder) GetFloat32OrDefault(key string, or float32) float32 { + return GetFloat32OrDefault(ckb.GetKey(key), or) +} + +func (ckb *KeyBuilder) GetFloat64OrDefault(key string, or float64) float64 { + return GetFloat64OrDefault(ckb.GetKey(key), or) +} + +func (ckb *KeyBuilder) GetBoolOrDefault(key string, or bool) bool { + return GetBoolOrDefault(ckb.GetKey(key), or) +} + +func (ckb *KeyBuilder) GetStringSliceOrDefault(key string, or []string) []string { + return GetStringSliceOrDefault(ckb.GetKey(key), or) +} + +func NewKeyBuilder(base string, rest ...string) (ckb *KeyBuilder) { + ckb = &KeyBuilder{ + Base: base, + Profile: KeyDefaultProfile, + Class: []string{}, + Src: viper.GetViper(), + } + if len(rest) > 0 { + ckb.Base = ckb.Join(base, ckb.Join(rest...)) + } + return +} diff --git a/config/keys.go b/config/keys.go new file mode 100644 index 0000000..b0bcdd5 --- /dev/null +++ b/config/keys.go @@ -0,0 +1,35 @@ +package config + +const ( + KeyDefaultProfile = "default" + + KeyRedisBase = "redis" + KeyRedisHost = "host" + KeyRedisPort = "port" + KeyRedisPass = "pass" + KeyRedisDb = "db" + + KeyMongoBase = "mongo" + KeyMongoAddrs = "addrs" + KeyMongoPerformAuth = "with_auth" + + KeyMongoAuthBase = "auth" + KeyMongoAuthDatabase = "db" + KeyMongoAuthUser = "user" + KeyMongoAuthPass = "pass" + KeyMongoAuthMechanism = "mechanism" + + KeyMongoTimeoutSec = "timeout_sec" + KeyMongoDatabase = "db" + + KeyMailBase = "mail" + + KeyMailSMTPBase = "smtp" + KeyMailSMTPHost = "host" + KeyMailSMTPPort = "port" + KeyMailSMTPSender = "sender" + KeyMailSMTPUser = "user" + KeyMailSMTPPass = "pass" + KeyMailSMTPOption = "option" + KeyMailSMTPOptionInsecureSkipVerify = "insecure_skip_verify" +) diff --git a/config/loader.go b/config/loader.go new file mode 100644 index 0000000..ce88ae1 --- /dev/null +++ b/config/loader.go @@ -0,0 +1,163 @@ +package config + +import ( + "fmt" + "github.com/argcv/stork/cntr" + "github.com/argcv/stork/log" + "github.com/pkg/errors" + "github.com/spf13/viper" + "os" + "path" + "path/filepath" + "strings" +) + +type Option struct { + Project string + Path string + FileMustExists bool + DefaultPath string + DefaultConfigureName string + ConfigSearchPath string + ConfigSearchPaths []string + ConfigFallbackSearchPath string +} + +func (c *Option) With(rhs *Option) *Option { + if rhs.Project != "" { + c.Project = rhs.Project + } + if rhs.Path != "" { + c.Path = rhs.Path + } + if rhs.FileMustExists { + c.FileMustExists = true + } + if rhs.DefaultPath != "" { + c.DefaultPath = rhs.DefaultPath + } + if rhs.DefaultConfigureName != "" { + c.DefaultConfigureName = rhs.DefaultConfigureName + } + if len(rhs.ConfigSearchPaths) > 0 { + c.ConfigSearchPaths = append(c.ConfigSearchPaths, rhs.ConfigSearchPaths...) + } + if len(rhs.ConfigSearchPath) > 0 { + c.ConfigSearchPaths = append(c.ConfigSearchPaths, rhs.ConfigSearchPath) + } + if len(rhs.ConfigFallbackSearchPath) > 0 { + c.ConfigFallbackSearchPath = rhs.ConfigFallbackSearchPath + } + return c +} + +func (c *Option) GetDefaultPath() string { + if c.DefaultPath != "" { + return c.DefaultPath + } else { + return c.Path + } +} + +// this function will search and load configurations +func LoadConfig(options ...Option) (err error) { + option := Option{} + + for _, opt := range options { + option.With(&opt) + } + + project := option.Project + + if project == "" { + return errors.New("Required parameter missing: project") + } + + viper.SetConfigName(project) + viper.SetEnvPrefix(project) + + if option.Path != "" { + viper.SetConfigFile(option.Path) + } else { + cfgPaths := option.ConfigSearchPaths + cfgPaths = append(cfgPaths, option.ConfigSearchPath) + + cfgPaths = append(cfgPaths, ".") // current folder + cfgPaths = append(cfgPaths, "..") // parent folder + cfgPaths = append(cfgPaths, "$HOME/") + cfgPaths = append(cfgPaths, fmt.Sprintf("$HOME/.%s/", project)) + cfgPaths = append(cfgPaths, "/etc/") + cfgPaths = append(cfgPaths, fmt.Sprintf("/etc/%s/", project)) + + cfgPaths = cntr.DistinctStrings(cfgPaths...) + + for _, cpath := range cfgPaths { + if len(cpath) > 0 { + viper.AddConfigPath(cpath) + } + } + + if len(option.ConfigFallbackSearchPath) > 0 { + viper.AddConfigPath(option.ConfigFallbackSearchPath) + } + + if conf := os.Getenv(fmt.Sprintf("%s_CFG", strings.ToUpper(project))); conf != "" { + viper.SetConfigFile(conf) + } + } + + readAndTestConfig := func() (string, error) { + err = viper.ReadInConfig() + if _, ok := err.(viper.ConfigFileNotFoundError); !ok && err != nil { + return "", err + } + if conf := viper.ConfigFileUsed(); conf != "" { + wd, _ := os.Getwd() + if rel, _ := filepath.Rel(wd, conf); rel != "" && strings.Count(rel, "..") < 3 { + conf = rel + } + log.Infof("using config file: %s", conf) + return conf, nil + } else { + return "", nil + } + } + + if conf, e := readAndTestConfig(); conf != "" { + return nil + } else if e != nil { + return e + } else if option.FileMustExists { + defaultConfigPath := option.GetDefaultPath() + defaultConfigName := option.DefaultConfigureName + if defaultConfigName == "" { + defaultConfigName = fmt.Sprintf("%s.yml", project) + } + if defaultConfigPath == "" { + msg := "No configure file: default path Not Assigned" + log.Warnf(msg) + return errors.New(msg) + } + if e := os.MkdirAll(defaultConfigPath, 0700); e != nil { + return e + } + defaultConfigPathFile := path.Join(defaultConfigPath, defaultConfigName) + log.Infof("configure file NOT found, using default file: %v", defaultConfigPathFile) + if e := viper.WriteConfigAs(defaultConfigPathFile); e != nil { + return e + } + viper.SetConfigFile(defaultConfigPathFile) + + if conf, e := readAndTestConfig(); conf != "" { + return nil + } else if e != nil { + return e + } else { + msg := "No configure file" + log.Warnf(msg) + return errors.New(msg) + } + } else { + return nil + } +} diff --git a/config/mail.go b/config/mail.go new file mode 100644 index 0000000..105311e --- /dev/null +++ b/config/mail.go @@ -0,0 +1,62 @@ +package config + +import ( + "github.com/pkg/errors" +) + +type SMTPConfigOption struct { + InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"` +} + +type SMTPConfig struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + Sender string `mapstructure:"sender"` + User string `mapstructure:"user"` + Pass string `mapstructure:"pass"` + // SkipTLS + Option SMTPConfigOption `mapstructure:"option"` +} + +func (cfg *SMTPConfig) verify() error { + if cfg == nil { + return errors.New("cfg is nil") + } + if len(cfg.Host) == 0 { + return errors.New("host is not defined") + } + return nil +} + +/* +Example Config File: + + mail: # base-0 + smtp: # base-1 + default: # profile + host: 127.0.0.1 + port: 25 + sender: Alice Ez + user: "aez@example.com" + pass: "your-password" # empty for no authorization + option: + insecure_skip_verify: true + nightly: # profile + host: 127.0.0.1 + port: 25 + sender: Alice Ez + user: "aez@example.com" + pass: "your-password" # empty for no authorization + option: + insecure_skip_verify: true + + */ +func LoadSMTPConfig(profile string) (cfg *SMTPConfig, err error) { + ckb := NewKeyBuilder(KeyMailBase, KeyMailSMTPBase).WithProfile(profile).SubViper("") + + cfg = &SMTPConfig{} + if err = ckb.Unmarshal(cfg); err == nil { + return cfg, cfg.verify() + } + return +} diff --git a/config/mongo.go b/config/mongo.go new file mode 100644 index 0000000..1f02993 --- /dev/null +++ b/config/mongo.go @@ -0,0 +1,59 @@ +package config + +import ( + "time" +) + +/** + * Database is the default database name used when the Session.DB method + * is called with an empty name, and is also used during the initial + * authentication if Source is unset. + * + * Username and Password inform the credentials for the initial authentication + * done on the database defined by the Source field. See Session.Login. + * + */ +type MongoAuth struct { + Source string + Username string + Password string + Mechanism string +} + +type MongoConfig struct { + Addrs [] string + Timeout time.Duration + DefaultDb string + Auth *MongoAuth +} + +func LoadMongoConfig(profile string) (cfg *MongoConfig) { + ckb := NewKeyBuilder(KeyMongoBase).WithProfile(profile) + var defaultAddrs []string + cfg = &MongoConfig{ + Addrs: ckb.GetStringSliceOrDefault(KeyMongoAddrs, defaultAddrs), + Timeout: time.Duration(ckb.GetInt64OrDefault(KeyMongoTimeoutSec, 0)) * time.Second, + DefaultDb: ckb.GetStringOrDefault(KeyMongoDatabase, ""), + Auth: GetDBMongoAuth(ckb), + } + + return +} + +func GetDBMongoAuth(ckb *KeyBuilder) *MongoAuth { + if ckb.GetBoolOrDefault(KeyMongoPerformAuth, false) { + auth := ckb.Clone().WithClass(KeyMongoAuthBase) + source := auth.GetStringOrDefault(KeyMongoAuthDatabase, "") + user := auth.GetStringOrDefault(KeyMongoAuthUser, "admin") + pass := auth.GetStringOrDefault(KeyMongoAuthPass, "") + mech := auth.GetStringOrDefault(KeyMongoAuthMechanism, "") + + return &MongoAuth{ + Source: source, + Username: user, + Password: pass, + Mechanism: mech, + } + } + return nil +} diff --git a/config/redis.go b/config/redis.go new file mode 100644 index 0000000..302f7b2 --- /dev/null +++ b/config/redis.go @@ -0,0 +1,28 @@ +package config + +type RedisConfig = struct { + Host string + Port int + Pass string + Db int +} + +/* +Example Config File: + + redis: + default: # profile + host: 127.0.0.1 + port: 6379 + pass: "" # empty for not encrypted + db: 0 # db number + */ +func LoadRedisConfig(profile string) *RedisConfig { + ckb := NewKeyBuilder(KeyRedisBase).WithProfile(profile) + return &RedisConfig{ + Host: ckb.GetStringOrDefault(KeyRedisHost, "127.0.0.1"), + Port: ckb.GetIntOrDefault(KeyRedisPort, 6379), + Pass: ckb.GetStringOrDefault(KeyRedisPass, ""), + Db: ckb.GetIntOrDefault(KeyRedisDb, 0), + } +} diff --git a/debug/README.md b/debug/README.md new file mode 100644 index 0000000..e69de29 diff --git a/debug/stopwatch.go b/debug/stopwatch.go new file mode 100644 index 0000000..f6838fb --- /dev/null +++ b/debug/stopwatch.go @@ -0,0 +1,213 @@ +package debug + +import ( + "fmt" + "github.com/argcv/stork/log" + "github.com/pkg/errors" + "sync" + "time" +) + +var ( + defaultStopWatch = NewStopwatch() +) + +type Stopwatch struct { + TimePoints []time.Time + Labels map[string]int + LabelsRev map[int]string + verbose bool + mx sync.Mutex +} + +func NewStopwatch() *Stopwatch { + return &Stopwatch{ + TimePoints: []time.Time{time.Now()}, + Labels: map[string]int{}, + LabelsRev: map[int]string{}, + verbose: false, + mx: sync.Mutex{}, + } +} + +func (sw *Stopwatch) addTimePoint(label string) time.Time { + sw.mx.Lock() + defer sw.mx.Unlock() + now := time.Now() + idx := len(sw.TimePoints) + sw.TimePoints = append(sw.TimePoints, now) + + labelSuffix := "" + if len(label) > 0 { + sw.Labels[label] = idx + sw.LabelsRev[idx] = label + labelSuffix = fmt.Sprintf(" label:[%v]", label) + } + + if sw.verbose { + log.Infof("New time point: %v%v", now, labelSuffix) + } + return now +} + +func (sw *Stopwatch) AddTimePoint() time.Time { + return sw.addTimePoint("") +} + +func (sw *Stopwatch) AddTimePointWithLabel(label string) time.Time { + return sw.addTimePoint(label) +} + +func (sw *Stopwatch) unsafeGetTimeByLabel(label string) (time.Time, bool) { + if val, ok := sw.Labels[label]; ok { + return sw.TimePoints[val], ok + } else { + return time.Unix(0, 0), false + } +} + +func (sw *Stopwatch) GetTimeByLabel(label string) (time.Time, bool) { + sw.mx.Lock() + defer sw.mx.Unlock() + return sw.unsafeGetTimeByLabel(label) +} + +func (sw *Stopwatch) unsafeGetLabelByIndex(idx int) string { + if val, ok := sw.LabelsRev[idx]; ok { + return val + } else { + return "" + } +} + +// return elapsed time +// it will return 2 results, the first is the duration before the previous one +// the second is the duration of all +func (sw *Stopwatch) TimeElapsed() (d1 time.Duration, d2 time.Duration) { + sw.mx.Lock() + defer sw.mx.Unlock() + d1 = 0 + d2 = 0 + ltp := len(sw.TimePoints) + if ltp == 0 { + if sw.verbose { + log.Errorf("Invalid time points!!, the counter is zero") + } + return + } + + if ltp > 1 { + d1 = sw.TimePoints[ltp-1].Sub(sw.TimePoints[ltp-2]) + } + d2 = sw.TimePoints[ltp-1].Sub(sw.TimePoints[0]) + if sw.verbose { + log.Infof("Duration [%v] before prev[%v]: %v, duration of all: %v", + sw.unsafeGetLabelByIndex(ltp-1), + sw.unsafeGetLabelByIndex(ltp-2), + d1, d2) + } + return +} + +func (sw *Stopwatch) TimeElapsedAfterLabel(label string) (dur time.Duration, err error) { + sw.mx.Lock() + defer sw.mx.Unlock() + dur = 0 + ltp := len(sw.TimePoints) + if ltp == 0 { + err = errors.New("Invalid time points!!, the counter is zero") + if sw.verbose { + log.Errorf("%v", err) + } + return + } + + ctime := sw.TimePoints[ltp-1] + target, ok := sw.unsafeGetTimeByLabel(label) + + if !ok { + err = errors.New("Invalid label!!, target label not found") + if sw.verbose { + log.Errorf("%v", err) + } + return + } + + dur = ctime.Sub(target) + if sw.verbose { + log.Infof("Duration [%v] before label[%v]: %v", + sw.unsafeGetLabelByIndex(ltp-1), + label, + dur) + } + return +} + +func (sw *Stopwatch) PrintAll() error { + sw.mx.Lock() + defer sw.mx.Unlock() + if len(sw.TimePoints) == 0 { + err := errors.New("Empty time points!!??") + if sw.verbose { + log.Errorf("%v", err) + } + return err + } + + prev := sw.TimePoints[0] + first := sw.TimePoints[0] + iprev := 0 + lprev := sw.unsafeGetLabelByIndex(0) + log.Infof("--------- Print All start --------") + for i, tp := range sw.TimePoints { + if i == 0 { + continue + } + label := sw.unsafeGetLabelByIndex(i) + log.Infof("Dur Gap: %v:%v => %v:%v -- %v;%v", + iprev, lprev, + i, label, + tp.Sub(prev), tp.Sub(first)) + prev = tp + lprev = label + iprev = i + } + log.Infof("--------- Print All end --------") + return nil +} + +func (sw *Stopwatch) Reset() { + sw.mx.Lock() + defer sw.mx.Unlock() + sw.TimePoints = []time.Time{time.Now()} + sw.Labels = map[string]int{} + sw.LabelsRev = map[int]string{} +} + +func AddTimePoint() time.Time { + return defaultStopWatch.AddTimePoint() +} + +func AddTimePointWithLabel(label string) time.Time { + return defaultStopWatch.AddTimePointWithLabel(label) +} + +func Verbose() { + defaultStopWatch.verbose = true +} + +func Reset() { + defaultStopWatch.Reset() +} + +func TimeElapsed() (d1 time.Duration, d2 time.Duration) { + return defaultStopWatch.TimeElapsed() +} + +func TimeElapsedAfterLabel(label string) (dur time.Duration, err error) { + return defaultStopWatch.TimeElapsedAfterLabel(label) +} + +func PrintAll() error { + return defaultStopWatch.PrintAll() +} diff --git a/debug/stopwatch_test.go b/debug/stopwatch_test.go new file mode 100644 index 0000000..1c35457 --- /dev/null +++ b/debug/stopwatch_test.go @@ -0,0 +1,21 @@ +package debug + +import ( + "testing" + "time" +) + +func TestNewStopwatch(t *testing.T) { + Verbose() + Reset() + time.Sleep(10 * time.Millisecond) + AddTimePointWithLabel("aa") + time.Sleep(20 * time.Millisecond) + AddTimePoint() + time.Sleep(20 * time.Millisecond) + AddTimePoint() + TimeElapsed() + _, _ = TimeElapsedAfterLabel("aa") + + _ = PrintAll() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b60b6fa --- /dev/null +++ b/go.mod @@ -0,0 +1,36 @@ +module github.com/argcv/stork + +go 1.20 + +require ( + github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 + github.com/go-mail/mail v2.3.1+incompatible + github.com/gorilla/sessions v1.2.1 + github.com/pkg/errors v0.9.1 + github.com/spf13/viper v1.17.0 + google.golang.org/protobuf v1.31.0 +) + +require ( + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/gorilla/securecookie v1.1.1 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.3.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.10.0 // indirect + github.com/spf13/cast v1.5.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/mail.v2 v2.3.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8cb720c --- /dev/null +++ b/go.sum @@ -0,0 +1,506 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-mail/mail v2.3.1+incompatible h1:UzNOn0k5lpfVtO31cK3hn6I4VEVGhe3lX8AJBAxXExM= +github.com/go-mail/mail v2.3.1+incompatible/go.mod h1:VPWjmmNyRsWXQZHVHT3g0YbIINUkSmuKOiLIDkWbL6M= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= +github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= +github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= +gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/log/README.md b/log/README.md new file mode 100644 index 0000000..56a3301 --- /dev/null +++ b/log/README.md @@ -0,0 +1,2 @@ +# Logger + diff --git a/log/logger.go b/log/logger.go new file mode 100644 index 0000000..173a3d8 --- /dev/null +++ b/log/logger.go @@ -0,0 +1,157 @@ +package log + +import ( + "fmt" + "github.com/argcv/stork/cli" + "io" + "log" + "os" + "sync" +) + +type LogLevel int + +const ( + DEBUG LogLevel = iota + INFO + WARN + ERROR + FATAL + DISABLED +) + +func (l LogLevel) String() (s string) { + s = "" + switch l { + case DEBUG: + s = cli.NewColoredText("DEBUG").SetFG(cli.Cyan).String() + case INFO: + s = cli.NewColoredText("INFO").SetFG(cli.Green).String() + case WARN: + s = cli.NewColoredText("WARN").SetFG(cli.Magenta).String() + case ERROR: + s = cli.NewColoredText("ERROR").SetFG(cli.BrightRed).String() + case FATAL: + s = cli.NewColoredText("FATAL").SetBG(cli.Red).SetFG(cli.BrightWhite).String() + //default: + //s = repl.NewColoredText(fmt.Sprintf("??:%v", l)).SetFG(repl.Cyan).String() + } + s = fmt.Sprintf("[%v] ", s) + return +} + +var ( + loggers = map[LogLevel]*log.Logger{ + DEBUG: nil, + INFO: nil, + WARN: nil, + ERROR: nil, + FATAL: nil, + } + loggersMtx = sync.Mutex{} + cLevel LogLevel = INFO +) + +func SetLevel(level LogLevel) { + cLevel = level +} + +func Verbose() { + SetLevel(DEBUG) +} + +func Quiet() { + SetLevel(DISABLED) +} + +func Output(level LogLevel, msg string, calldepth int) { + if cLevel <= level { + loggers[level].Output(calldepth+1, msg) + } +} + +func Debug(v ...interface{}) { + Output(DEBUG, fmt.Sprintln(v...), 2) +} + +func Info(v ...interface{}) { + Output(INFO, fmt.Sprintln(v...), 2) +} + +func Warn(v ...interface{}) { + Output(WARN, fmt.Sprintln(v...), 2) +} + +func Error(v ...interface{}) { + Output(ERROR, fmt.Sprintln(v...), 2) +} + +func Fatal(v ...interface{}) { + Output(FATAL, fmt.Sprintln(v...), 2) +} + +// calldepth == 0 == current +func Debugd(calldepth int, v ...interface{}) { + Output(DEBUG, fmt.Sprintln(v...), calldepth+2) +} + +func Infod(calldepth int, v ...interface{}) { + Output(INFO, fmt.Sprintln(v...), calldepth+2) +} + +func Warnd(calldepth int, v ...interface{}) { + Output(WARN, fmt.Sprintln(v...), calldepth+2) +} + +func Errord(calldepth int, v ...interface{}) { + Output(ERROR, fmt.Sprintln(v...), calldepth+2) +} + +func Fatald(calldepth int, v ...interface{}) { + Output(FATAL, fmt.Sprintln(v...), calldepth+2) +} + +func Debugf(f string, v ...interface{}) { + Output(DEBUG, fmt.Sprintf(f, v...), 2) +} + +func Infof(f string, v ...interface{}) { + Output(INFO, fmt.Sprintf(f, v...), 2) +} + +func Warnf(f string, v ...interface{}) { + Output(WARN, fmt.Sprintf(f, v...), 2) +} + +func Errorf(f string, v ...interface{}) { + Output(ERROR, fmt.Sprintf(f, v...), 2) +} + +func Fatalf(f string, v ...interface{}) { + Output(FATAL, fmt.Sprintf(f, v...), 2) +} + +func IfEligible(level LogLevel, f func()) bool { + if cLevel <= level { + f() + return true + } else { + return false + } +} + +func IfDebug(f func()) bool { + return IfEligible(DEBUG, f) +} + +func SetLogger(out io.Writer, flag int) { + loggersMtx.Lock() + defer loggersMtx.Unlock() + for k, _ := range loggers { + loggers[k] = log.New(out, k.String(), flag) + } +} + +func init() { + SetLogger(os.Stderr, log.Ldate|log.Ltime|log.Lmicroseconds|log.Lshortfile) +} diff --git a/log/logger_test.go b/log/logger_test.go new file mode 100644 index 0000000..1f40099 --- /dev/null +++ b/log/logger_test.go @@ -0,0 +1,41 @@ +package log + +import "testing" + +func TestInfo(t *testing.T) { + SetLevel(ERROR) + Info("AA", "BB", "CC") + SetLevel(INFO) + Info("DD", "EE", "FF") + Verbose() + IfDebug(func() { + Debugf("in debug...") + }) + + IfDebug(func() { + Debugf("in debug...") + }) + + Debug("a DEBUG message") + Info("a INFO message") + Warn("a WARN message") + Error("a ERROR message") + Fatal("a FATAL message") + Debugf("a DEBUG message") + Infof("a INFO message") + Warnf("a WARN message") + Errorf("a ERROR message") + Fatalf("a FATAL message") + + Debugd(0, "a DEBUG message") + Infod(0, "a INFO message") + Warnd(0, "a WARN message") + Errord(0, "a ERROR message") + Fatald(0, "a FATAL message") + Quiet() + IfDebug(func() { + t.Fatalf("what happened!!") + }) + Fatal("Should Be Disabled") + +} diff --git a/mail/closer.go b/mail/closer.go new file mode 100644 index 0000000..64795fa --- /dev/null +++ b/mail/closer.go @@ -0,0 +1,118 @@ +package mail + +import ( + "github.com/argcv/stork/log" + "sync" + "time" +) + +type SMTPCloseSch struct { + Delay time.Duration + OnClose func() + + lastUpdate time.Time + stopped bool + locker *sync.Mutex + lockerGlobal *sync.Mutex + canceled bool +} + +func (c *SMTPCloseSch) SafeExec(f func() error) error { + c.locker.Lock() + defer c.locker.Unlock() + return f() +} + +func (c *SMTPCloseSch) WithLock(gm *sync.Mutex) *SMTPCloseSch { + c.locker.Lock() + defer c.locker.Unlock() + prevGm := c.lockerGlobal + if prevGm != nil { + prevGm.Lock() + } + c.lockerGlobal = gm + if prevGm != nil { + prevGm.Unlock() + } + return c +} + +func (c *SMTPCloseSch) SetDelaySeconds(sec int) *SMTPCloseSch { + c.locker.Lock() + defer c.locker.Unlock() + c.Delay = time.Duration(sec) * time.Second + return c +} + +func (c *SMTPCloseSch) Cancel() { + c.locker.Lock() + defer c.locker.Unlock() + c.canceled = true +} + +func (c *SMTPCloseSch) Activate() { + c.locker.Lock() + defer c.locker.Unlock() + if ! c.stopped { + c.lastUpdate = time.Now() + } else { + c.stopped = false + c.canceled = false + go func() { + log.Debugf("New closer session..") + for { + log.Debugf("Closer: wait and check") + time.Sleep(c.Delay + 1*time.Millisecond) + log.Debugf("Closer: lock and check") + c.locker.Lock() + log.Debugf("Closer: locked, check") + now := time.Now() + if now.After(c.lastUpdate.Add(c.Delay)) { + log.Debugf("Closer: close and jump out") + // stop & close + c.LockGlobal() + + if c.canceled { + log.Debugf("canceled") + } else if c.OnClose != nil { + c.OnClose() + } + c.stopped = true + c.UnlockGlobal() + log.Debugf("Closer: closed") + + c.locker.Unlock() + return + } + c.locker.Unlock() + } + }() + } + +} + +func (c *SMTPCloseSch) LockGlobal() { + locker := c.lockerGlobal + if locker != nil { + locker.Lock() + } +} + +func (c *SMTPCloseSch) UnlockGlobal() { + locker := c.lockerGlobal + if locker != nil { + locker.Unlock() + } +} + +func NewSMTPCloser(onClose func()) *SMTPCloseSch { + return &SMTPCloseSch{ + Delay: 5 * time.Second, // 30s in default + OnClose: onClose, + lastUpdate: time.Now(), + stopped: true, + canceled: false, + locker: &sync.Mutex{}, + lockerGlobal: nil, + } +} diff --git a/mail/closer_test.go b/mail/closer_test.go new file mode 100644 index 0000000..240cfad --- /dev/null +++ b/mail/closer_test.go @@ -0,0 +1,77 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Yu Jing + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +package mail + +import ( + "github.com/argcv/stork/log" + "github.com/pkg/errors" + "sync" + "testing" + "time" +) + +func TestNewSMTPCloser(t *testing.T) { + log.Verbose() + c := NewSMTPCloser(func() { + t.Errorf("really closed") + }) + c.Delay = 1 * time.Second + c.Activate() + time.Sleep(500 * time.Millisecond) + c.Activate() + time.Sleep(500 * time.Millisecond) + c.Activate() + c.Cancel() + time.Sleep(1500 * time.Millisecond) + + c.WithLock(&sync.Mutex{}) + c.LockGlobal() + isLocked := true + wg := &sync.WaitGroup{} + wg2 := &sync.WaitGroup{} + wg.Add(1) + wg2.Add(1) + go func() { + wg.Done() + c.LockGlobal() + if isLocked { + t.Errorf("is locked!!!!") + } + wg2.Done() + }() + wg.Wait() + isLocked = false + c.UnlockGlobal() + wg2.Wait() + + ed := errors.New("expected error") + er := c.SafeExec(func() error { + return ed + }) + if ed != er { + t.Errorf("Unexpected error: %v vs. %v", ed, er) + } + +} diff --git a/mail/mail.go b/mail/mail.go new file mode 100644 index 0000000..cf54f4d --- /dev/null +++ b/mail/mail.go @@ -0,0 +1,5 @@ +// package mail integrated a few functions to send emails +// it may used to notify something sometimes. +// +// it does not support imap/pop3 yet +package mail diff --git a/mail/mail_test.go b/mail/mail_test.go new file mode 100644 index 0000000..9ca19e8 --- /dev/null +++ b/mail/mail_test.go @@ -0,0 +1 @@ +package mail diff --git a/mail/smtp.go b/mail/smtp.go new file mode 100644 index 0000000..d1aa81e --- /dev/null +++ b/mail/smtp.go @@ -0,0 +1,227 @@ +package mail + +import ( + "crypto/tls" + "github.com/argcv/stork/config" + "github.com/go-mail/mail" + "io" + "sync" + "time" +) + +// active fork +// https://github.com/go-mail/mail +type SMTPSession struct { + Dialer *mail.Dialer + Config *config.SMTPConfig + + isClosed bool + closeSch *SMTPCloseSch + sendCloser mail.SendCloser +} + +func (s *SMTPSession) NewMessage() (*SMTPMessage) { + m := &SMTPMessage{ + Dialer: s.Dialer, + Config: s.Config, + Message: mail.NewMessage(), + + locker: &sync.Mutex{}, + ss: s, + } + m.DefaultFrom() + return m +} + +func (s *SMTPSession) Dial(f func(mail.SendCloser) error) error { + s.closeSch.Activate() + err := s.closeSch.SafeExec(func() error { + if s.isClosed { + //s.locker.Lock() + //defer s.locker.Unlock() + + // dial new session + sc, err := s.Dialer.Dial() + + if err != nil { + return err + } + s.sendCloser = sc + s.isClosed = false + } + return f(s.sendCloser) + }) + return err +} + +func (s *SMTPSession) close() error { + if !s.isClosed { + s.isClosed = true + return s.sendCloser.Close() + } + return nil +} + +func NewSMTPSession(c config.SMTPConfig) (s *SMTPSession, err error) { + dialer := mail.NewDialer(c.Host, c.Port, c.User, c.Pass) + if c.Option.InsecureSkipVerify { + dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true} + } + s = &SMTPSession{ + Dialer: dialer, + Config: &c, + + isClosed: true, + } + s.closeSch = NewSMTPCloser(func() { + s.close() + }) + + return +} + +type SMTPMessage struct { + Dialer *mail.Dialer + Message *mail.Message + Config *config.SMTPConfig + + locker *sync.Mutex + ss *SMTPSession +} + +func (m *SMTPMessage) DefaultFrom() *SMTPMessage { + if len(m.Config.Sender) > 0 { + m.From(m.Config.User, m.Config.Sender) + } else { + m.From(m.Config.User) + } + return m +} + +/** From Header can be used 1th. + */ +func (m *SMTPMessage) From(from string, name ...string) *SMTPMessage { + if len(name) > 0 { + m.SetHeader("From", m.FormatAddress(from, name[0])) + } else { + m.SetHeader("From", from) + } + return m +} + +func (m *SMTPMessage) To(to string, name ...string) *SMTPMessage { + if len(name) > 0 { + m.AddToHeader("To", m.FormatAddress(to, name[0])) + } else { + m.AddToHeader("To", to) + } + return m +} + +func (m *SMTPMessage) Cc(cc string, name ...string) *SMTPMessage { + if len(name) > 0 { + m.AddToHeader("Cc", m.FormatAddress(cc, name[0])) + } else { + m.AddToHeader("Cc", cc) + } + return m +} + +func (m *SMTPMessage) Subject(subject string) *SMTPMessage { + m.SetHeader("Subject", subject) + return m +} + +func (m *SMTPMessage) PlainBody(body string, alternative ...string) *SMTPMessage { + m.SetBody("text/plain", body) + if len(alternative) > 0 { + m.AddAlternative("text/html", alternative[0]) + } + return m +} + +func (m *SMTPMessage) HtmlBody(body string) *SMTPMessage { + m.SetBody("text/html", body) + return m +} + +func (m *SMTPMessage) Attach(path string, name ...string) *SMTPMessage { + if len(name) > 0 { + m.Message.Attach(path, mail.Rename(name[0])) + } else { + m.Message.Attach(path) + } + return m +} + +func (m *SMTPMessage) AttachBytes(name string, data []byte) *SMTPMessage { + m.Message.Attach(name, mail.SetCopyFunc(func(w io.Writer) error { + _, err := w.Write(data) + return err + })) + return m +} + +func (m *SMTPMessage) WithDate() *SMTPMessage { + m.SetHeader("X-Date", m.FormatDate(time.Now())) + return m +} + +func (m *SMTPMessage) SetHeader(field string, value ...string) *SMTPMessage { + m.locker.Lock() + defer m.locker.Unlock() + m.Message.SetHeader(field, value...) + return m +} + +func (m *SMTPMessage) AddToHeader(field string, value ...string) *SMTPMessage { + m.locker.Lock() + defer m.locker.Unlock() + oh := m.Message.GetHeader(field) + m.Message.SetHeaders(map[string][]string{ + field: append(oh, value...), + }) + //locker.Message.SetHeader(field, append(locker.Message.GetHeader(field), value...)...) + return m +} + +func (m *SMTPMessage) AddAlternative(contentType, body string, settings ...mail.PartSetting) *SMTPMessage { + m.Message.AddAlternative(contentType, body, settings...) + return m +} + +func (m *SMTPMessage) FormatDate(date time.Time) string { + return m.Message.FormatDate(date) +} + +func (m *SMTPMessage) FormatAddress(address, name string) string { + return m.Message.FormatAddress(address, name) +} + +//func (locker *SMTPMessage) SetHeaders(h map[string][]string) *SMTPMessage { +// locker.Message.SetHeaders(h) +// return locker +//} + +func (m *SMTPMessage) SetBody(contentType, body string, settings ...mail.PartSetting) *SMTPMessage { + m.Message.SetBody(contentType, body, settings...) + return m +} + +func (m *SMTPMessage) Perform() error { + return m.ss.Dial(func(s mail.SendCloser) (err error) { + err = mail.Send(s, m.Message) + if err != nil { + m.ss.close() + } + return + }) + //return locker.Dialer.DialAndSend(locker.Message) +} + +func (m *SMTPMessage) Send(to, subject, body string) error { + m.To(to) + m.Subject(subject) + m.HtmlBody(body) + return m.Perform() +} diff --git a/mail/smtp_test.go b/mail/smtp_test.go new file mode 100644 index 0000000..7cd790d --- /dev/null +++ b/mail/smtp_test.go @@ -0,0 +1,7 @@ +package mail + +import "testing" + +func TestNewSMTPSession(t *testing.T) { + +} diff --git a/mongo/ac.go b/mongo/ac.go new file mode 100644 index 0000000..dac92b5 --- /dev/null +++ b/mongo/ac.go @@ -0,0 +1,408 @@ +// Mongo Auto Committer is a simple tool to submit +// a batch of mongo request automatically +// It was based on Oliver's bulk_processor.go for Elastic +// see: https://git.io/fjK56 +package mongo + +import ( + "errors" + "fmt" + "github.com/argcv/stork/log" + "sync" + "time" +) + +/** + * + */ +type AutoCommitBuilder struct { + client *Client // mongo client + name string // name of processor + coll string + numWorkers int // # of workers (>= 1) + bulkActions int // # of requests after which to commit + flushInterval time.Duration // periodic flush interval + verbose bool // verbose +} + +// Name is an optional name to identify this bulk processor. +func (s *AutoCommitBuilder) Name(name string) *AutoCommitBuilder { + s.name = name + return s +} + +// Collection defined the target coll +func (s *AutoCommitBuilder) Collection(coll string) *AutoCommitBuilder { + s.coll = coll + return s +} + +// Workers is the number of concurrent workers allowed to be +// executed. Defaults to 1 and must be greater or equal to 1. +func (s *AutoCommitBuilder) Workers(num int) *AutoCommitBuilder { + s.numWorkers = num + return s +} + +// BulkActions specifies when to flush based on the number of actions +// currently added. Defaults to 1000 and can be set to -1 to be disabled. +func (s *AutoCommitBuilder) BulkActions(bulkActions int) *AutoCommitBuilder { + s.bulkActions = bulkActions + return s +} + +func (s *AutoCommitBuilder) FlushInterval(interval time.Duration) *AutoCommitBuilder { + s.flushInterval = interval + return s +} + +func (s *AutoCommitBuilder) Verbose(verbose bool) *AutoCommitBuilder { + s.verbose = verbose + return s +} + +func (s *AutoCommitBuilder) Start() (mac *AutoCommitter, err error) { + mac = newAutoCommitter(s) + err = mac.Start() + if err != nil { + return nil, err + } + return +} + +type AutoCommitter struct { + client *Client // mongo client + name string // name of committer # note: this is just a label for verbose saver + coll string // name of coll + numWorkers int // # of workers (>= 1) + bulkActions int // # of requests after which to commit + flushInterval time.Duration // periodic flush interval + verbose bool // verbose + workerWg sync.WaitGroup + workers []*mongoAutoCommitWorker + docsInsert chan interface{} + docsUpdate chan []interface{} + docsUpsert chan []interface{} + + // todo: Remove, RemoveAll, UpdateAll + + flusherStopC chan struct{} + + startedMu sync.Mutex // guards the following block + started bool +} + +func newAutoCommitter(builder *AutoCommitBuilder) *AutoCommitter { + return &AutoCommitter{ + client: builder.client, + name: builder.name, + coll: builder.coll, + numWorkers: builder.numWorkers, + bulkActions: builder.bulkActions, + flushInterval: builder.flushInterval, + verbose: builder.verbose, + } +} + +func (p *AutoCommitter) Start() error { + p.startedMu.Lock() + defer p.startedMu.Unlock() + + if p.started { + return nil + } + + // We must have at least one worker. + if p.numWorkers < 1 { + p.numWorkers = 1 + } + + p.docsInsert = make(chan interface{}) + p.docsUpdate = make(chan []interface{}) + p.docsUpsert = make(chan []interface{}) + + // Create and start up workers. + p.workers = make([]*mongoAutoCommitWorker, p.numWorkers) + for i := 0; i < p.numWorkers; i++ { + p.workerWg.Add(1) + p.workers[i] = newMongoAutoCommitWorker(p, i) + go p.workers[i].work() + } + + // Start the ticker for flush (if enabled) + if int64(p.flushInterval) > 0 { + p.flusherStopC = make(chan struct{}) + go p.flusher(p.flushInterval) + } + + p.started = true + + return nil +} + +// Stop is an alias for Close. +func (p *AutoCommitter) Stop() error { + return p.Close() +} + +// Close stops the bulk processor previously started with Do. +// If it is already stopped, this is a no-op and nil is returned. +// +// By implementing Close, BulkProcessor implements the io.Closer interface. +func (p *AutoCommitter) Close() error { + p.startedMu.Lock() + defer p.startedMu.Unlock() + + // Already stopped? Do nothing. + if !p.started { + return nil + } + + // Stop flusher (if enabled) + if p.flusherStopC != nil { + p.flusherStopC <- struct{}{} + <-p.flusherStopC + close(p.flusherStopC) + p.flusherStopC = nil + } + + // Stop all workers. + close(p.docsInsert) + close(p.docsUpdate) + close(p.docsUpsert) + p.workerWg.Wait() + + p.started = false + + return nil +} + +// Add adds a single request to commit by the BulkProcessorService. +// +// The caller is responsible for setting the index and type on the request. +func (p *AutoCommitter) Insert(doc interface{}) (e error) { + if p.started { + p.docsInsert <- doc + } else { + e = errors.New(fmt.Sprintf("AutoCommitter-%s(%s)_is_closed", p.name, p.coll)) + } + return +} + +// Add adds a single request to commit by the BulkProcessorService. +// +// The caller is responsible for setting the index and type on the request. +func (p *AutoCommitter) Update(pair []interface{}) (e error) { + if p.started { + p.docsUpdate <- pair + } else { + e = errors.New(fmt.Sprintf("AutoCommitter-%s(%s)_is_closed", p.name, p.coll)) + } + return +} + +// Add adds a single request to commit by the BulkProcessorService. +// +// The caller is responsible for setting the index and type on the request. +func (p *AutoCommitter) Upsert(pair []interface{}) (e error) { + if p.started { + p.docsUpsert <- pair + } else { + e = errors.New(fmt.Sprintf("AutoCommitter-%s(%s)_is_closed", p.name, p.coll)) + } + return +} + +// Flush manually asks all workers to commit their outstanding requests. +// It returns only when all workers acknowledge completion. +func (p *AutoCommitter) Flush() error { + if p.verbose { + log.Info(fmt.Sprintf("AutoCommitter-%s(%s) a new flush is comming", p.name, p.coll)) + } + for _, w := range p.workers { + w.flushC <- struct{}{} + <-w.flushAckC // wait for completion + } + if p.verbose { + log.Info(fmt.Sprintf("AutoCommitter-%s(%s) a new flush is finished", p.name, p.coll)) + } + return nil +} + +// flusher is a single goroutine that periodically asks all workers to +// commit their outstanding bulk requests. It is only started if +// FlushInterval is greater than 0. +func (p *AutoCommitter) flusher(interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: // Periodic flush + p.Flush() + case <-p.flusherStopC: + p.flusherStopC <- struct{}{} + return + } + } +} + +type mongoAutoCommitWorker struct { + p *AutoCommitter + i int + flushC chan struct{} + flushAckC chan struct{} + + docMu sync.Mutex // guards the following block + docInsert []interface{} + docUpdate []interface{} + docUpsert []interface{} +} + +func newMongoAutoCommitWorker(p *AutoCommitter, i int) *mongoAutoCommitWorker { + return &mongoAutoCommitWorker{ + p: p, + i: i, + flushC: make(chan struct{}), + flushAckC: make(chan struct{}), + } +} + +func (w *mongoAutoCommitWorker) bulkActions() int { + return w.p.bulkActions +} + +func (w *mongoAutoCommitWorker) insert(doc interface{}) { + w.docMu.Lock() + defer w.docMu.Unlock() + w.docInsert = append(w.docInsert, doc) +} + +func (w *mongoAutoCommitWorker) update(pair []interface{}) { + w.docMu.Lock() + defer w.docMu.Unlock() + w.docUpdate = append(w.docUpdate, pair...) +} + +func (w *mongoAutoCommitWorker) upsert(pair []interface{}) { + w.docMu.Lock() + defer w.docMu.Unlock() + w.docUpsert = append(w.docUpsert, pair...) +} + +// work waits for bulk requests and manual flush calls on the respective +// channels and is invoked as a goroutine when the bulk processor is started. +func (w *mongoAutoCommitWorker) work() { + defer func() { + w.p.workerWg.Done() + close(w.flushAckC) + close(w.flushC) + }() + + var stop bool + for !stop { + select { + case req, open := <-w.p.docsInsert: + if open { + // Received a new request + w.insert(req) + if w.commitRequired() { + w.commit() + } + } else { + // Channel closed: Stop. + stop = true + w.commit() + } + case req, open := <-w.p.docsUpdate: + if open { + // Received a new request + w.update(req) + if w.commitRequired() { + w.commit() + } + } else { + // Channel closed: Stop. + stop = true + w.commit() + } + case req, open := <-w.p.docsUpsert: + if open { + // Received a new request + w.upsert(req) + if w.commitRequired() { + w.commit() + } + } else { + // Channel closed: Stop. + stop = true + w.commit() + } + + case <-w.flushC: + // Commit outstanding requests + if w.capacity() > 0 { + w.commit() + } + w.flushAckC <- struct{}{} + } + } +} + +// commit commits the bulk requests in the given service, +// invoking callbacks as specified. +func (w *mongoAutoCommitWorker) commit() (err error) { + w.docMu.Lock() + defer w.docMu.Unlock() + if w.capacity() > 0 { + if w.p.verbose { + log.Info(fmt.Sprintf("AutoCommitter-%s(%s)-%d commiting size: %d", w.p.name, w.p.coll, w.i, len(w.docInsert))) + } + session := w.p.client.Session.Copy() + defer session.Close() + db := w.p.client.Db + collName := w.p.coll + coll := session.DB(db).C(collName) + bulk := coll.Bulk() + bulk.Unordered() + + if len(w.docInsert) > 0 { + bulk.Insert(w.docInsert...) + } + + if len(w.docUpdate) > 0 { + bulk.Update(w.docUpdate...) + } + + if len(w.docUpsert) > 0 { + bulk.Upsert(w.docUpsert...) + } + + w.docInsert = []interface{}{} + w.docUpdate = []interface{}{} + w.docUpsert = []interface{}{} + + if _, err = bulk.Run(); err != nil { + log.Info(fmt.Sprintf("AutoCommitter-%s(%s)-%d >>ERROR<<: %v", w.p.name, w.p.coll, w.i, err.Error())) + } + if w.p.verbose { + log.Info(fmt.Sprintf("AutoCommitter-%s(%s)-%d committed size: %d", w.p.name, w.p.coll, w.i, len(w.docInsert))) + } + } else { + if w.p.verbose { + log.Info(fmt.Sprintf("AutoCommitter-%s(%s)-%d committed nothing", w.p.name, w.p.coll, w.i)) + } + } + return +} + +func (w *mongoAutoCommitWorker) commitRequired() bool { + if w.bulkActions() >= 0 && w.capacity() >= w.bulkActions() { + return true + } + return false +} + +func (w *mongoAutoCommitWorker) capacity() int { + return len(w.docInsert) + len(w.docUpdate) + len(w.docUpsert) +} diff --git a/mongo/mongo.go b/mongo/mongo.go new file mode 100644 index 0000000..aa830b7 --- /dev/null +++ b/mongo/mongo.go @@ -0,0 +1,300 @@ +package mongo + +import ( + "github.com/argcv/stork/config" + "github.com/argcv/stork/log" + "github.com/pkg/errors" + "github.com/globalsign/mgo" + "github.com/globalsign/mgo/bson" + "math" + "time" +) + +var ( + MongoBatchSize = 2000 +) + +type Client struct { + Session *mgo.Session + Db string + Config config.MongoConfig + isRoot bool + root *Client +} + +func UpdateDialInfoFromAuth(info *mgo.DialInfo, auth *config.MongoAuth) { + if auth != nil { + info.Source = auth.Source + info.Username = auth.Username + info.Password = auth.Password + info.Mechanism = auth.Mechanism + } +} + +/** + * + * Addrs holds the addresses for the seed servers. + * + * Database is the default database name used when the Session.DB method + * is called with an empty name, and is also used during the initial + * authentication if Source is unset. + * + * Username and Password inform the credentials for the initial authentication + * done on the database defined by the Source field. See Session.Login. + * + * Timeout is the amount of time to wait for a server to respond when + * first connecting and on follow up operations in the session. If + * timeout is zero, the call may block forever waiting for a connection + * to be established. Timeout does not affect logic in DialServer. + */ +func NewMongoClient(cfg config.MongoConfig) (client *Client, err error) { + addrs := cfg.Addrs + if len(addrs) == 0 { + msg := "Invalid Address: empty" + log.Error(msg) + return nil, errors.New(msg) + } + + auth := cfg.Auth + + if cfg.DefaultDb == "" && auth != nil { + cfg.DefaultDb = auth.Source + } + + info := &mgo.DialInfo{ + Addrs: cfg.Addrs, + Timeout: cfg.Timeout, + Database: cfg.DefaultDb, + } + UpdateDialInfoFromAuth(info, auth) + var ses *mgo.Session + ses, err = mgo.DialWithInfo(info) + if err != nil { + return nil, err + } + client = &Client{ + Session: ses, + Db: cfg.DefaultDb, + Config: cfg, + isRoot: true, + } + return +} + +func (client *Client) Spawn() (sub *Client) { + if client.isRoot { + sub = &Client{ + Session: client.Session.Copy(), + Db: client.Db, + Config: client.Config, + isRoot: false, + root: client, + } + } else { + log.Output(log.ERROR, "Spawning from Non-Root", 2) + sub = nil + } + return +} + +func (client *Client) Root() (sub *Client) { + if client.isRoot { + return client + } else { + return client.root + } +} + +func (client *Client) Close() { + if client.isRoot { + log.Output(log.ERROR, "Trying to **Close** root!!??", 2) + client.Session.Close() + } else { + client.Session.Close() + } +} + +func (client *Client) UseDB(db string) *Client { + client.Db = db + return client +} + +// A quick-short of pinging... +func (client *Client) Ping() error { + return client.Session.Ping() +} + +// Insert inserts one or more documents in the respective collection. In +// case the session is in safe mode (see the SetSafe method) and an error +// happens while inserting the provided documents, the returned error will +// be of type *LastError. +func (client *Client) Insert(coll string, data interface{}) error { + //data must be an address + return client.Session.DB(client.Db).C(coll).Insert(data) +} + +func (client *Client) Remove(coll string, query bson.M) error { + _, err := client.Session.DB(client.Db).C(coll).RemoveAll(query) + return err +} + +func (client *Client) Count(coll string, query bson.M) (int, error) { + return client.Session.DB(client.Db).C(coll).Find(query).Count() +} + +func (client *Client) All(coll string, query bson.M, selector bson.M, results interface{}) error { + //results must me an address + return client.Session.DB(client.Db).C(coll).Find(query).Select(selector).All(results) +} + +func (client *Client) AllSorted(coll string, query bson.M, sort []string, selector bson.M, results interface{}) error { + //results must me an address + return client.Session.DB(client.Db).C(coll).Find(query).Sort(sort...).Select(selector).All(results) +} + +func (client *Client) Search(coll string, query bson.M, sort []string, offset int, size int, results interface{}) error { + //results must me an address + return client.Session.DB(client.Db).C(coll).Find(query).Sort(sort...).Skip(offset).Limit(size).All(results) +} + +func (client *Client) One(coll string, query bson.M, result interface{}) error { + //result must be an address + return client.Session.DB(client.Db).C(coll).Find(query).One(result) +} + +type docExistsTest struct { + ID bson.ObjectId `json:"id" bson:"_id,omitempty"` // ID +} + +func (client *Client) Exists(coll string, query bson.M) bool { + r := &docExistsTest{} + return client.One(coll, query, r) == nil +} + +func (client *Client) Update(coll string, query bson.M, update interface{}) error { + //update must be an address + return client.Session.DB(client.Db).C(coll).Update(query, update) +} + +// Upsert finds a single document matching the provided selector document +// and modifies it according to the update document. If no document matching +// the selector is found, the update document is applied to the selector +// document and the result is inserted in the collection. +// If the session is in safe mode (see SetSafe) details of the executed +// operation are returned in info, or an error of type *LastError when +// some problem is detected. +// +// Relevant documentation: +// +// http://www.mongodb.org/display/DOCS/Updating +// http://www.mongodb.org/display/DOCS/Atomic+Operations +// +func (client *Client) Upsert(coll string, query bson.M, update interface{}) error { + // update must be an address + _, err := client.Session.DB(client.Db).C(coll).Upsert(query, update) + return err +} + +func (client *Client) BulkUpdate(coll string, pairs []interface{}) (err error) { + c := client.Session.DB(client.Db).C(coll) + + batch := MongoBatchSize + n := int(math.Ceil(float64(len(pairs)) / float64(batch))) + for i := 0; i < n; i++ { + start, end := i*batch, (i+1)*batch + if end > len(pairs) { + end = len(pairs) + } + log.Info(i, start, end, len(pairs), len(pairs[start:end])) + + bulk := c.Bulk() + bulk.Unordered() + bulk.Update(pairs[start:end]...) + if _, err = bulk.Run(); err != nil { + log.Info(err) + } + } + + return +} + +func (client *Client) UpdateAll(coll string, query bson.M, update interface{}) (err error) { + _, err = client.Session.DB(client.Db).C(coll).UpdateAll(query, update) + return +} + +func (client *Client) Iter(coll string, query bson.M, out chan bson.M) error { + iter := client.Session.DB(client.Db).C(coll).Find(query).Iter() + go func() { + defer iter.Close() + var result bson.M + for iter.Next(&result) { + out <- result + result = bson.M{} + } + close(out) + }() + return nil +} + +func (client *Client) IterSelect(coll string, query bson.M, selector bson.M, out chan bson.M) error { + iter := client.Session.DB(client.Db).C(coll).Find(query).Select(selector).Iter() + go func() { + defer iter.Close() + var result bson.M + for iter.Next(&result) { + out <- result + result = bson.M{} + } + close(out) + }() + return nil +} + +func (client *Client) IterSync(coll string, query bson.M, selector bson.M, f func(bson.M) error) error { + iter := client.Session.DB(client.Db).C(coll).Find(query).Select(selector).Iter() + var result bson.M + for iter.Next(&result) { + if f != nil { + if e := f(result); e != nil { + return e + } + } + result = bson.M{} + } + return nil +} + +func (client *Client) IterBatch(coll string, query bson.M, selector bson.M, batchSize int, f func([]bson.M) error) (err error) { + var buff []bson.M + if err = client.IterSync(coll, query, selector, func(m bson.M) (e error) { + buff = append(buff, m) + if len(buff) >= batchSize { + e = f(buff) + buff = []bson.M{} + } + return + }); err != nil { + return + } + if len(buff) > 0 { + err = f(buff) + } + return +} + +func (client *Client) EnsureIndexKey(coll string, key ...string) (err error) { + return client.Session.DB(client.Db).C(coll).EnsureIndexKey(key...) +} + +func (client *Client) NewAutoCommitBuilder(coll string) *AutoCommitBuilder { + return &AutoCommitBuilder{ + client: client, + name: coll, + coll: coll, + numWorkers: 1, + bulkActions: 1000, + flushInterval: time.Duration(2) * time.Second, + verbose: true, + } +} diff --git a/mongo/utils.go b/mongo/utils.go new file mode 100644 index 0000000..9663509 --- /dev/null +++ b/mongo/utils.go @@ -0,0 +1,89 @@ +package mongo + +import ( + "errors" + "fmt" + "github.com/globalsign/mgo/bson" +) + +// NewObjectId returns a new unique ObjectId. +func NewObjectId() bson.ObjectId { + return bson.NewObjectId() +} + +func SafeToObjectId(s string) (bson.ObjectId, error) { + if bson.IsObjectIdHex(s) { + return bson.ObjectIdHex(s), nil + } else { + return bson.NewObjectId(), errors.New(fmt.Sprintf("INVALID_ID:%s", s)) + } +} + +func SafeToObjectIdOrEmpty(s string) bson.ObjectId { + if bson.IsObjectIdHex(s) { + return bson.ObjectIdHex(s) + } else { + return "" + } +} + +func IsObjectIdHex(s string) bool { + return bson.IsObjectIdHex(s) +} + +func ToObjectIdHex(s string) bson.ObjectId { + return bson.ObjectIdHex(s) +} + +// m bson.M +// s SomeStruct +// BsonM2Struct(m, &s) +func BsonM2Struct(m bson.M, s interface{}) (status bool) { + if bsonBytes, err := bson.Marshal(m); err == nil { + bson.Unmarshal(bsonBytes, s) + status = true + } else { + status = false + } + return +} + +func FuzzyQuery(value interface{}) bson.M { + return bson.M{"$regex": bson.RegEx{value.(string), "i"}} +} + +func SizeQuery(size int) bson.M { + return bson.M{"$size": size} +} + +func InQuery(valueList interface{}) bson.M { + return bson.M{"$in": valueList} +} + +func NinQuery(valueList interface{}) bson.M { + return bson.M{"$nin": valueList} +} + +func OrQuery(qs []bson.M) bson.M { + return bson.M{"$or": qs} +} + +func AndQuery(qs []bson.M) bson.M { + return bson.M{"$and": qs} +} + +func SetOperator(value interface{}) bson.M { + return bson.M{"$set": value} +} + +func IncOperator(valueList interface{}) bson.M { + return bson.M{"$inc": valueList} +} + +func NotQuery(value interface{}) bson.M { + return bson.M{"$ne": value} +} + +func NExistQuery() bson.M { + return bson.M{"$exists": false} +} diff --git a/mtx/critical_section.go b/mtx/critical_section.go new file mode 100644 index 0000000..09d97f5 --- /dev/null +++ b/mtx/critical_section.go @@ -0,0 +1,179 @@ +// mutex +package mtx + +import ( + "github.com/argcv/stork/cntr" + "github.com/argcv/stork/log" + "runtime" + "sync" + "time" +) + +/** + * CriticalSection Provides a thread safe set, so that + * we can define a set of lockers in one object + * + * + */ +type CriticalSection struct { + m sync.Mutex // global mutex + mj sync.Mutex // mutex for joining + d map[string]bool +} + +func NewCriticalSection() *CriticalSection { + return &CriticalSection{ + m: sync.Mutex{}, + mj: sync.Mutex{}, + d: map[string]bool{}, + } +} + +/** + * check and release one id + */ +func (c *CriticalSection) ReleaseOne(entry string) { + c.m.Lock() + defer c.m.Unlock() + delete(c.d, entry) +} + +/** + * This is a non-blocking method. + * Given a set of entries, if all of them are released + * lock all and return true + * otherwise do nothing and return false + */ +func (c *CriticalSection) TryLockAll(entries ...string) bool { + c.m.Lock() + defer c.m.Unlock() + + // check entries, make sure all of them are unlocked + for _, entry := range entries { + if c.unsafeIsLocked(entry) { + return false + } + } + + // lock all + for _, entry := range entries { + c.d[entry] = true + } + return true +} + +/** + * This is a non-blocking method. + * Given a set of entries + * try to lock some of the entries, and return the locked items + */ +func (c *CriticalSection) TryLockPartial(entries ...string) (locked []string) { + c.m.Lock() + defer c.m.Unlock() + + // check entries, make sure all of them are unlocked + for _, entry := range entries { + if !c.unsafeIsLocked(entry) { + // is NOT locked + + // lock it + c.d[entry] = true + + // add to locked list + locked = append(locked, entry) + } + } + + return +} + +/** + * Caution: This is a blocking method + * Wait until all the functions are locked + * Maybe it is not a good idea..? + */ +func (c *CriticalSection) Join(entries ...string) { + c.mj.Lock() + defer c.mj.Unlock() + + var locked []string + timeIn := time.Now() + timeLast := timeIn + + entries = cntr.DistinctStrings(entries...) + + for len(locked) < len(entries) { + //log.Infof("from: %v", len(locked)) + clocked := c.TryLockPartial(entries...) + locked = append(locked, clocked...) + //log.Infof("to: %v", len(locked)) + runtime.Gosched() + + timeCurr := time.Now() + if timeCurr.Sub(timeLast) > 3*time.Second { + mCaptured := map[string]bool{} + var waiting []string + for _, entry := range locked { + mCaptured[entry] = true + } + for _, entry := range entries { + if _, ok := mCaptured[entry]; !ok { + waiting = append(waiting, entry) + } + } + + n := 3 + if len(waiting) < 3 { + n = len(waiting) + } + log.Warnf("Possible Deadlock!!! Start Time: %v, obtaining size: %v, missing: %v... in total %v entries", + timeIn, + len(entries), + waiting[:n], + len(waiting), + ) + + timeLast = timeCurr + } + } +} + +/** + * release all the entries + */ +func (c *CriticalSection) Release(entries ...string) { + for _, entry := range entries { + c.ReleaseOne(entry) + } +} + +/** + * return true if all the entries are unlocked + */ +func (c *CriticalSection) Check(entries ...string) bool { + c.m.Lock() + defer c.m.Unlock() + for _, entry := range entries { + if c.unsafeIsLocked(entry) { + return false + } + } + return true +} + +/** + * return true if at least 1 of the entries are unlocked + * NOTE: **NOT** ALL of them are locked + */ +func (c *CriticalSection) Locked(entries ...string) bool { + return !c.Check(entries...) +} + +/** + * an internal function, a simple helper to check the status of + * data set. return true if this entry already exists right now + */ +func (c *CriticalSection) unsafeIsLocked(entry string) bool { + _, ok := c.d[entry] + return ok +} diff --git a/mtx/critical_section_test.go b/mtx/critical_section_test.go new file mode 100644 index 0000000..24eee97 --- /dev/null +++ b/mtx/critical_section_test.go @@ -0,0 +1,160 @@ +package mtx + +import ( + "testing" + "time" +) + +func TestNewCriticalSection(t *testing.T) { + if NewCriticalSection() == nil { + t.Errorf("init failed") + } +} + +func TestCriticalSection_Check(t *testing.T) { + cs := NewCriticalSection() + + if !cs.Check("aa") { + t.Errorf("check failed #1") + } + + cs.Join("aa", "bb") + + if cs.Check("aa") { + t.Errorf("check failed #1") + } +} + +func TestCriticalSection_Join(t *testing.T) { + cs := NewCriticalSection() + cs.Join("aa") + isUnlocked := false + go func() { + time.Sleep(100 * time.Millisecond) + isUnlocked = true + cs.ReleaseOne("aa") + }() + + if cs.Check("aa") { + t.Errorf("lock failed") + } + + cs.Join("aa") + if isUnlocked == false { + t.Errorf("join failed") + } + +} + +func TestCriticalSection_Join2(t *testing.T) { + cs := NewCriticalSection() + cs.Join("aa", "bb", "aa") + isUnlocked := false + go func() { + time.Sleep(20 * time.Millisecond) + isUnlocked = true + cs.ReleaseOne("aa") + cs.ReleaseOne("bb") + }() + + if cs.Check("aa") { + t.Errorf("lock failed") + } + + if cs.Check("bb") { + t.Errorf("lock failed") + } + + cs.Join("aa", "bb") + if isUnlocked == false { + t.Errorf("join failed") + } +} + +func TestCriticalSection_JoinWarning(t *testing.T) { + cs := NewCriticalSection() + cs.Join("aa") + isUnlocked := false + go func() { + time.Sleep(7 * time.Second) + isUnlocked = true + cs.ReleaseOne("aa") + }() + + if cs.Check("aa") { + t.Errorf("lock failed") + } + + cs.Join("aa") + if isUnlocked == false { + t.Errorf("join failed") + } + +} + +func TestCriticalSection_TryLockAll(t *testing.T) { + cs := NewCriticalSection() + cs.Join("aa") + + if cs.TryLockAll("aa", "bb") { + t.Errorf("check failed") + } + + if cs.Check("aa") { + t.Errorf("lock failed") + } + + if cs.Locked("bb") { + t.Errorf("incorrectly locked") + } + + cs.Release("aa", "bb") + + if cs.Locked("aa") { + t.Errorf("incorrectly failed") + } + + if cs.Locked("bb") { + t.Errorf("incorrectly locked") + } + + if !cs.TryLockAll("aa", "bb") { + t.Errorf("lock failed") + } + + if cs.Check("aa") { + t.Errorf("lock failed") + } + + if cs.Check("bb") { + t.Errorf("lock locked") + } + +} + +func TestCriticalSection_TryLockPartial(t *testing.T) { + cs := NewCriticalSection() + cs.Join("aa") + + if cs.Check("aa") { + t.Errorf("lock failed") + } + + locked := cs.TryLockPartial("aa", "bb") + if len(locked) != 1 { + t.Fatalf("incorrect partial locked size") + } + + if locked[0] != "bb" { + t.Errorf("incorrect partial locked string") + } + + if cs.Check("aa") { + t.Errorf("lock failed") + } + + if cs.Check("bb") { + t.Errorf("lock failed") + } + +} diff --git a/mtx/mtx.go b/mtx/mtx.go new file mode 100644 index 0000000..0336e70 --- /dev/null +++ b/mtx/mtx.go @@ -0,0 +1,2 @@ +// mutex related helpers +package mtx diff --git a/mtx/singleton.go b/mtx/singleton.go new file mode 100644 index 0000000..e8ee33a --- /dev/null +++ b/mtx/singleton.go @@ -0,0 +1,29 @@ +package mtx + +import ( + "sync/atomic" +) + +/** + * SingletonDesc provides a simple way to **SKIP** one function + * If another function is in executing + * + */ +type SingletonDesc struct { + cnt int32 +} + +func NewSingleton() *SingletonDesc { + return &SingletonDesc{ + cnt: 0, + } +} + +// We can call Acquire multiple times, however, there is only +// 1 running callback +func (s *SingletonDesc) Acquire(f func()) { + if atomic.CompareAndSwapInt32(&s.cnt, 0, 1) { + defer atomic.StoreInt32(&s.cnt, 0) + f() + } +} diff --git a/mtx/singleton_test.go b/mtx/singleton_test.go new file mode 100644 index 0000000..1ca69bf --- /dev/null +++ b/mtx/singleton_test.go @@ -0,0 +1,34 @@ +package mtx + +import ( + "github.com/argcv/stork/assert" + "sync" + "testing" + "time" +) + +func TestNewSingleton(t *testing.T) { + s := NewSingleton() + i := 0 + mx := sync.Mutex{} + go s.Acquire(func() { + time.Sleep(10 * time.Millisecond) + + mx.Lock() + i += 1 // + mx.Unlock() + }) + + s.Acquire(func() { + mx.Lock() + i += 1 + mx.Unlock() + + time.Sleep(10 * time.Millisecond) + }) + time.Sleep(15 * time.Millisecond) + + mx.Lock() + assert.ExpectEQ(t, 1, i) + mx.Unlock() +} diff --git a/mtx/wg_state.go b/mtx/wg_state.go new file mode 100644 index 0000000..611b787 --- /dev/null +++ b/mtx/wg_state.go @@ -0,0 +1,57 @@ +package mtx + +import ( + "sync" + "sync/atomic" + + "github.com/argcv/stork/log" +) + +// Compare with waiting group +// it will return current state +// aka **How many workers are still working** +type WaitGroupWithState interface { + Add(delta int64) int64 + Done() int64 + State() int64 + Wait() +} + +type waitGroupWithStateImpl struct { + st int64 + cv *sync.Cond +} + +func NewWaitGroupWithState() WaitGroupWithState { + return &waitGroupWithStateImpl{ + st: 0, + cv: sync.NewCond(&sync.Mutex{}), + } +} + +func (wg *waitGroupWithStateImpl) Add(delta int64) int64 { + newSt := atomic.AddInt64(&(wg.st), delta) + if newSt < 0 { + log.Fatalf("ERROR: status is lower than 0!!! (%v)", newSt) + } + return newSt +} + +// minus one, return current value +func (wg *waitGroupWithStateImpl) Done() int64 { + newSt := wg.Add(-1) + wg.cv.Broadcast() + return newSt +} + +func (wg *waitGroupWithStateImpl) State() int64 { + return atomic.LoadInt64(&(wg.st)) +} + +func (wg *waitGroupWithStateImpl) Wait() { + for wg.State() > 0 { + wg.cv.L.Lock() + wg.cv.Wait() + wg.cv.L.Unlock() + } +} diff --git a/mtx/wg_state_test.go b/mtx/wg_state_test.go new file mode 100644 index 0000000..4b8f2a6 --- /dev/null +++ b/mtx/wg_state_test.go @@ -0,0 +1,23 @@ +package mtx + +import ( + "github.com/argcv/stork/assert" + "testing" + "time" +) + +func TestNewWaitGroupWithState(t *testing.T) { + wg := NewWaitGroupWithState() + wg.State() + assert.ExpectEQ(t, int64(0), wg.State()) + for i := 0; i < 10; i ++ { + wg.Add(1) + go func() { + time.Sleep(1 * time.Millisecond) + assert.ExpectLT(t, int64(-1), wg.Done()) + }() + } + assert.ExpectLT(t, int64(-1), wg.State()) + wg.Wait() + assert.ExpectEQ(t, int64(0), wg.State()) +} diff --git a/pb/struct.go b/pb/struct.go new file mode 100644 index 0000000..c32f152 --- /dev/null +++ b/pb/struct.go @@ -0,0 +1,218 @@ +package pb + +import ( + "fmt" + "reflect" + + st "google.golang.org/protobuf/types/known/structpb" +) + +// ToStruct converts a map[string]interface{} to a ptypes.Struct +// This function is based on +// https://gist.github.com/jsmouret/2bc876e8def6c63410556350eca3e43d +// Caution: +// It may cause deadlock currently +func ToStruct(v map[string]interface{}) *st.Struct { + size := len(v) + if size == 0 { + return nil + } + fields := make(map[string]*st.Value, size) + for k, v := range v { + fields[k] = ToValue(v) + } + return &st.Struct{ + Fields: fields, + } +} + +// ToValue converts an interface{} to a ptypes.Value +func ToValue(v interface{}) *st.Value { + switch v := v.(type) { + case nil: + return nil + case bool: + return &st.Value{ + Kind: &st.Value_BoolValue{ + BoolValue: v, + }, + } + case int: + return &st.Value{ + Kind: &st.Value_NumberValue{ + NumberValue: float64(v), + }, + } + case int8: + return &st.Value{ + Kind: &st.Value_NumberValue{ + NumberValue: float64(v), + }, + } + case int32: + return &st.Value{ + Kind: &st.Value_NumberValue{ + NumberValue: float64(v), + }, + } + case int64: + return &st.Value{ + Kind: &st.Value_NumberValue{ + NumberValue: float64(v), + }, + } + case uint: + return &st.Value{ + Kind: &st.Value_NumberValue{ + NumberValue: float64(v), + }, + } + case uint8: + return &st.Value{ + Kind: &st.Value_NumberValue{ + NumberValue: float64(v), + }, + } + case uint32: + return &st.Value{ + Kind: &st.Value_NumberValue{ + NumberValue: float64(v), + }, + } + case uint64: + return &st.Value{ + Kind: &st.Value_NumberValue{ + NumberValue: float64(v), + }, + } + case float32: + return &st.Value{ + Kind: &st.Value_NumberValue{ + NumberValue: float64(v), + }, + } + case float64: + return &st.Value{ + Kind: &st.Value_NumberValue{ + NumberValue: v, + }, + } + case string: + return &st.Value{ + Kind: &st.Value_StringValue{ + StringValue: v, + }, + } + case error: + return &st.Value{ + Kind: &st.Value_StringValue{ + StringValue: v.Error(), + }, + } + default: + // Fallback to reflection for other types + return toValue(reflect.ValueOf(v)) + } +} + +func toValue(v reflect.Value) *st.Value { + switch v.Kind() { + case reflect.Bool: + return &st.Value{ + Kind: &st.Value_BoolValue{ + BoolValue: v.Bool(), + }, + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return &st.Value{ + Kind: &st.Value_NumberValue{ + NumberValue: float64(v.Int()), + }, + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return &st.Value{ + Kind: &st.Value_NumberValue{ + NumberValue: float64(v.Uint()), + }, + } + case reflect.Float32, reflect.Float64: + return &st.Value{ + Kind: &st.Value_NumberValue{ + NumberValue: v.Float(), + }, + } + case reflect.Ptr: + if v.IsNil() { + return nil + } + return toValue(reflect.Indirect(v)) + case reflect.Array, reflect.Slice: + size := v.Len() + if size == 0 { + return nil + } + values := make([]*st.Value, size) + for i := 0; i < size; i++ { + values[i] = toValue(v.Index(i)) + } + return &st.Value{ + Kind: &st.Value_ListValue{ + ListValue: &st.ListValue{ + Values: values, + }, + }, + } + case reflect.Struct: + t := v.Type() + size := v.NumField() + if size == 0 { + return nil + } + fields := make(map[string]*st.Value, size) + for i := 0; i < size; i++ { + name := t.Field(i).Name + // Better way? + if len(name) > 0 && 'A' <= name[0] && name[0] <= 'Z' { + fields[name] = toValue(v.Field(i)) + } + } + if len(fields) == 0 { + return nil + } + return &st.Value{ + Kind: &st.Value_StructValue{ + StructValue: &st.Struct{ + Fields: fields, + }, + }, + } + case reflect.Map: + keys := v.MapKeys() + if len(keys) == 0 { + return nil + } + fields := make(map[string]*st.Value, len(keys)) + for _, k := range keys { + if k.Kind() == reflect.String { + fields[k.String()] = toValue(v.MapIndex(k)) + } + } + if len(fields) == 0 { + return nil + } + return &st.Value{ + Kind: &st.Value_StructValue{ + StructValue: &st.Struct{ + Fields: fields, + }, + }, + } + default: + // Last resort + return &st.Value{ + Kind: &st.Value_StringValue{ + StringValue: fmt.Sprint(v), + }, + } + } +} diff --git a/pb/struct_test.go b/pb/struct_test.go new file mode 100644 index 0000000..262189c --- /dev/null +++ b/pb/struct_test.go @@ -0,0 +1,64 @@ +package pb + +import ( + "testing" + + st "google.golang.org/protobuf/types/known/structpb" + + "github.com/argcv/stork/assert" +) + +func TestToStruct(t *testing.T) { + type cst struct { + foo int + } + + baseMap := map[string]interface{}{ + "nil": nil, + "bool": true, + "int": 1, + "int8": int8(8), + "int16": int8(16), + "int32": int8(32), + "int64": int8(64), + "uint8": uint8(8), + "uint16": uint8(16), + "uint32": uint8(32), + "uint64": uint8(64), + "float": 2.01, + "float32": float32(32.0), + "float64": float32(64.0), + "string": "hello", + "cst": cst{ + foo: 1, + }, + } + + foo := map[string]interface{}{} + for k, v := range baseMap { + foo[k] = v + } + foo["sub"] = baseMap + + val := ToStruct(foo) + _, ok := val.Fields["empty"] + assert.ExpectFalse(t, ok, "no field empty") + nilVal, ok := val.Fields["nil"] + assert.ExpectTrue(t, ok, "found field nil") + assert.ExpectEQ(t, st.NullValue_NULL_VALUE, nilVal.GetNullValue()) + boolVal, ok := val.Fields["bool"] + assert.ExpectTrue(t, ok, "found field bool") + assert.ExpectEQ(t, true, boolVal.GetBoolValue()) + intVal, ok := val.Fields["int"] + assert.ExpectTrue(t, ok, "found field int") + assert.ExpectEQ(t, 1.0, intVal.GetNumberValue()) + floatVal, ok := val.Fields["float"] + assert.ExpectTrue(t, ok, "found field float") + assert.ExpectEQ(t, 2.01, floatVal.GetNumberValue()) + stringVal, ok := val.Fields["string"] + assert.ExpectTrue(t, ok, "found field string") + assert.ExpectEQ(t, "hello", stringVal.GetStringValue()) + cstVal, ok := val.Fields["cst"] + assert.ExpectTrue(t, ok, "found field cst") + assert.ExpectNE(t, nil, cstVal.GetStructValue()) +} diff --git a/schd/fifo_queue.go b/schd/fifo_queue.go new file mode 100644 index 0000000..49fc4c9 --- /dev/null +++ b/schd/fifo_queue.go @@ -0,0 +1,27 @@ +package schd + +type FifoQueue struct { + q *TaskQueue +} + +func NewFifoQueue() *FifoQueue { + return &FifoQueue{ + q: NewTaskQueue(), + } +} + +func (q *FifoQueue) Enqueue(f func()) { + q.q.Enqueue(f) +} + +// flush will wait +func (q *FifoQueue) Flush() { + q.q.Flush() +} + +// closing is NOT required in Golang +// It just used to tell the receiver +// everything are sent +func (q *FifoQueue) Close() { + q.q.Close() +} diff --git a/schd/fifo_queue_test.go b/schd/fifo_queue_test.go new file mode 100644 index 0000000..8384d62 --- /dev/null +++ b/schd/fifo_queue_test.go @@ -0,0 +1,27 @@ +package schd + +import ( + "github.com/argcv/stork/assert" + "testing" + "time" +) + +func TestNewFifoQueue(t *testing.T) { + fq := NewFifoQueue() + + prev := -1 + for i := 0; i < 100; i++ { + ci := i + fq.Enqueue(func() { + assert.ExpectEQ(t, 1, ci-prev) + time.Sleep(1 * time.Millisecond) + assert.ExpectEQ(t, 1, ci-prev) + t.Logf("From %v to %v", prev, ci) + prev = ci + }) + } + + t.Logf("Close..") + fq.Close() + assert.ExpectEQ(t, 99, prev) +} diff --git a/schd/multi_task_ticker.go b/schd/multi_task_ticker.go new file mode 100644 index 0000000..3841024 --- /dev/null +++ b/schd/multi_task_ticker.go @@ -0,0 +1,178 @@ +package schd + +import ( + "context" + "errors" + "sync" + "time" + + "github.com/argcv/stork/log" +) + +type runningState struct { + bucket map[int]bool + mx *sync.RWMutex +} + +func newRunningState() *runningState { + return &runningState{ + bucket: map[int]bool{}, + mx: &sync.RWMutex{}, + } +} + +func (rs *runningState) add(id int) bool { + rs.mx.Lock() + defer rs.mx.Unlock() + if _, ok := rs.bucket[id]; ok { + // add failed + return false + } else { + rs.bucket[id] = true + return true + } +} + +func (rs *runningState) remove(id int) { + rs.mx.Lock() + defer rs.mx.Unlock() + rs.bucket[id] = true + delete(rs.bucket, id) +} + +func (rs *runningState) check(id int) bool { + rs.mx.RLock() + defer rs.mx.RUnlock() + _, ok := rs.bucket[id] + return ok +} + +func (rs *runningState) stat() int { + rs.mx.RLock() + defer rs.mx.RUnlock() + return len(rs.bucket) +} + +type MultiTaskTickerFunc func(ctx context.Context, param interface{}) + +type MultiTaskTicker struct { + period time.Duration + nWorkers int + params []interface{} + rs *runningState + cancel context.CancelFunc + wg *sync.WaitGroup + m *sync.Mutex + isStarted bool +} + +func NewMultiTaskTicker() *MultiTaskTicker { + return &MultiTaskTicker{ + period: 100 * time.Millisecond, + nWorkers: 10, + params: nil, + rs: newRunningState(), + wg: &sync.WaitGroup{}, + m: &sync.Mutex{}, + isStarted: false, + } +} + +func (mtt *MultiTaskTicker) SetPeriod(period time.Duration) *MultiTaskTicker { + mtt.period = period + return mtt +} + +func (mtt *MultiTaskTicker) SetNumWorkers(nWorkers int) *MultiTaskTicker { + mtt.nWorkers = nWorkers + return mtt +} + +func (mtt *MultiTaskTicker) GetNumWorkers() int { + return mtt.nWorkers +} + +func (mtt *MultiTaskTicker) AddTask(params ...interface{}) *MultiTaskTicker { + mtt.params = append(mtt.params, params...) + return mtt +} + +func (mtt *MultiTaskTicker) SetTasks(params ...interface{}) *MultiTaskTicker { + mtt.params = params + return mtt +} + +func (mtt *MultiTaskTicker) Start(ctx context.Context, f MultiTaskTickerFunc) (err error) { + mtt.m.Lock() + defer mtt.m.Unlock() + if mtt.isStarted { + return errors.New("already_started") + } + mtt.isStarted = true + var cctx context.Context + cctx, mtt.cancel = context.WithCancel(ctx) + wkr := NewTaskQueue() + wkr.SetNumWorkers(mtt.nWorkers + 1) + wkr.Enqueue(func() { + mtt.wg.Add(1) + defer mtt.wg.Done() + ticker := time.NewTicker(mtt.period) + for { + select { + case <-cctx.Done(): + log.Infof("canceled...") + return + case <-ticker.C: + params := mtt.params + for id := range params { + cid := id + param := params[id] + if mtt.rs.add(cid) { + // start it + mtt.wg.Add(1) + wkr.Enqueue(func() { + defer mtt.wg.Done() + f(cctx, param) + mtt.rs.remove(cid) + }) + } else { + // task adding failed + } + } + } + } + }) + return +} + +func (mtt *MultiTaskTicker) Stop(ctx context.Context) (err error) { + mtt.m.Lock() + defer mtt.m.Unlock() + if !mtt.isStarted { + return errors.New("not_started") + } + stop := make(chan bool) + go func() { + mtt.cancel() + mtt.wg.Wait() + mtt.isStarted = false + stop <- true + }() + select { + case <-ctx.Done(): + return errors.New("timeout") + case <-stop: + } + return +} + +func (mtt *MultiTaskTicker) StopWait() { + mtt.wg.Wait() + return +} + +func (mtt *MultiTaskTicker) IsStarted() bool { + mtt.m.Lock() + defer mtt.m.Unlock() + return mtt.isStarted +} diff --git a/schd/multi_task_ticker_test.go b/schd/multi_task_ticker_test.go new file mode 100644 index 0000000..dace65c --- /dev/null +++ b/schd/multi_task_ticker_test.go @@ -0,0 +1,40 @@ +package schd + +import ( + "context" + "testing" + "time" +) + +func TestMultiTaskTicker_Start(t *testing.T) { + c1 := 0 + c2 := 0 + mtt := NewMultiTaskTicker() + mtt.SetNumWorkers(3) + mtt.SetPeriod(10 * time.Millisecond) + mtt.AddTask("1", "2") + mtt.SetTasks("1", "2") + st := mtt.Start(context.Background(), func(ctx context.Context, label interface{}) { + switch label { + case "1": + c1 += 1 + t.Logf("c1 => %v", c1) + time.Sleep(1 * time.Second) + case "2": + c2 += 2 + t.Logf("c2 => %v", c2) + default: + t.Errorf("Unknown label: %v", label) + } + }) + if st != nil { + t.Errorf("failed: %v", st) + } + time.Sleep(100 * time.Millisecond) + ctx, _ := context.WithTimeout(context.TODO(), 1*time.Second) + st = mtt.Stop(ctx) + if st != nil { + t.Errorf("failed: %v", st) + } + t.Logf("c1: %v c2: %v", c1, c2) +} diff --git a/schd/schd.go b/schd/schd.go new file mode 100644 index 0000000..ffded80 --- /dev/null +++ b/schd/schd.go @@ -0,0 +1,26 @@ +package schd + +//var ( +// defaultTaskQueue = NewTaskQueue() +//) +// +//func SetNumWorkers(numWorkers int) int { +// return defaultTaskQueue.SetNumWorkers(numWorkers) +//} +// +//func GetNumWorkers() int { +// return defaultTaskQueue.GetNumWorkers() +//} +// +//func Enqueue(f func()) { +// defaultTaskQueue.Enqueue(f) +//} +// +//func Flush() { +// defaultTaskQueue.Flush() +//} +// +//// return current size +//func State() int64 { +// return defaultTaskQueue.State() +//} diff --git a/schd/task_queue.go b/schd/task_queue.go new file mode 100644 index 0000000..51aedee --- /dev/null +++ b/schd/task_queue.go @@ -0,0 +1,152 @@ +package schd + +import ( + "runtime" + "sync" + + "github.com/argcv/stork/mtx" +) + +type taskQueueWorker struct { + //id string + + q *TaskQueue // global environment + i int // current index + m sync.Mutex // mutex for current worker + + stop chan struct{} + stopAck chan struct{} +} + +func (w *taskQueueWorker) work() { + defer close(w.stopAck) + + running := true + for running { + select { + case f, ok := <-w.q.c: + if ok { + //log.Infof("exec: #1 %v", w.id) + f() // perform current job + runtime.Gosched() + w.q.wg.Done() // done + } else { + running = false + // this channel is closed + w.stopAck <- struct{}{} + //log.Infof("quit #1 %v", w.id) + } + case <-w.stop: + running = false + w.stopAck <- struct{}{} + //log.Infof("quit #2 %v", w.id) + } + } +} + +func newTaskQueueWorker(q *TaskQueue, i int) *taskQueueWorker { + return &taskQueueWorker{ + q: q, + i: i, + + stop: make(chan struct{}), + stopAck: make(chan struct{}), + } +} + +/** + * Task Queue: it is used to help us schedule a + * sequence of executions + * + * The default num workers is 1 + * If this number is 1, it could also treated as a + * strict FIFO Queue + * + */ +type TaskQueue struct { + numWorkers int + workers []*taskQueueWorker + + c chan func() + wg mtx.WaitGroupWithState + sd *mtx.SingletonDesc +} + +func NewTaskQueue() *TaskQueue { + return &TaskQueue{ + numWorkers: 1, + c: make(chan func()), + wg: mtx.NewWaitGroupWithState(), + sd: mtx.NewSingleton(), + } +} + +func (q *TaskQueue) SetNumWorkers(numWorkers int) int { + if numWorkers > 0 { + q.numWorkers = numWorkers + } + return q.GetNumWorkers() +} + +func (q *TaskQueue) GetNumWorkers() int { + return q.numWorkers +} + +func (q *TaskQueue) Perform() { + go q.sd.Acquire(func() { + for q.wg.State() > 0 { + // launch workers + q.workers = make([]*taskQueueWorker, q.numWorkers) + for i := 0; i < q.numWorkers; i++ { + //log.Infof("start worker... %v", i) + cw := newTaskQueueWorker(q, i) + //cw.id = xid.New().String() + q.workers[i] = cw + go cw.work() + } + + // waiting for job's finished + q.wg.Wait() + + for _, w := range q.workers { + w.stop <- struct{}{} + close(w.stop) + } + + for _, w := range q.workers { + <-w.stopAck // wait for completion + } + //log.Infof("stop workers") + } + }) +} + +// add a new task +func (q *TaskQueue) Enqueue(f func()) { + // to announce a new job is comming + q.wg.Add(1) + // try launch the worker + q.Perform() + // enqueue the task + q.c <- f +} + +// return current work loader +func (q *TaskQueue) State() int64 { + return q.wg.State() +} + +func (q *TaskQueue) Flush() { + q.Perform() + q.wg.Wait() +} + +// It's OK to leave a Go channel open forever and never close it. +// When the channel is no longer used, it will be garbage collected. +// -- +// However we could provide a close + wait interface, which is used +// to indicate its finishing +func (q *TaskQueue) Close() { + q.Flush() + close(q.c) +} diff --git a/schd/task_queue_test.go b/schd/task_queue_test.go new file mode 100644 index 0000000..aeaa984 --- /dev/null +++ b/schd/task_queue_test.go @@ -0,0 +1,101 @@ +package schd + +import ( + "fmt" + "github.com/argcv/stork/assert" + "sync" + "testing" + "time" +) + +func TestNewTaskQueue(t *testing.T) { + fq := NewTaskQueue() + + prev := -1 + for i := 0; i < 100; i++ { + ci := i + fq.Enqueue(func() { + assert.ExpectEQ(t, 1, ci-prev, fmt.Sprintf("%v vs. %v", ci, prev)) + //time.Sleep(1 * time.Millisecond) + assert.ExpectEQ(t, 1, ci-prev, fmt.Sprintf("%v vs. %v", ci, prev)) + t.Logf("From %v to %v", prev, ci) + prev = ci + }) + } + + t.Logf("Close..") + fq.Close() + assert.ExpectEQ(t, int64(0), fq.State()) + assert.ExpectEQ(t, 99, prev) +} + +func TestTaskQueue_SetNumWorkers(t *testing.T) { + fq := NewTaskQueue() + + assert.ExpectEQ(t, 10, fq.SetNumWorkers(10)) + + prev := 0 + mx := sync.Mutex{} + + // batch 1 + for i := 0; i < 100; i++ { + ci := i + fq.Enqueue(func() { + mx.Lock() + prev += 1 + t.Logf("a' ci: %v prev: %v", ci, prev) + mx.Unlock() + //time.Sleep(20 * time.Millisecond) + mx.Lock() + prev += 1 + t.Logf("b' ci: %v prev: %v", ci, prev) + mx.Unlock() + }) + } + + time.Sleep(100 * time.Millisecond) + + // sleep, and another batch + for i := 0; i < 100; i++ { + ci := i + fq.Enqueue(func() { + mx.Lock() + prev += 1 + t.Logf("a' ci: %v prev: %v", ci, prev) + mx.Unlock() + time.Sleep(20 * time.Millisecond) + mx.Lock() + prev += 1 + t.Logf("b' ci: %v prev: %v", ci, prev) + mx.Unlock() + }) + } + + fq.Flush() + + // flush and another batch + for i := 0; i < 100; i++ { + ci := i + fq.Enqueue(func() { + mx.Lock() + prev += 1 + t.Logf("a' ci: %v prev: %v", ci, prev) + mx.Unlock() + time.Sleep(20 * time.Millisecond) + mx.Lock() + prev += 1 + t.Logf("b' ci: %v prev: %v", ci, prev) + mx.Unlock() + }) + } + + t.Logf("Staring to Close...") + fq.Close() + t.Logf("Closed") + assert.ExpectEQ(t, 600, prev) +} + +func TestTaskQueue_Close(t *testing.T) { + fq := NewTaskQueue() + fq.Close() +} diff --git a/sigr/README.md b/sigr/README.md new file mode 100644 index 0000000..ff71f6f --- /dev/null +++ b/sigr/README.md @@ -0,0 +1,79 @@ +# Signal Register + +[![Build Status][badge-travis]][link-travis] +[![MIT License][badge-license]](LICENSE) + +Package `argcv/sigr` implements a simple signal proxy, which is used to manage the signal easier for multiple tasks. + +You can add one or a few functions after interrupt/quit/.... signal comes and befure really quit. + +Currently, the process sequence are not in order. + +## Install + +```bash +go get -u github.com/argcv/stork/sigr +``` + +## Example + +```go +package main + +import ( + "fmt" + "github.com/argcv/sigr" + "time" +) + +func main() { + // register a new name + // this is used to + name1 := sigr.RegisterOnStopFuncAutoName(func() { + fmt.Println("Hello, World! #1") + }) + name2 := sigr.RegisterOnStopFuncAutoName(func() { + fmt.Println("Hello, World! #2") + }) + sigr.RegisterOnStopFunc("customized name", func() { + fmt.Println("Hello, World! #3") + }) + fmt.Println("name1:", name1, "name2:", name2) + sigr.RegisterOnStopFunc(name1, func() { + fmt.Println("Hello, World! #1 #overridden") + }) + sigr.UnregisterOnStopFunc(name2) + // default: true + sigr.SetQuitDirectyl(true) + // get verbose log + sigr.VerboseLog() + // without verbose log, your printing is still work here + //sigr.NoLog() + // you may type ctrl+C to excute the functions (name1 and "customized name") + time.Sleep(time.Duration(20 * time.Second)) +} + +``` + +The output seems as follow: + +```bash +$ go run example.go +name1: __auto_1 name2: __auto_2 +^C2017/10/26 19:06:17.804737 sigr.go:74: sig: [interrupt] Processing... +2017/10/26 19:06:17.804935 sigr.go:78: Processing task [__auto_1] +Hello, World! #1 #overridden +2017/10/26 19:06:17.804949 sigr.go:78: Processing task [customized name] +Hello, World! #3 +2017/10/26 19:06:17.805069 sigr.go:85: sig: [interrupt] Processed, quitting directly +signal: interrupt +``` + + +[badge-travis]: https://travis-ci.org/argcv/sigr.svg?branch=master +[link-travis]: https://travis-ci.org/argcv/sigr +[image-travis]: https://github.com/argcv/sigr/blob/master/img/TravisCI.png +[badge-license]: https://img.shields.io/badge/license-MIT-007EC7.svg + + + diff --git a/sigr/doc.go b/sigr/doc.go new file mode 100644 index 0000000..9b60885 --- /dev/null +++ b/sigr/doc.go @@ -0,0 +1 @@ +package sigr diff --git a/sigr/sigr.go b/sigr/sigr.go new file mode 100644 index 0000000..85cff70 --- /dev/null +++ b/sigr/sigr.go @@ -0,0 +1,99 @@ +package sigr + +import ( + "fmt" + "github.com/argcv/stork/log" + "os" + "os/signal" + "sync" + "sync/atomic" + "syscall" +) + +var onStopService = struct { + m sync.Mutex + handers map[string]func() + state int32 +}{ + m: sync.Mutex{}, + handers: map[string]func(){}, + state: 0, +} + +var autoIncId uint64 = 0 +var quitDirectly = true + +func SetQuitDirectly(setting bool) { + quitDirectly = setting +} + +func handlerNameExists(name string) bool { + onStopService.m.Lock() + defer onStopService.m.Unlock() + _, ok := onStopService.handers[name] + return ok +} + +func RegisterOnStopFuncAutoName(f func()) (name string) { + atomic.AddUint64(&autoIncId, 1) + name = fmt.Sprintf("$%d", autoIncId) + for handlerNameExists(name) { + atomic.AddUint64(&autoIncId, 1) + name = fmt.Sprintf("$%d", autoIncId) + } + RegisterOnStopFunc(name, f) + return +} + +func RegisterOnStopFunc(name string, f func()) { + // register a new function on signal int(interrupt) and term(terminate) + onStopService.m.Lock() + defer onStopService.m.Unlock() + if atomic.CompareAndSwapInt32(&onStopService.state, 0, 1) { + log.Debug("OnStopService Initialized..") + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT) + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + wg.Done() + log.Debugf("sig: waiting...") + var sig os.Signal = <-sigs + + log.Debugf("sig: [%v] Processing...", sig) + for k, v := range onStopService.handers { + log.Debugf("Processing task [%v]", k) + v() + } + signal.Stop(sigs) + + if quitDirectly { + log.Debugf("sig: [%v] Processed, quitting directly", sig) + } else { + log.Debugf("sig: [%v] Processed, please try again to terminate the process", sig) + } + + atomic.CompareAndSwapInt32(&onStopService.state, 1, 0) + if quitDirectly { + if p, e := os.FindProcess(syscall.Getpid()); e == nil { + e = p.Signal(sig) + if e != nil { + log.Errorf("sig: pid[%v] send sig %v failed: %v", p.Pid, sig, e) + } + } + } + }() + wg.Wait() + } + onStopService.handers[name] = f +} + +func UnregisterOnStopFunc(name string) { + onStopService.m.Lock() + defer onStopService.m.Unlock() + delete(onStopService.handers, name) +} diff --git a/sigr/sigr_test.go b/sigr/sigr_test.go new file mode 100644 index 0000000..c9823f4 --- /dev/null +++ b/sigr/sigr_test.go @@ -0,0 +1,85 @@ +package sigr + +import ( + "fmt" + "github.com/argcv/stork/assert" + "github.com/argcv/stork/log" + "os" + "os/exec" + "sync" + "syscall" + "testing" + "time" +) + +// hmm... +// how to let them appeared in codecov.io? +func TestRegisterOnStopFunc(t *testing.T) { + log.Verbose() + + if os.Getenv("BE_CRASHER") == "1" { + log.Verbose() + isCalled1 := false + isCalled2 := false + + wg := &sync.WaitGroup{} + + wg.Add(2) + + SetQuitDirectly(false) + RegisterOnStopFunc("f1", func() { + log.Infof("called: f1") + isCalled1 = true + wg.Done() + }) + + //isCalled1 = true + //isCalled2 = true + + RegisterOnStopFuncAutoName(func() { + log.Infof("called: f2") + isCalled2 = true + wg.Done() + }) + + log.Infof("Killing") + if p, e := os.FindProcess(syscall.Getpid()); e == nil { + log.Infof("pid: %v", p.Pid) + _ = p.Signal(syscall.SIGINT) + } + log.Infof("Killed") + wg.Wait() + assert.ExpectTrue(t, isCalled1, fmt.Sprintf("Not called!!! %v", isCalled1)) + assert.ExpectTrue(t, isCalled2, fmt.Sprintf("Not called!!! %v", isCalled2)) + + if isCalled1 && isCalled2 { + os.Exit(0) + } else { + os.Exit(1) + } + } + cmd := exec.Command(os.Args[0], "-test.run=TestRegisterOnStopFunc") + cmd.Env = append(os.Environ(), "BE_CRASHER=1") + + wg := &sync.WaitGroup{} + wg.Add(1) + cstop := make(chan bool, 1) + go func() { + wg.Done() + output, err := cmd.CombinedOutput() + t.Logf("\n----------- OUTPUT ---------------\n%v\n----------- OUTPUT ---------------\n", string(output)) + ee, ok := err.(*exec.ExitError) + t.Logf("err: %v | %v", err, ee) + assert.ExpectFalse(t, ok, ) + assert.ExpectEQ(t, nil, err) + + cstop <- true + }() + wg.Wait() + select { + case stop := <-cstop: + t.Logf("finished: %v", stop) + case <-time.After(3 * time.Second): + t.Errorf("timeout in 3 seconds") + } +} diff --git a/stork.go b/stork.go new file mode 100644 index 0000000..32cca98 --- /dev/null +++ b/stork.go @@ -0,0 +1 @@ +package stork diff --git a/web/cookie.go b/web/cookie.go new file mode 100644 index 0000000..a927283 --- /dev/null +++ b/web/cookie.go @@ -0,0 +1,55 @@ +package web + +import ( + "github.com/gorilla/sessions" + "net/http" + "sync" +) + +var ( + cookieKeyPairs = "update-me-please!" + cookieStore *sessions.CookieStore = nil + cookieStoreOnceFlag *sync.Once = &sync.Once{} +) + +func SetCookieKeyPairs(keyPairs string) { + cookieKeyPairs = keyPairs + cookieStoreOnceFlag = &sync.Once{} +} + +func GetCookieKeyPairs() (keyPairs string) { + keyPairs = cookieKeyPairs + return +} + +func GetCookieStore() *sessions.CookieStore { + cookieStoreOnceFlag.Do(func() { + cookieStore = sessions.NewCookieStore([]byte(cookieKeyPairs)) + }) + return cookieStore +} + +// Get returns a session for the given name after adding it to the registry. +// +// It returns a new session if the sessions doesn't exist. Access IsNew on +// the session to check if it is an existing session or a new one. +// +// It returns a new session and an error if the session exists but could +// not be decoded. +func GetCookie(r *http.Request, name string) (*sessions.Session, error) { + return GetCookieStore().Get(r, name) +} + +// New returns a session for the given name without adding it to the registry. +// +// The difference between New() and Get() is that calling New() twice will +// decode the session data twice, while Get() registers and reuses the same +// decoded session after the first call. +func NewCookie(r *http.Request, name string) (*sessions.Session, error) { + return GetCookieStore().New(r, name) +} + +// Save adds a single session to the response. +func SaveCookie(r *http.Request, w http.ResponseWriter, s *sessions.Session) error { + return GetCookieStore().Save(r, w, s) +} diff --git a/web/request.go b/web/request.go new file mode 100644 index 0000000..46e05ca --- /dev/null +++ b/web/request.go @@ -0,0 +1,23 @@ +package web + +import ( + "net" + "net/http" +) + +// NOT tested in ipv6 environment +func GetUserIp(r *http.Request) string { + agentRemoteHeaderUpyunKey := "Client-IP" + agentRemoteHeaderKey := "X-Real-IP" + if ip := r.Header.Get(agentRemoteHeaderUpyunKey); ip != "" { + return ip + } else if ip := r.Header.Get(agentRemoteHeaderKey); ip != "" { + return ip + } else { + if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { + return ip + } else { + return "0.0.0.0" + } + } +}