feat: add registry config, image upload/download, and OCI format support
Backend: - Add registry_address configuration API (GET/POST) - Add tar image upload with OCI and Docker format support - Add image download with streaming optimization - Fix blob download using c.Send (Fiber v3 SendStream bug) - Add registry_address prefix stripping for all OCI v2 endpoints - Add AGENTS.md for project documentation Frontend: - Add settings store with Snackbar notifications - Add image upload dialog with progress bar - Add download state tracking with multi-stage feedback - Replace alert() with MUI Snackbar messages - Display image names without registry_address prefix 🤖 Generated with [Qoder](https://qoder.com)
This commit is contained in:
38
pkg/tool/ctx.go
Normal file
38
pkg/tool/ctx.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Timeout(seconds ...int) (ctx context.Context) {
|
||||
var (
|
||||
duration time.Duration
|
||||
)
|
||||
|
||||
if len(seconds) > 0 && seconds[0] > 0 {
|
||||
duration = time.Duration(seconds[0]) * time.Second
|
||||
} else {
|
||||
duration = time.Duration(30) * time.Second
|
||||
}
|
||||
|
||||
ctx, _ = context.WithTimeout(context.Background(), duration)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TimeoutCtx(ctx context.Context, seconds ...int) context.Context {
|
||||
var (
|
||||
duration time.Duration
|
||||
)
|
||||
|
||||
if len(seconds) > 0 && seconds[0] > 0 {
|
||||
duration = time.Duration(seconds[0]) * time.Second
|
||||
} else {
|
||||
duration = time.Duration(30) * time.Second
|
||||
}
|
||||
|
||||
nctx, _ := context.WithTimeout(ctx, duration)
|
||||
|
||||
return nctx
|
||||
}
|
||||
157
pkg/tool/file.go
Normal file
157
pkg/tool/file.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// FileMD5 calculate file md5
|
||||
// - if file not exist, return ""
|
||||
// - if _path is dir, return ""
|
||||
func FileMD5(_path string) string {
|
||||
// 检查文件是否存在
|
||||
fileInfo, err := os.Stat(_path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return ""
|
||||
}
|
||||
// 其他错误也返回空字符串
|
||||
return ""
|
||||
}
|
||||
|
||||
// 检查是否是目录
|
||||
if fileInfo.IsDir() {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 打开文件
|
||||
file, err := os.Open(_path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 创建MD5哈希计算器
|
||||
hash := md5.New()
|
||||
|
||||
// 将文件内容复制到哈希计算器
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 计算并返回MD5哈希值(十六进制字符串)
|
||||
return fmt.Sprintf("%x", hash.Sum(nil))
|
||||
}
|
||||
|
||||
// CopyFile copies a file from src to dst.
|
||||
// Returns an error if source/destination are the same, source isn't a regular file,
|
||||
// or any step in the copy process fails.
|
||||
func CopyFile(src, dst string) error {
|
||||
// Open source file
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open source: %w", err)
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// Get source file metadata
|
||||
srcInfo, err := srcFile.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get source info: %w", err)
|
||||
}
|
||||
|
||||
// Verify source is a regular file
|
||||
if !srcInfo.Mode().IsRegular() {
|
||||
return fmt.Errorf("source is not a regular file")
|
||||
}
|
||||
|
||||
// Check if source and destination are the same file
|
||||
if same, err := sameFile(src, dst, srcInfo); same {
|
||||
return fmt.Errorf("source and destination are the same file")
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create destination directory structure
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||||
return fmt.Errorf("failed to create destination directory: %w", err)
|
||||
}
|
||||
|
||||
// Create destination file with source permissions
|
||||
dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create destination: %w", err)
|
||||
}
|
||||
|
||||
// Copy contents and handle destination close errors
|
||||
_, err = io.Copy(dstFile, srcFile)
|
||||
if closeErr := dstFile.Close(); closeErr != nil && err == nil {
|
||||
err = fmt.Errorf("failed to close destination: %w", closeErr)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("copy failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sameFile checks if src and dst refer to the same file using device/inode numbers
|
||||
func sameFile(src, dst string, srcInfo os.FileInfo) (bool, error) {
|
||||
dstInfo, err := os.Stat(dst)
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil // Destination doesn't exist
|
||||
}
|
||||
if err != nil {
|
||||
return false, err // Other errors
|
||||
}
|
||||
return os.SameFile(srcInfo, dstInfo), nil
|
||||
}
|
||||
|
||||
func CopyDir(src, dst string) error {
|
||||
// todo: copy src dir to dst dir recursively
|
||||
// if dst is not exist, create it
|
||||
// if file exist, overwrite it
|
||||
srcInfo, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("stat src dir failed: %w", err)
|
||||
}
|
||||
if !srcInfo.IsDir() {
|
||||
return fmt.Errorf("source is not a directory")
|
||||
}
|
||||
|
||||
// Create destination directory if it does not exist
|
||||
if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil {
|
||||
return fmt.Errorf("failed to create destination directory: %w", err)
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read source directory: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
srcPath := filepath.Join(src, entry.Name())
|
||||
dstPath := filepath.Join(dst, entry.Name())
|
||||
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get info for %s: %w", srcPath, err)
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
// Recursively copy subdirectory
|
||||
if err := CopyDir(srcPath, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Copy file, overwrite if exists
|
||||
if err := CopyFile(srcPath, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
76
pkg/tool/human.go
Normal file
76
pkg/tool/human.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package tool
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
KB = 1 << (10 * iota) // 1 KB = 1024 bytes
|
||||
MB // 1 MB = 1024 KB
|
||||
GB // 1 GB = 1024 MB
|
||||
TB // 1 TB = 1024 GB
|
||||
PB // 1 PB = 1024 TB
|
||||
)
|
||||
|
||||
func HumanDuration(nano int64) string {
|
||||
duration := float64(nano)
|
||||
unit := "ns"
|
||||
if duration >= 1000 {
|
||||
duration /= 1000
|
||||
unit = "us"
|
||||
}
|
||||
|
||||
if duration >= 1000 {
|
||||
duration /= 1000
|
||||
unit = "ms"
|
||||
}
|
||||
|
||||
if duration >= 1000 {
|
||||
duration /= 1000
|
||||
unit = " s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%6.2f%s", duration, unit)
|
||||
}
|
||||
|
||||
func HumanSize(size int64) string {
|
||||
|
||||
switch {
|
||||
case size >= PB:
|
||||
return fmt.Sprintf("%.2f PB", float64(size)/PB)
|
||||
case size >= TB:
|
||||
return fmt.Sprintf("%.2f TB", float64(size)/TB)
|
||||
case size >= GB:
|
||||
return fmt.Sprintf("%.2f GB", float64(size)/GB)
|
||||
case size >= MB:
|
||||
return fmt.Sprintf("%.2f MB", float64(size)/MB)
|
||||
case size >= KB:
|
||||
return fmt.Sprintf("%.2f KB", float64(size)/KB)
|
||||
default:
|
||||
return fmt.Sprintf("%d bytes", size)
|
||||
}
|
||||
}
|
||||
|
||||
// BytesToUnit 将字节转换为指定单位
|
||||
func BytesToUnit(bytes int64, unit float64) float64 {
|
||||
return float64(bytes) / unit
|
||||
}
|
||||
|
||||
// BytesToKB 转换为 KB
|
||||
func BytesToKB(bytes int64) float64 {
|
||||
return BytesToUnit(bytes, KB)
|
||||
}
|
||||
|
||||
// BytesToMB 转换为 MB
|
||||
func BytesToMB(bytes int64) float64 {
|
||||
return BytesToUnit(bytes, MB)
|
||||
}
|
||||
|
||||
// BytesToGB 转换为 GB
|
||||
func BytesToGB(bytes int64) float64 {
|
||||
return BytesToUnit(bytes, GB)
|
||||
}
|
||||
|
||||
// BytesToTB 转换为 TB
|
||||
func BytesToTB(bytes int64) float64 {
|
||||
return BytesToUnit(bytes, TB)
|
||||
}
|
||||
229
pkg/tool/ip.go
Normal file
229
pkg/tool/ip.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
var (
|
||||
privateIPv4Blocks []*net.IPNet
|
||||
privateIPv6Blocks []*net.IPNet
|
||||
)
|
||||
|
||||
func init() {
|
||||
// IPv4私有地址段
|
||||
for _, cidr := range []string{
|
||||
"10.0.0.0/8", // A类私有地址
|
||||
"172.16.0.0/12", // B类私有地址
|
||||
"192.168.0.0/16", // C类私有地址
|
||||
"169.254.0.0/16", // 链路本地地址
|
||||
"127.0.0.0/8", // 环回地址
|
||||
} {
|
||||
_, block, _ := net.ParseCIDR(cidr)
|
||||
privateIPv4Blocks = append(privateIPv4Blocks, block)
|
||||
}
|
||||
|
||||
// IPv6私有地址段
|
||||
for _, cidr := range []string{
|
||||
"fc00::/7", // 唯一本地地址
|
||||
"fe80::/10", // 链路本地地址
|
||||
"::1/128", // 环回地址
|
||||
} {
|
||||
_, block, _ := net.ParseCIDR(cidr)
|
||||
privateIPv6Blocks = append(privateIPv6Blocks, block)
|
||||
}
|
||||
}
|
||||
|
||||
func IsPrivateIP(ipStr string) bool {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 处理IPv4和IPv4映射的IPv6地址
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
for _, block := range privateIPv4Blocks {
|
||||
if block.Contains(ip4) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 处理IPv6地址
|
||||
for _, block := range privateIPv6Blocks {
|
||||
if block.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IP2Int(ip net.IP) uint32 {
|
||||
if ip == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
ip = ip.To4()
|
||||
if ip == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return binary.BigEndian.Uint32(ip)
|
||||
}
|
||||
|
||||
func Int2IP(ip uint32) net.IP {
|
||||
data := make(net.IP, 4)
|
||||
binary.BigEndian.PutUint32(data, ip)
|
||||
return data
|
||||
}
|
||||
|
||||
func IPStr2Int(ipStr string) *uint32 {
|
||||
ip := IP2Int(net.ParseIP(ipStr))
|
||||
if ip == 0 {
|
||||
return nil
|
||||
}
|
||||
return &ip
|
||||
}
|
||||
|
||||
func Int2IPStr(ip uint32) string {
|
||||
return Int2IP(ip).String()
|
||||
}
|
||||
|
||||
func GetLastIP4(cidr string) (lastIP4 uint32, err error) {
|
||||
ip, ipNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
firstIP4 := IP2Int(ip.Mask(ipNet.Mask))
|
||||
ipNetMaskInt := binary.BigEndian.Uint32(ipNet.Mask)
|
||||
lastIP4 = firstIP4 | ^ipNetMaskInt
|
||||
return
|
||||
}
|
||||
|
||||
func IsCIDRConflict(cidr1, cidr2 string) (conflict bool, err error) {
|
||||
_, ipNet1, err := net.ParseCIDR(cidr1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, ipNet2, err := net.ParseCIDR(cidr2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ipNet2.Contains(ipNet1.IP) || ipNet1.Contains(ipNet2.IP) {
|
||||
conflict = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func GetCIDRs(startCIDR, endCIDR string, mask uint8) (cidrs []string, err error) {
|
||||
cidrs = append(cidrs, startCIDR)
|
||||
|
||||
currentCIDR := startCIDR
|
||||
for {
|
||||
lastIP4, err := GetLastIP4(currentCIDR)
|
||||
if err != nil {
|
||||
return cidrs, err
|
||||
}
|
||||
|
||||
nextCIDR := Int2IPStr(lastIP4+1) + fmt.Sprintf("/%d", mask)
|
||||
cidrs = append(cidrs, nextCIDR)
|
||||
|
||||
conflict, err := IsCIDRConflict(nextCIDR, endCIDR)
|
||||
if err != nil {
|
||||
return cidrs, err
|
||||
}
|
||||
if conflict {
|
||||
break
|
||||
}
|
||||
|
||||
currentCIDR = nextCIDR
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func GetLocalIP() (ip string, err error) {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, iface := range ifaces {
|
||||
if iface.Name != "eth0" {
|
||||
continue
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP.String()
|
||||
if ip == "192.168.88.88" {
|
||||
continue
|
||||
}
|
||||
return ip, err
|
||||
case *net.IPAddr:
|
||||
ip = v.IP.String()
|
||||
if ip == "192.168.88.88" {
|
||||
continue
|
||||
}
|
||||
return ip, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ip = "127.0.0.1"
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func LookupIP(host string) (ip string, err error) {
|
||||
ips, err := net.LookupIP(host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, i := range ips {
|
||||
if ipv4 := i.To4(); ipv4 != nil {
|
||||
ip = ipv4.String()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
func LookupIPv4(host string) (uint32, error) {
|
||||
ips, err := net.LookupIP(host)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
for _, i := range ips {
|
||||
if ipv4 := i.To4(); ipv4 != nil {
|
||||
return binary.BigEndian.Uint32(ipv4), nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, errors.New("host not found " + host)
|
||||
}
|
||||
|
||||
func ResolveIPv4(host string) (uint32, error) {
|
||||
addr, err := net.ResolveIPAddr("ip4", host)
|
||||
if err == nil && addr != nil {
|
||||
addrV4 := binary.BigEndian.Uint32(addr.IP.To4())
|
||||
|
||||
return addrV4, err
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
76
pkg/tool/loadash.go
Normal file
76
pkg/tool/loadash.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package tool
|
||||
|
||||
import "math"
|
||||
|
||||
func Map[T, R any](vals []T, fn func(item T, index int) R) []R {
|
||||
var result = make([]R, len(vals))
|
||||
for idx, v := range vals {
|
||||
result[idx] = fn(v, idx)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func Chunk[T any](vals []T, size int) [][]T {
|
||||
if size <= 0 {
|
||||
panic("Second parameter must be greater than 0")
|
||||
}
|
||||
|
||||
chunksNum := len(vals) / size
|
||||
if len(vals)%size != 0 {
|
||||
chunksNum += 1
|
||||
}
|
||||
|
||||
result := make([][]T, 0, chunksNum)
|
||||
|
||||
for i := 0; i < chunksNum; i++ {
|
||||
last := (i + 1) * size
|
||||
if last > len(vals) {
|
||||
last = len(vals)
|
||||
}
|
||||
result = append(result, vals[i*size:last:last])
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 对 vals 取样 x 个
|
||||
func Sample[T any](vals []T, x int) []T {
|
||||
if x < 0 {
|
||||
panic("Second parameter can't be negative")
|
||||
}
|
||||
|
||||
n := len(vals)
|
||||
if n == 0 {
|
||||
return []T{}
|
||||
}
|
||||
|
||||
if x >= n {
|
||||
return vals
|
||||
}
|
||||
|
||||
// 处理x=1的特殊情况
|
||||
if x == 1 {
|
||||
return []T{vals[(n-1)/2]}
|
||||
}
|
||||
|
||||
// 计算采样步长并生成结果数组
|
||||
step := float64(n-1) / float64(x-1)
|
||||
result := make([]T, x)
|
||||
|
||||
for i := 0; i < x; i++ {
|
||||
// 计算采样位置并四舍五入
|
||||
pos := float64(i) * step
|
||||
index := int(math.Round(pos))
|
||||
result[i] = vals[index]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func If[T any](cond bool, trueVal, falseVal T) T {
|
||||
if cond {
|
||||
return trueVal
|
||||
}
|
||||
|
||||
return falseVal
|
||||
}
|
||||
57
pkg/tool/mask.go
Normal file
57
pkg/tool/mask.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func MaskJWT(token string) string {
|
||||
if token == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
return MaskString(token, 5, 5, -1, "*")
|
||||
}
|
||||
|
||||
h, p, s := parts[0], parts[1], parts[2]
|
||||
h, p, s = MaskString(h, 5, 5, 8, "*"), MaskString(p, 5, 5, 8, "*"), MaskString(s, 5, 5, 8, "*")
|
||||
return h + "." + p + "." + s
|
||||
}
|
||||
|
||||
// MaskString 将字符串中间部分替换为 maskChar
|
||||
//
|
||||
// start: 保留前 start 个字符
|
||||
// end: 保留后 end 个字符
|
||||
// maskLen: 中间打码长度 (小于 0 标识保持原有长度)
|
||||
// maskChar: 打码字符
|
||||
func MaskString(s string, start, end, maskLen int, maskChar string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
totalLen := len(s)
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
|
||||
if end < 0 {
|
||||
end = 0
|
||||
}
|
||||
|
||||
if maskChar == "" {
|
||||
maskChar = "*"
|
||||
}
|
||||
|
||||
maxMaskLen := totalLen - start - end
|
||||
if maxMaskLen <= 0 {
|
||||
return strings.Repeat(maskChar, totalLen)
|
||||
}
|
||||
|
||||
if maskLen < 0 || maskLen > maxMaskLen {
|
||||
maskLen = maxMaskLen
|
||||
}
|
||||
|
||||
startPart, endPart := s[:start], s[totalLen-end:]
|
||||
return startPart + strings.Repeat(maskChar, maskLen) + endPart
|
||||
}
|
||||
76
pkg/tool/must.go
Normal file
76
pkg/tool/must.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func Must(errs ...error) {
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
log.Panic(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func MustWithData[T any](data T, err error) T {
|
||||
Must(err)
|
||||
return data
|
||||
}
|
||||
|
||||
func MustStop(ctx context.Context, stopFns ...func(ctx context.Context) error) {
|
||||
getFunctionName := func(i interface{}) string {
|
||||
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
|
||||
}
|
||||
|
||||
if len(stopFns) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ok := make(chan struct{})
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
for _, fn := range stopFns {
|
||||
|
||||
if fn != nil {
|
||||
wg.Add(1)
|
||||
|
||||
go func(c context.Context) {
|
||||
defer func() {
|
||||
wg.Done()
|
||||
log.Printf("stop func[%s] done", getFunctionName(fn))
|
||||
}()
|
||||
|
||||
if err := fn(c); err != nil {
|
||||
log.Printf("stop function failed, err = %s", err.Error())
|
||||
}
|
||||
}(ctx)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Fatal("stop function timeout, force down")
|
||||
case _, _ = <-ok:
|
||||
log.Printf("shutdown gracefully...")
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
close(ok)
|
||||
}
|
||||
|
||||
func IgnoreError[T any](item T, err error) T {
|
||||
if err != nil {
|
||||
log.Printf("[W] !!! ignore error: %s", err.Error())
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
85
pkg/tool/password.go
Normal file
85
pkg/tool/password.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
const (
|
||||
EncryptHeader string = "pbkdf2:sha256" // 用户密码加密
|
||||
)
|
||||
|
||||
func NewPassword(password string) string {
|
||||
return EncryptPassword(password, RandomString(8), int(RandomInt(50000)+100000))
|
||||
}
|
||||
|
||||
func ComparePassword(in, db string) bool {
|
||||
strs := strings.Split(db, "$")
|
||||
if len(strs) != 3 {
|
||||
log.Printf("[E] password in db invalid: %s", db)
|
||||
return false
|
||||
}
|
||||
|
||||
encs := strings.Split(strs[0], ":")
|
||||
if len(encs) != 3 {
|
||||
log.Printf("[E] password in db invalid: %s", db)
|
||||
return false
|
||||
}
|
||||
|
||||
encIteration, err := strconv.Atoi(encs[2])
|
||||
if err != nil {
|
||||
log.Printf("[E] password in db invalid: %s, convert iter err: %s", db, err)
|
||||
return false
|
||||
}
|
||||
|
||||
return EncryptPassword(in, strs[1], encIteration) == db
|
||||
}
|
||||
|
||||
func EncryptPassword(password, salt string, iter int) string {
|
||||
hash := pbkdf2.Key([]byte(password), []byte(salt), iter, 32, sha256.New)
|
||||
encrypted := hex.EncodeToString(hash)
|
||||
return fmt.Sprintf("%s:%d$%s$%s", EncryptHeader, iter, salt, encrypted)
|
||||
}
|
||||
|
||||
func CheckPassword(password string) error {
|
||||
if len(password) < 8 || len(password) > 32 {
|
||||
return errors.New("密码长度不符合")
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
match bool
|
||||
patternList = []string{`[0-9]+`, `[a-z]+`, `[A-Z]+`, `[!@#%]+`} //, `[~!@#$%^&*?_-]+`}
|
||||
matchAccount = 0
|
||||
tips = []string{"缺少数字", "缺少小写字母", "缺少大写字母", "缺少'!@#%'"}
|
||||
locktips = make([]string, 0)
|
||||
)
|
||||
|
||||
for idx, pattern := range patternList {
|
||||
match, err = regexp.MatchString(pattern, password)
|
||||
if err != nil {
|
||||
log.Printf("[E] regex match string err, reg_str: %s, err: %v", pattern, err)
|
||||
return errors.New("密码强度不够")
|
||||
}
|
||||
|
||||
if match {
|
||||
matchAccount++
|
||||
} else {
|
||||
locktips = append(locktips, tips[idx])
|
||||
}
|
||||
}
|
||||
|
||||
if matchAccount < 3 {
|
||||
return fmt.Errorf("密码强度不够, 可能 %s", strings.Join(locktips, ", "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
20
pkg/tool/password_test.go
Normal file
20
pkg/tool/password_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package tool
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEncPassword(t *testing.T) {
|
||||
password := "123456"
|
||||
|
||||
result := EncryptPassword(password, RandomString(8), 50000)
|
||||
|
||||
t.Logf("sum => %s", result)
|
||||
}
|
||||
|
||||
func TestPassword(t *testing.T) {
|
||||
p := "wahaha@123"
|
||||
p = NewPassword(p)
|
||||
t.Logf("password => %s", p)
|
||||
|
||||
result := ComparePassword("wahaha@123", p)
|
||||
t.Logf("compare result => %v", result)
|
||||
}
|
||||
75
pkg/tool/random.go
Normal file
75
pkg/tool/random.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
mrand "math/rand"
|
||||
)
|
||||
|
||||
var (
|
||||
letters = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
letterNum = []byte("0123456789")
|
||||
letterLow = []byte("abcdefghijklmnopqrstuvwxyz")
|
||||
letterCap = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
letterSyb = []byte("!@#$%^&*()_+-=")
|
||||
adjectives = []string{
|
||||
"开心的", "灿烂的", "温暖的", "阳光的", "活泼的",
|
||||
"聪明的", "优雅的", "幸运的", "甜蜜的", "勇敢的",
|
||||
"宁静的", "热情的", "温柔的", "幽默的", "坚强的",
|
||||
"迷人的", "神奇的", "快乐的", "健康的", "自由的",
|
||||
"梦幻的", "勤劳的", "真诚的", "浪漫的", "自信的",
|
||||
}
|
||||
|
||||
plants = []string{
|
||||
"苹果", "香蕉", "橘子", "葡萄", "草莓",
|
||||
"西瓜", "樱桃", "菠萝", "柠檬", "蜜桃",
|
||||
"蓝莓", "芒果", "石榴", "甜瓜", "雪梨",
|
||||
"番茄", "南瓜", "土豆", "青椒", "洋葱",
|
||||
"黄瓜", "萝卜", "豌豆", "玉米", "蘑菇",
|
||||
"菠菜", "茄子", "芹菜", "莲藕", "西兰花",
|
||||
}
|
||||
)
|
||||
|
||||
func RandomInt(max int64) int64 {
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(max))
|
||||
return num.Int64()
|
||||
}
|
||||
|
||||
func RandomString(length int) string {
|
||||
result := make([]byte, length)
|
||||
for i := 0; i < length; i++ {
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
||||
result[i] = letters[num.Int64()]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func RandomPassword(length int, withSymbol bool) string {
|
||||
result := make([]byte, length)
|
||||
kind := 3
|
||||
if withSymbol {
|
||||
kind++
|
||||
}
|
||||
|
||||
for i := 0; i < length; i++ {
|
||||
switch i % kind {
|
||||
case 0:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterNum))))
|
||||
result[i] = letterNum[num.Int64()]
|
||||
case 1:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterLow))))
|
||||
result[i] = letterLow[num.Int64()]
|
||||
case 2:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterCap))))
|
||||
result[i] = letterCap[num.Int64()]
|
||||
case 3:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterSyb))))
|
||||
result[i] = letterSyb[num.Int64()]
|
||||
}
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func RandomName() string {
|
||||
return adjectives[mrand.Intn(len(adjectives))] + plants[mrand.Intn(len(plants))]
|
||||
}
|
||||
71
pkg/tool/string.go
Normal file
71
pkg/tool/string.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func BytesToString(b []byte) string {
|
||||
return unsafe.String(unsafe.SliceData(b), len(b))
|
||||
}
|
||||
|
||||
func StringToBytes(s string) []byte {
|
||||
return unsafe.Slice(unsafe.StringData(s), len(s))
|
||||
}
|
||||
|
||||
func CopyString(s string) string {
|
||||
return string([]byte(s))
|
||||
}
|
||||
|
||||
// ToSnakeCase 将给定的字符串转换为 snake_case 风格。
|
||||
//
|
||||
// 参数:
|
||||
//
|
||||
// str: 待转换的字符串,只考虑 ASCII 字符。
|
||||
func ToSnakeCase(str string) string {
|
||||
if str == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var (
|
||||
sb strings.Builder
|
||||
isLower = func(c byte) bool {
|
||||
return c >= 'a' && c <= 'z'
|
||||
}
|
||||
isUpper = func(c byte) bool {
|
||||
return c >= 'A' && c <= 'Z'
|
||||
}
|
||||
)
|
||||
|
||||
for i := 0; i < len(str); i++ {
|
||||
c := str[i]
|
||||
|
||||
var prev byte
|
||||
if i > 0 {
|
||||
prev = str[i-1]
|
||||
}
|
||||
|
||||
var next byte
|
||||
if i < len(str)-1 {
|
||||
next = str[i+1]
|
||||
}
|
||||
|
||||
if isUpper(c) && (isLower(prev) || isLower(next)) {
|
||||
sb.WriteRune('_')
|
||||
sb.WriteByte(c + ('a' - 'A'))
|
||||
} else if isUpper(c) {
|
||||
sb.WriteByte(c + ('a' - 'A'))
|
||||
} else {
|
||||
sb.WriteRune(rune(c))
|
||||
}
|
||||
}
|
||||
|
||||
// 去除首尾下划线
|
||||
return strings.Trim(sb.String(), "_")
|
||||
}
|
||||
|
||||
func PrettyJSON(v any) string {
|
||||
b, _ := json.MarshalIndent(v, "", " ")
|
||||
return string(b)
|
||||
}
|
||||
125
pkg/tool/table.go
Normal file
125
pkg/tool/table.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
)
|
||||
|
||||
func TablePrinter(data any, writers ...io.Writer) {
|
||||
var w io.Writer = os.Stdout
|
||||
if len(writers) > 0 && writers[0] != nil {
|
||||
w = writers[0]
|
||||
}
|
||||
|
||||
t := table.NewWriter()
|
||||
structPrinter(t, "", data)
|
||||
_, _ = fmt.Fprintln(w, t.Render())
|
||||
}
|
||||
|
||||
func structPrinter(w table.Writer, prefix string, item any) {
|
||||
Start:
|
||||
rv := reflect.ValueOf(item)
|
||||
if rv.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
for rv.Type().Kind() == reflect.Pointer {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
|
||||
switch rv.Type().Kind() {
|
||||
case reflect.Invalid,
|
||||
reflect.Uintptr,
|
||||
reflect.Chan,
|
||||
reflect.Func,
|
||||
reflect.UnsafePointer:
|
||||
case reflect.Bool,
|
||||
reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64,
|
||||
reflect.Float32,
|
||||
reflect.Float64,
|
||||
reflect.Complex64,
|
||||
reflect.Complex128,
|
||||
reflect.Interface:
|
||||
w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), rv.Interface()})
|
||||
case reflect.String:
|
||||
val := rv.String()
|
||||
if len(val) <= 160 {
|
||||
w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), val})
|
||||
return
|
||||
}
|
||||
|
||||
w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), val[0:64] + "..." + val[len(val)-64:]})
|
||||
case reflect.Array, reflect.Slice:
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
p := strings.Join([]string{prefix, fmt.Sprintf("[%d]", i)}, ".")
|
||||
structPrinter(w, p, rv.Index(i).Interface())
|
||||
}
|
||||
case reflect.Map:
|
||||
for _, k := range rv.MapKeys() {
|
||||
structPrinter(w, fmt.Sprintf("%s.{%v}", prefix, k), rv.MapIndex(k).Interface())
|
||||
}
|
||||
case reflect.Pointer:
|
||||
goto Start
|
||||
case reflect.Struct:
|
||||
for i := 0; i < rv.NumField(); i++ {
|
||||
p := fmt.Sprintf("%s.%s", prefix, rv.Type().Field(i).Name)
|
||||
field := rv.Field(i)
|
||||
|
||||
//log.Debug("TablePrinter: prefix: %s, field: %v", p, rv.Field(i))
|
||||
|
||||
if !field.CanInterface() {
|
||||
return
|
||||
}
|
||||
|
||||
structPrinter(w, p, field.Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TableMapPrinter(data []byte) {
|
||||
m := make(map[string]any)
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
log.Printf("[E] unmarshal json err: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
t := table.NewWriter()
|
||||
addRow(t, "", m)
|
||||
fmt.Println(t.Render())
|
||||
}
|
||||
|
||||
func addRow(w table.Writer, prefix string, m any) {
|
||||
rv := reflect.ValueOf(m)
|
||||
switch rv.Type().Kind() {
|
||||
case reflect.Map:
|
||||
for _, k := range rv.MapKeys() {
|
||||
key := k.String()
|
||||
if prefix != "" {
|
||||
key = strings.Join([]string{prefix, k.String()}, ".")
|
||||
}
|
||||
addRow(w, key, rv.MapIndex(k).Interface())
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
addRow(w, fmt.Sprintf("%s[%d]", prefix, i), rv.Index(i).Interface())
|
||||
}
|
||||
default:
|
||||
w.AppendRow(table.Row{prefix, m})
|
||||
}
|
||||
}
|
||||
128
pkg/tool/tls.go
Normal file
128
pkg/tool/tls.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GenerateTlsConfig(serverName ...string) (serverTLSConf *tls.Config, clientTLSConf *tls.Config, err error) {
|
||||
ca := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(2019),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Company, INC."},
|
||||
Country: []string{"US"},
|
||||
Province: []string{""},
|
||||
Locality: []string{"San Francisco"},
|
||||
StreetAddress: []string{"Golden Gate Bridge"},
|
||||
PostalCode: []string{"94016"},
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(99, 0, 0),
|
||||
IsCA: true,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
// create our private and public key
|
||||
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// create the CA
|
||||
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// pem encode
|
||||
caPEM := new(bytes.Buffer)
|
||||
pem.Encode(caPEM, &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: caBytes,
|
||||
})
|
||||
caPrivKeyPEM := new(bytes.Buffer)
|
||||
pem.Encode(caPrivKeyPEM, &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey),
|
||||
})
|
||||
|
||||
_serverName := ""
|
||||
if len(serverName) > 0 && serverName[0] != "" {
|
||||
_serverName = serverName[0]
|
||||
}
|
||||
// set up our server certificate
|
||||
cert := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(2019),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Company, INC."},
|
||||
Country: []string{"US"},
|
||||
Province: []string{""},
|
||||
Locality: []string{"San Francisco"},
|
||||
StreetAddress: []string{"Golden Gate Bridge"},
|
||||
PostalCode: []string{"94016"},
|
||||
CommonName: _serverName,
|
||||
},
|
||||
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(10, 0, 0),
|
||||
SubjectKeyId: []byte{1, 2, 3, 4, 6},
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
}
|
||||
|
||||
// add DNS names to SAN if serverName is provided
|
||||
if _serverName != "" {
|
||||
cert.DNSNames = []string{_serverName}
|
||||
}
|
||||
certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
certPEM := new(bytes.Buffer)
|
||||
pem.Encode(certPEM, &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certBytes,
|
||||
})
|
||||
certPrivKeyPEM := new(bytes.Buffer)
|
||||
pem.Encode(certPrivKeyPEM, &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
|
||||
})
|
||||
serverCert, err := tls.X509KeyPair(certPEM.Bytes(), certPrivKeyPEM.Bytes())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
serverTLSConf = &tls.Config{
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
}
|
||||
certpool := x509.NewCertPool()
|
||||
certpool.AppendCertsFromPEM(caPEM.Bytes())
|
||||
clientTLSConf = &tls.Config{
|
||||
RootCAs: certpool,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func LoadTLSConfigFromFile(certFile, keyFile string) (*tls.Config, error) {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tlsConf := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
|
||||
return tlsConf, nil
|
||||
}
|
||||
73
pkg/tool/tools.go
Normal file
73
pkg/tool/tools.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
func Min[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](a, b T) T {
|
||||
if a <= b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func Mins[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](vals ...T) T {
|
||||
var val T
|
||||
|
||||
if len(vals) == 0 {
|
||||
return val
|
||||
}
|
||||
|
||||
val = vals[0]
|
||||
|
||||
for _, item := range vals[1:] {
|
||||
if item < val {
|
||||
val = item
|
||||
}
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func Max[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](a, b T) T {
|
||||
if a >= b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func Maxs[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](vals ...T) T {
|
||||
var val T
|
||||
|
||||
if len(vals) == 0 {
|
||||
return val
|
||||
}
|
||||
|
||||
for _, item := range vals {
|
||||
if item > val {
|
||||
val = item
|
||||
}
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func Sum[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](vals ...T) T {
|
||||
var sum T = 0
|
||||
for i := range vals {
|
||||
sum += vals[i]
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func Percent(val, minVal, maxVal, minPercent, maxPercent float64) string {
|
||||
return fmt.Sprintf(
|
||||
"%d%%",
|
||||
int(math.Round(
|
||||
((val-minVal)/(maxVal-minVal)*(maxPercent-minPercent)+minPercent)*100,
|
||||
)),
|
||||
)
|
||||
}
|
||||
81
pkg/tool/tools_test.go
Normal file
81
pkg/tool/tools_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package tool
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPercent(t *testing.T) {
|
||||
type args struct {
|
||||
val float64
|
||||
minVal float64
|
||||
maxVal float64
|
||||
minPercent float64
|
||||
maxPercent float64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "case 1",
|
||||
args: args{
|
||||
val: 0.5,
|
||||
minVal: 0,
|
||||
maxVal: 1,
|
||||
minPercent: 0,
|
||||
maxPercent: 1,
|
||||
},
|
||||
want: "50%",
|
||||
},
|
||||
{
|
||||
name: "case 2",
|
||||
args: args{
|
||||
val: 0.3,
|
||||
minVal: 0.1,
|
||||
maxVal: 0.6,
|
||||
minPercent: 0,
|
||||
maxPercent: 1,
|
||||
},
|
||||
want: "40%",
|
||||
},
|
||||
{
|
||||
name: "case 3",
|
||||
args: args{
|
||||
val: 700,
|
||||
minVal: 700,
|
||||
maxVal: 766,
|
||||
minPercent: 0.1,
|
||||
maxPercent: 0.7,
|
||||
},
|
||||
want: "10%",
|
||||
},
|
||||
{
|
||||
name: "case 4",
|
||||
args: args{
|
||||
val: 766,
|
||||
minVal: 700,
|
||||
maxVal: 766,
|
||||
minPercent: 0.1,
|
||||
maxPercent: 0.7,
|
||||
},
|
||||
want: "70%",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := Percent(tt.args.val, tt.args.minVal, tt.args.maxVal, tt.args.minPercent, tt.args.maxPercent); got != tt.want {
|
||||
t.Errorf("Percent() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyString(t *testing.T) {
|
||||
s1 := "hello"
|
||||
s2 := CopyString(s1)
|
||||
|
||||
if &s1 == &s2 {
|
||||
t.Errorf("CopyString fail")
|
||||
}
|
||||
|
||||
t.Log(s1, s2)
|
||||
}
|
||||
1
pkg/tool/tree.go
Normal file
1
pkg/tool/tree.go
Normal file
@@ -0,0 +1 @@
|
||||
package tool
|
||||
14
pkg/tool/uuid.go
Normal file
14
pkg/tool/uuid.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
func NewV4() (string, error) {
|
||||
uid, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return uid.String(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user