This commit is contained in:
unknown
2026-02-04 20:27:13 +08:00
commit 3b042d1dad
9410 changed files with 1488147 additions and 0 deletions

View 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
}

View 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)
}
}

View 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)
}

View 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))
}
}

View 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 ""
}

View 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
}

View 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)
}

View 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"))
}

View File

@@ -0,0 +1,11 @@
package utils
import (
"github.com/iwind/TeaGo/logs"
)
// 打印错误
func PrintError(err error) {
// TODO 记录调用的文件名、行数
logs.Println("[ERROR]" + err.Error())
}

View 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
}

View 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())
}

View 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,
}
}

View 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)
}

View 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)
}

View 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)
}

View 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
}

View 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)
}
}
})
}

View 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()
}

View 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()
}
}
}
}
}

View 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
}

View 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
}

View 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))
}

View 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
}

View 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))
}

View 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()
}

View 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"))
}
}

View 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)
}

View 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"))
}

View 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])
}

View 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))
}
}

View 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"`
}

View 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)://")
)

View 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"))
}

View 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
}

View 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
}

View 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() {
}

View 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)
}

View 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()
}

View 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
}

View 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")
}

View 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())
}
}

View 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)))
}

View 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)
}
}

View 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
)

View 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)
}

View 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
}

View 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))
}
}

View 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
}

View 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())
}

View 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
}

View 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)
}
}

View 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)
}
}()
}

View 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()
}

View 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
}

View 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)
}
}

View 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()
}
}

View 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()
}
})
}

View File

@@ -0,0 +1,6 @@
package ttlcache
type Item struct {
Value interface{}
expiredAt int64
}

View 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)
}

View 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}
}

View 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()
}

View 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())
}

View File

@@ -0,0 +1,7 @@
package ttlcache
import "github.com/cespare/xxhash"
func HashKey(key []byte) uint64 {
return xxhash.Sum64(key)
}

View 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"))
}
}

View 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)
}

View 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
}

View 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")
}

View 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())
}

View 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"))
}
}