1.4.5.2
This commit is contained in:
40
EdgeAPI/internal/utils/cache_map.go
Normal file
40
EdgeAPI/internal/utils/cache_map.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type CacheMap struct {
|
||||
locker sync.Mutex
|
||||
m maps.Map
|
||||
}
|
||||
|
||||
func NewCacheMap() *CacheMap {
|
||||
return &CacheMap{m: maps.Map{}}
|
||||
}
|
||||
|
||||
func (this *CacheMap) Get(key string) (value interface{}, ok bool) {
|
||||
this.locker.Lock()
|
||||
value, ok = this.m[key]
|
||||
this.locker.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (this *CacheMap) Put(key string, value interface{}) {
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
this.locker.Lock()
|
||||
this.m[key] = value
|
||||
this.locker.Unlock()
|
||||
}
|
||||
|
||||
func (this *CacheMap) Len() int {
|
||||
this.locker.Lock()
|
||||
var l = len(this.m)
|
||||
this.locker.Unlock()
|
||||
return l
|
||||
}
|
||||
26
EdgeAPI/internal/utils/cache_map_test.go
Normal file
26
EdgeAPI/internal/utils/cache_map_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewCacheMap(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
|
||||
m := NewCacheMap()
|
||||
{
|
||||
m.Put("Hello", "World")
|
||||
v, ok := m.Get("Hello")
|
||||
a.IsTrue(ok)
|
||||
a.IsTrue(v == "World")
|
||||
}
|
||||
|
||||
{
|
||||
v, ok := m.Get("Hello1")
|
||||
a.IsFalse(ok)
|
||||
a.IsTrue(v == nil)
|
||||
}
|
||||
}
|
||||
15
EdgeAPI/internal/utils/compare.go
Normal file
15
EdgeAPI/internal/utils/compare.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// EqualConfig 使用JSON对比配置
|
||||
func EqualConfig(config1 any, config2 any) bool {
|
||||
config1JSON, _ := json.Marshal(config1)
|
||||
config2JSON, _ := json.Marshal(config2)
|
||||
return bytes.Equal(config1JSON, config2JSON)
|
||||
}
|
||||
38
EdgeAPI/internal/utils/compare_test.go
Normal file
38
EdgeAPI/internal/utils/compare_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEqualConfig(t *testing.T) {
|
||||
type testType struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
{
|
||||
var c1 = &testType{
|
||||
Name: "Lily",
|
||||
Age: 12,
|
||||
}
|
||||
var c2 = &testType{
|
||||
Name: "Lucy",
|
||||
Age: 12,
|
||||
}
|
||||
t.Log(utils.EqualConfig(c1, c2))
|
||||
}
|
||||
|
||||
{
|
||||
var c1 = &testType{
|
||||
Name: "Lily",
|
||||
Age: 12,
|
||||
}
|
||||
var c2 = &testType{
|
||||
Age: 12,
|
||||
Name: "Lily",
|
||||
}
|
||||
t.Log(utils.EqualConfig(c1, c2))
|
||||
}
|
||||
}
|
||||
26
EdgeAPI/internal/utils/domain.go
Normal file
26
EdgeAPI/internal/utils/domain.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var cacheKeyDomainReg1 = regexp.MustCompile(`^(?i)(?:http|https)://([\w-.*]+)`) // 这里支持 *.example.com
|
||||
var cacheKeyDomainReg2 = regexp.MustCompile(`^([\w-.]+)`)
|
||||
|
||||
// ParseDomainFromKey 从Key中获取域名
|
||||
func ParseDomainFromKey(key string) (domain string) {
|
||||
var pieces = cacheKeyDomainReg1.FindStringSubmatch(key)
|
||||
if len(pieces) > 1 {
|
||||
return strings.ToLower(pieces[1])
|
||||
}
|
||||
|
||||
pieces = cacheKeyDomainReg2.FindStringSubmatch(key)
|
||||
if len(pieces) > 1 {
|
||||
return strings.ToLower(pieces[1])
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
31
EdgeAPI/internal/utils/domainutils/utils.go
Normal file
31
EdgeAPI/internal/utils/domainutils/utils.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package domainutils
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ValidateDomainFormat 校验域名格式
|
||||
func ValidateDomainFormat(domain string) bool {
|
||||
pieces := strings.Split(domain, ".")
|
||||
for _, piece := range pieces {
|
||||
if piece == "-" ||
|
||||
strings.HasPrefix(piece, "-") ||
|
||||
strings.HasSuffix(piece, "-") ||
|
||||
//strings.Contains(piece, "--") ||
|
||||
len(piece) > 63 ||
|
||||
// 支持中文、大写字母、下划线
|
||||
!regexp.MustCompile(`^[\p{Han}_a-zA-Z0-9-]+$`).MatchString(piece) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 最后一段不能是全数字
|
||||
if regexp.MustCompile(`^(\d+)$`).MatchString(pieces[len(pieces)-1]) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
12
EdgeAPI/internal/utils/email.go
Normal file
12
EdgeAPI/internal/utils/email.go
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils
|
||||
|
||||
import "regexp"
|
||||
|
||||
var emailReg = regexp.MustCompile(`(?i)^[a-z\d]+([._+-]*[a-z\d]+)*@([a-z\d]+[a-z\d-]*[a-z\d]+\.)+[a-z\d]+$`)
|
||||
|
||||
// ValidateEmail 校验电子邮箱格式
|
||||
func ValidateEmail(email string) bool {
|
||||
return emailReg.MatchString(email)
|
||||
}
|
||||
22
EdgeAPI/internal/utils/email_test.go
Normal file
22
EdgeAPI/internal/utils/email_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateEmail(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
a.IsTrue(utils.ValidateEmail("aaaa@gmail.com"))
|
||||
a.IsTrue(utils.ValidateEmail("a.b@gmail.com"))
|
||||
a.IsTrue(utils.ValidateEmail("a.b.c.d@gmail.com"))
|
||||
a.IsTrue(utils.ValidateEmail("aaaa@gmail.com.cn"))
|
||||
a.IsTrue(utils.ValidateEmail("hello.world.123@gmail.123.com"))
|
||||
a.IsTrue(utils.ValidateEmail("10000@qq.com"))
|
||||
a.IsFalse(utils.ValidateEmail("aaaa.@gmail.com"))
|
||||
a.IsFalse(utils.ValidateEmail("aaaa@gmail"))
|
||||
a.IsFalse(utils.ValidateEmail("aaaa@123"))
|
||||
}
|
||||
11
EdgeAPI/internal/utils/errors.go
Normal file
11
EdgeAPI/internal/utils/errors.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
)
|
||||
|
||||
// 打印错误
|
||||
func PrintError(err error) {
|
||||
// TODO 记录调用的文件名、行数
|
||||
logs.Println("[ERROR]" + err.Error())
|
||||
}
|
||||
162
EdgeAPI/internal/utils/exec/cmd.go
Normal file
162
EdgeAPI/internal/utils/exec/cmd.go
Normal file
@@ -0,0 +1,162 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package executils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Cmd struct {
|
||||
name string
|
||||
args []string
|
||||
env []string
|
||||
dir string
|
||||
|
||||
ctx context.Context
|
||||
timeout time.Duration
|
||||
cancelFunc func()
|
||||
|
||||
captureStdout bool
|
||||
captureStderr bool
|
||||
|
||||
stdout *bytes.Buffer
|
||||
stderr *bytes.Buffer
|
||||
|
||||
rawCmd *exec.Cmd
|
||||
}
|
||||
|
||||
func NewCmd(name string, args ...string) *Cmd {
|
||||
return &Cmd{
|
||||
name: name,
|
||||
args: args,
|
||||
}
|
||||
}
|
||||
|
||||
func NewTimeoutCmd(timeout time.Duration, name string, args ...string) *Cmd {
|
||||
return (&Cmd{
|
||||
name: name,
|
||||
args: args,
|
||||
}).WithTimeout(timeout)
|
||||
}
|
||||
|
||||
func (this *Cmd) WithTimeout(timeout time.Duration) *Cmd {
|
||||
this.timeout = timeout
|
||||
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
|
||||
this.ctx = ctx
|
||||
this.cancelFunc = cancelFunc
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) WithStdout() *Cmd {
|
||||
this.captureStdout = true
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) WithStderr() *Cmd {
|
||||
this.captureStderr = true
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) WithEnv(env []string) *Cmd {
|
||||
this.env = env
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) WithDir(dir string) *Cmd {
|
||||
this.dir = dir
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) Start() error {
|
||||
var cmd = this.compose()
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
func (this *Cmd) Wait() error {
|
||||
var cmd = this.compose()
|
||||
return cmd.Wait()
|
||||
}
|
||||
|
||||
func (this *Cmd) Run() error {
|
||||
if this.cancelFunc != nil {
|
||||
defer this.cancelFunc()
|
||||
}
|
||||
|
||||
var cmd = this.compose()
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (this *Cmd) RawStdout() string {
|
||||
if this.stdout != nil {
|
||||
return this.stdout.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (this *Cmd) Stdout() string {
|
||||
return strings.TrimSpace(this.RawStdout())
|
||||
}
|
||||
|
||||
func (this *Cmd) RawStderr() string {
|
||||
if this.stderr != nil {
|
||||
return this.stderr.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (this *Cmd) Stderr() string {
|
||||
return strings.TrimSpace(this.RawStderr())
|
||||
}
|
||||
|
||||
func (this *Cmd) String() string {
|
||||
if this.rawCmd != nil {
|
||||
return this.rawCmd.String()
|
||||
}
|
||||
var newCmd = exec.Command(this.name, this.args...)
|
||||
return newCmd.String()
|
||||
}
|
||||
|
||||
func (this *Cmd) Process() *os.Process {
|
||||
if this.rawCmd != nil {
|
||||
return this.rawCmd.Process
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *Cmd) compose() *exec.Cmd {
|
||||
if this.rawCmd != nil {
|
||||
return this.rawCmd
|
||||
}
|
||||
|
||||
if this.ctx != nil {
|
||||
this.rawCmd = exec.CommandContext(this.ctx, this.name, this.args...)
|
||||
} else {
|
||||
this.rawCmd = exec.Command(this.name, this.args...)
|
||||
}
|
||||
|
||||
if this.env != nil {
|
||||
this.rawCmd.Env = this.env
|
||||
}
|
||||
|
||||
if len(this.dir) > 0 {
|
||||
this.rawCmd.Dir = this.dir
|
||||
}
|
||||
|
||||
if this.captureStdout {
|
||||
this.stdout = &bytes.Buffer{}
|
||||
this.rawCmd.Stdout = this.stdout
|
||||
}
|
||||
if this.captureStderr {
|
||||
this.stderr = &bytes.Buffer{}
|
||||
this.rawCmd.Stderr = this.stderr
|
||||
}
|
||||
|
||||
return this.rawCmd
|
||||
}
|
||||
61
EdgeAPI/internal/utils/exec/cmd_test.go
Normal file
61
EdgeAPI/internal/utils/exec/cmd_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package executils_test
|
||||
|
||||
import (
|
||||
executils "github.com/TeaOSLab/EdgeAPI/internal/utils/exec"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewTimeoutCmd_Sleep(t *testing.T) {
|
||||
var cmd = executils.NewTimeoutCmd(1*time.Second, "sleep", "3")
|
||||
cmd.WithStdout()
|
||||
cmd.WithStderr()
|
||||
err := cmd.Run()
|
||||
t.Log("error:", err)
|
||||
t.Log("stdout:", cmd.Stdout())
|
||||
t.Log("stderr:", cmd.Stderr())
|
||||
}
|
||||
|
||||
func TestNewTimeoutCmd_Echo(t *testing.T) {
|
||||
var cmd = executils.NewTimeoutCmd(10*time.Second, "echo", "-n", "hello")
|
||||
cmd.WithStdout()
|
||||
cmd.WithStderr()
|
||||
err := cmd.Run()
|
||||
t.Log("error:", err)
|
||||
t.Log("stdout:", cmd.Stdout())
|
||||
t.Log("stderr:", cmd.Stderr())
|
||||
}
|
||||
|
||||
func TestNewTimeoutCmd_Echo2(t *testing.T) {
|
||||
var cmd = executils.NewCmd("echo", "hello")
|
||||
cmd.WithStdout()
|
||||
cmd.WithStderr()
|
||||
err := cmd.Run()
|
||||
t.Log("error:", err)
|
||||
t.Log("stdout:", cmd.Stdout())
|
||||
t.Log("raw stdout:", cmd.RawStdout())
|
||||
t.Log("stderr:", cmd.Stderr())
|
||||
t.Log("raw stderr:", cmd.RawStderr())
|
||||
}
|
||||
|
||||
func TestNewTimeoutCmd_Echo3(t *testing.T) {
|
||||
var cmd = executils.NewCmd("echo", "-n", "hello")
|
||||
err := cmd.Run()
|
||||
t.Log("error:", err)
|
||||
t.Log("stdout:", cmd.Stdout())
|
||||
t.Log("stderr:", cmd.Stderr())
|
||||
}
|
||||
|
||||
func TestCmd_Process(t *testing.T) {
|
||||
var cmd = executils.NewCmd("echo", "-n", "hello")
|
||||
err := cmd.Run()
|
||||
t.Log("error:", err)
|
||||
t.Log(cmd.Process())
|
||||
}
|
||||
|
||||
func TestNewTimeoutCmd_String(t *testing.T) {
|
||||
var cmd = executils.NewCmd("echo", "-n", "hello")
|
||||
t.Log("stdout:", cmd.String())
|
||||
}
|
||||
58
EdgeAPI/internal/utils/exec/look_linux.go
Normal file
58
EdgeAPI/internal/utils/exec/look_linux.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
//go:build linux
|
||||
|
||||
package executils
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// LookPath customize our LookPath() function, to work in broken $PATH environment variable
|
||||
func LookPath(file string) (string, error) {
|
||||
result, err := exec.LookPath(file)
|
||||
if err == nil && len(result) > 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// add common dirs contains executable files these may be excluded in $PATH environment variable
|
||||
var binPaths = []string{
|
||||
"/usr/sbin",
|
||||
"/usr/bin",
|
||||
"/usr/local/sbin",
|
||||
"/usr/local/bin",
|
||||
}
|
||||
|
||||
for _, binPath := range binPaths {
|
||||
var fullPath = binPath + string(os.PathSeparator) + file
|
||||
|
||||
stat, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if stat.IsDir() {
|
||||
return "", syscall.EISDIR
|
||||
}
|
||||
|
||||
var mode = stat.Mode()
|
||||
if mode.IsDir() {
|
||||
return "", syscall.EISDIR
|
||||
}
|
||||
err = syscall.Faccessat(unix.AT_FDCWD, fullPath, unix.X_OK, unix.AT_EACCESS)
|
||||
if err == nil || (err != syscall.ENOSYS && err != syscall.EPERM) {
|
||||
return fullPath, err
|
||||
}
|
||||
if mode&0111 != 0 {
|
||||
return fullPath, nil
|
||||
}
|
||||
return "", fs.ErrPermission
|
||||
}
|
||||
|
||||
return "", &exec.Error{
|
||||
Name: file,
|
||||
Err: exec.ErrNotFound,
|
||||
}
|
||||
}
|
||||
10
EdgeAPI/internal/utils/exec/look_others.go
Normal file
10
EdgeAPI/internal/utils/exec/look_others.go
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
//go:build !linux
|
||||
|
||||
package executils
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func LookPath(file string) (string, error) {
|
||||
return exec.LookPath(file)
|
||||
}
|
||||
60
EdgeAPI/internal/utils/expires/id_key_map.go
Normal file
60
EdgeAPI/internal/utils/expires/id_key_map.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package expires
|
||||
|
||||
type IdKeyMap struct {
|
||||
idKeys map[int64]string // id => key
|
||||
keyIds map[string]int64 // key => id
|
||||
}
|
||||
|
||||
func NewIdKeyMap() *IdKeyMap {
|
||||
return &IdKeyMap{
|
||||
idKeys: map[int64]string{},
|
||||
keyIds: map[string]int64{},
|
||||
}
|
||||
}
|
||||
|
||||
func (this *IdKeyMap) Add(id int64, key string) {
|
||||
oldKey, ok := this.idKeys[id]
|
||||
if ok {
|
||||
delete(this.keyIds, oldKey)
|
||||
}
|
||||
|
||||
oldId, ok := this.keyIds[key]
|
||||
if ok {
|
||||
delete(this.idKeys, oldId)
|
||||
}
|
||||
|
||||
this.idKeys[id] = key
|
||||
this.keyIds[key] = id
|
||||
}
|
||||
|
||||
func (this *IdKeyMap) Key(id int64) (key string, ok bool) {
|
||||
key, ok = this.idKeys[id]
|
||||
return
|
||||
}
|
||||
|
||||
func (this *IdKeyMap) Id(key string) (id int64, ok bool) {
|
||||
id, ok = this.keyIds[key]
|
||||
return
|
||||
}
|
||||
|
||||
func (this *IdKeyMap) DeleteId(id int64) {
|
||||
key, ok := this.idKeys[id]
|
||||
if ok {
|
||||
delete(this.keyIds, key)
|
||||
}
|
||||
delete(this.idKeys, id)
|
||||
}
|
||||
|
||||
func (this *IdKeyMap) DeleteKey(key string) {
|
||||
id, ok := this.keyIds[key]
|
||||
if ok {
|
||||
delete(this.idKeys, id)
|
||||
}
|
||||
delete(this.keyIds, key)
|
||||
}
|
||||
|
||||
func (this *IdKeyMap) Len() int {
|
||||
return len(this.idKeys)
|
||||
}
|
||||
46
EdgeAPI/internal/utils/expires/id_key_map_test.go
Normal file
46
EdgeAPI/internal/utils/expires/id_key_map_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package expires
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewIdKeyMap(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
|
||||
var m = NewIdKeyMap()
|
||||
m.Add(1, "1")
|
||||
m.Add(1, "2")
|
||||
m.Add(100, "100")
|
||||
logs.PrintAsJSON(m.idKeys, t)
|
||||
logs.PrintAsJSON(m.keyIds, t)
|
||||
|
||||
{
|
||||
k, ok := m.Key(1)
|
||||
a.IsTrue(ok)
|
||||
a.IsTrue(k == "2")
|
||||
}
|
||||
|
||||
{
|
||||
_, ok := m.Key(2)
|
||||
a.IsFalse(ok)
|
||||
}
|
||||
|
||||
m.DeleteKey("2")
|
||||
|
||||
{
|
||||
_, ok := m.Key(1)
|
||||
a.IsFalse(ok)
|
||||
}
|
||||
|
||||
logs.PrintAsJSON(m.idKeys, t)
|
||||
logs.PrintAsJSON(m.keyIds, t)
|
||||
|
||||
m.DeleteId(100)
|
||||
|
||||
logs.PrintAsJSON(m.idKeys, t)
|
||||
logs.PrintAsJSON(m.keyIds, t)
|
||||
}
|
||||
161
EdgeAPI/internal/utils/expires/list.go
Normal file
161
EdgeAPI/internal/utils/expires/list.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package expires
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/zero"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ItemMap = map[uint64]zero.Zero
|
||||
|
||||
type List struct {
|
||||
expireMap map[int64]ItemMap // expires timestamp => map[id]ItemMap
|
||||
itemsMap map[uint64]int64 // itemId => timestamp
|
||||
|
||||
locker sync.Mutex
|
||||
|
||||
gcCallback func(itemId uint64)
|
||||
gcBatchCallback func(itemIds ItemMap)
|
||||
|
||||
lastTimestamp int64
|
||||
}
|
||||
|
||||
func NewList() *List {
|
||||
var list = &List{
|
||||
expireMap: map[int64]ItemMap{},
|
||||
itemsMap: map[uint64]int64{},
|
||||
}
|
||||
|
||||
SharedManager.Add(list)
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
func NewSingletonList() *List {
|
||||
var list = &List{
|
||||
expireMap: map[int64]ItemMap{},
|
||||
itemsMap: map[uint64]int64{},
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
// Add 添加条目
|
||||
// 如果条目已经存在,则覆盖
|
||||
func (this *List) Add(itemId uint64, expiresAt int64) {
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
|
||||
if this.lastTimestamp == 0 || this.lastTimestamp > expiresAt {
|
||||
this.lastTimestamp = expiresAt
|
||||
}
|
||||
|
||||
// 是否已经存在
|
||||
oldExpiresAt, ok := this.itemsMap[itemId]
|
||||
if ok {
|
||||
if oldExpiresAt == expiresAt {
|
||||
return
|
||||
}
|
||||
delete(this.expireMap[oldExpiresAt], itemId)
|
||||
if len(this.expireMap[oldExpiresAt]) == 0 {
|
||||
delete(this.expireMap, oldExpiresAt)
|
||||
}
|
||||
}
|
||||
|
||||
expireItemMap, ok := this.expireMap[expiresAt]
|
||||
if ok {
|
||||
expireItemMap[itemId] = zero.New()
|
||||
} else {
|
||||
this.expireMap[expiresAt] = ItemMap{
|
||||
itemId: zero.New(),
|
||||
}
|
||||
}
|
||||
|
||||
this.itemsMap[itemId] = expiresAt
|
||||
}
|
||||
|
||||
func (this *List) Remove(itemId uint64) {
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
this.removeItem(itemId)
|
||||
}
|
||||
|
||||
func (this *List) ExpiresAt(itemId uint64) int64 {
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
return this.itemsMap[itemId]
|
||||
}
|
||||
|
||||
func (this *List) GC(timestamp int64) ItemMap {
|
||||
if this.lastTimestamp > timestamp+1 {
|
||||
return nil
|
||||
}
|
||||
this.locker.Lock()
|
||||
var itemMap = this.gcItems(timestamp)
|
||||
if len(itemMap) == 0 {
|
||||
this.locker.Unlock()
|
||||
return itemMap
|
||||
}
|
||||
this.locker.Unlock()
|
||||
|
||||
if this.gcCallback != nil {
|
||||
for itemId := range itemMap {
|
||||
this.gcCallback(itemId)
|
||||
}
|
||||
}
|
||||
if this.gcBatchCallback != nil {
|
||||
this.gcBatchCallback(itemMap)
|
||||
}
|
||||
|
||||
return itemMap
|
||||
}
|
||||
|
||||
func (this *List) Clean() {
|
||||
this.locker.Lock()
|
||||
this.itemsMap = map[uint64]int64{}
|
||||
this.expireMap = map[int64]ItemMap{}
|
||||
this.locker.Unlock()
|
||||
}
|
||||
|
||||
func (this *List) Count() int {
|
||||
this.locker.Lock()
|
||||
var count = len(this.itemsMap)
|
||||
this.locker.Unlock()
|
||||
return count
|
||||
}
|
||||
|
||||
func (this *List) OnGC(callback func(itemId uint64)) *List {
|
||||
this.gcCallback = callback
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *List) OnGCBatch(callback func(itemMap ItemMap)) *List {
|
||||
this.gcBatchCallback = callback
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *List) removeItem(itemId uint64) {
|
||||
expiresAt, ok := this.itemsMap[itemId]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
delete(this.itemsMap, itemId)
|
||||
|
||||
expireItemMap, ok := this.expireMap[expiresAt]
|
||||
if ok {
|
||||
delete(expireItemMap, itemId)
|
||||
if len(expireItemMap) == 0 {
|
||||
delete(this.expireMap, expiresAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (this *List) gcItems(timestamp int64) ItemMap {
|
||||
expireItemsMap, ok := this.expireMap[timestamp]
|
||||
if ok {
|
||||
for itemId := range expireItemsMap {
|
||||
delete(this.itemsMap, itemId)
|
||||
}
|
||||
delete(this.expireMap, timestamp)
|
||||
}
|
||||
return expireItemsMap
|
||||
}
|
||||
216
EdgeAPI/internal/utils/expires/list_test.go
Normal file
216
EdgeAPI/internal/utils/expires/list_test.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package expires
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||
"math"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestList_Add(t *testing.T) {
|
||||
list := NewList()
|
||||
list.Add(1, time.Now().Unix())
|
||||
t.Log("===BEFORE===")
|
||||
logs.PrintAsJSON(list.expireMap, t)
|
||||
logs.PrintAsJSON(list.itemsMap, t)
|
||||
|
||||
list.Add(1, time.Now().Unix()+1)
|
||||
list.Add(2, time.Now().Unix()+1)
|
||||
list.Add(3, time.Now().Unix()+2)
|
||||
t.Log("===AFTER===")
|
||||
logs.PrintAsJSON(list.expireMap, t)
|
||||
logs.PrintAsJSON(list.itemsMap, t)
|
||||
}
|
||||
|
||||
func TestList_Add_Overwrite(t *testing.T) {
|
||||
var timestamp = time.Now().Unix()
|
||||
|
||||
list := NewList()
|
||||
list.Add(1, timestamp+1)
|
||||
list.Add(1, timestamp+1)
|
||||
list.Add(1, timestamp+2)
|
||||
logs.PrintAsJSON(list.expireMap, t)
|
||||
logs.PrintAsJSON(list.itemsMap, t)
|
||||
|
||||
var a = assert.NewAssertion(t)
|
||||
a.IsTrue(len(list.itemsMap) == 1)
|
||||
a.IsTrue(len(list.expireMap) == 1)
|
||||
a.IsTrue(list.itemsMap[1] == timestamp+2)
|
||||
}
|
||||
|
||||
func TestList_Remove(t *testing.T) {
|
||||
list := NewList()
|
||||
list.Add(1, time.Now().Unix()+1)
|
||||
list.Remove(1)
|
||||
logs.PrintAsJSON(list.expireMap, t)
|
||||
logs.PrintAsJSON(list.itemsMap, t)
|
||||
}
|
||||
|
||||
func TestList_GC(t *testing.T) {
|
||||
var unixTime = time.Now().Unix()
|
||||
t.Log("unixTime:", unixTime)
|
||||
|
||||
var list = NewList()
|
||||
list.Add(1, unixTime+1)
|
||||
list.Add(2, unixTime+1)
|
||||
list.Add(3, unixTime+2)
|
||||
list.OnGC(func(itemId uint64) {
|
||||
t.Log("gc:", itemId)
|
||||
})
|
||||
t.Log("last unixTime:", list.lastTimestamp)
|
||||
list.GC(time.Now().Unix() + 2)
|
||||
logs.PrintAsJSON(list.expireMap, t)
|
||||
logs.PrintAsJSON(list.itemsMap, t)
|
||||
|
||||
t.Log(list.Count())
|
||||
}
|
||||
|
||||
func TestList_GC_Batch(t *testing.T) {
|
||||
list := NewList()
|
||||
list.Add(1, time.Now().Unix()+1)
|
||||
list.Add(2, time.Now().Unix()+1)
|
||||
list.Add(3, time.Now().Unix()+2)
|
||||
list.Add(4, time.Now().Unix()+2)
|
||||
list.OnGCBatch(func(itemMap ItemMap) {
|
||||
t.Log("gc:", itemMap)
|
||||
})
|
||||
list.GC(time.Now().Unix() + 2)
|
||||
logs.PrintAsJSON(list.expireMap, t)
|
||||
logs.PrintAsJSON(list.itemsMap, t)
|
||||
}
|
||||
|
||||
func TestList_Start_GC(t *testing.T) {
|
||||
list := NewList()
|
||||
list.Add(1, time.Now().Unix()+1)
|
||||
list.Add(2, time.Now().Unix()+1)
|
||||
list.Add(3, time.Now().Unix()+2)
|
||||
list.Add(4, time.Now().Unix()+5)
|
||||
list.Add(5, time.Now().Unix()+5)
|
||||
list.Add(6, time.Now().Unix()+6)
|
||||
list.Add(7, time.Now().Unix()+6)
|
||||
list.Add(8, time.Now().Unix()+6)
|
||||
|
||||
list.OnGC(func(itemId uint64) {
|
||||
t.Log("gc:", itemId, timeutil.Format("H:i:s"))
|
||||
time.Sleep(2 * time.Second)
|
||||
})
|
||||
|
||||
go func() {
|
||||
SharedManager.Add(list)
|
||||
}()
|
||||
|
||||
time.Sleep(20 * time.Second)
|
||||
}
|
||||
|
||||
func TestList_ManyItems(t *testing.T) {
|
||||
list := NewList()
|
||||
for i := 0; i < 1_000; i++ {
|
||||
list.Add(uint64(i), time.Now().Unix())
|
||||
}
|
||||
for i := 0; i < 1_000; i++ {
|
||||
list.Add(uint64(i), time.Now().Unix()+1)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
count := 0
|
||||
list.OnGC(func(itemId uint64) {
|
||||
count++
|
||||
})
|
||||
list.GC(time.Now().Unix() + 1)
|
||||
t.Log("gc", count, "items")
|
||||
t.Log(time.Since(now))
|
||||
}
|
||||
|
||||
func TestList_Map_Performance(t *testing.T) {
|
||||
t.Log("max uint32", math.MaxUint32)
|
||||
|
||||
var timestamp = time.Now().Unix()
|
||||
|
||||
{
|
||||
m := map[int64]int64{}
|
||||
for i := 0; i < 1_000_000; i++ {
|
||||
m[int64(i)] = timestamp
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for i := 0; i < 100_000; i++ {
|
||||
delete(m, int64(i))
|
||||
}
|
||||
t.Log(time.Since(now))
|
||||
}
|
||||
|
||||
{
|
||||
m := map[uint64]int64{}
|
||||
for i := 0; i < 1_000_000; i++ {
|
||||
m[uint64(i)] = timestamp
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for i := 0; i < 100_000; i++ {
|
||||
delete(m, uint64(i))
|
||||
}
|
||||
t.Log(time.Since(now))
|
||||
}
|
||||
|
||||
{
|
||||
m := map[uint32]int64{}
|
||||
for i := 0; i < 1_000_000; i++ {
|
||||
m[uint32(i)] = timestamp
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for i := 0; i < 100_000; i++ {
|
||||
delete(m, uint32(i))
|
||||
}
|
||||
t.Log(time.Since(now))
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_Map_Uint64(b *testing.B) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
var timestamp = uint64(time.Now().Unix())
|
||||
|
||||
var i uint64
|
||||
var count uint64 = 1_000_000
|
||||
|
||||
m := map[uint64]uint64{}
|
||||
for i = 0; i < count; i++ {
|
||||
m[i] = timestamp
|
||||
}
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
for i = 0; i < count; i++ {
|
||||
_ = m[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkList_GC(b *testing.B) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
|
||||
var lists = []*List{}
|
||||
|
||||
for m := 0; m < 1_000; m++ {
|
||||
var list = NewList()
|
||||
for j := 0; j < 10_000; j++ {
|
||||
list.Add(uint64(j), utils.UnixTime()+100)
|
||||
}
|
||||
lists = append(lists, list)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
var timestamp = time.Now().Unix()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
for _, list := range lists {
|
||||
list.GC(timestamp)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
73
EdgeAPI/internal/utils/expires/manager.go
Normal file
73
EdgeAPI/internal/utils/expires/manager.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package expires
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/goman"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/zero"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var SharedManager = NewManager()
|
||||
|
||||
type Manager struct {
|
||||
listMap map[*List]zero.Zero
|
||||
locker sync.Mutex
|
||||
ticker *time.Ticker
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
var manager = &Manager{
|
||||
listMap: map[*List]zero.Zero{},
|
||||
ticker: time.NewTicker(1 * time.Second),
|
||||
}
|
||||
goman.New(func() {
|
||||
manager.init()
|
||||
})
|
||||
return manager
|
||||
}
|
||||
|
||||
func (this *Manager) init() {
|
||||
var lastTimestamp = int64(0)
|
||||
for range this.ticker.C {
|
||||
var currentTime = time.Now().Unix()
|
||||
if lastTimestamp == 0 {
|
||||
lastTimestamp = currentTime - 3600
|
||||
}
|
||||
|
||||
if currentTime >= lastTimestamp {
|
||||
for i := lastTimestamp; i <= currentTime; i++ {
|
||||
this.locker.Lock()
|
||||
for list := range this.listMap {
|
||||
list.GC(i)
|
||||
}
|
||||
this.locker.Unlock()
|
||||
}
|
||||
} else {
|
||||
// 如果过去的时间比现在大,则从这一秒重新开始
|
||||
for i := currentTime; i <= currentTime; i++ {
|
||||
this.locker.Lock()
|
||||
for list := range this.listMap {
|
||||
list.GC(i)
|
||||
}
|
||||
this.locker.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// 这样做是为了防止系统时钟突变
|
||||
lastTimestamp = currentTime
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Manager) Add(list *List) {
|
||||
this.locker.Lock()
|
||||
this.listMap[list] = zero.New()
|
||||
this.locker.Unlock()
|
||||
}
|
||||
|
||||
func (this *Manager) Remove(list *List) {
|
||||
this.locker.Lock()
|
||||
delete(this.listMap, list)
|
||||
this.locker.Unlock()
|
||||
}
|
||||
29
EdgeAPI/internal/utils/firewall.go
Normal file
29
EdgeAPI/internal/utils/firewall.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/remotelogs"
|
||||
executils "github.com/TeaOSLab/EdgeAPI/internal/utils/exec"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func AddPortsToFirewall(ports []int) {
|
||||
for _, port := range ports {
|
||||
// Linux
|
||||
if runtime.GOOS == "linux" {
|
||||
// firewalld
|
||||
firewallCmd, _ := executils.LookPath("firewall-cmd")
|
||||
if len(firewallCmd) > 0 {
|
||||
err := exec.Command(firewallCmd, "--add-port="+types.String(port)+"/tcp").Run()
|
||||
if err == nil {
|
||||
remotelogs.Println("API_NODE", "add port '"+types.String(port)+"' to firewalld")
|
||||
|
||||
_ = exec.Command(firewallCmd, "--add-port="+types.String(port)+"/tcp", "--permanent").Run()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
EdgeAPI/internal/utils/http.go
Normal file
56
EdgeAPI/internal/utils/http.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HTTP请求客户端管理
|
||||
var timeoutClientMap = map[time.Duration]*http.Client{} // timeout => Client
|
||||
var timeoutClientLocker = sync.Mutex{}
|
||||
|
||||
// DumpResponse 导出响应
|
||||
func DumpResponse(resp *http.Response) (header []byte, body []byte, err error) {
|
||||
header, err = httputil.DumpResponse(resp, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
return
|
||||
}
|
||||
|
||||
// NewHTTPClient 获取一个新的Client
|
||||
func NewHTTPClient(timeout time.Duration) *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 4096,
|
||||
MaxIdleConnsPerHost: 32,
|
||||
MaxConnsPerHost: 32,
|
||||
IdleConnTimeout: 2 * time.Minute,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
TLSHandshakeTimeout: 0,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SharedHttpClient 获取一个公用的Client
|
||||
func SharedHttpClient(timeout time.Duration) *http.Client {
|
||||
timeoutClientLocker.Lock()
|
||||
defer timeoutClientLocker.Unlock()
|
||||
|
||||
client, ok := timeoutClientMap[timeout]
|
||||
if ok {
|
||||
return client
|
||||
}
|
||||
client = NewHTTPClient(timeout)
|
||||
timeoutClientMap[timeout] = client
|
||||
return client
|
||||
}
|
||||
74
EdgeAPI/internal/utils/json.go
Normal file
74
EdgeAPI/internal/utils/json.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// JSONClone 使用JSON协议克隆对象
|
||||
func JSONClone[T any](ptr T) (newPtr T, err error) {
|
||||
var ptrType = reflect.TypeOf(ptr)
|
||||
var kind = ptrType.Kind()
|
||||
if kind != reflect.Ptr && kind != reflect.Slice {
|
||||
err = errors.New("JSONClone: input must be a ptr or slice")
|
||||
return
|
||||
}
|
||||
var jsonData []byte
|
||||
jsonData, err = json.Marshal(ptr)
|
||||
if err != nil {
|
||||
return ptr, errors.New("JSONClone: marshal failed: " + err.Error())
|
||||
}
|
||||
|
||||
var newValue any
|
||||
switch kind {
|
||||
case reflect.Ptr:
|
||||
newValue = reflect.New(ptrType.Elem()).Interface()
|
||||
case reflect.Slice:
|
||||
newValue = reflect.New(reflect.SliceOf(ptrType.Elem())).Interface()
|
||||
default:
|
||||
return ptr, errors.New("JSONClone: unknown data type")
|
||||
}
|
||||
err = json.Unmarshal(jsonData, newValue)
|
||||
if err != nil {
|
||||
err = errors.New("JSONClone: unmarshal failed: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if kind == reflect.Slice {
|
||||
newValue = reflect.Indirect(reflect.ValueOf(newValue)).Interface()
|
||||
}
|
||||
|
||||
return newValue.(T), nil
|
||||
}
|
||||
|
||||
// JSONDecodeConfig 解码并重新编码
|
||||
// 是为了去除原有JSON中不需要的数据
|
||||
func JSONDecodeConfig(data []byte, ptr any) (encodeJSON []byte, err error) {
|
||||
err = json.Unmarshal(data, ptr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON, err = json.Marshal(ptr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// validate config
|
||||
if ptr != nil {
|
||||
config, ok := ptr.(interface {
|
||||
Init() error
|
||||
})
|
||||
if ok {
|
||||
initErr := config.Init()
|
||||
if initErr != nil {
|
||||
err = errors.New("validate config failed: " + initErr.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
87
EdgeAPI/internal/utils/json_test.go
Normal file
87
EdgeAPI/internal/utils/json_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJSONClone(t *testing.T) {
|
||||
type user struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
var u = &user{
|
||||
Name: "Jack",
|
||||
Age: 20,
|
||||
}
|
||||
|
||||
newU, err := utils.JSONClone[*user](u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("%#v", newU)
|
||||
}
|
||||
|
||||
func TestJSONClone_Slice(t *testing.T) {
|
||||
type user struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
var u = []*user{
|
||||
{
|
||||
Name: "Jack",
|
||||
Age: 20,
|
||||
},
|
||||
{
|
||||
Name: "Lily",
|
||||
Age: 18,
|
||||
},
|
||||
}
|
||||
|
||||
newU, err := utils.JSONClone[[]*user](u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
logs.PrintAsJSON(newU, t)
|
||||
}
|
||||
|
||||
type jsonUserType struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
|
||||
func (this *jsonUserType) Init() error {
|
||||
if len(this.Name) < 10 {
|
||||
return errors.New("'name' too short")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestJSONDecodeConfig(t *testing.T) {
|
||||
var data = []byte(`{ "name":"Lily", "age":20, "description": "Nice" }`)
|
||||
|
||||
var u = &jsonUserType{}
|
||||
newJSON, err := utils.JSONDecodeConfig(data, u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("%+v, %s", u, string(newJSON))
|
||||
}
|
||||
|
||||
func TestJSONDecodeConfig_Validate(t *testing.T) {
|
||||
var data = []byte(`{ "name":"Lily", "age":20, "description": "Nice" }`)
|
||||
|
||||
var u = &jsonUserType{}
|
||||
|
||||
newJSON, err := utils.JSONDecodeConfig(data, u)
|
||||
if err != nil {
|
||||
t.Log("ignore error:", err) // error expected
|
||||
}
|
||||
t.Logf("%+v, %s", u, string(newJSON))
|
||||
}
|
||||
198
EdgeAPI/internal/utils/lookup.go
Normal file
198
EdgeAPI/internal/utils/lookup.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils/taskutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/dnsconfigs"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/iwind/TeaGo/lists"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"github.com/miekg/dns"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var sharedDNSClient *dns.Client
|
||||
var sharedDNSConfig *dns.ClientConfig
|
||||
var sharedDNSLocker = &sync.RWMutex{}
|
||||
|
||||
func init() {
|
||||
if !teaconst.IsMain {
|
||||
return
|
||||
}
|
||||
|
||||
var resolvConfFile = "/etc/resolv.conf"
|
||||
config, err := dns.ClientConfigFromFile(resolvConfFile)
|
||||
if err != nil {
|
||||
logs.Println("ERROR: configure dns client failed: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
sharedDNSConfig = config
|
||||
sharedDNSClient = &dns.Client{}
|
||||
|
||||
// 监视文件变化,以便及时更新配置
|
||||
go func() {
|
||||
watcher, watcherErr := fsnotify.NewWatcher()
|
||||
if watcherErr == nil {
|
||||
err = watcher.Add(resolvConfFile)
|
||||
for range watcher.Events {
|
||||
newConfig, err := dns.ClientConfigFromFile(resolvConfFile)
|
||||
if err == nil && newConfig != nil {
|
||||
sharedDNSLocker.Lock()
|
||||
sharedDNSConfig = newConfig
|
||||
sharedDNSLocker.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// LookupCNAME 查询CNAME记录
|
||||
// TODO 可以设置使用的DNS主机地址
|
||||
func LookupCNAME(host string) (string, error) {
|
||||
if sharedDNSClient == nil {
|
||||
return "", errors.New("could not find dns client")
|
||||
}
|
||||
|
||||
var m = new(dns.Msg)
|
||||
|
||||
m.SetQuestion(host+".", dns.TypeCNAME)
|
||||
m.RecursionDesired = true
|
||||
|
||||
var lastErr error
|
||||
var serverAddrs = composeDNSResolverAddrs(nil)
|
||||
|
||||
for _, serverAddr := range serverAddrs {
|
||||
r, _, err := sharedDNSClient.Exchange(m, serverAddr)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if len(r.Answer) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
return r.Answer[0].(*dns.CNAME).Target, nil
|
||||
}
|
||||
return "", lastErr
|
||||
}
|
||||
|
||||
// LookupNS 查询NS记录
|
||||
func LookupNS(host string, extraResolvers []*dnsconfigs.DNSResolver) ([]string, error) {
|
||||
var m = new(dns.Msg)
|
||||
|
||||
m.SetQuestion(host+".", dns.TypeNS)
|
||||
m.RecursionDesired = true
|
||||
|
||||
var result = []string{}
|
||||
|
||||
var lastErr error
|
||||
var hasValidServer = false
|
||||
var serverAddrs = composeDNSResolverAddrs(extraResolvers)
|
||||
if len(serverAddrs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
taskErr := taskutils.RunConcurrent(serverAddrs, taskutils.DefaultConcurrent, func(task any, locker *sync.RWMutex) {
|
||||
var serverAddr = task.(string)
|
||||
r, _, err := sharedDNSClient.Exchange(m, serverAddr)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
return
|
||||
}
|
||||
|
||||
hasValidServer = true
|
||||
|
||||
if len(r.Answer) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, answer := range r.Answer {
|
||||
var value = answer.(*dns.NS).Ns
|
||||
locker.Lock()
|
||||
if len(value) > 0 && !lists.ContainsString(result, value) {
|
||||
result = append(result, value)
|
||||
}
|
||||
locker.Unlock()
|
||||
}
|
||||
})
|
||||
if taskErr != nil {
|
||||
return result, taskErr
|
||||
}
|
||||
|
||||
if hasValidServer {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
// LookupTXT 获取CNAME
|
||||
func LookupTXT(host string, extraResolvers []*dnsconfigs.DNSResolver) ([]string, error) {
|
||||
var m = new(dns.Msg)
|
||||
|
||||
m.SetQuestion(host+".", dns.TypeTXT)
|
||||
m.RecursionDesired = true
|
||||
|
||||
var lastErr error
|
||||
var result = []string{}
|
||||
var hasValidServer = false
|
||||
var serverAddrs = composeDNSResolverAddrs(extraResolvers)
|
||||
if len(serverAddrs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
taskErr := taskutils.RunConcurrent(serverAddrs, taskutils.DefaultConcurrent, func(task any, locker *sync.RWMutex) {
|
||||
var serverAddr = task.(string)
|
||||
r, _, err := sharedDNSClient.Exchange(m, serverAddr)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
return
|
||||
}
|
||||
hasValidServer = true
|
||||
|
||||
if len(r.Answer) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, answer := range r.Answer {
|
||||
for _, txt := range answer.(*dns.TXT).Txt {
|
||||
locker.Lock()
|
||||
if len(txt) > 0 && !lists.ContainsString(result, txt) {
|
||||
result = append(result, txt)
|
||||
}
|
||||
locker.Unlock()
|
||||
}
|
||||
}
|
||||
})
|
||||
if taskErr != nil {
|
||||
return result, taskErr
|
||||
}
|
||||
|
||||
if hasValidServer {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
// 组合DNS解析服务器地址
|
||||
func composeDNSResolverAddrs(extraResolvers []*dnsconfigs.DNSResolver) []string {
|
||||
sharedDNSLocker.RLock()
|
||||
defer sharedDNSLocker.RUnlock()
|
||||
|
||||
// 这里不处理重复,方便我们可以多次重试
|
||||
var servers = sharedDNSConfig.Servers
|
||||
var port = sharedDNSConfig.Port
|
||||
|
||||
var serverAddrs = []string{}
|
||||
for _, serverAddr := range servers {
|
||||
serverAddrs = append(serverAddrs, configutils.QuoteIP(serverAddr)+":"+port)
|
||||
}
|
||||
for _, resolver := range extraResolvers {
|
||||
serverAddrs = append(serverAddrs, resolver.Addr())
|
||||
}
|
||||
return serverAddrs
|
||||
}
|
||||
37
EdgeAPI/internal/utils/lookup_test.go
Normal file
37
EdgeAPI/internal/utils/lookup_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/dnsconfigs"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLookupCNAME(t *testing.T) {
|
||||
t.Log(utils.LookupCNAME("www.yun4s.cn"))
|
||||
}
|
||||
|
||||
func TestLookupNS(t *testing.T) {
|
||||
t.Log(utils.LookupNS("goedge.cn", nil))
|
||||
}
|
||||
|
||||
func TestLookupNSExtra(t *testing.T) {
|
||||
t.Log(utils.LookupNS("goedge.cn", []*dnsconfigs.DNSResolver{
|
||||
{
|
||||
Host: "192.168.2.2",
|
||||
},
|
||||
{
|
||||
Host: "192.168.2.2",
|
||||
Port: 58,
|
||||
},
|
||||
{
|
||||
Host: "8.8.8.8",
|
||||
Port: 53,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
func TestLookupTXT(t *testing.T) {
|
||||
t.Log(utils.LookupTXT("yanzheng.goedge.cn", nil))
|
||||
}
|
||||
77
EdgeAPI/internal/utils/maps/fixed_map.go
Normal file
77
EdgeAPI/internal/utils/maps/fixed_map.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package maputils
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type FixedMap struct {
|
||||
m map[string]any
|
||||
keys []string
|
||||
|
||||
maxSize int
|
||||
|
||||
locker sync.RWMutex
|
||||
}
|
||||
|
||||
func NewFixedMap(maxSize int) *FixedMap {
|
||||
return &FixedMap{
|
||||
m: map[string]any{},
|
||||
maxSize: maxSize,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *FixedMap) Set(key string, item any) {
|
||||
if this.maxSize <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
|
||||
_, ok := this.m[key]
|
||||
if ok {
|
||||
this.m[key] = item
|
||||
|
||||
// TODO 将key转到keys末尾
|
||||
} else {
|
||||
// 是否已满
|
||||
if len(this.keys) >= this.maxSize {
|
||||
var firstKey = this.keys[0]
|
||||
delete(this.m, firstKey)
|
||||
this.keys = this.keys[1:]
|
||||
}
|
||||
|
||||
// 新加入
|
||||
this.m[key] = item
|
||||
this.keys = append(this.keys, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (this *FixedMap) Get(key string) (value any, ok bool) {
|
||||
this.locker.RLock()
|
||||
value, ok = this.m[key]
|
||||
this.locker.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (this *FixedMap) Has(key string) bool {
|
||||
this.locker.RLock()
|
||||
_, ok := this.m[key]
|
||||
this.locker.RUnlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
func (this *FixedMap) Size() int {
|
||||
this.locker.RLock()
|
||||
defer this.locker.RUnlock()
|
||||
return len(this.keys)
|
||||
}
|
||||
|
||||
func (this *FixedMap) Reset() {
|
||||
this.locker.Lock()
|
||||
this.m = map[string]any{}
|
||||
this.keys = []string{}
|
||||
this.locker.Unlock()
|
||||
}
|
||||
42
EdgeAPI/internal/utils/maps/fixed_map_test.go
Normal file
42
EdgeAPI/internal/utils/maps/fixed_map_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package maputils_test
|
||||
|
||||
import (
|
||||
maputils "github.com/TeaOSLab/EdgeAPI/internal/utils/maps"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewFixedMap(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
|
||||
{
|
||||
var m = maputils.NewFixedMap(5)
|
||||
m.Set("a", 1)
|
||||
m.Set("b", 2)
|
||||
a.IsTrue(m.Has("a"))
|
||||
a.IsTrue(m.Has("b"))
|
||||
a.IsFalse(m.Has("c"))
|
||||
}
|
||||
|
||||
{
|
||||
var m = maputils.NewFixedMap(5)
|
||||
m.Set("a", 1)
|
||||
m.Set("b", 2)
|
||||
m.Set("c", 3)
|
||||
m.Set("d", 4)
|
||||
m.Set("e", 5)
|
||||
a.IsTrue(m.Size() == 5)
|
||||
m.Set("f", 6)
|
||||
a.IsTrue(m.Size() == 5)
|
||||
a.IsFalse(m.Has("a"))
|
||||
}
|
||||
|
||||
{
|
||||
var m = maputils.NewFixedMap(5)
|
||||
m.Set("a", 1)
|
||||
t.Log(m.Get("a"))
|
||||
t.Log(m.Get("b"))
|
||||
}
|
||||
}
|
||||
12
EdgeAPI/internal/utils/mobile.go
Normal file
12
EdgeAPI/internal/utils/mobile.go
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils
|
||||
|
||||
import "regexp"
|
||||
|
||||
var mobileRegex = regexp.MustCompile(`^1\d{10}$`)
|
||||
|
||||
// IsValidMobile validate mobile number
|
||||
func IsValidMobile(mobile string) bool {
|
||||
return mobileRegex.MatchString(mobile)
|
||||
}
|
||||
17
EdgeAPI/internal/utils/mobile_test.go
Normal file
17
EdgeAPI/internal/utils/mobile_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsValidMobile(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
a.IsFalse(utils.IsValidMobile("138"))
|
||||
a.IsFalse(utils.IsValidMobile("1382222"))
|
||||
a.IsFalse(utils.IsValidMobile("1381234567890"))
|
||||
a.IsTrue(utils.IsValidMobile("13812345678"))
|
||||
}
|
||||
60
EdgeAPI/internal/utils/numberutils/utils.go
Normal file
60
EdgeAPI/internal/utils/numberutils/utils.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package numberutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func FormatInt64(value int64) string {
|
||||
return strconv.FormatInt(value, 10)
|
||||
}
|
||||
|
||||
func FormatInt(value int) string {
|
||||
return strconv.Itoa(value)
|
||||
}
|
||||
|
||||
func Max[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64](values ...T) T {
|
||||
if len(values) == 0 {
|
||||
return 0
|
||||
}
|
||||
var max T
|
||||
for index, value := range values {
|
||||
if index == 0 {
|
||||
max = value
|
||||
} else if value > max {
|
||||
max = value
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
func Min[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64](values ...T) T {
|
||||
if len(values) == 0 {
|
||||
return 0
|
||||
}
|
||||
var min T
|
||||
for index, value := range values {
|
||||
if index == 0 {
|
||||
min = value
|
||||
} else if value < min {
|
||||
min = value
|
||||
}
|
||||
}
|
||||
return min
|
||||
}
|
||||
|
||||
func FloorFloat64(f float64, decimal int) float64 {
|
||||
if decimal <= 0 {
|
||||
return f
|
||||
}
|
||||
|
||||
var s = fmt.Sprintf("%f", f)
|
||||
var index = strings.Index(s, ".")
|
||||
if index < 0 || len(s[index:]) <= decimal+1 {
|
||||
return f
|
||||
}
|
||||
|
||||
return types.Float64(s[:index+decimal+1])
|
||||
}
|
||||
43
EdgeAPI/internal/utils/numberutils/utils_test.go
Normal file
43
EdgeAPI/internal/utils/numberutils/utils_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package numberutils_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils/numberutils"
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMax(t *testing.T) {
|
||||
t.Log(numberutils.Max[int](1, 2, 3))
|
||||
t.Log(numberutils.Max[int32](1, 2, 3))
|
||||
t.Log(numberutils.Max[float32](1.2, 2.3, 3.4))
|
||||
}
|
||||
|
||||
func TestMin(t *testing.T) {
|
||||
t.Log(numberutils.Min[int](1, 2, 3))
|
||||
t.Log(numberutils.Min[int32](1, 2, 3))
|
||||
t.Log(numberutils.Min[float32](1.2, 2.3, 3.4))
|
||||
}
|
||||
|
||||
func TestMaxFloat32(t *testing.T) {
|
||||
t.Logf("%f", math.MaxFloat32/(1<<100))
|
||||
}
|
||||
|
||||
func TestFloorFloat64(t *testing.T) {
|
||||
t.Logf("%f", numberutils.FloorFloat64(123.456, -1))
|
||||
t.Logf("%f", numberutils.FloorFloat64(123.456, 0))
|
||||
t.Logf("%f", numberutils.FloorFloat64(123, 2))
|
||||
t.Logf("%f, %f", numberutils.FloorFloat64(123.456, 1), 123.456*10)
|
||||
t.Logf("%f, %f", numberutils.FloorFloat64(123.456, 2), 123.456*10*10)
|
||||
t.Logf("%f, %f", numberutils.FloorFloat64(123.456, 3), 123.456*10*10*10)
|
||||
t.Logf("%f, %f", numberutils.FloorFloat64(123.456, 4), 123.456*10*10*10*10)
|
||||
t.Logf("%f, %f", numberutils.FloorFloat64(123.456789, 4), 123.456789*10*10*10*10)
|
||||
t.Logf("%f", numberutils.FloorFloat64(-123.45678, 2))
|
||||
}
|
||||
|
||||
func TestFloorFloat64_Special(t *testing.T) {
|
||||
for _, f := range []float64{17.88, 1.11, 1.23456} {
|
||||
t.Logf("%f", numberutils.FloorFloat64(f, 2))
|
||||
}
|
||||
}
|
||||
9
EdgeAPI/internal/utils/progress.go
Normal file
9
EdgeAPI/internal/utils/progress.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils
|
||||
|
||||
// Progress 进度表示
|
||||
type Progress struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
15
EdgeAPI/internal/utils/regexputils/expr.go
Normal file
15
EdgeAPI/internal/utils/regexputils/expr.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package regexputils
|
||||
|
||||
import "regexp"
|
||||
|
||||
var (
|
||||
YYYYMMDDHH = regexp.MustCompile(`^\d{10}$`)
|
||||
YYYYMMDD = regexp.MustCompile(`^\d{8}$`)
|
||||
YYYYMM = regexp.MustCompile(`^\d{6}$`)
|
||||
)
|
||||
|
||||
var (
|
||||
HTTPProtocol = regexp.MustCompile("^(?i)(http|https)://")
|
||||
)
|
||||
19
EdgeAPI/internal/utils/regexputils/expr_test.go
Normal file
19
EdgeAPI/internal/utils/regexputils/expr_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package regexputils_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils/regexputils"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExpr(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
|
||||
a.IsTrue(regexputils.YYYYMMDD.MatchString("20221011"))
|
||||
a.IsFalse(regexputils.YYYYMMDD.MatchString("202210"))
|
||||
|
||||
a.IsTrue(regexputils.YYYYMM.MatchString("202210"))
|
||||
a.IsFalse(regexputils.YYYYMM.MatchString("20221011"))
|
||||
}
|
||||
30
EdgeAPI/internal/utils/rlimit_darwin.go
Normal file
30
EdgeAPI/internal/utils/rlimit_darwin.go
Normal file
@@ -0,0 +1,30 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// set resource limit
|
||||
func SetRLimit(limit uint64) error {
|
||||
rLimit := &syscall.Rlimit{}
|
||||
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, rLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rLimit.Cur < limit {
|
||||
rLimit.Cur = limit
|
||||
}
|
||||
if rLimit.Max < limit {
|
||||
rLimit.Max = limit
|
||||
}
|
||||
return syscall.Setrlimit(syscall.RLIMIT_NOFILE, rLimit)
|
||||
}
|
||||
|
||||
// set best resource limit value
|
||||
func SetSuitableRLimit() {
|
||||
_ = SetRLimit(4096 * 100) // 1M=100Files
|
||||
}
|
||||
30
EdgeAPI/internal/utils/rlimit_linux.go
Normal file
30
EdgeAPI/internal/utils/rlimit_linux.go
Normal file
@@ -0,0 +1,30 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// set resource limit
|
||||
func SetRLimit(limit uint64) error {
|
||||
rLimit := &syscall.Rlimit{}
|
||||
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, rLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rLimit.Cur < limit {
|
||||
rLimit.Cur = limit
|
||||
}
|
||||
if rLimit.Max < limit {
|
||||
rLimit.Max = limit
|
||||
}
|
||||
return syscall.Setrlimit(syscall.RLIMIT_NOFILE, rLimit)
|
||||
}
|
||||
|
||||
// set best resource limit value
|
||||
func SetSuitableRLimit() {
|
||||
SetRLimit(4096 * 100) // 1M=100Files
|
||||
}
|
||||
14
EdgeAPI/internal/utils/rlimit_others.go
Normal file
14
EdgeAPI/internal/utils/rlimit_others.go
Normal file
@@ -0,0 +1,14 @@
|
||||
//go:build !linux && !darwin
|
||||
// +build !linux,!darwin
|
||||
|
||||
package utils
|
||||
|
||||
// set resource limit
|
||||
func SetRLimit(limit uint64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// set best resource limit value
|
||||
func SetSuitableRLimit() {
|
||||
|
||||
}
|
||||
111
EdgeAPI/internal/utils/service.go
Normal file
111
EdgeAPI/internal/utils/service.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/files"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// 服务管理器
|
||||
type ServiceManager struct {
|
||||
Name string
|
||||
Description string
|
||||
|
||||
fp *os.File
|
||||
logger *log.Logger
|
||||
onceLocker sync.Once
|
||||
}
|
||||
|
||||
// 获取对象
|
||||
func NewServiceManager(name, description string) *ServiceManager {
|
||||
manager := &ServiceManager{
|
||||
Name: name,
|
||||
Description: description,
|
||||
}
|
||||
|
||||
// root
|
||||
manager.resetRoot()
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
// 设置服务
|
||||
func (this *ServiceManager) setup() {
|
||||
this.onceLocker.Do(func() {
|
||||
logFile := files.NewFile(Tea.Root + "/logs/service.log")
|
||||
if logFile.Exists() {
|
||||
_ = logFile.Delete()
|
||||
}
|
||||
|
||||
//logger
|
||||
fp, err := os.OpenFile(Tea.Root+"/logs/service.log", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
return
|
||||
}
|
||||
this.fp = fp
|
||||
this.logger = log.New(fp, "", log.LstdFlags)
|
||||
})
|
||||
}
|
||||
|
||||
// 记录普通日志
|
||||
func (this *ServiceManager) Log(msg string) {
|
||||
this.setup()
|
||||
if this.logger == nil {
|
||||
return
|
||||
}
|
||||
this.logger.Println("[info]" + msg)
|
||||
}
|
||||
|
||||
// 记录错误日志
|
||||
func (this *ServiceManager) LogError(msg string) {
|
||||
this.setup()
|
||||
if this.logger == nil {
|
||||
return
|
||||
}
|
||||
this.logger.Println("[error]" + msg)
|
||||
}
|
||||
|
||||
// 关闭
|
||||
func (this *ServiceManager) Close() error {
|
||||
if this.fp != nil {
|
||||
return this.fp.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 重置Root
|
||||
func (this *ServiceManager) resetRoot() {
|
||||
if !Tea.IsTesting() {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
exePath = os.Args[0]
|
||||
}
|
||||
link, err := filepath.EvalSymlinks(exePath)
|
||||
if err == nil {
|
||||
exePath = link
|
||||
}
|
||||
fullPath, err := filepath.Abs(exePath)
|
||||
if err == nil {
|
||||
Tea.UpdateRoot(filepath.Dir(filepath.Dir(fullPath)))
|
||||
}
|
||||
}
|
||||
Tea.SetPublicDir(Tea.Root + Tea.DS + "web" + Tea.DS + "public")
|
||||
Tea.SetViewsDir(Tea.Root + Tea.DS + "web" + Tea.DS + "views")
|
||||
Tea.SetTmpDir(Tea.Root + Tea.DS + "web" + Tea.DS + "tmp")
|
||||
}
|
||||
|
||||
// 保持命令行窗口是打开的
|
||||
func (this *ServiceManager) PauseWindow() {
|
||||
if runtime.GOOS != "windows" {
|
||||
return
|
||||
}
|
||||
|
||||
b := make([]byte, 1)
|
||||
_, _ = os.Stdin.Read(b)
|
||||
}
|
||||
162
EdgeAPI/internal/utils/service_linux.go
Normal file
162
EdgeAPI/internal/utils/service_linux.go
Normal file
@@ -0,0 +1,162 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
|
||||
executils "github.com/TeaOSLab/EdgeAPI/internal/utils/exec"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/files"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var systemdServiceFile = "/etc/systemd/system/edge-api.service"
|
||||
var initServiceFile = "/etc/init.d/" + teaconst.SystemdServiceName
|
||||
|
||||
// Install 安装服务
|
||||
func (this *ServiceManager) Install(exePath string, args []string) error {
|
||||
if os.Getgid() != 0 {
|
||||
return errors.New("only root users can install the service")
|
||||
}
|
||||
|
||||
systemd, err := executils.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return this.installInitService(exePath, args)
|
||||
}
|
||||
|
||||
return this.installSystemdService(systemd, exePath, args)
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (this *ServiceManager) Start() error {
|
||||
if os.Getgid() != 0 {
|
||||
return errors.New("only root users can start the service")
|
||||
}
|
||||
|
||||
if files.NewFile(systemdServiceFile).Exists() {
|
||||
systemd, err := executils.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return exec.Command(systemd, "start", teaconst.SystemdServiceName+".service").Start()
|
||||
}
|
||||
return exec.Command("service", teaconst.ProcessName, "start").Start()
|
||||
}
|
||||
|
||||
// Uninstall 删除服务
|
||||
func (this *ServiceManager) Uninstall() error {
|
||||
if os.Getgid() != 0 {
|
||||
return errors.New("only root users can uninstall the service")
|
||||
}
|
||||
|
||||
if files.NewFile(systemdServiceFile).Exists() {
|
||||
systemd, err := executils.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// disable service
|
||||
_ = exec.Command(systemd, "disable", teaconst.SystemdServiceName+".service").Start()
|
||||
|
||||
// reload
|
||||
_ = exec.Command(systemd, "daemon-reload").Start()
|
||||
|
||||
return files.NewFile(systemdServiceFile).Delete()
|
||||
}
|
||||
|
||||
f := files.NewFile(initServiceFile)
|
||||
if f.Exists() {
|
||||
return f.Delete()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// install init service
|
||||
func (this *ServiceManager) installInitService(exePath string, args []string) error {
|
||||
shortName := teaconst.SystemdServiceName
|
||||
scriptFile := Tea.Root + "/scripts/" + shortName
|
||||
if !files.NewFile(scriptFile).Exists() {
|
||||
return errors.New("'scripts/" + shortName + "' file not exists")
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(scriptFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data = regexp.MustCompile("INSTALL_DIR=.+").ReplaceAll(data, []byte("INSTALL_DIR="+Tea.Root))
|
||||
err = os.WriteFile(initServiceFile, data, 0777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chkCmd, err := executils.LookPath("chkconfig")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = exec.Command(chkCmd, "--add", teaconst.ProcessName).Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// install systemd service
|
||||
func (this *ServiceManager) installSystemdService(systemd, exePath string, args []string) error {
|
||||
shortName := teaconst.SystemdServiceName
|
||||
longName := "GoEdge API" // TODO 将来可以修改
|
||||
|
||||
var startCmd = exePath + " daemon"
|
||||
bashPath, _ := executils.LookPath("bash")
|
||||
if len(bashPath) > 0 {
|
||||
startCmd = bashPath + " -c \"" + startCmd + "\""
|
||||
}
|
||||
|
||||
desc := `### BEGIN INIT INFO
|
||||
# Provides: ` + shortName + `
|
||||
# Required-Start: $all
|
||||
# Required-Stop:
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop:
|
||||
# Short-Description: ` + longName + ` Service
|
||||
### END INIT INFO
|
||||
|
||||
[Unit]
|
||||
Description=` + longName + ` Service
|
||||
Before=shutdown.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=1s
|
||||
ExecStart=` + startCmd + `
|
||||
ExecStop=` + exePath + ` stop
|
||||
ExecReload=` + exePath + ` reload
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target`
|
||||
|
||||
// write file
|
||||
err := os.WriteFile(systemdServiceFile, []byte(desc), 0777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// stop current systemd service if running
|
||||
_ = exec.Command(systemd, "stop", shortName+".service").Start()
|
||||
|
||||
// reload
|
||||
_ = exec.Command(systemd, "daemon-reload").Start()
|
||||
|
||||
// enable
|
||||
cmd := exec.Command(systemd, "enable", shortName+".service")
|
||||
return cmd.Run()
|
||||
}
|
||||
19
EdgeAPI/internal/utils/service_others.go
Normal file
19
EdgeAPI/internal/utils/service_others.go
Normal file
@@ -0,0 +1,19 @@
|
||||
//go:build !linux && !windows
|
||||
// +build !linux,!windows
|
||||
|
||||
package utils
|
||||
|
||||
// 安装服务
|
||||
func (this *ServiceManager) Install(exePath string, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 启动服务
|
||||
func (this *ServiceManager) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 删除服务
|
||||
func (this *ServiceManager) Uninstall() error {
|
||||
return nil
|
||||
}
|
||||
12
EdgeAPI/internal/utils/service_test.go
Normal file
12
EdgeAPI/internal/utils/service_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestServiceManager_Log(t *testing.T) {
|
||||
manager := NewServiceManager(teaconst.ProductName, teaconst.ProductName+" Server")
|
||||
manager.Log("Hello, World")
|
||||
manager.LogError("Hello, World")
|
||||
}
|
||||
175
EdgeAPI/internal/utils/service_windows.go
Normal file
175
EdgeAPI/internal/utils/service_windows.go
Normal file
@@ -0,0 +1,175 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/mgr"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// 安装服务
|
||||
func (this *ServiceManager) Install(exePath string, args []string) error {
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting: %w please 'Run as administrator' again", err)
|
||||
}
|
||||
defer m.Disconnect()
|
||||
s, err := m.OpenService(this.Name)
|
||||
if err == nil {
|
||||
s.Close()
|
||||
return fmt.Errorf("service %s already exists", this.Name)
|
||||
}
|
||||
|
||||
s, err = m.CreateService(this.Name, exePath, mgr.Config{
|
||||
DisplayName: this.Name,
|
||||
Description: this.Description,
|
||||
StartType: windows.SERVICE_AUTO_START,
|
||||
}, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating: %w", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 启动服务
|
||||
func (this *ServiceManager) Start() error {
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.Disconnect()
|
||||
s, err := m.OpenService(this.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not access service: %w", err)
|
||||
}
|
||||
defer s.Close()
|
||||
err = s.Start("service")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not start service: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 删除服务
|
||||
func (this *ServiceManager) Uninstall() error {
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting: %w please 'Run as administrator' again", err)
|
||||
}
|
||||
defer m.Disconnect()
|
||||
s, err := m.OpenService(this.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open service: %w", err)
|
||||
}
|
||||
|
||||
// shutdown service
|
||||
_, err = s.Control(svc.Stop)
|
||||
if err != nil {
|
||||
fmt.Printf("shutdown service: %s\n", err.Error())
|
||||
}
|
||||
|
||||
defer s.Close()
|
||||
err = s.Delete()
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 运行
|
||||
func (this *ServiceManager) Run() {
|
||||
err := svc.Run(this.Name, this)
|
||||
if err != nil {
|
||||
this.LogError(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 同服务管理器的交互
|
||||
func (this *ServiceManager) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
|
||||
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue
|
||||
|
||||
changes <- svc.Status{
|
||||
State: svc.StartPending,
|
||||
}
|
||||
|
||||
changes <- svc.Status{
|
||||
State: svc.Running,
|
||||
Accepts: cmdsAccepted,
|
||||
}
|
||||
|
||||
// start service
|
||||
this.Log("start")
|
||||
this.cmdStart()
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case c := <-r:
|
||||
switch c.Cmd {
|
||||
case svc.Interrogate:
|
||||
this.Log("cmd: Interrogate")
|
||||
changes <- c.CurrentStatus
|
||||
case svc.Stop, svc.Shutdown:
|
||||
this.Log("cmd: Stop|Shutdown")
|
||||
|
||||
// stop service
|
||||
this.cmdStop()
|
||||
|
||||
break loop
|
||||
case svc.Pause:
|
||||
this.Log("cmd: Pause")
|
||||
|
||||
// stop service
|
||||
this.cmdStop()
|
||||
|
||||
changes <- svc.Status{
|
||||
State: svc.Paused,
|
||||
Accepts: cmdsAccepted,
|
||||
}
|
||||
case svc.Continue:
|
||||
this.Log("cmd: Continue")
|
||||
|
||||
// start service
|
||||
this.cmdStart()
|
||||
|
||||
changes <- svc.Status{
|
||||
State: svc.Running,
|
||||
Accepts: cmdsAccepted,
|
||||
}
|
||||
default:
|
||||
this.LogError(fmt.Sprintf("unexpected control request #%d\r\n", c))
|
||||
}
|
||||
}
|
||||
}
|
||||
changes <- svc.Status{
|
||||
State: svc.StopPending,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 启动Web服务
|
||||
func (this *ServiceManager) cmdStart() {
|
||||
cmd := exec.Command(Tea.Root+Tea.DS+"bin"+Tea.DS+teaconst.SystemdServiceName+".exe", "start")
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
this.LogError(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 停止Web服务
|
||||
func (this *ServiceManager) cmdStop() {
|
||||
cmd := exec.Command(Tea.Root+Tea.DS+"bin"+Tea.DS+teaconst.SystemdServiceName+".exe", "stop")
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
this.LogError(err.Error())
|
||||
}
|
||||
}
|
||||
21
EdgeAPI/internal/utils/sha1.go
Normal file
21
EdgeAPI/internal/utils/sha1.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"github.com/iwind/TeaGo/rands"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const sha1RandomPrefix = "SHA1_RANDOM"
|
||||
|
||||
var sha1Id int64 = 0
|
||||
|
||||
func Sha1RandomString() string {
|
||||
var s = sha1RandomPrefix + types.String(time.Now().UnixNano()) + "@" + types.String(rands.Int64()) + "@" + types.String(atomic.AddInt64(&sha1Id, 1))
|
||||
return fmt.Sprintf("%x", sha1.Sum([]byte(s)))
|
||||
}
|
||||
16
EdgeAPI/internal/utils/sha1_test.go
Normal file
16
EdgeAPI/internal/utils/sha1_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSha1Random(t *testing.T) {
|
||||
for i := 0; i < 10; i++ {
|
||||
var s = utils.Sha1RandomString()
|
||||
t.Log("["+types.String(len(s))+"]", s)
|
||||
}
|
||||
}
|
||||
10
EdgeAPI/internal/utils/sizes/sizes.go
Normal file
10
EdgeAPI/internal/utils/sizes/sizes.go
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package sizes
|
||||
|
||||
const (
|
||||
K int64 = 1024
|
||||
M = 1024 * K
|
||||
G = 1024 * M
|
||||
T = 1024 * G
|
||||
)
|
||||
17
EdgeAPI/internal/utils/sizes/sizes_test.go
Normal file
17
EdgeAPI/internal/utils/sizes/sizes_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package sizes_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils/sizes"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSizes(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
a.IsTrue(sizes.K == 1024)
|
||||
a.IsTrue(sizes.M == 1024*1024)
|
||||
a.IsTrue(sizes.G == 1024*1024*1024)
|
||||
a.IsTrue(sizes.T == 1024*1024*1024*1024)
|
||||
}
|
||||
135
EdgeAPI/internal/utils/strings.go
Normal file
135
EdgeAPI/internal/utils/strings.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SplitStrings 分隔字符串
|
||||
// 忽略其中为空的片段
|
||||
func SplitStrings(s string, glue string) []string {
|
||||
var result = []string{}
|
||||
|
||||
if len(s) > 0 {
|
||||
for _, p := range strings.Split(s, glue) {
|
||||
p = strings.TrimSpace(p)
|
||||
if len(p) > 0 {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ContainsStringInsensitive 检查是否包含某个字符串,并且不区分大小写
|
||||
func ContainsStringInsensitive(list []string, search string) bool {
|
||||
search = strings.ToLower(search)
|
||||
for _, s := range list {
|
||||
if strings.ToLower(s) == search {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Similar 计算相似度
|
||||
// between 0-1
|
||||
func Similar(s1 string, s2 string) float32 {
|
||||
var r1s = []rune(s1)
|
||||
var r2s = []rune(s2)
|
||||
var l1 = len(r1s)
|
||||
var l2 = len(r2s)
|
||||
|
||||
if l1 > l2 {
|
||||
r1s, r2s = r2s, r1s
|
||||
}
|
||||
|
||||
if len(r1s) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
var count = 0
|
||||
for _, r := range r1s {
|
||||
for index, r2 := range r2s {
|
||||
if r == r2 {
|
||||
count++
|
||||
r2s = r2s[index+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (float32(count)/float32(l1) + float32(count)/float32(l2)) / 2
|
||||
}
|
||||
|
||||
// LimitString 限制字符串长度
|
||||
func LimitString(s string, maxLength int) string {
|
||||
if len(s) <= maxLength {
|
||||
return s
|
||||
}
|
||||
|
||||
if maxLength <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var runes = []rune(s)
|
||||
var rs = len(runes)
|
||||
for i := 0; i < rs; i++ {
|
||||
if len(string(runes[:i+1])) > maxLength {
|
||||
return string(runes[:i])
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// SplitKeywordArgs 分隔关键词参数
|
||||
// 支持:hello, "hello", name:hello, name:"hello", name:\"hello\"
|
||||
func SplitKeywordArgs(s string) (args []splitArg) {
|
||||
var value []rune
|
||||
var beginQuote = false
|
||||
var runes = []rune(s)
|
||||
for index, r := range runes {
|
||||
if r == '"' && (index == 0 || runes[index-1] != '\\') {
|
||||
beginQuote = !beginQuote
|
||||
continue
|
||||
}
|
||||
if !beginQuote && (r == ' ' || r == '\t' || r == '\n' || r == '\r') {
|
||||
if len(value) > 0 {
|
||||
args = append(args, parseKeywordValue(string(value)))
|
||||
value = nil
|
||||
}
|
||||
} else {
|
||||
value = append(value, r)
|
||||
}
|
||||
}
|
||||
|
||||
if len(value) > 0 {
|
||||
args = append(args, parseKeywordValue(string(value)))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type splitArg struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (this *splitArg) String() string {
|
||||
if len(this.Key) > 0 {
|
||||
return this.Key + ":" + this.Value
|
||||
}
|
||||
return this.Value
|
||||
}
|
||||
|
||||
func parseKeywordValue(value string) (arg splitArg) {
|
||||
var colonIndex = strings.Index(value, ":")
|
||||
if colonIndex > 0 {
|
||||
arg.Key = value[:colonIndex]
|
||||
arg.Value = value[colonIndex+1:]
|
||||
} else {
|
||||
arg.Value = value
|
||||
}
|
||||
return
|
||||
}
|
||||
69
EdgeAPI/internal/utils/strings_test.go
Normal file
69
EdgeAPI/internal/utils/strings_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitStrings(t *testing.T) {
|
||||
t.Log(utils.SplitStrings("a, b, c", ","))
|
||||
t.Log(utils.SplitStrings("a, b, c, ", ","))
|
||||
}
|
||||
|
||||
func TestContainsStringInsensitive(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
a.IsTrue(utils.ContainsStringInsensitive([]string{"a", "b", "C"}, "A"))
|
||||
a.IsTrue(utils.ContainsStringInsensitive([]string{"a", "b", "C"}, "b"))
|
||||
a.IsTrue(utils.ContainsStringInsensitive([]string{"a", "b", "C"}, "c"))
|
||||
a.IsFalse(utils.ContainsStringInsensitive([]string{"a", "b", "C"}, "d"))
|
||||
}
|
||||
|
||||
func TestSimilar(t *testing.T) {
|
||||
t.Log(utils.Similar("", ""))
|
||||
t.Log(utils.Similar("", "a"))
|
||||
t.Log(utils.Similar("abc", "bcd"))
|
||||
t.Log(utils.Similar("efgj", "hijk"))
|
||||
t.Log(utils.Similar("efgj", "klmn"))
|
||||
}
|
||||
|
||||
func TestLimitString(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
a.IsTrue(utils.LimitString("", 4) == "")
|
||||
a.IsTrue(utils.LimitString("abcd", 0) == "")
|
||||
a.IsTrue(utils.LimitString("abcd", 5) == "abcd")
|
||||
a.IsTrue(utils.LimitString("abcd", 4) == "abcd")
|
||||
a.IsTrue(utils.LimitString("abcd", 3) == "abc")
|
||||
a.IsTrue(utils.LimitString("abcd", 1) == "a")
|
||||
a.IsTrue(utils.LimitString("中文测试", 1) == "")
|
||||
a.IsTrue(utils.LimitString("中文测试", 3) == "中")
|
||||
}
|
||||
|
||||
func TestSplitKeywordArgs(t *testing.T) {
|
||||
{
|
||||
var keyword = ""
|
||||
t.Logf("%+v", utils.SplitKeywordArgs(keyword))
|
||||
}
|
||||
{
|
||||
var keyword = "abc"
|
||||
t.Logf("%+v", utils.SplitKeywordArgs(keyword))
|
||||
}
|
||||
{
|
||||
var keyword = "abc def ghi123"
|
||||
t.Logf("%+v", utils.SplitKeywordArgs(keyword))
|
||||
}
|
||||
{
|
||||
var keyword = "\"hello world\""
|
||||
t.Logf("%+v", utils.SplitKeywordArgs(keyword))
|
||||
}
|
||||
{
|
||||
var keyword = "\"hello world\" hello \"world\" \"my name\" call:\"zip name\" slash:\\\"SLASH"
|
||||
t.Logf("%+v", utils.SplitKeywordArgs(keyword))
|
||||
}
|
||||
{
|
||||
var keyword = "name:abc"
|
||||
t.Logf("%+v", utils.SplitKeywordArgs(keyword))
|
||||
}
|
||||
}
|
||||
27
EdgeAPI/internal/utils/system.go
Normal file
27
EdgeAPI/internal/utils/system.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
)
|
||||
|
||||
var systemTotalMemory = -1
|
||||
|
||||
func init() {
|
||||
_ = SystemMemoryGB()
|
||||
}
|
||||
|
||||
func SystemMemoryGB() int {
|
||||
if systemTotalMemory > 0 {
|
||||
return systemTotalMemory
|
||||
}
|
||||
|
||||
stat, err := mem.VirtualMemory()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
systemTotalMemory = int(stat.Total / 1024 / 1024 / 1024)
|
||||
return systemTotalMemory
|
||||
}
|
||||
14
EdgeAPI/internal/utils/system_test.go
Normal file
14
EdgeAPI/internal/utils/system_test.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSystemMemoryGB(t *testing.T) {
|
||||
t.Log(utils.SystemMemoryGB())
|
||||
t.Log(utils.SystemMemoryGB())
|
||||
t.Log(utils.SystemMemoryGB())
|
||||
}
|
||||
61
EdgeAPI/internal/utils/taskutils/concurrent.go
Normal file
61
EdgeAPI/internal/utils/taskutils/concurrent.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package taskutils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const DefaultConcurrent = 16
|
||||
|
||||
func RunConcurrent(tasks any, concurrent int, f func(task any, locker *sync.RWMutex)) error {
|
||||
if tasks == nil {
|
||||
return nil
|
||||
}
|
||||
var tasksValue = reflect.ValueOf(tasks)
|
||||
if tasksValue.Type().Kind() != reflect.Slice {
|
||||
return errors.New("ony works for slice")
|
||||
}
|
||||
|
||||
var countTasks = tasksValue.Len()
|
||||
if countTasks == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if concurrent <= 0 {
|
||||
concurrent = 8
|
||||
}
|
||||
if concurrent > countTasks {
|
||||
concurrent = countTasks
|
||||
}
|
||||
|
||||
var taskChan = make(chan any, countTasks)
|
||||
for i := 0; i < countTasks; i++ {
|
||||
taskChan <- tasksValue.Index(i).Interface()
|
||||
}
|
||||
|
||||
var wg = &sync.WaitGroup{}
|
||||
wg.Add(concurrent)
|
||||
|
||||
var locker = &sync.RWMutex{}
|
||||
|
||||
for i := 0; i < concurrent; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case task := <-taskChan:
|
||||
f(task, locker)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
18
EdgeAPI/internal/utils/taskutils/concurrent_test.go
Normal file
18
EdgeAPI/internal/utils/taskutils/concurrent_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package taskutils_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils/taskutils"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRunConcurrent(t *testing.T) {
|
||||
err := taskutils.RunConcurrent([]string{"a", "b", "c", "d", "e"}, 3, func(task any, locker *sync.RWMutex) {
|
||||
t.Log("run", task)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
51
EdgeAPI/internal/utils/testutils/memory.go
Normal file
51
EdgeAPI/internal/utils/testutils/memory.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func StartMemoryStatsGC(t *testing.T) {
|
||||
var ticker = time.NewTicker(1 * time.Second)
|
||||
go func() {
|
||||
var stat = &runtime.MemStats{}
|
||||
var lastHeapInUse uint64
|
||||
|
||||
for range ticker.C {
|
||||
runtime.ReadMemStats(stat)
|
||||
if stat.HeapInuse == lastHeapInUse {
|
||||
return
|
||||
}
|
||||
lastHeapInUse = stat.HeapInuse
|
||||
|
||||
var before = time.Now()
|
||||
runtime.GC()
|
||||
var cost = time.Since(before).Seconds()
|
||||
|
||||
t.Log(timeutil.Format("H:i:s"), "HeapInuse:", fmt.Sprintf("%.2fM", float64(stat.HeapInuse)/1024/1024), "NumGC:", stat.NumGC, "Cost:", fmt.Sprintf("%.4f", cost*1000), "ms")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func StartMemoryStats(t *testing.T) {
|
||||
var ticker = time.NewTicker(1 * time.Second)
|
||||
go func() {
|
||||
var stat = &runtime.MemStats{}
|
||||
var lastHeapInUse uint64
|
||||
|
||||
for range ticker.C {
|
||||
runtime.ReadMemStats(stat)
|
||||
if stat.HeapInuse == lastHeapInUse {
|
||||
return
|
||||
}
|
||||
lastHeapInUse = stat.HeapInuse
|
||||
|
||||
t.Log(timeutil.Format("H:i:s"), "HeapInuse:", fmt.Sprintf("%.2fM", float64(stat.HeapInuse)/1024/1024), "NumGC:", stat.NumGC)
|
||||
}
|
||||
}()
|
||||
}
|
||||
34
EdgeAPI/internal/utils/ticker.go
Normal file
34
EdgeAPI/internal/utils/ticker.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package utils
|
||||
|
||||
import "time"
|
||||
|
||||
type Ticker struct {
|
||||
raw *time.Ticker
|
||||
isDone bool
|
||||
done chan bool
|
||||
}
|
||||
|
||||
func NewTicker(duration time.Duration) *Ticker {
|
||||
return &Ticker{
|
||||
raw: time.NewTicker(duration),
|
||||
done: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Ticker) Wait() bool {
|
||||
select {
|
||||
case <-this.raw.C:
|
||||
return true
|
||||
case <-this.done:
|
||||
this.isDone = true
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Ticker) Stop() {
|
||||
if this.isDone {
|
||||
return
|
||||
}
|
||||
this.done <- true
|
||||
this.raw.Stop()
|
||||
}
|
||||
296
EdgeAPI/internal/utils/time.go
Normal file
296
EdgeAPI/internal/utils/time.go
Normal file
@@ -0,0 +1,296 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/errors"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils/regexputils"
|
||||
"github.com/iwind/TeaGo/lists"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 分钟时间点
|
||||
type timeDayMinute struct {
|
||||
Day string
|
||||
Minute string
|
||||
}
|
||||
|
||||
// 分钟时间范围
|
||||
type timeDayMinuteRange struct {
|
||||
Day string
|
||||
MinuteFrom string
|
||||
MinuteTo string
|
||||
}
|
||||
|
||||
// RangeDays 计算日期之间的所有日期,格式为YYYYMMDD
|
||||
func RangeDays(dayFrom string, dayTo string) ([]string, error) {
|
||||
var ok = regexputils.YYYYMMDD.MatchString(dayFrom)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid 'dayFrom'")
|
||||
}
|
||||
|
||||
ok = regexputils.YYYYMMDD.MatchString(dayTo)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid 'dayTo'")
|
||||
}
|
||||
|
||||
if dayFrom > dayTo {
|
||||
dayFrom, dayTo = dayTo, dayFrom
|
||||
}
|
||||
|
||||
// 不能超过N天
|
||||
maxDays := 100 - 1 // -1 是去掉默认加入的dayFrom
|
||||
result := []string{dayFrom}
|
||||
|
||||
year := types.Int(dayFrom[:4])
|
||||
month := types.Int(dayFrom[4:6])
|
||||
day := types.Int(dayFrom[6:])
|
||||
t := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local)
|
||||
for {
|
||||
t = t.AddDate(0, 0, 1)
|
||||
newDay := timeutil.Format("Ymd", t)
|
||||
if newDay <= dayTo {
|
||||
result = append(result, newDay)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
maxDays--
|
||||
if maxDays <= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RangeMonths 计算日期之间的所有月份,格式为YYYYMM
|
||||
func RangeMonths(dayFrom string, dayTo string) ([]string, error) {
|
||||
var ok = regexputils.YYYYMMDD.MatchString(dayFrom)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid 'dayFrom'")
|
||||
}
|
||||
|
||||
ok = regexputils.YYYYMMDD.MatchString(dayTo)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid 'dayTo'")
|
||||
}
|
||||
|
||||
if dayFrom > dayTo {
|
||||
dayFrom, dayTo = dayTo, dayFrom
|
||||
}
|
||||
|
||||
var result = []string{dayFrom[:6]}
|
||||
|
||||
var year = types.Int(dayFrom[:4])
|
||||
var month = types.Int(dayFrom[4:6])
|
||||
var day = types.Int(dayFrom[6:])
|
||||
var t = time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local)
|
||||
for {
|
||||
t = t.AddDate(0, 0, 20)
|
||||
var newDay = timeutil.Format("Ymd", t)
|
||||
if newDay <= dayTo {
|
||||
var monthString = newDay[:6]
|
||||
if !lists.ContainsString(result, monthString) {
|
||||
result = append(result, monthString)
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var endMonth = dayTo[:6]
|
||||
if !lists.ContainsString(result, endMonth) {
|
||||
result = append(result, endMonth)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RangeHours 计算小时之间的所有小时,格式为YYYYMMDDHH
|
||||
func RangeHours(hourFrom string, hourTo string) ([]string, error) {
|
||||
ok, err := regexp.MatchString(`^\d{10}$`, hourFrom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, errors.New("invalid 'hourFrom'")
|
||||
}
|
||||
|
||||
ok, err = regexp.MatchString(`^\d{10}$`, hourTo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, errors.New("invalid 'hourTo'")
|
||||
}
|
||||
|
||||
if hourFrom > hourTo {
|
||||
hourFrom, hourTo = hourTo, hourFrom
|
||||
}
|
||||
|
||||
// 不能超过N天
|
||||
var maxHours = 100 - 1 // -1 是去掉默认加入的dayFrom
|
||||
var result = []string{hourFrom}
|
||||
|
||||
var year = types.Int(hourFrom[:4])
|
||||
var month = types.Int(hourFrom[4:6])
|
||||
var day = types.Int(hourFrom[6:8])
|
||||
var hour = types.Int(hourFrom[8:])
|
||||
var t = time.Date(year, time.Month(month), day, hour, 0, 0, 0, time.Local)
|
||||
for {
|
||||
t = t.Add(1 * time.Hour)
|
||||
var newHour = timeutil.Format("YmdH", t)
|
||||
if newHour <= hourTo {
|
||||
result = append(result, newHour)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
maxHours--
|
||||
if maxHours <= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RangeMinutes 计算若干个时间点,返回结果为 [ [day1, minute1], [day2, minute2] ... ]
|
||||
func RangeMinutes(toTime time.Time, count int, everyMinutes int32) []timeDayMinute {
|
||||
var everySeconds = everyMinutes * 60
|
||||
if everySeconds <= 0 {
|
||||
everySeconds = 300
|
||||
}
|
||||
var result = []timeDayMinute{}
|
||||
var fromTime = time.Unix(toTime.Unix()-int64(everySeconds)*int64(count-1), 0)
|
||||
for {
|
||||
var timestamp = fromTime.Unix() / int64(everySeconds) * int64(everySeconds)
|
||||
result = append(result, timeDayMinute{
|
||||
Day: timeutil.FormatTime("Ymd", timestamp),
|
||||
Minute: timeutil.FormatTime("Hi", timestamp),
|
||||
})
|
||||
fromTime = time.Unix(fromTime.Unix()+int64(everySeconds), 0)
|
||||
|
||||
count--
|
||||
if count <= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GroupMinuteRanges 将时间点分组
|
||||
func GroupMinuteRanges(minutes []timeDayMinute) []timeDayMinuteRange {
|
||||
var result = []*timeDayMinuteRange{}
|
||||
var lastDay = ""
|
||||
var lastRange *timeDayMinuteRange
|
||||
for _, minute := range minutes {
|
||||
if minute.Day != lastDay {
|
||||
lastDay = minute.Day
|
||||
lastRange = &timeDayMinuteRange{
|
||||
Day: minute.Day,
|
||||
MinuteFrom: minute.Minute,
|
||||
MinuteTo: minute.Minute,
|
||||
}
|
||||
result = append(result, lastRange)
|
||||
} else {
|
||||
if lastRange != nil {
|
||||
lastRange.MinuteTo = minute.Minute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var finalResult = []timeDayMinuteRange{}
|
||||
for _, minutePtr := range result {
|
||||
finalResult = append(finalResult, *minutePtr)
|
||||
}
|
||||
return finalResult
|
||||
}
|
||||
|
||||
// RangeTimes 计算时间点
|
||||
func RangeTimes(timeFrom string, timeTo string, everyMinutes int32) (result []string, err error) {
|
||||
if everyMinutes <= 0 {
|
||||
return nil, errors.New("invalid 'everyMinutes'")
|
||||
}
|
||||
|
||||
var reg = regexp.MustCompile(`^\d{4}$`)
|
||||
if !reg.MatchString(timeFrom) {
|
||||
return nil, errors.New("invalid timeFrom '" + timeFrom + "'")
|
||||
}
|
||||
if !reg.MatchString(timeTo) {
|
||||
return nil, errors.New("invalid timeTo '" + timeTo + "'")
|
||||
}
|
||||
|
||||
if timeFrom > timeTo {
|
||||
// swap
|
||||
timeFrom, timeTo = timeTo, timeFrom
|
||||
}
|
||||
|
||||
var everyMinutesInt = int(everyMinutes)
|
||||
|
||||
var fromHour = types.Int(timeFrom[:2])
|
||||
var fromMinute = types.Int(timeFrom[2:])
|
||||
var toHour = types.Int(timeTo[:2])
|
||||
var toMinute = types.Int(timeTo[2:])
|
||||
|
||||
if fromMinute%everyMinutesInt == 0 {
|
||||
result = append(result, timeFrom)
|
||||
}
|
||||
|
||||
for {
|
||||
fromMinute += everyMinutesInt
|
||||
if fromMinute > 59 {
|
||||
fromHour += fromMinute / 60
|
||||
fromMinute = fromMinute % 60
|
||||
}
|
||||
if fromHour > toHour || (fromHour == toHour && fromMinute > toMinute) {
|
||||
break
|
||||
}
|
||||
result = append(result, fmt.Sprintf("%02d%02d", fromHour, fromMinute))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Range24HourTimes 计算24小时时间点
|
||||
// 从 00:00 - 23:59
|
||||
func Range24HourTimes(everyMinutes int32) ([]string, error) {
|
||||
if everyMinutes <= 0 {
|
||||
return nil, errors.New("invalid 'everyMinutes'")
|
||||
}
|
||||
|
||||
return RangeTimes("0000", "2359", everyMinutes)
|
||||
}
|
||||
|
||||
// LastDayInMonth 某月的最后一天
|
||||
// month: YYYYMM
|
||||
// 返回 YYYYMMDD
|
||||
func LastDayInMonth(month string) (string, error) {
|
||||
if !regexputils.YYYYMM.MatchString(month) {
|
||||
return "", errors.New("invalid month '" + month + "'")
|
||||
}
|
||||
|
||||
var year = types.Int(month[:4])
|
||||
var monthInt = types.Int(month[4:])
|
||||
return month + timeutil.Format("t", time.Date(year, time.Month(monthInt), 1, 0, 0, 0, 0, time.Local)), nil
|
||||
}
|
||||
|
||||
// FixMonthMaxDay 修正日期最大值
|
||||
func FixMonthMaxDay(day string) (string, error) {
|
||||
if !regexputils.YYYYMMDD.MatchString(day) {
|
||||
return "", errors.New("invalid day '" + day + "'")
|
||||
}
|
||||
|
||||
maxDay, err := LastDayInMonth(day[:6])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if day > maxDay {
|
||||
return maxDay, nil
|
||||
}
|
||||
return day, nil
|
||||
}
|
||||
123
EdgeAPI/internal/utils/time_test.go
Normal file
123
EdgeAPI/internal/utils/time_test.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRangeDays(t *testing.T) {
|
||||
days, err := utils.RangeDays("20210101", "20210115")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(days)
|
||||
}
|
||||
|
||||
func TestRangeMonth(t *testing.T) {
|
||||
days, err := utils.RangeMonths("20200101", "20210115")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(days)
|
||||
}
|
||||
|
||||
func TestRangeHours(t *testing.T) {
|
||||
{
|
||||
hours, err := utils.RangeHours("2021010100", "2021010123")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(hours)
|
||||
}
|
||||
|
||||
{
|
||||
hours, err := utils.RangeHours("2021010105", "2021010112")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(hours)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRangeMinutes(t *testing.T) {
|
||||
{
|
||||
var minutes = utils.RangeMinutes(time.Now(), 5, 5)
|
||||
t.Log(minutes)
|
||||
}
|
||||
|
||||
{
|
||||
var minutes = utils.RangeMinutes(time.Now(), 5, 3)
|
||||
t.Log(minutes)
|
||||
}
|
||||
|
||||
{
|
||||
var now = time.Now()
|
||||
var hour = now.Hour()
|
||||
var minute = now.Minute()
|
||||
now = now.Add(-time.Duration(hour) * time.Hour)
|
||||
now = now.Add(-time.Duration(minute-7) * time.Minute) // 后一天的 00:07 开始往前计算
|
||||
var minutes = utils.RangeMinutes(now, 5, 5)
|
||||
t.Log(minutes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRangeTimes(t *testing.T) {
|
||||
for _, r := range [][2]string{
|
||||
{"0000", "2359"},
|
||||
{"0000", "0230"},
|
||||
{"0300", "0230"},
|
||||
{"1021", "1131"},
|
||||
} {
|
||||
result, err := utils.RangeTimes(r[0], r[1], 5)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(r, "=>", result, len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRange24HourTimes(t *testing.T) {
|
||||
t.Log(utils.Range24HourTimes(5))
|
||||
}
|
||||
|
||||
func TestGroupMinuteRanges(t *testing.T) {
|
||||
{
|
||||
var minutes = utils.GroupMinuteRanges(utils.RangeMinutes(time.Now(), 5, 5))
|
||||
t.Log(minutes)
|
||||
}
|
||||
|
||||
{
|
||||
var now = time.Now()
|
||||
var hour = now.Hour()
|
||||
var minute = now.Minute()
|
||||
now = now.Add(-time.Duration(hour) * time.Hour)
|
||||
now = now.Add(-time.Duration(minute-7) * time.Minute) // 后一天的 00:07 开始往前计算
|
||||
var minutes = utils.GroupMinuteRanges(utils.RangeMinutes(now, 5, 5))
|
||||
t.Log(minutes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastDayInMonth(t *testing.T) {
|
||||
t.Log(utils.LastDayInMonth("202209"))
|
||||
t.Log(utils.LastDayInMonth("202210"))
|
||||
t.Log(utils.LastDayInMonth("202202"))
|
||||
}
|
||||
|
||||
func TestFixMonthMaxDay(t *testing.T) {
|
||||
for _, day := range []string{
|
||||
"20220930",
|
||||
"20220929",
|
||||
"20220931",
|
||||
"20220932",
|
||||
"20220222",
|
||||
"20220228",
|
||||
"20220229",
|
||||
} {
|
||||
afterDay, err := utils.FixMonthMaxDay(day)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(day, "=>", afterDay)
|
||||
}
|
||||
}
|
||||
155
EdgeAPI/internal/utils/ttlcache/cache.go
Normal file
155
EdgeAPI/internal/utils/ttlcache/cache.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package ttlcache
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
var SharedCache = NewCache()
|
||||
|
||||
// Cache TTL缓存
|
||||
// 最大的缓存时间为30 * 86400
|
||||
// Piece数据结构:
|
||||
//
|
||||
// Piece1 | Piece2 | Piece3 | ...
|
||||
// [ Item1, Item2, ... ] | ...
|
||||
//
|
||||
// KeyMap列表数据结构
|
||||
// { timestamp1 => [key1, key2, ...] }, ...
|
||||
type Cache struct {
|
||||
isDestroyed bool
|
||||
pieces []*Piece
|
||||
countPieces uint64
|
||||
maxItems int
|
||||
|
||||
gcPieceIndex int
|
||||
}
|
||||
|
||||
func NewCache(opt ...OptionInterface) *Cache {
|
||||
var countPieces = 256
|
||||
var maxItems = 1_000_000
|
||||
|
||||
var totalMemory = utils.SystemMemoryGB()
|
||||
if totalMemory < 2 {
|
||||
// 我们限制内存过小的服务能够使用的数量
|
||||
maxItems = 500_000
|
||||
} else {
|
||||
var delta = totalMemory / 8
|
||||
if delta > 0 {
|
||||
maxItems *= delta
|
||||
}
|
||||
}
|
||||
|
||||
for _, option := range opt {
|
||||
if option == nil {
|
||||
continue
|
||||
}
|
||||
switch o := option.(type) {
|
||||
case *PiecesOption:
|
||||
if o.Count > 0 {
|
||||
countPieces = o.Count
|
||||
}
|
||||
case *MaxItemsOption:
|
||||
if o.Count > 0 {
|
||||
maxItems = o.Count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cache = &Cache{
|
||||
countPieces: uint64(countPieces),
|
||||
maxItems: maxItems,
|
||||
}
|
||||
|
||||
for i := 0; i < countPieces; i++ {
|
||||
cache.pieces = append(cache.pieces, NewPiece(maxItems/countPieces))
|
||||
}
|
||||
|
||||
// Add to manager
|
||||
SharedManager.Add(cache)
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
func (this *Cache) Write(key string, value interface{}, expiredAt int64) (ok bool) {
|
||||
if this.isDestroyed {
|
||||
return
|
||||
}
|
||||
|
||||
var currentTimestamp = utils.UnixTime()
|
||||
if expiredAt <= currentTimestamp {
|
||||
return
|
||||
}
|
||||
|
||||
var maxExpiredAt = currentTimestamp + 30*86400
|
||||
if expiredAt > maxExpiredAt {
|
||||
expiredAt = maxExpiredAt
|
||||
}
|
||||
uint64Key := HashKey([]byte(key))
|
||||
pieceIndex := uint64Key % this.countPieces
|
||||
return this.pieces[pieceIndex].Add(uint64Key, &Item{
|
||||
Value: value,
|
||||
expiredAt: expiredAt,
|
||||
})
|
||||
}
|
||||
|
||||
func (this *Cache) IncreaseInt64(key string, delta int64, expiredAt int64, extend bool) int64 {
|
||||
if this.isDestroyed {
|
||||
return 0
|
||||
}
|
||||
|
||||
currentTimestamp := time.Now().Unix()
|
||||
if expiredAt <= currentTimestamp {
|
||||
return 0
|
||||
}
|
||||
|
||||
maxExpiredAt := currentTimestamp + 30*86400
|
||||
if expiredAt > maxExpiredAt {
|
||||
expiredAt = maxExpiredAt
|
||||
}
|
||||
uint64Key := HashKey([]byte(key))
|
||||
pieceIndex := uint64Key % this.countPieces
|
||||
return this.pieces[pieceIndex].IncreaseInt64(uint64Key, delta, expiredAt, extend)
|
||||
}
|
||||
|
||||
func (this *Cache) Read(key string) (item *Item) {
|
||||
uint64Key := HashKey([]byte(key))
|
||||
return this.pieces[uint64Key%this.countPieces].Read(uint64Key)
|
||||
}
|
||||
|
||||
func (this *Cache) Delete(key string) {
|
||||
uint64Key := HashKey([]byte(key))
|
||||
this.pieces[uint64Key%this.countPieces].Delete(uint64Key)
|
||||
}
|
||||
|
||||
func (this *Cache) Count() (count int) {
|
||||
for _, piece := range this.pieces {
|
||||
count += piece.Count()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (this *Cache) GC() {
|
||||
this.pieces[this.gcPieceIndex].GC()
|
||||
newIndex := this.gcPieceIndex + 1
|
||||
if newIndex >= int(this.countPieces) {
|
||||
newIndex = 0
|
||||
}
|
||||
this.gcPieceIndex = newIndex
|
||||
}
|
||||
|
||||
func (this *Cache) Clean() {
|
||||
for _, piece := range this.pieces {
|
||||
piece.Clean()
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Cache) Destroy() {
|
||||
SharedManager.Remove(this)
|
||||
|
||||
this.isDestroyed = true
|
||||
|
||||
for _, piece := range this.pieces {
|
||||
piece.Destroy()
|
||||
}
|
||||
}
|
||||
246
EdgeAPI/internal/utils/ttlcache/cache_test.go
Normal file
246
EdgeAPI/internal/utils/ttlcache/cache_test.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package ttlcache
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils/testutils"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"github.com/iwind/TeaGo/rands"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewCache(t *testing.T) {
|
||||
var cache = NewCache()
|
||||
cache.Write("a", 1, time.Now().Unix()+3600)
|
||||
cache.Write("b", 2, time.Now().Unix()+3601)
|
||||
cache.Write("a", 1, time.Now().Unix()+3602)
|
||||
cache.Write("d", 1, time.Now().Unix()+1)
|
||||
|
||||
for _, piece := range cache.pieces {
|
||||
if len(piece.m) > 0 {
|
||||
for k, item := range piece.m {
|
||||
t.Log(k, "=>", item.Value, item.expiredAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Log("a:", cache.Read("a"))
|
||||
time.Sleep(2 * time.Second)
|
||||
t.Log("d:", cache.Read("d")) // should be nil
|
||||
t.Log("left:", cache.Count(), "items")
|
||||
}
|
||||
|
||||
func TestCache_Memory(t *testing.T) {
|
||||
testutils.StartMemoryStats(t)
|
||||
|
||||
var cache = NewCache()
|
||||
var count = 2_000_000
|
||||
for i := 0; i < count; i++ {
|
||||
cache.Write("a"+strconv.Itoa(i), 1, time.Now().Unix()+3600)
|
||||
}
|
||||
|
||||
t.Log(cache.Count())
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
for i := 0; i < count; i++ {
|
||||
if i%2 == 0 {
|
||||
cache.Delete("a" + strconv.Itoa(i))
|
||||
}
|
||||
}
|
||||
|
||||
t.Log(cache.Count())
|
||||
|
||||
cache.Count()
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
|
||||
func TestCache_IncreaseInt64(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
|
||||
var cache = NewCache()
|
||||
var unixTime = time.Now().Unix()
|
||||
|
||||
{
|
||||
cache.IncreaseInt64("a", 1, unixTime+3600, false)
|
||||
var item = cache.Read("a")
|
||||
t.Log(item)
|
||||
a.IsTrue(item.Value == int64(1))
|
||||
a.IsTrue(item.expiredAt == unixTime+3600)
|
||||
}
|
||||
{
|
||||
cache.IncreaseInt64("a", 1, unixTime+3600+1, true)
|
||||
var item = cache.Read("a")
|
||||
t.Log(item)
|
||||
a.IsTrue(item.Value == int64(2))
|
||||
a.IsTrue(item.expiredAt == unixTime+3600+1)
|
||||
}
|
||||
{
|
||||
cache.Write("b", 1, time.Now().Unix()+3600+2)
|
||||
t.Log(cache.Read("b"))
|
||||
}
|
||||
{
|
||||
cache.IncreaseInt64("b", 1, time.Now().Unix()+3600+3, false)
|
||||
t.Log(cache.Read("b"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_Read(t *testing.T) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
|
||||
var cache = NewCache(PiecesOption{Count: 32})
|
||||
|
||||
for i := 0; i < 10_000_000; i++ {
|
||||
cache.Write("HELLO_WORLD_"+strconv.Itoa(i), i, time.Now().Unix()+int64(i%10240)+1)
|
||||
}
|
||||
time.Sleep(10 * time.Second)
|
||||
|
||||
total := 0
|
||||
for _, piece := range cache.pieces {
|
||||
//t.Log(len(piece.m), "keys")
|
||||
total += len(piece.m)
|
||||
}
|
||||
t.Log(total, "total keys")
|
||||
|
||||
before := time.Now()
|
||||
for i := 0; i < 10_240; i++ {
|
||||
_ = cache.Read("HELLO_WORLD_" + strconv.Itoa(i))
|
||||
}
|
||||
t.Log(time.Since(before).Seconds()*1000, "ms")
|
||||
}
|
||||
|
||||
func TestCache_GC(t *testing.T) {
|
||||
var cache = NewCache(&PiecesOption{Count: 5})
|
||||
cache.Write("a", 1, time.Now().Unix()+1)
|
||||
cache.Write("b", 2, time.Now().Unix()+2)
|
||||
cache.Write("c", 3, time.Now().Unix()+3)
|
||||
cache.Write("d", 4, time.Now().Unix()+4)
|
||||
cache.Write("e", 5, time.Now().Unix()+10)
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 1000; i++ {
|
||||
cache.Write("f", 1, time.Now().Unix()+1)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
cache.GC()
|
||||
t.Log("items:", cache.Count())
|
||||
|
||||
if cache.Count() == 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
t.Log("now:", time.Now().Unix())
|
||||
for _, p := range cache.pieces {
|
||||
t.Log("expire list:", p.expiresList.Count(), p.expiresList)
|
||||
for k, v := range p.m {
|
||||
t.Log(k, v.Value, v.expiredAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_GC2(t *testing.T) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
|
||||
var cache1 = NewCache(NewPiecesOption(32))
|
||||
for i := 0; i < 1_000_000; i++ {
|
||||
cache1.Write(strconv.Itoa(i), i, time.Now().Unix()+int64(rands.Int(0, 10)))
|
||||
}
|
||||
|
||||
var cache2 = NewCache(NewPiecesOption(5))
|
||||
for i := 0; i < 1_000_000; i++ {
|
||||
cache2.Write(strconv.Itoa(i), i, time.Now().Unix()+int64(rands.Int(0, 10)))
|
||||
}
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
t.Log(cache1.Count(), "items", cache2.Count(), "items")
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheDestroy(t *testing.T) {
|
||||
var cache = NewCache()
|
||||
t.Log("count:", SharedManager.Count())
|
||||
cache.Destroy()
|
||||
t.Log("count:", SharedManager.Count())
|
||||
}
|
||||
|
||||
func BenchmarkNewCache(b *testing.B) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
|
||||
var cache = NewCache(NewPiecesOption(128))
|
||||
for i := 0; i < 2_000_000; i++ {
|
||||
cache.Write(strconv.Itoa(i), i, time.Now().Unix()+int64(rands.Int(10, 100)))
|
||||
}
|
||||
b.Log("start reading ...")
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
cache.Read(strconv.Itoa(rands.Int(0, 999999)))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkCache_Add(b *testing.B) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
|
||||
var cache = NewCache()
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache.Write(strconv.Itoa(i), i, utils.UnixTime()+int64(i%1024))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCache_Add_Parallel(b *testing.B) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
|
||||
var cache = NewCache()
|
||||
var i int64
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
var j = atomic.AddInt64(&i, 1)
|
||||
cache.Write(types.String(j), j, utils.UnixTime()+i%1024)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkNewCacheGC(b *testing.B) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
|
||||
var cache = NewCache(NewPiecesOption(1024))
|
||||
for i := 0; i < 3_000_000; i++ {
|
||||
cache.Write(strconv.Itoa(i), i, time.Now().Unix()+int64(rands.Int(0, 100)))
|
||||
}
|
||||
//b.Log(cache.pieces[0].Count())
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
cache.GC()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkNewCacheClean(b *testing.B) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
|
||||
var cache = NewCache(NewPiecesOption(128))
|
||||
for i := 0; i < 3_000_000; i++ {
|
||||
cache.Write(strconv.Itoa(i), i, time.Now().Unix()+int64(rands.Int(10, 100)))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
cache.Clean()
|
||||
}
|
||||
})
|
||||
}
|
||||
6
EdgeAPI/internal/utils/ttlcache/item.go
Normal file
6
EdgeAPI/internal/utils/ttlcache/item.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package ttlcache
|
||||
|
||||
type Item struct {
|
||||
Value interface{}
|
||||
expiredAt int64
|
||||
}
|
||||
60
EdgeAPI/internal/utils/ttlcache/manager.go
Normal file
60
EdgeAPI/internal/utils/ttlcache/manager.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package ttlcache
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/goman"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/zero"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var SharedManager = NewManager()
|
||||
|
||||
type Manager struct {
|
||||
ticker *time.Ticker
|
||||
locker sync.Mutex
|
||||
|
||||
cacheMap map[*Cache]zero.Zero
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
var manager = &Manager{
|
||||
ticker: time.NewTicker(2 * time.Second),
|
||||
cacheMap: map[*Cache]zero.Zero{},
|
||||
}
|
||||
|
||||
goman.New(func() {
|
||||
manager.init()
|
||||
})
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
func (this *Manager) init() {
|
||||
for range this.ticker.C {
|
||||
this.locker.Lock()
|
||||
for cache := range this.cacheMap {
|
||||
cache.GC()
|
||||
}
|
||||
this.locker.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Manager) Add(cache *Cache) {
|
||||
this.locker.Lock()
|
||||
this.cacheMap[cache] = zero.New()
|
||||
this.locker.Unlock()
|
||||
}
|
||||
|
||||
func (this *Manager) Remove(cache *Cache) {
|
||||
this.locker.Lock()
|
||||
delete(this.cacheMap, cache)
|
||||
this.locker.Unlock()
|
||||
}
|
||||
|
||||
func (this *Manager) Count() int {
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
return len(this.cacheMap)
|
||||
}
|
||||
20
EdgeAPI/internal/utils/ttlcache/option.go
Normal file
20
EdgeAPI/internal/utils/ttlcache/option.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package ttlcache
|
||||
|
||||
type OptionInterface interface {
|
||||
}
|
||||
|
||||
type PiecesOption struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
func NewPiecesOption(count int) *PiecesOption {
|
||||
return &PiecesOption{Count: count}
|
||||
}
|
||||
|
||||
type MaxItemsOption struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
func NewMaxItemsOption(count int) *MaxItemsOption {
|
||||
return &MaxItemsOption{Count: count}
|
||||
}
|
||||
138
EdgeAPI/internal/utils/ttlcache/piece.go
Normal file
138
EdgeAPI/internal/utils/ttlcache/piece.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package ttlcache
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils/expires"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Piece struct {
|
||||
m map[uint64]*Item
|
||||
expiresList *expires.List
|
||||
maxItems int
|
||||
lastGCTime int64
|
||||
|
||||
locker sync.RWMutex
|
||||
}
|
||||
|
||||
func NewPiece(maxItems int) *Piece {
|
||||
return &Piece{
|
||||
m: map[uint64]*Item{},
|
||||
expiresList: expires.NewSingletonList(),
|
||||
maxItems: maxItems,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Piece) Add(key uint64, item *Item) (ok bool) {
|
||||
this.locker.Lock()
|
||||
if len(this.m) >= this.maxItems {
|
||||
this.locker.Unlock()
|
||||
return
|
||||
}
|
||||
this.m[key] = item
|
||||
this.locker.Unlock()
|
||||
|
||||
this.expiresList.Add(key, item.expiredAt)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (this *Piece) IncreaseInt64(key uint64, delta int64, expiredAt int64, extend bool) (result int64) {
|
||||
this.locker.Lock()
|
||||
item, ok := this.m[key]
|
||||
if ok && item.expiredAt > time.Now().Unix() {
|
||||
result = types.Int64(item.Value) + delta
|
||||
item.Value = result
|
||||
if extend {
|
||||
item.expiredAt = expiredAt
|
||||
}
|
||||
this.expiresList.Add(key, expiredAt)
|
||||
} else {
|
||||
if len(this.m) < this.maxItems {
|
||||
result = delta
|
||||
this.m[key] = &Item{
|
||||
Value: delta,
|
||||
expiredAt: expiredAt,
|
||||
}
|
||||
this.expiresList.Add(key, expiredAt)
|
||||
}
|
||||
}
|
||||
this.locker.Unlock()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (this *Piece) Delete(key uint64) {
|
||||
this.expiresList.Remove(key)
|
||||
|
||||
this.locker.Lock()
|
||||
delete(this.m, key)
|
||||
this.locker.Unlock()
|
||||
}
|
||||
|
||||
func (this *Piece) Read(key uint64) (item *Item) {
|
||||
this.locker.RLock()
|
||||
item = this.m[key]
|
||||
if item != nil && item.expiredAt < utils.UnixTime() {
|
||||
item = nil
|
||||
}
|
||||
this.locker.RUnlock()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (this *Piece) Count() (count int) {
|
||||
this.locker.RLock()
|
||||
count = len(this.m)
|
||||
this.locker.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (this *Piece) GC() {
|
||||
var currentTime = time.Now().Unix()
|
||||
if this.lastGCTime == 0 {
|
||||
this.lastGCTime = currentTime - 3600
|
||||
}
|
||||
|
||||
var min = this.lastGCTime
|
||||
var max = currentTime
|
||||
if min > max {
|
||||
// 过去的时间比现在大,则从这一秒重新开始
|
||||
min = max
|
||||
}
|
||||
|
||||
for i := min; i <= max; i++ {
|
||||
var itemMap = this.expiresList.GC(i)
|
||||
if len(itemMap) > 0 {
|
||||
this.gcItemMap(itemMap)
|
||||
}
|
||||
}
|
||||
|
||||
this.lastGCTime = currentTime
|
||||
}
|
||||
|
||||
func (this *Piece) Clean() {
|
||||
this.locker.Lock()
|
||||
this.m = map[uint64]*Item{}
|
||||
this.locker.Unlock()
|
||||
|
||||
this.expiresList.Clean()
|
||||
}
|
||||
|
||||
func (this *Piece) Destroy() {
|
||||
this.locker.Lock()
|
||||
this.m = nil
|
||||
this.locker.Unlock()
|
||||
|
||||
this.expiresList.Clean()
|
||||
}
|
||||
|
||||
func (this *Piece) gcItemMap(itemMap expires.ItemMap) {
|
||||
this.locker.Lock()
|
||||
for key := range itemMap {
|
||||
delete(this.m, key)
|
||||
}
|
||||
this.locker.Unlock()
|
||||
}
|
||||
60
EdgeAPI/internal/utils/ttlcache/piece_test.go
Normal file
60
EdgeAPI/internal/utils/ttlcache/piece_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package ttlcache
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/rands"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPiece_Add(t *testing.T) {
|
||||
piece := NewPiece(10)
|
||||
piece.Add(1, &Item{expiredAt: time.Now().Unix() + 3600})
|
||||
piece.Add(2, &Item{})
|
||||
piece.Add(3, &Item{})
|
||||
piece.Delete(3)
|
||||
for key, item := range piece.m {
|
||||
t.Log(key, item.Value)
|
||||
}
|
||||
t.Log(piece.Read(1))
|
||||
}
|
||||
|
||||
func TestPiece_MaxItems(t *testing.T) {
|
||||
piece := NewPiece(10)
|
||||
for i := 0; i < 1000; i++ {
|
||||
piece.Add(uint64(i), &Item{expiredAt: time.Now().Unix() + 3600})
|
||||
}
|
||||
t.Log(len(piece.m))
|
||||
}
|
||||
|
||||
func TestPiece_GC(t *testing.T) {
|
||||
piece := NewPiece(10)
|
||||
piece.Add(1, &Item{Value: 1, expiredAt: time.Now().Unix() + 1})
|
||||
piece.Add(2, &Item{Value: 2, expiredAt: time.Now().Unix() + 1})
|
||||
piece.Add(3, &Item{Value: 3, expiredAt: time.Now().Unix() + 1})
|
||||
t.Log("before gc ===")
|
||||
for key, item := range piece.m {
|
||||
t.Log(key, item.Value)
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
piece.GC()
|
||||
|
||||
t.Log("after gc ===")
|
||||
for key, item := range piece.m {
|
||||
t.Log(key, item.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPiece_GC2(t *testing.T) {
|
||||
piece := NewPiece(10)
|
||||
for i := 0; i < 10_000; i++ {
|
||||
piece.Add(uint64(i), &Item{Value: 1, expiredAt: time.Now().Unix() + int64(rands.Int(1, 10))})
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
before := time.Now()
|
||||
piece.GC()
|
||||
t.Log(time.Since(before).Seconds()*1000, "ms")
|
||||
t.Log(piece.Count())
|
||||
}
|
||||
7
EdgeAPI/internal/utils/ttlcache/utils.go
Normal file
7
EdgeAPI/internal/utils/ttlcache/utils.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package ttlcache
|
||||
|
||||
import "github.com/cespare/xxhash"
|
||||
|
||||
func HashKey(key []byte) uint64 {
|
||||
return xxhash.Sum64(key)
|
||||
}
|
||||
13
EdgeAPI/internal/utils/ttlcache/utils_test.go
Normal file
13
EdgeAPI/internal/utils/ttlcache/utils_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package ttlcache
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkHashKey(b *testing.B) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
for i := 0; i < b.N; i++ {
|
||||
HashKey([]byte("HELLO,WORLDHELLO,WORLDHELLO,WORLDHELLO,WORLDHELLO,WORLDHELLO,WORLD"))
|
||||
}
|
||||
}
|
||||
63
EdgeAPI/internal/utils/unix_time.go
Normal file
63
EdgeAPI/internal/utils/unix_time.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/goman"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"time"
|
||||
)
|
||||
|
||||
var unixTime = time.Now().Unix()
|
||||
var unixTimeMilli = time.Now().UnixMilli()
|
||||
var unixTimeMilliString = types.String(unixTimeMilli)
|
||||
|
||||
func init() {
|
||||
var ticker = time.NewTicker(200 * time.Millisecond)
|
||||
goman.New(func() {
|
||||
for range ticker.C {
|
||||
unixTime = time.Now().Unix()
|
||||
unixTimeMilli = time.Now().UnixMilli()
|
||||
unixTimeMilliString = types.String(unixTimeMilli)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// UnixTime 最快获取时间戳的方式,通常用在不需要特别精确时间戳的场景
|
||||
func UnixTime() int64 {
|
||||
return unixTime
|
||||
}
|
||||
|
||||
// FloorUnixTime 取整
|
||||
func FloorUnixTime(seconds int) int64 {
|
||||
return UnixTime() / int64(seconds) * int64(seconds)
|
||||
}
|
||||
|
||||
// CeilUnixTime 取整并加1
|
||||
func CeilUnixTime(seconds int) int64 {
|
||||
return UnixTime()/int64(seconds)*int64(seconds) + int64(seconds)
|
||||
}
|
||||
|
||||
// NextMinuteUnixTime 获取下一分钟开始的时间戳
|
||||
func NextMinuteUnixTime() int64 {
|
||||
return CeilUnixTime(60)
|
||||
}
|
||||
|
||||
// UnixTimeMilli 获取时间戳,精确到毫秒
|
||||
func UnixTimeMilli() int64 {
|
||||
return unixTimeMilli
|
||||
}
|
||||
|
||||
func UnixTimeMilliString() (int64, string) {
|
||||
return unixTimeMilli, unixTimeMilliString
|
||||
}
|
||||
|
||||
// GMTUnixTime 计算GMT时间戳
|
||||
func GMTUnixTime(timestamp int64) int64 {
|
||||
_, offset := time.Now().Zone()
|
||||
return timestamp - int64(offset)
|
||||
}
|
||||
|
||||
// GMTTime 计算GMT时间
|
||||
func GMTTime(t time.Time) time.Time {
|
||||
_, offset := time.Now().Zone()
|
||||
return t.Add(-time.Duration(offset) * time.Second)
|
||||
}
|
||||
91
EdgeAPI/internal/utils/unzip.go
Normal file
91
EdgeAPI/internal/utils/unzip.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Unzip struct {
|
||||
zipFile string
|
||||
targetDir string
|
||||
}
|
||||
|
||||
func NewUnzip(zipFile string, targetDir string) *Unzip {
|
||||
return &Unzip{
|
||||
zipFile: zipFile,
|
||||
targetDir: targetDir,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Unzip) Run() error {
|
||||
if len(this.zipFile) == 0 {
|
||||
return errors.New("zip file should not be empty")
|
||||
}
|
||||
if len(this.targetDir) == 0 {
|
||||
return errors.New("target dir should not be empty")
|
||||
}
|
||||
|
||||
reader, err := zip.OpenReader(this.zipFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = reader.Close()
|
||||
}()
|
||||
|
||||
for _, file := range reader.File {
|
||||
info := file.FileInfo()
|
||||
target := this.targetDir + "/" + file.Name
|
||||
|
||||
// 目录
|
||||
if info.IsDir() {
|
||||
stat, err := os.Stat(target)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
} else {
|
||||
err = os.MkdirAll(target, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if !stat.IsDir() {
|
||||
err = os.MkdirAll(target, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 文件
|
||||
err := func(file *zip.File, target string) error {
|
||||
fileReader, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = fileReader.Close()
|
||||
}()
|
||||
|
||||
fileWriter, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, file.FileInfo().Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = fileWriter.Close()
|
||||
}()
|
||||
|
||||
_, err = io.Copy(fileWriter, fileReader)
|
||||
return err
|
||||
}(file, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
16
EdgeAPI/internal/utils/unzip_test.go
Normal file
16
EdgeAPI/internal/utils/unzip_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnzip_Run(t *testing.T) {
|
||||
unzip := NewUnzip(Tea.Root+"/deploy/edge-node-v0.0.1.zip", Tea.Root+"/deploy/")
|
||||
err := unzip.Run()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("OK")
|
||||
}
|
||||
24
EdgeAPI/internal/utils/version.go
Normal file
24
EdgeAPI/internal/utils/version.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// VersionToLong 计算版本代号
|
||||
func VersionToLong(version string) uint32 {
|
||||
var countDots = strings.Count(version, ".")
|
||||
if countDots == 2 {
|
||||
version += ".0"
|
||||
} else if countDots == 1 {
|
||||
version += ".0.0"
|
||||
} else if countDots == 0 {
|
||||
version += ".0.0.0"
|
||||
}
|
||||
var ip = net.ParseIP(version)
|
||||
if ip == nil || ip.To4() == nil {
|
||||
return 0
|
||||
}
|
||||
return binary.BigEndian.Uint32(ip.To4())
|
||||
}
|
||||
31
EdgeAPI/internal/utils/version_test.go
Normal file
31
EdgeAPI/internal/utils/version_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package utils
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNodeStatus_ComputerBuildVersionCode(t *testing.T) {
|
||||
{
|
||||
t.Log("", VersionToLong(""))
|
||||
}
|
||||
|
||||
{
|
||||
t.Log("0.0.6", VersionToLong("0.0.6"))
|
||||
}
|
||||
|
||||
{
|
||||
t.Log("0.0.6.1", VersionToLong("0.0.6.1"))
|
||||
}
|
||||
|
||||
{
|
||||
t.Log("0.0.7", VersionToLong("0.0.7"))
|
||||
}
|
||||
|
||||
{
|
||||
t.Log("0.7", VersionToLong("0.7"))
|
||||
}
|
||||
{
|
||||
t.Log("7", VersionToLong("7"))
|
||||
}
|
||||
{
|
||||
t.Log("7.0.1", VersionToLong("7.0.1"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user