init repo

This commit is contained in:
tiglog 2023-06-15 21:22:51 +08:00
commit f21ddce0c8
96 changed files with 8348 additions and 0 deletions

18
.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
# ---> Go
# # Binaries for programs and plugins
# *.exe
# *.exe~
# *.dll
# *.so
# *.dylib
#
# # Test binary, built with `go test -c`
# *.test
#
# # Output of the go coverage tool, specifically when used with LiteIDE
# *.out
#
# # Dependency directories (remove the comment below to include it)
#
.env_test.sh
*.log

59
console/cli_about.go Normal file
View File

@ -0,0 +1,59 @@
//
// cli_about.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package console
import (
"fmt"
"path/filepath"
"runtime"
)
type cAbout struct {
BaseCmd
cli IConsole
}
func NewAboutCmd(cli IConsole) *cAbout {
c := new(cAbout)
c.Name = "about"
c.Desc = "应用基本环境"
c.cli = cli
return c
}
func (c *cAbout) Init(args []string) {
c.Action = func() error {
fmt.Printf("About:\n")
fmt.Printf("==================================================\n")
fmt.Printf(" Version: %s\n", c.cli.GetVersion())
wd, _ := filepath.Abs("./")
fmt.Printf(" BaseDir: %s\n", wd)
fmt.Printf(" Env: %s\n", "dev")
fmt.Printf(" Debug: %v\n", true)
fmt.Printf(" GOOS: %s %s\n", runtime.GOOS, runtime.GOARCH)
hd := c.cli.GetExtraAbout()
if hd != nil {
return hd()
}
// if sqldb.Db != nil {
// fmt.Printf(" Db Type: %s\n", config.Conf.Db.Type)
// fmt.Printf(" Db Host: %s\n", config.Conf.Db.Host)
// fmt.Printf(" Db Port: %d\n", config.Conf.Db.Port)
// fmt.Printf(" Db Name: %s\n", config.Conf.Db.Name)
// fmt.Printf(" Db User: %s\n", config.Conf.Db.Username)
// }
return nil
}
}
func (c *cAbout) GetHelp() string {
return ""
}

142
console/cli_air.go Normal file
View File

@ -0,0 +1,142 @@
//
// cli_air.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package console
import (
"flag"
"fmt"
"io/ioutil"
"os"
"os/exec"
"hexq.cn/tiglog/golib/gfile"
)
type cAir struct {
BaseCmd
}
func NewAirCmd(cli IConsole) *cAir {
return &cAir{
BaseCmd{
Name: "serve",
Desc: "代码变动时自动重启应用",
},
}
}
func (c *cAir) Init(args []string) {
var cmd string
if len(args) == 0 {
cmd = "dev"
} else {
cmd = args[0]
args = args[1:]
}
if cmd == "conf" {
c.initConfCmd(args)
} else {
c.initDevCmd(args)
}
}
func (c *cAir) initDevCmd(args []string) {
cmd := flag.NewFlagSet("dev", flag.ExitOnError)
cmd.Parse(args)
c.Action = func() error {
fmt.Println("run dev cmd")
cm := exec.Command("air")
cm.Stderr = os.Stderr
cm.Stdout = os.Stdout
err := cm.Start()
if err != nil {
return err
}
err = cm.Wait()
if err != nil {
return err
}
return nil
}
}
func (c *cAir) initConfCmd(args []string) {
cmd := flag.NewFlagSet("conf", flag.ExitOnError)
output := cmd.String("output", "./.air.toml", "指定文件路径")
show := cmd.Bool("show", false, "显示配置文件内容")
cmd.Parse(args)
c.Action = func() error {
if *show {
fmt.Println(conf)
} else {
if gfile.Exists(*output) {
fmt.Printf("Conf file %s exists, SKIP\n\n", *output)
} else {
fmt.Printf("Writing conf to %s ...\n", *output)
ioutil.WriteFile(*output, []byte(conf), 0644)
fmt.Println("Done")
}
}
return nil
}
}
func (c *cAir) GetHelp() string {
p1 := fmt.Sprintf("%s <command>\n%s\n\nSub Commands:\n", c.Name, c.Desc)
p2 := fmt.Sprintf("%10s: %s\n", "dev", "启动开发服务")
p3 := fmt.Sprintf("%10s: %s\n", "conf", "生成配置信息")
return p1 + p2 + p3
}
var conf = `# [Air](https://github.com/cosmtrek/air) TOML 格式的配置文件
# 工作目录
# 使用 . 或绝对路径请注意 tmp_dir 目录必须在 root 目录下
root = "."
tmp_dir = "var/tmp"
[build]
# 只需要写你平常编译使用的shell命令你也可以使用 make
cmd = "go build -o ./var/tmp/main entry/web/main.go"
# cmd 命令得到的二进制文件名
bin = "var/tmp/main"
# 自定义的二进制可以添加额外的编译标识例如添加 GIN_MODE=release
# full_bin = "APP_ENV=dev APP_USER=air ./var/tmp/main"
# 监听以下文件扩展名的文件.
include_ext = ["go", "tpl", "tmpl", "html"]
# 忽略这些文件扩展名或目录
exclude_dir = ["assets", "var", "vendor", "frontend/node_modules"]
# 监听以下指定目录的文件
include_dir = []
# 排除以下文件
exclude_file = []
# 如果文件更改过于频繁则没有必要在每次更改时都触发构建可以设置触发构建的延迟时间
delay = 1000 # ms
# 发生构建错误时停止运行旧的二进制文件
stop_on_error = true
# air的日志文件名该日志文件放置在你的 tmp_dir
log = "./var/log/air_errors.log"
[log]
# 显示日志时间
time = true
[color]
# 自定义每个部分显示的颜色如果找不到颜色使用原始的应用程序日志
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"
[misc]
# 退出时删除tmp目录
clean_on_exit = true
`

28
console/cli_base.go Normal file
View File

@ -0,0 +1,28 @@
//
// cli_base.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package console
import "flag"
type BaseCmd struct {
Name string
Desc string
FlagSet *flag.FlagSet
Action ActionHandler
}
func (c *BaseCmd) GetName() string {
return c.Name
}
func (c *BaseCmd) GetDesc() string {
return c.Desc
}
func (c *BaseCmd) GetAction() ActionHandler {
return c.Action
}

25
console/cli_contract.go Normal file
View File

@ -0,0 +1,25 @@
//
// cli_contact.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package console
type ICommand interface {
Init(args []string)
GetName() string
GetDesc() string
GetAction() ActionHandler
GetHelp() string
}
type IConsole interface {
GetCmds() map[string]ICommand
GetName() string
GetDesc() string
GetVersion() string
GetExtraAbout() ActionHandler
}
type ActionHandler func() error

47
console/cli_help.go Normal file
View File

@ -0,0 +1,47 @@
//
// cli_help.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package console
import (
"errors"
"fmt"
)
type cHelp struct {
BaseCmd
cli IConsole
}
func NewHelpCmd(cli IConsole) *cHelp {
return &cHelp{
BaseCmd{
Name: "help",
Desc: "查看命令的使用方法",
},
cli,
}
}
func (c *cHelp) Init(args []string) {
if len(args) == 0 {
return
}
c.Action = func() error {
cmds := c.cli.GetCmds()
cmd, ok := cmds[args[0]]
if !ok {
return errors.New("指定的命令不存在")
}
fmt.Println(cmd.GetHelp())
return nil
}
}
func (c *cHelp) GetHelp() string {
return c.Desc
}

42
console/cli_list.go Normal file
View File

@ -0,0 +1,42 @@
//
// cli_list.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package console
import "fmt"
type cList struct {
BaseCmd
cli IConsole
}
func NewListCmd(cli IConsole) *cList {
return &cList{
BaseCmd{
Name: "list",
Desc: "列出支持的命令",
},
cli,
}
}
func (c *cList) Init(args []string) {
c.Action = func() error {
fmt.Printf("Usage: %s <command> [<args>]\n\n%s\n\nCommands:\n", c.cli.GetName(), c.cli.GetDesc())
cmds := c.cli.GetCmds()
for _, cmd := range cmds {
fmt.Printf("%8s: %s\n", cmd.GetName(), cmd.GetDesc())
}
fmt.Println()
return nil
}
}
func (c *cList) GetHelp() string {
return c.Desc
}

96
console/console.go Normal file
View File

@ -0,0 +1,96 @@
//
// cli.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package console
import (
"fmt"
"os"
)
type sApp struct {
name string
version string
desc string
cmds map[string]ICommand
about ActionHandler
}
func New(name, desc string) *sApp {
app := &sApp{
name: name,
version: "v0.1.0",
desc: desc,
cmds: make(map[string]ICommand),
}
app.AddCmd(NewAboutCmd(app))
app.AddCmd(NewAirCmd(app))
app.AddCmd(NewListCmd(app))
app.AddCmd(NewHelpCmd(app))
return app
}
func (s *sApp) GetCmds() map[string]ICommand {
return s.cmds
}
func (s *sApp) GetName() string {
return s.name
}
func (s *sApp) GetDesc() string {
return s.desc
}
func (s *sApp) GetVersion() string {
return s.version
}
func (s *sApp) AddCmd(cmd ICommand) {
s.cmds[cmd.GetName()] = cmd
}
func (s *sApp) HasCmd(cmd string) bool {
_, ok := s.cmds[cmd]
return ok
}
func (s *sApp) SetExtraAbout(about ActionHandler) {
s.about = about
}
func (s *sApp) GetExtraAbout() ActionHandler {
return s.about
}
func (s *sApp) Run(args []string) {
cmd := "list"
if len(args) == 1 {
args = []string{cmd}
} else {
cmd = args[1]
args = args[2:]
}
if !s.HasCmd(cmd) {
fmt.Printf("%q is not valid command.\n", cmd)
os.Exit(2)
}
scmd := s.cmds[cmd]
scmd.Init(args)
act := scmd.GetAction()
if act == nil {
// fmt.Println("未定义 Action无法执行 Action.")
fmt.Println(scmd.GetHelp())
os.Exit(1)
}
err := act()
if err != nil {
fmt.Printf("执行异常:%v\n", err)
os.Exit(1)
}
}

View File

@ -0,0 +1,53 @@
//
// csv.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package tablewriter
import (
"encoding/csv"
"io"
"os"
)
// Start A new table by importing from a CSV file
// Takes io.Writer and csv File name
func NewCSV(writer io.Writer, fileName string, hasHeader bool) (*Table, error) {
file, err := os.Open(fileName)
if err != nil {
return &Table{}, err
}
defer file.Close()
csvReader := csv.NewReader(file)
t, err := NewCSVReader(writer, csvReader, hasHeader)
return t, err
}
// Start a New Table Writer with csv.Reader
//
// This enables customisation such as reader.Comma = ';'
// See http://golang.org/src/pkg/encoding/csv/reader.go?s=3213:3671#L94
func NewCSVReader(writer io.Writer, csvReader *csv.Reader, hasHeader bool) (*Table, error) {
t := NewWriter(writer)
if hasHeader {
// Read the first row
headers, err := csvReader.Read()
if err != nil {
return &Table{}, err
}
t.SetHeader(headers)
}
for {
record, err := csvReader.Read()
if err == io.EOF {
break
} else if err != nil {
return &Table{}, err
}
t.Append(record)
}
return t, nil
}

1057
console/tablewriter/table.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,143 @@
//
// table_with_color.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package tablewriter
import (
"fmt"
"strconv"
"strings"
)
const ESC = "\033"
const SEP = ";"
const (
BgBlackColor int = iota + 40
BgRedColor
BgGreenColor
BgYellowColor
BgBlueColor
BgMagentaColor
BgCyanColor
BgWhiteColor
)
const (
FgBlackColor int = iota + 30
FgRedColor
FgGreenColor
FgYellowColor
FgBlueColor
FgMagentaColor
FgCyanColor
FgWhiteColor
)
const (
BgHiBlackColor int = iota + 100
BgHiRedColor
BgHiGreenColor
BgHiYellowColor
BgHiBlueColor
BgHiMagentaColor
BgHiCyanColor
BgHiWhiteColor
)
const (
FgHiBlackColor int = iota + 90
FgHiRedColor
FgHiGreenColor
FgHiYellowColor
FgHiBlueColor
FgHiMagentaColor
FgHiCyanColor
FgHiWhiteColor
)
const (
Normal = 0
Bold = 1
UnderlineSingle = 4
Italic
)
type Colors []int
func startFormat(seq string) string {
return fmt.Sprintf("%s[%sm", ESC, seq)
}
func stopFormat() string {
return fmt.Sprintf("%s[%dm", ESC, Normal)
}
// Making the SGR (Select Graphic Rendition) sequence.
func makeSequence(codes []int) string {
codesInString := []string{}
for _, code := range codes {
codesInString = append(codesInString, strconv.Itoa(code))
}
return strings.Join(codesInString, SEP)
}
// Adding ANSI escape sequences before and after string
func format(s string, codes interface{}) string {
var seq string
switch v := codes.(type) {
case string:
seq = v
case []int:
seq = makeSequence(v)
case Colors:
seq = makeSequence(v)
default:
return s
}
if len(seq) == 0 {
return s
}
return startFormat(seq) + s + stopFormat()
}
// Adding header colors (ANSI codes)
func (t *Table) SetHeaderColor(colors ...Colors) {
if t.colSize != len(colors) {
panic("Number of header colors must be equal to number of headers.")
}
for i := 0; i < len(colors); i++ {
t.headerParams = append(t.headerParams, makeSequence(colors[i]))
}
}
// Adding column colors (ANSI codes)
func (t *Table) SetColumnColor(colors ...Colors) {
if t.colSize != len(colors) {
panic("Number of column colors must be equal to number of headers.")
}
for i := 0; i < len(colors); i++ {
t.columnsParams = append(t.columnsParams, makeSequence(colors[i]))
}
}
// Adding column colors (ANSI codes)
func (t *Table) SetFooterColor(colors ...Colors) {
if len(t.footers) != len(colors) {
panic("Number of footer colors must be equal to number of footer.")
}
for i := 0; i < len(colors); i++ {
t.footerParams = append(t.footerParams, makeSequence(colors[i]))
}
}
func Color(colors ...int) []int {
return colors
}

View File

@ -0,0 +1,93 @@
//
// util.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package tablewriter
import (
"math"
"regexp"
"strings"
"github.com/mattn/go-runewidth"
)
var ansi = regexp.MustCompile("\033\\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]")
func DisplayWidth(str string) int {
return runewidth.StringWidth(ansi.ReplaceAllLiteralString(str, ""))
}
// Simple Condition for string
// Returns value based on condition
func ConditionString(cond bool, valid, inValid string) string {
if cond {
return valid
}
return inValid
}
func isNumOrSpace(r rune) bool {
return ('0' <= r && r <= '9') || r == ' '
}
// Format Table Header
// Replace _ , . and spaces
func Title(name string) string {
origLen := len(name)
rs := []rune(name)
for i, r := range rs {
switch r {
case '_':
rs[i] = ' '
case '.':
// ignore floating number 0.0
if (i != 0 && !isNumOrSpace(rs[i-1])) || (i != len(rs)-1 && !isNumOrSpace(rs[i+1])) {
rs[i] = ' '
}
}
}
name = string(rs)
name = strings.TrimSpace(name)
if len(name) == 0 && origLen > 0 {
// Keep at least one character. This is important to preserve
// empty lines in multi-line headers/footers.
name = " "
}
return strings.ToUpper(name)
}
// Pad String
// Attempts to place string in the center
func Pad(s, pad string, width int) string {
gap := width - DisplayWidth(s)
if gap > 0 {
gapLeft := int(math.Ceil(float64(gap / 2)))
gapRight := gap - gapLeft
return strings.Repeat(string(pad), gapLeft) + s + strings.Repeat(string(pad), gapRight)
}
return s
}
// Pad String Right position
// This would place string at the left side of the screen
func PadRight(s, pad string, width int) string {
gap := width - DisplayWidth(s)
if gap > 0 {
return s + strings.Repeat(string(pad), gap)
}
return s
}
// Pad String Left position
// This would place string at the right side of the screen
func PadLeft(s, pad string, width int) string {
gap := width - DisplayWidth(s)
if gap > 0 {
return strings.Repeat(string(pad), gap) + s
}
return s
}

View File

@ -0,0 +1,99 @@
//
// wrap.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package tablewriter
import (
"math"
"strings"
"github.com/mattn/go-runewidth"
)
var (
nl = "\n"
sp = " "
)
const defaultPenalty = 1e5
// Wrap wraps s into a paragraph of lines of length lim, with minimal
// raggedness.
func WrapString(s string, lim int) ([]string, int) {
words := strings.Split(strings.Replace(s, nl, sp, -1), sp)
var lines []string
max := 0
for _, v := range words {
max = runewidth.StringWidth(v)
if max > lim {
lim = max
}
}
for _, line := range WrapWords(words, 1, lim, defaultPenalty) {
lines = append(lines, strings.Join(line, sp))
}
return lines, lim
}
// WrapWords is the low-level line-breaking algorithm, useful if you need more
// control over the details of the text wrapping process. For most uses,
// WrapString will be sufficient and more convenient.
//
// WrapWords splits a list of words into lines with minimal "raggedness",
// treating each rune as one unit, accounting for spc units between adjacent
// words on each line, and attempting to limit lines to lim units. Raggedness
// is the total error over all lines, where error is the square of the
// difference of the length of the line and lim. Too-long lines (which only
// happen when a single word is longer than lim units) have pen penalty units
// added to the error.
func WrapWords(words []string, spc, lim, pen int) [][]string {
n := len(words)
length := make([][]int, n)
for i := 0; i < n; i++ {
length[i] = make([]int, n)
length[i][i] = runewidth.StringWidth(words[i])
for j := i + 1; j < n; j++ {
length[i][j] = length[i][j-1] + spc + runewidth.StringWidth(words[j])
}
}
nbrk := make([]int, n)
cost := make([]int, n)
for i := range cost {
cost[i] = math.MaxInt32
}
for i := n - 1; i >= 0; i-- {
if length[i][n-1] <= lim {
cost[i] = 0
nbrk[i] = n
} else {
for j := i + 1; j < n; j++ {
d := lim - length[i][j-1]
c := d*d + cost[j]
if length[i][j-1] > lim {
c += pen // too-long lines get a worse penalty
}
if c < cost[i] {
cost[i] = c
nbrk[i] = j
}
}
}
}
var lines [][]string
i := 0
for i < n {
lines = append(lines, words[i:nbrk[i]])
i = nbrk[i]
}
return lines
}
// getLines decomposes a multiline string into a slice of strings.
func getLines(s string) []string {
return strings.Split(s, nl)
}

172
crypto/gaes/aes.go Normal file
View File

@ -0,0 +1,172 @@
//
// aes.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gaes
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"errors"
)
const (
// IVDefaultValue is the default value for IV.
IVDefaultValue = "I Love Xiao Quan"
)
// Encrypt is alias of EncryptCBC.
func Encrypt(plainText []byte, key []byte, iv ...[]byte) ([]byte, error) {
return EncryptCBC(plainText, key, iv...)
}
// Decrypt is alias of DecryptCBC.
func Decrypt(cipherText []byte, key []byte, iv ...[]byte) ([]byte, error) {
return DecryptCBC(cipherText, key, iv...)
}
// EncryptCBC encrypts `plainText` using CBC mode.
// Note that the key must be 16/24/32 bit length.
// The parameter `iv` initialization vector is unnecessary.
func EncryptCBC(plainText []byte, key []byte, iv ...[]byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockSize := block.BlockSize()
plainText = PKCS5Padding(plainText, blockSize)
ivValue := ([]byte)(nil)
if len(iv) > 0 {
ivValue = iv[0]
} else {
ivValue = []byte(IVDefaultValue)
}
blockMode := cipher.NewCBCEncrypter(block, ivValue)
cipherText := make([]byte, len(plainText))
blockMode.CryptBlocks(cipherText, plainText)
return cipherText, nil
}
// DecryptCBC decrypts `cipherText` using CBC mode.
// Note that the key must be 16/24/32 bit length.
// The parameter `iv` initialization vector is unnecessary.
func DecryptCBC(cipherText []byte, key []byte, iv ...[]byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockSize := block.BlockSize()
if len(cipherText) < blockSize {
return nil, errors.New("cipherText too short")
}
ivValue := ([]byte)(nil)
if len(iv) > 0 {
ivValue = iv[0]
} else {
ivValue = []byte(IVDefaultValue)
}
if len(cipherText)%blockSize != 0 {
return nil, errors.New("cipherText is not a multiple of the block size")
}
blockModel := cipher.NewCBCDecrypter(block, ivValue)
plainText := make([]byte, len(cipherText))
blockModel.CryptBlocks(plainText, cipherText)
plainText, e := PKCS5UnPadding(plainText, blockSize)
if e != nil {
return nil, e
}
return plainText, nil
}
func PKCS5Padding(src []byte, blockSize int) []byte {
padding := blockSize - len(src)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(src, padtext...)
}
func PKCS5UnPadding(src []byte, blockSize int) ([]byte, error) {
length := len(src)
if blockSize <= 0 {
return nil, errors.New("invalid blocklen")
}
if length%blockSize != 0 || length == 0 {
return nil, errors.New("invalid data len")
}
unpadding := int(src[length-1])
if unpadding > blockSize || unpadding == 0 {
return nil, errors.New("invalid padding")
}
padding := src[length-unpadding:]
for i := 0; i < unpadding; i++ {
if padding[i] != byte(unpadding) {
return nil, errors.New("invalid padding")
}
}
return src[:(length - unpadding)], nil
}
// EncryptCFB encrypts `plainText` using CFB mode.
// Note that the key must be 16/24/32 bit length.
// The parameter `iv` initialization vector is unnecessary.
func EncryptCFB(plainText []byte, key []byte, padding *int, iv ...[]byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockSize := block.BlockSize()
plainText, *padding = ZeroPadding(plainText, blockSize)
ivValue := ([]byte)(nil)
if len(iv) > 0 {
ivValue = iv[0]
} else {
ivValue = []byte(IVDefaultValue)
}
stream := cipher.NewCFBEncrypter(block, ivValue)
cipherText := make([]byte, len(plainText))
stream.XORKeyStream(cipherText, plainText)
return cipherText, nil
}
// DecryptCFB decrypts `plainText` using CFB mode.
// Note that the key must be 16/24/32 bit length.
// The parameter `iv` initialization vector is unnecessary.
func DecryptCFB(cipherText []byte, key []byte, unPadding int, iv ...[]byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(cipherText) < aes.BlockSize {
return nil, errors.New("cipherText too short")
}
ivValue := ([]byte)(nil)
if len(iv) > 0 {
ivValue = iv[0]
} else {
ivValue = []byte(IVDefaultValue)
}
stream := cipher.NewCFBDecrypter(block, ivValue)
plainText := make([]byte, len(cipherText))
stream.XORKeyStream(plainText, cipherText)
plainText = ZeroUnPadding(plainText, unPadding)
return plainText, nil
}
func ZeroPadding(cipherText []byte, blockSize int) ([]byte, int) {
padding := blockSize - len(cipherText)%blockSize
padText := bytes.Repeat([]byte{byte(0)}, padding)
return append(cipherText, padText...), padding
}
func ZeroUnPadding(plaintext []byte, unPadding int) []byte {
length := len(plaintext)
return plaintext[:(length - unPadding)]
}

91
crypto/gmd5/md5.go Normal file
View File

@ -0,0 +1,91 @@
//
// md5.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gmd5
import (
"crypto/md5"
"fmt"
"io"
"os"
)
// Encrypt encrypts any type of variable using MD5 algorithms.
// It uses gconv package to convert `v` to its bytes type.
func Encrypt(in string) (encrypt string, err error) {
return EncryptBytes([]byte(in))
}
// MustEncrypt encrypts any type of variable using MD5 algorithms.
// It uses gconv package to convert `v` to its bytes type.
// It panics if any error occurs.
func MustEncrypt(in string) string {
result, err := Encrypt(in)
if err != nil {
panic(err)
}
return result
}
// EncryptBytes encrypts `data` using MD5 algorithms.
func EncryptBytes(data []byte) (encrypt string, err error) {
h := md5.New()
if _, err = h.Write(data); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
// MustEncryptBytes encrypts `data` using MD5 algorithms.
// It panics if any error occurs.
func MustEncryptBytes(data []byte) string {
result, err := EncryptBytes(data)
if err != nil {
panic(err)
}
return result
}
// EncryptString encrypts string `data` using MD5 algorithms.
func EncryptString(data string) (encrypt string, err error) {
return EncryptBytes([]byte(data))
}
// MustEncryptString encrypts string `data` using MD5 algorithms.
// It panics if any error occurs.
func MustEncryptString(data string) string {
result, err := EncryptString(data)
if err != nil {
panic(err)
}
return result
}
// EncryptFile encrypts file content of `path` using MD5 algorithms.
func EncryptFile(path string) (encrypt string, err error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := md5.New()
_, err = io.Copy(h, f)
if err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
// MustEncryptFile encrypts file content of `path` using MD5 algorithms.
// It panics if any error occurs.
func MustEncryptFile(path string) string {
result, err := EncryptFile(path)
if err != nil {
panic(err)
}
return result
}

47
crypto/gsha1/sha1.go Normal file
View File

@ -0,0 +1,47 @@
//
// sha1.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gsha1
import (
"crypto/sha1"
"encoding/hex"
"io"
"os"
)
// Encrypt encrypts any type of variable using SHA1 algorithms.
// It uses package gconv to convert `v` to its bytes type.
func Encrypt(in string) string {
r := sha1.Sum([]byte(in))
return hex.EncodeToString(r[:])
}
// EncryptFile encrypts file content of `path` using SHA1 algorithms.
func EncryptFile(path string) (encrypt string, err error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := sha1.New()
_, err = io.Copy(h, f)
if err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// MustEncryptFile encrypts file content of `path` using SHA1 algorithms.
// It panics if any error occurs.
func MustEncryptFile(path string) string {
result, err := EncryptFile(path)
if err != nil {
panic(err)
}
return result
}

121
encoding/gbase64/base64.go Normal file
View File

@ -0,0 +1,121 @@
//
// base64.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gbase64
import (
"encoding/base64"
"io/ioutil"
)
// Encode encodes bytes with BASE64 algorithm.
func Encode(src []byte) []byte {
dst := make([]byte, base64.StdEncoding.EncodedLen(len(src)))
base64.StdEncoding.Encode(dst, src)
return dst
}
// EncodeString encodes string with BASE64 algorithm.
func EncodeString(src string) string {
return EncodeToString([]byte(src))
}
// EncodeToString encodes bytes to string with BASE64 algorithm.
func EncodeToString(src []byte) string {
return string(Encode(src))
}
// EncodeFile encodes file content of `path` using BASE64 algorithms.
func EncodeFile(path string) ([]byte, error) {
content, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return Encode(content), nil
}
// MustEncodeFile encodes file content of `path` using BASE64 algorithms.
// It panics if any error occurs.
func MustEncodeFile(path string) []byte {
result, err := EncodeFile(path)
if err != nil {
panic(err)
}
return result
}
// EncodeFileToString encodes file content of `path` to string using BASE64 algorithms.
func EncodeFileToString(path string) (string, error) {
content, err := EncodeFile(path)
if err != nil {
return "", err
}
return string(content), nil
}
// MustEncodeFileToString encodes file content of `path` to string using BASE64 algorithms.
// It panics if any error occurs.
func MustEncodeFileToString(path string) string {
result, err := EncodeFileToString(path)
if err != nil {
panic(err)
}
return result
}
// Decode decodes bytes with BASE64 algorithm.
func Decode(data []byte) ([]byte, error) {
var (
src = make([]byte, base64.StdEncoding.DecodedLen(len(data)))
n, err = base64.StdEncoding.Decode(src, data)
)
if err != nil {
return nil, err
}
return src[:n], nil
}
// MustDecode decodes bytes with BASE64 algorithm.
// It panics if any error occurs.
func MustDecode(data []byte) []byte {
result, err := Decode(data)
if err != nil {
panic(err)
}
return result
}
// DecodeString decodes string with BASE64 algorithm.
func DecodeString(data string) ([]byte, error) {
return Decode([]byte(data))
}
// MustDecodeString decodes string with BASE64 algorithm.
// It panics if any error occurs.
func MustDecodeString(data string) []byte {
result, err := DecodeString(data)
if err != nil {
panic(err)
}
return result
}
// DecodeToString decodes string with BASE64 algorithm.
func DecodeToString(data string) (string, error) {
b, err := DecodeString(data)
return string(b), err
}
// MustDecodeToString decodes string with BASE64 algorithm.
// It panics if any error occurs.
func MustDecodeToString(data string) string {
result, err := DecodeToString(data)
if err != nil {
panic(err)
}
return result
}

87
encoding/gurl/url.go Normal file
View File

@ -0,0 +1,87 @@
//
// url.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gurl
import (
"net/url"
"strings"
)
// Encode escapes the string so it can be safely placed
// inside a URL query.
func Encode(str string) string {
return url.QueryEscape(str)
}
// Decode does the inverse transformation of Encode,
// converting each 3-byte encoded substring of the form "%AB" into the
// hex-decoded byte 0xAB.
// It returns an error if any % is not followed by two hexadecimal
// digits.
func Decode(str string) (string, error) {
return url.QueryUnescape(str)
}
// RawEncode does encode the given string according
// URL-encode according to RFC 3986.
// See http://php.net/manual/en/function.rawurlencode.php.
func RawEncode(str string) string {
return strings.Replace(url.QueryEscape(str), "+", "%20", -1)
}
// RawDecode does decode the given string
// Decode URL-encoded strings.
// See http://php.net/manual/en/function.rawurldecode.php.
func RawDecode(str string) (string, error) {
return url.QueryUnescape(strings.Replace(str, "%20", "+", -1))
}
// BuildQuery Generate URL-encoded query string.
// See http://php.net/manual/en/function.http-build-query.php.
func BuildQuery(queryData url.Values) string {
return queryData.Encode()
}
// ParseURL Parse a URL and return its components.
// -1: all; 1: scheme; 2: host; 4: port; 8: user; 16: pass; 32: path; 64: query; 128: fragment.
// See http://php.net/manual/en/function.parse-url.php.
func ParseURL(str string, component int) (map[string]string, error) {
u, err := url.Parse(str)
if err != nil {
return nil, err
}
if component == -1 {
component = 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128
}
var components = make(map[string]string)
if (component & 1) == 1 {
components["scheme"] = u.Scheme
}
if (component & 2) == 2 {
components["host"] = u.Hostname()
}
if (component & 4) == 4 {
components["port"] = u.Port()
}
if (component & 8) == 8 {
components["user"] = u.User.Username()
}
if (component & 16) == 16 {
components["pass"], _ = u.User.Password()
}
if (component & 32) == 32 {
components["path"] = u.Path
}
if (component & 64) == 64 {
components["query"] = u.RawQuery
}
if (component & 128) == 128 {
components["fragment"] = u.Fragment
}
return components, nil
}

20
gauth/auth.go Normal file
View File

@ -0,0 +1,20 @@
//
// auth.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gauth
type sAuth struct {
}
func New() *sAuth {
return &sAuth{}
}
// 是否支持
func (s *sAuth) Support() error {
return nil
}

25
gauth/helper.go Normal file
View File

@ -0,0 +1,25 @@
//
// helper.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gauth
import "golang.org/x/crypto/bcrypt"
// 加密密码
func EncryptPassword(password string) (string, error) {
bt, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(bt), nil
}
// 检查密码
func CheckPassword(pwd_plain, pwd_hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(pwd_hash), []byte(pwd_plain))
return err == nil
}

19
gauth/middleware.go Normal file
View File

@ -0,0 +1,19 @@
//
// middleware.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gauth
import "github.com/gin-gonic/gin"
func GinAuth() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
}
}

17
gauth/readme.adoc Normal file
View File

@ -0,0 +1,17 @@
= 认证
:author: tiglog
:experimental:
:toc: left
:toclevels: 3
:toc-title: 目录
:sectnums:
:icons: font
:!webfonts:
:autofit-option:
:source-highlighter: rouge
:rouge-style: github
:source-linenums-option:
:revdate: 2022-12-01
:imagesdir: ./img

10
gcache/adapter_file.go Normal file
View File

@ -0,0 +1,10 @@
//
// adapter_file.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
// 本地文件缓存

10
gcache/adapter_local.go Normal file
View File

@ -0,0 +1,10 @@
//
// adapter_local.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
// 本地内存缓存

10
gcache/adapter_redis.go Normal file
View File

@ -0,0 +1,10 @@
//
// adapter_redis.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
// 使用 redis 服务缓存

16
gcache/cache.go Normal file
View File

@ -0,0 +1,16 @@
//
// cache.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
type Engine struct {
ICacheAdapter
}
func New(adapter ICacheAdapter) *Engine {
return &Engine{adapter}
}

18
gcache/cache_contract.go Normal file
View File

@ -0,0 +1,18 @@
//
// cache_contact.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
import "time"
type ICacheAdapter interface {
Get(key string) (string, error)
Set(key string, val interface{}, exp time.Duration) error
Del(keys ...string) int64
Has(key string) bool
End()
}

54
gcache/readme.adoc Normal file
View File

@ -0,0 +1,54 @@
= 缓存设计
:author: tiglog
:experimental:
:toc: left
:toclevels: 3
:toc-title: 目录
:sectnums:
:icons: font
:!webfonts:
:autofit-option:
:source-highlighter: rouge
:rouge-style: github
:source-linenums-option:
:revdate: 2022-11-30
:imagesdir: ./img
**注:** 暂时直接使用 `go-resdis/cache`
从使用倒推设计。
== 场景1
自己管理 `key`:
[source,golang]
----
ck := "key_foo"
data := cache.get(ck)
if !data { // <1>
data = FETCH_DATA()
cache.set(ck, data, 7200) // <2>
}
return data
----
<1> `get` 值为 `false` 表示没有缓存或缓存已过期
<2> 7200 为缓存有效期(单位为秒),若指定为 0 表示不过期。
== 场景2
程序自动管理 `key`:
[source,golang]
----
cache.get(func() {
return 'foo'
}, 7200)
----
这种方式一般情况下比较方便,要是需要手动使缓存失效,则要麻烦一些。因此,这种方
式暂时不实现。

172
gcasbin/adapter_redis.go Normal file
View File

@ -0,0 +1,172 @@
//
// adapter_redis.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcasbin
import (
"context"
"errors"
"fmt"
"strings"
"github.com/casbin/casbin/v2/model"
"github.com/casbin/casbin/v2/persist"
"github.com/casbin/casbin/v2/util"
"github.com/go-redis/redis/v8"
)
const (
// The key under which the policies are stored in redis
PolicyKey = "casbin:policy"
)
// Adapter is an adapter for policy storage based on Redis
type RedisAdapter struct {
redisCli *redis.Client
}
// NewFromDSN returns a new Adapter by using the given DSN.
// Format: redis://:{password}@{host}:{port}/{database}
// Example: redis://:123@localhost:6379/0
func NewRedisAdapterFromURL(url string) (adapter *RedisAdapter, err error) {
opt, err := redis.ParseURL(url)
if err != nil {
return nil, err
}
redisCli := redis.NewClient(opt)
if err = redisCli.Ping(context.Background()).Err(); err != nil {
return nil, fmt.Errorf("failed to ping redis: %v", err)
}
return NewRedisAdapterFromClient(redisCli), nil
}
// NewFromClient returns a new instance of Adapter from an already existing go-redis client.
func NewRedisAdapterFromClient(redisCli *redis.Client) (adapter *RedisAdapter) {
return &RedisAdapter{redisCli: redisCli}
}
// LoadPolicy loads all policy rules from the storage.
func (a *RedisAdapter) LoadPolicy(model model.Model) (err error) {
ctx := context.Background()
// Using the LoadPolicyLine handler from the Casbin repo for building rules
return a.loadPolicy(ctx, model, persist.LoadPolicyArray)
}
func (a *RedisAdapter) loadPolicy(ctx context.Context, model model.Model, handler func([]string, model.Model) error) (err error) {
// 0, -1 fetches all entries from the list
rules, err := a.redisCli.LRange(ctx, PolicyKey, 0, -1).Result()
if err != nil {
return err
}
// Parse the rules from Redis
for _, rule := range rules {
handler(strings.Split(rule, ", "), model)
}
return
}
// SavePolicy saves all policy rules to the storage.
func (a *RedisAdapter) SavePolicy(model model.Model) (err error) {
ctx := context.Background()
var rules []string
// Serialize the policies into a string slice
for ptype, assertion := range model["p"] {
for _, rule := range assertion.Policy {
rules = append(rules, buildRuleStr(ptype, rule))
}
}
// Append the group policies to the slice
for ptype, assertion := range model["g"] {
for _, rule := range assertion.Policy {
rules = append(rules, buildRuleStr(ptype, rule))
}
}
// If an empty ruleset is saved, the policy is completely deleted from Redis.
if len(rules) > 0 {
return a.savePolicy(ctx, rules)
}
return a.delPolicy(ctx)
}
func (a *RedisAdapter) savePolicy(ctx context.Context, rules []string) (err error) {
// Use a transaction for deleting the key & creating a new one.
// This only uses one round trip to Redis and also makes sure nothing bad happens.
cmd, err := a.redisCli.TxPipelined(ctx, func(tx redis.Pipeliner) error {
tx.Del(ctx, PolicyKey)
tx.RPush(ctx, PolicyKey, strToInterfaceSlice(rules)...)
return nil
})
if err = cmd[0].Err(); err != nil {
return fmt.Errorf("failed to delete policy key: %v", err)
}
if err = cmd[1].Err(); err != nil {
return fmt.Errorf("failed to save policy: %v", err)
}
return
}
func (a *RedisAdapter) delPolicy(ctx context.Context) (err error) {
if err = a.redisCli.Del(ctx, PolicyKey).Err(); err != nil {
return err
}
return
}
// AddPolicy adds a policy rule to the storage.
func (a *RedisAdapter) AddPolicy(_ string, ptype string, rule []string) (err error) {
ctx := context.Background()
return a.addPolicy(ctx, buildRuleStr(ptype, rule))
}
func (a *RedisAdapter) addPolicy(ctx context.Context, rule string) (err error) {
if err = a.redisCli.RPush(ctx, PolicyKey, rule).Err(); err != nil {
return err
}
return
}
// RemovePolicy removes a policy rule from the storage.
func (a *RedisAdapter) RemovePolicy(_ string, ptype string, rule []string) (err error) {
ctx := context.Background()
return a.removePolicy(ctx, buildRuleStr(ptype, rule))
}
func (a *RedisAdapter) removePolicy(ctx context.Context, rule string) (err error) {
if err = a.redisCli.LRem(ctx, PolicyKey, 1, rule).Err(); err != nil {
return err
}
return
}
// RemoveFilteredPolicy removes policy rules that match the filter from the storage.
func (a *RedisAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
return errors.New("not implemented")
}
// Converts a string slice to an interface{} slice.
// Needed for pushing elements to a redis list.
func strToInterfaceSlice(ss []string) (is []interface{}) {
for _, s := range ss {
is = append(is, s)
}
return
}
func buildRuleStr(ptype string, rule []string) string {
return ptype + ", " + util.ArrayToString(rule)
}

View File

@ -0,0 +1,160 @@
//
// adapter_redis_test.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcasbin_test
import (
"context"
"os"
"testing"
"github.com/casbin/casbin/v2"
"github.com/go-redis/redis/v8"
"hexq.cn/tiglog/golib/gcasbin"
"hexq.cn/tiglog/golib/gtest"
)
func getRedis() *redis.Client {
addr := os.Getenv("REDIS_ADDR")
username := os.Getenv("REDIS_USERNAME")
pass := os.Getenv("REDIS_PASSWORD")
return redis.NewClient(&redis.Options{
Addr: addr,
Username: username,
Password: pass,
DB: 1,
})
}
func TestNewRedisAdapterFromClient(t *testing.T) {
rdb := getRedis()
defer rdb.Close()
a := gcasbin.NewRedisAdapterFromClient(rdb)
gtest.NotEmpty(t, a)
}
func TestNewRedisAdapterFromURL(t *testing.T) {
url := os.Getenv("REDIS_URL")
a, err := gcasbin.NewRedisAdapterFromURL(url)
gtest.NotEmpty(t, a)
gtest.NoError(t, err)
}
func TestSavePolicy(t *testing.T) {
e, err := casbin.NewEnforcer("testdata/model.conf", "testdata/policy.csv")
gtest.NoError(t, err)
fileModel := e.GetModel()
rdb := getRedis()
defer rdb.Close()
// Create the adapter
a := gcasbin.NewRedisAdapterFromClient(rdb)
// Save the file model to redis
err = a.SavePolicy(fileModel)
gtest.NoError(t, err)
// Create a new Enforcer, this time with the redis adapter
e, err = casbin.NewEnforcer("testdata/model.conf", a)
gtest.NoError(t, err)
// Load policies from redis
err = e.LoadPolicy()
gtest.NoError(t, err)
// gtest.Equal(t, fileModel, e.GetModel())
_ = e.SavePolicy()
polLength, err := rdb.LLen(context.Background(), gcasbin.PolicyKey).Result()
gtest.NoError(t, err)
gtest.Equal(t, int64(3), polLength)
// Delete current policies
e.ClearPolicy()
// Save empty model for comparison
// emptyModel := e.GetModel()
// Save empty model
err = e.SavePolicy()
gtest.NoError(t, err)
// Load empty model again
err = e.LoadPolicy()
gtest.NoError(t, err)
// Check if the loaded model equals the empty model from before
// gtest.Equal(t, emptyModel, e.GetModel())
}
func TestLoadPolicy(t *testing.T) {
gtest.True(t, true)
}
func TestAddPolicy(t *testing.T) {
rdb := getRedis()
defer rdb.Close()
// Create the adapter
a := gcasbin.NewRedisAdapterFromClient(rdb)
// Create a new Enforcer, this time with the redis adapter
e, err := casbin.NewEnforcer("testdata/model.conf", a)
gtest.NoError(t, err)
// Add policies
_, err = e.AddPolicy("bob", "data1", "read")
gtest.NoError(t, err)
_, err = e.AddPolicy("alice", "data1", "write")
gtest.NoError(t, err)
// Clear all policies from memory
e.ClearPolicy()
// Policy is deleted now
hasPol := e.HasPolicy("bob", "data1", "read")
gtest.False(t, hasPol)
// Load policies from redis
err = e.LoadPolicy()
gtest.NoError(t, err)
// Policy is there again
hasPol = e.HasPolicy("bob", "data1", "read")
gtest.True(t, hasPol)
hasPol = e.HasPolicy("alice", "data1", "write")
gtest.True(t, hasPol)
}
func TestRmovePolicy(t *testing.T) {
rdb := getRedis()
defer rdb.Close()
// Create the adapter
a := gcasbin.NewRedisAdapterFromClient(rdb)
// Create a new Enforcer, this time with the redis adapter
e, err := casbin.NewEnforcer("testdata/model.conf", a)
gtest.NoError(t, err)
// Add policy
_, err = e.AddPolicy("bob", "data1", "read")
gtest.NoError(t, err)
// Policy is available
hasPol := e.HasPolicy("bob", "data1", "read")
gtest.True(t, hasPol)
// Remove the policy
_, err = e.RemovePolicy("bob", "data1", "read")
gtest.NoError(t, err)
// Policy is gone
hasPol = e.HasPolicy("bob", "data1", "read")
gtest.False(t, hasPol)
}

749
gcasbin/adapter_sqlx.go Normal file
View File

@ -0,0 +1,749 @@
//
// adapter_sqlx.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcasbin
import (
"bytes"
"context"
"errors"
"fmt"
"strconv"
"github.com/casbin/casbin/v2/model"
"github.com/casbin/casbin/v2/persist"
"github.com/jmoiron/sqlx"
)
// defaultTableName if tableName == "", the Adapter will use this default table name.
const defaultTableName = "casbin_rule"
// maxParamLength .
const maxParamLength = 7
// general sql
const (
sqlCreateTable = `
CREATE TABLE %[1]s(
p_type VARCHAR(32),
v0 VARCHAR(255),
v1 VARCHAR(255),
v2 VARCHAR(255),
v3 VARCHAR(255),
v4 VARCHAR(255),
v5 VARCHAR(255)
);
CREATE INDEX idx_%[1]s ON %[1]s (p_type,v0,v1);`
sqlTruncateTable = "TRUNCATE TABLE %s"
sqlIsTableExist = "SELECT 1 FROM %s"
sqlInsertRow = "INSERT INTO %s (p_type,v0,v1,v2,v3,v4,v5) VALUES (?,?,?,?,?,?,?)"
sqlUpdateRow = "UPDATE %s SET p_type=?,v0=?,v1=?,v2=?,v3=?,v4=?,v5=? WHERE p_type=? AND v0=? AND v1=? AND v2=? AND v3=? AND v4=? AND v5=?"
sqlDeleteAll = "DELETE FROM %s"
sqlDeleteRow = "DELETE FROM %s WHERE p_type=? AND v0=? AND v1=? AND v2=? AND v3=? AND v4=? AND v5=?"
sqlDeleteByArgs = "DELETE FROM %s WHERE p_type=?"
sqlSelectAll = "SELECT p_type,v0,v1,v2,v3,v4,v5 FROM %s"
sqlSelectWhere = "SELECT p_type,v0,v1,v2,v3,v4,v5 FROM %s WHERE "
)
// for Sqlite3
const (
sqlCreateTableSqlite3 = `
CREATE TABLE IF NOT EXISTS %[1]s(
p_type VARCHAR(32) DEFAULT '' NOT NULL,
v0 VARCHAR(255) DEFAULT '' NOT NULL,
v1 VARCHAR(255) DEFAULT '' NOT NULL,
v2 VARCHAR(255) DEFAULT '' NOT NULL,
v3 VARCHAR(255) DEFAULT '' NOT NULL,
v4 VARCHAR(255) DEFAULT '' NOT NULL,
v5 VARCHAR(255) DEFAULT '' NOT NULL,
CHECK (TYPEOF("p_type") = "text" AND
LENGTH("p_type") <= 32),
CHECK (TYPEOF("v0") = "text" AND
LENGTH("v0") <= 255),
CHECK (TYPEOF("v1") = "text" AND
LENGTH("v1") <= 255),
CHECK (TYPEOF("v2") = "text" AND
LENGTH("v2") <= 255),
CHECK (TYPEOF("v3") = "text" AND
LENGTH("v3") <= 255),
CHECK (TYPEOF("v4") = "text" AND
LENGTH("v4") <= 255),
CHECK (TYPEOF("v5") = "text" AND
LENGTH("v5") <= 255)
);
CREATE INDEX IF NOT EXISTS idx_%[1]s ON %[1]s (p_type,v0,v1);`
sqlTruncateTableSqlite3 = "DROP TABLE IF EXISTS %[1]s;" + sqlCreateTableSqlite3
)
// for Mysql
const (
sqlCreateTableMysql = `
CREATE TABLE IF NOT EXISTS %[1]s(
p_type VARCHAR(32) DEFAULT '' NOT NULL,
v0 VARCHAR(255) DEFAULT '' NOT NULL,
v1 VARCHAR(255) DEFAULT '' NOT NULL,
v2 VARCHAR(255) DEFAULT '' NOT NULL,
v3 VARCHAR(255) DEFAULT '' NOT NULL,
v4 VARCHAR(255) DEFAULT '' NOT NULL,
v5 VARCHAR(255) DEFAULT '' NOT NULL,
INDEX idx_%[1]s (p_type,v0,v1)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;`
)
// for Postgres
const (
sqlCreateTablePostgres = `
CREATE TABLE IF NOT EXISTS %[1]s(
p_type VARCHAR(32) DEFAULT '' NOT NULL,
v0 VARCHAR(255) DEFAULT '' NOT NULL,
v1 VARCHAR(255) DEFAULT '' NOT NULL,
v2 VARCHAR(255) DEFAULT '' NOT NULL,
v3 VARCHAR(255) DEFAULT '' NOT NULL,
v4 VARCHAR(255) DEFAULT '' NOT NULL,
v5 VARCHAR(255) DEFAULT '' NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_%[1]s ON %[1]s (p_type,v0,v1);`
sqlInsertRowPostgres = "INSERT INTO %s (p_type,v0,v1,v2,v3,v4,v5) VALUES ($1,$2,$3,$4,$5,$6,$7)"
sqlUpdateRowPostgres = "UPDATE %s SET p_type=$1,v0=$2,v1=$3,v2=$4,v3=$5,v4=$6,v5=$7 WHERE p_type=$8 AND v0=$9 AND v1=$10 AND v2=$11 AND v3=$12 AND v4=$13 AND v5=$14"
sqlDeleteRowPostgres = "DELETE FROM %s WHERE p_type=$1 AND v0=$2 AND v1=$3 AND v2=$4 AND v3=$5 AND v4=$6 AND v5=$7"
)
// for Sqlserver
const (
sqlCreateTableSqlserver = `
CREATE TABLE %[1]s(
p_type NVARCHAR(32) DEFAULT '' NOT NULL,
v0 NVARCHAR(255) DEFAULT '' NOT NULL,
v1 NVARCHAR(255) DEFAULT '' NOT NULL,
v2 NVARCHAR(255) DEFAULT '' NOT NULL,
v3 NVARCHAR(255) DEFAULT '' NOT NULL,
v4 NVARCHAR(255) DEFAULT '' NOT NULL,
v5 NVARCHAR(255) DEFAULT '' NOT NULL
);
CREATE INDEX idx_%[1]s ON %[1]s (p_type, v0, v1);`
sqlInsertRowSqlserver = "INSERT INTO %s (p_type,v0,v1,v2,v3,v4,v5) VALUES (@p1,@p2,@p3,@p4,@p5,@p6,@p7)"
sqlUpdateRowSqlserver = "UPDATE %s SET p_type=@p1,v0=@p2,v1=@p3,v2=@p4,v3=@p5,v4=@p6,v5=@p7 WHERE p_type=@p8 AND v0=@p9 AND v1=@p10 AND v2=@p11 AND v3=@p12 AND v4=@p13 AND v5=@p14"
sqlDeleteRowSqlserver = "DELETE FROM %s WHERE p_type=@p1 AND v0=@p2 AND v1=@p3 AND v2=@p4 AND v3=@p5 AND v4=@p6 AND v5=@p7"
)
// CasbinRule defines the casbin rule model.
// It used for save or load policy lines from sqlx connected database.
type SqlCasbinRule struct {
PType string `db:"p_type"`
V0 string `db:"v0"`
V1 string `db:"v1"`
V2 string `db:"v2"`
V3 string `db:"v3"`
V4 string `db:"v4"`
V5 string `db:"v5"`
}
// Adapter define the sqlx adapter for Casbin.
// It can load policy lines or save policy lines from sqlx connected database.
type SqlAdapter struct {
db *sqlx.DB
ctx context.Context
tableName string
isFiltered bool
SqlCreateTable string
SqlTruncateTable string
SqlIsTableExist string
SqlInsertRow string
SqlUpdateRow string
SqlDeleteAll string
SqlDeleteRow string
SqlDeleteByArgs string
SqlSelectAll string
SqlSelectWhere string
}
// Filter defines the filtering rules for a FilteredAdapter's policy.
// Empty values are ignored, but all others must match the filter.
type SqlFilter struct {
PType []string
V0 []string
V1 []string
V2 []string
V3 []string
V4 []string
V5 []string
}
// NewAdapter the constructor for Adapter.
// db should connected to database and controlled by user.
// If tableName == "", the Adapter will automatically create a table named "casbin_rule".
func NewSqlAdapter(db *sqlx.DB, tableName string) (*SqlAdapter, error) {
return NewSqlAdapterContext(context.Background(), db, tableName)
}
// NewAdapterContext the constructor for Adapter.
// db should connected to database and controlled by user.
// If tableName == "", the Adapter will automatically create a table named "casbin_rule".
func NewSqlAdapterContext(ctx context.Context, db *sqlx.DB, tableName string) (*SqlAdapter, error) {
if db == nil {
return nil, errors.New("db is nil")
}
// check db connecting
err := db.PingContext(ctx)
if err != nil {
return nil, err
}
switch db.DriverName() {
case "oci8", "ora", "goracle":
return nil, errors.New("sqlxadapter: please checkout 'oracle' branch")
}
if tableName == "" {
tableName = defaultTableName
}
adapter := SqlAdapter{
db: db,
ctx: ctx,
tableName: tableName,
}
// generate different databases sql
adapter.genSQL()
if !adapter.IsTableExist() {
if err = adapter.CreateTable(); err != nil {
return nil, err
}
}
return &adapter, nil
}
// genSQL generate sql based on db driver name.
func (p *SqlAdapter) genSQL() {
p.SqlCreateTable = fmt.Sprintf(sqlCreateTable, p.tableName)
p.SqlTruncateTable = fmt.Sprintf(sqlTruncateTable, p.tableName)
p.SqlIsTableExist = fmt.Sprintf(sqlIsTableExist, p.tableName)
p.SqlInsertRow = fmt.Sprintf(sqlInsertRow, p.tableName)
p.SqlUpdateRow = fmt.Sprintf(sqlUpdateRow, p.tableName)
p.SqlDeleteAll = fmt.Sprintf(sqlDeleteAll, p.tableName)
p.SqlDeleteRow = fmt.Sprintf(sqlDeleteRow, p.tableName)
p.SqlDeleteByArgs = fmt.Sprintf(sqlDeleteByArgs, p.tableName)
p.SqlSelectAll = fmt.Sprintf(sqlSelectAll, p.tableName)
p.SqlSelectWhere = fmt.Sprintf(sqlSelectWhere, p.tableName)
switch p.db.DriverName() {
case "postgres", "pgx", "pq-timeouts", "cloudsqlpostgres":
p.SqlCreateTable = fmt.Sprintf(sqlCreateTablePostgres, p.tableName)
p.SqlInsertRow = fmt.Sprintf(sqlInsertRowPostgres, p.tableName)
p.SqlUpdateRow = fmt.Sprintf(sqlUpdateRowPostgres, p.tableName)
p.SqlDeleteRow = fmt.Sprintf(sqlDeleteRowPostgres, p.tableName)
case "mysql":
p.SqlCreateTable = fmt.Sprintf(sqlCreateTableMysql, p.tableName)
case "sqlite3":
p.SqlCreateTable = fmt.Sprintf(sqlCreateTableSqlite3, p.tableName)
p.SqlTruncateTable = fmt.Sprintf(sqlTruncateTableSqlite3, p.tableName)
case "sqlserver":
p.SqlCreateTable = fmt.Sprintf(sqlCreateTableSqlserver, p.tableName)
p.SqlInsertRow = fmt.Sprintf(sqlInsertRowSqlserver, p.tableName)
p.SqlUpdateRow = fmt.Sprintf(sqlUpdateRowSqlserver, p.tableName)
p.SqlDeleteRow = fmt.Sprintf(sqlDeleteRowSqlserver, p.tableName)
}
}
// createTable create a not exists table.
func (p *SqlAdapter) CreateTable() error {
_, err := p.db.ExecContext(p.ctx, p.SqlCreateTable)
return err
}
// truncateTable clear the table.
func (p *SqlAdapter) TruncateTable() error {
_, err := p.db.ExecContext(p.ctx, p.SqlTruncateTable)
return err
}
// deleteAll clear the table.
func (p *SqlAdapter) DeleteAll() error {
_, err := p.db.ExecContext(p.ctx, p.SqlDeleteAll)
return err
}
// isTableExist check the table exists.
func (p *SqlAdapter) IsTableExist() bool {
_, err := p.db.ExecContext(p.ctx, p.SqlIsTableExist)
return err == nil
}
// deleteRows delete eligible data.
func (p *SqlAdapter) DeleteRows(query string, args ...interface{}) error {
query = p.db.Rebind(query)
_, err := p.db.ExecContext(p.ctx, query, args...)
return err
}
// truncateAndInsertRows clear table and insert new rows.
func (p *SqlAdapter) TruncateAndInsertRows(rules [][]interface{}) error {
if err := p.TruncateTable(); err != nil {
return err
}
return p.execTxSqlRows(p.SqlInsertRow, rules)
}
// deleteAllAndInsertRows clear table and insert new rows.
func (p *SqlAdapter) DeleteAllAndInsertRows(rules [][]interface{}) error {
if err := p.DeleteAll(); err != nil {
return err
}
return p.execTxSqlRows(p.SqlInsertRow, rules)
}
// execTxSqlRows exec sql rows.
func (p *SqlAdapter) execTxSqlRows(query string, rules [][]interface{}) (err error) {
tx, err := p.db.BeginTx(p.ctx, nil)
if err != nil {
return
}
var action string
stmt, err := tx.PrepareContext(p.ctx, query)
if err != nil {
action = "prepare context"
goto ROLLBACK
}
for _, rule := range rules {
if _, err = stmt.ExecContext(p.ctx, rule...); err != nil {
action = "stmt exec"
goto ROLLBACK
}
}
if err = stmt.Close(); err != nil {
action = "stmt close"
goto ROLLBACK
}
if err = tx.Commit(); err != nil {
action = "commit"
goto ROLLBACK
}
return
ROLLBACK:
if err1 := tx.Rollback(); err1 != nil {
err = fmt.Errorf("%s err: %v, rollback err: %v", action, err, err1)
}
return
}
// selectRows select eligible data by args from the table.
func (p *SqlAdapter) SelectRows(query string, args ...interface{}) ([]*SqlCasbinRule, error) {
// make a slice with capacity
lines := make([]*SqlCasbinRule, 0, 64)
if len(args) == 0 {
return lines, p.db.SelectContext(p.ctx, &lines, query)
}
query = p.db.Rebind(query)
return lines, p.db.SelectContext(p.ctx, &lines, query, args...)
}
// selectWhereIn select eligible data by filter from the table.
func (p *SqlAdapter) SelectWhereIn(filter *SqlFilter) (lines []*SqlCasbinRule, err error) {
var sqlBuf bytes.Buffer
sqlBuf.Grow(64)
sqlBuf.WriteString(p.SqlSelectWhere)
args := make([]interface{}, 0, 4)
hasInCond := false
for _, col := range [maxParamLength]struct {
name string
arg []string
}{
{"p_type", filter.PType},
{"v0", filter.V0},
{"v1", filter.V1},
{"v2", filter.V2},
{"v3", filter.V3},
{"v4", filter.V4},
{"v5", filter.V5},
} {
l := len(col.arg)
if l == 0 {
continue
}
switch sqlBuf.Bytes()[sqlBuf.Len()-1] {
case '?', ')':
sqlBuf.WriteString(" AND ")
}
sqlBuf.WriteString(col.name)
if l == 1 {
sqlBuf.WriteString("=?")
args = append(args, col.arg[0])
} else {
sqlBuf.WriteString(" IN (?)")
args = append(args, col.arg)
hasInCond = true
}
}
var query string
if hasInCond {
if query, args, err = sqlx.In(sqlBuf.String(), args...); err != nil {
return
}
} else {
query = sqlBuf.String()
}
return p.SelectRows(query, args...)
}
// LoadPolicy load all policy rules from the storage.
func (p *SqlAdapter) LoadPolicy(model model.Model) error {
lines, err := p.SelectRows(p.SqlSelectAll)
if err != nil {
return err
}
for _, line := range lines {
p.loadPolicyLine(line, model)
}
return nil
}
// SavePolicy save policy rules to the storage.
func (p *SqlAdapter) SavePolicy(model model.Model) error {
args := make([][]interface{}, 0, 64)
for ptype, ast := range model["p"] {
for _, rule := range ast.Policy {
arg := p.GenArgs(ptype, rule)
args = append(args, arg)
}
}
for ptype, ast := range model["g"] {
for _, rule := range ast.Policy {
arg := p.GenArgs(ptype, rule)
args = append(args, arg)
}
}
return p.DeleteAllAndInsertRows(args)
}
// AddPolicy add one policy rule to the storage.
func (p *SqlAdapter) AddPolicy(sec string, ptype string, rule []string) error {
args := p.GenArgs(ptype, rule)
_, err := p.db.ExecContext(p.ctx, p.SqlInsertRow, args...)
return err
}
// AddPolicies add multiple policy rules to the storage.
func (p *SqlAdapter) AddPolicies(sec string, ptype string, rules [][]string) error {
args := make([][]interface{}, 0, 8)
for _, rule := range rules {
arg := p.GenArgs(ptype, rule)
args = append(args, arg)
}
return p.execTxSqlRows(p.SqlInsertRow, args)
}
// RemovePolicy remove policy rules from the storage.
func (p *SqlAdapter) RemovePolicy(sec string, ptype string, rule []string) error {
var sqlBuf bytes.Buffer
sqlBuf.Grow(64)
sqlBuf.WriteString(p.SqlDeleteByArgs)
args := make([]interface{}, 0, 4)
args = append(args, ptype)
for idx, arg := range rule {
if arg != "" {
sqlBuf.WriteString(" AND v")
sqlBuf.WriteString(strconv.Itoa(idx))
sqlBuf.WriteString("=?")
args = append(args, arg)
}
}
return p.DeleteRows(sqlBuf.String(), args...)
}
// RemoveFilteredPolicy remove policy rules that match the filter from the storage.
func (p *SqlAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
var sqlBuf bytes.Buffer
sqlBuf.Grow(64)
sqlBuf.WriteString(p.SqlDeleteByArgs)
args := make([]interface{}, 0, 4)
args = append(args, ptype)
var value string
l := fieldIndex + len(fieldValues)
for idx := 0; idx < 6; idx++ {
if fieldIndex <= idx && idx < l {
value = fieldValues[idx-fieldIndex]
if value != "" {
sqlBuf.WriteString(" AND v")
sqlBuf.WriteString(strconv.Itoa(idx))
sqlBuf.WriteString("=?")
args = append(args, value)
}
}
}
return p.DeleteRows(sqlBuf.String(), args...)
}
// RemovePolicies remove policy rules.
func (p *SqlAdapter) RemovePolicies(sec string, ptype string, rules [][]string) (err error) {
args := make([][]interface{}, 0, 8)
for _, rule := range rules {
arg := p.GenArgs(ptype, rule)
args = append(args, arg)
}
return p.execTxSqlRows(p.SqlDeleteRow, args)
}
// LoadFilteredPolicy load policy rules that match the filter.
// filterPtr must be a pointer.
func (p *SqlAdapter) LoadFilteredPolicy(model model.Model, filterPtr interface{}) error {
if filterPtr == nil {
return p.LoadPolicy(model)
}
filter, ok := filterPtr.(*SqlFilter)
if !ok {
return errors.New("invalid filter type")
}
lines, err := p.SelectWhereIn(filter)
if err != nil {
return err
}
for _, line := range lines {
p.loadPolicyLine(line, model)
}
p.isFiltered = true
return nil
}
// IsFiltered returns true if the loaded policy rules has been filtered.
func (p *SqlAdapter) IsFiltered() bool {
return p.isFiltered
}
// UpdatePolicy update a policy rule from storage.
// This is part of the Auto-Save feature.
func (p *SqlAdapter) UpdatePolicy(sec, ptype string, oldRule, newPolicy []string) error {
oldArg := p.GenArgs(ptype, oldRule)
newArg := p.GenArgs(ptype, newPolicy)
_, err := p.db.ExecContext(p.ctx, p.SqlUpdateRow, append(newArg, oldArg...)...)
return err
}
// UpdatePolicies updates policy rules to storage.
func (p *SqlAdapter) UpdatePolicies(sec, ptype string, oldRules, newRules [][]string) (err error) {
if len(oldRules) != len(newRules) {
return errors.New("old rules size not equal to new rules size")
}
args := make([][]interface{}, 0, 16)
for idx := range oldRules {
oldArg := p.GenArgs(ptype, oldRules[idx])
newArg := p.GenArgs(ptype, newRules[idx])
args = append(args, append(newArg, oldArg...))
}
return p.execTxSqlRows(p.SqlUpdateRow, args)
}
// UpdateFilteredPolicies deletes old rules and adds new rules.
func (p *SqlAdapter) UpdateFilteredPolicies(sec, ptype string, newPolicies [][]string, fieldIndex int, fieldValues ...string) (oldPolicies [][]string, err error) {
var value string
var whereBuf bytes.Buffer
whereBuf.Grow(32)
l := fieldIndex + len(fieldValues)
whereArgs := make([]interface{}, 0, 4)
whereArgs = append(whereArgs, ptype)
for idx := 0; idx < 6; idx++ {
if fieldIndex <= idx && idx < l {
value = fieldValues[idx-fieldIndex]
if value != "" {
whereBuf.WriteString(" AND v")
whereBuf.WriteString(strconv.Itoa(idx))
whereBuf.WriteString("=?")
whereArgs = append(whereArgs, value)
}
}
}
var selectBuf bytes.Buffer
selectBuf.Grow(64)
selectBuf.WriteString(p.SqlSelectWhere)
selectBuf.WriteString("p_type=?")
selectBuf.Write(whereBuf.Bytes())
var oldRows []*SqlCasbinRule
value = p.db.Rebind(selectBuf.String())
oldRows, err = p.SelectRows(value, whereArgs...)
if err != nil {
return
}
var deleteBuf bytes.Buffer
deleteBuf.Grow(64)
deleteBuf.WriteString(p.SqlDeleteByArgs)
deleteBuf.Write(whereBuf.Bytes())
var tx *sqlx.Tx
tx, err = p.db.BeginTxx(p.ctx, nil)
if err != nil {
return
}
var (
stmt *sqlx.Stmt
action string
)
value = p.db.Rebind(deleteBuf.String())
if _, err = tx.ExecContext(p.ctx, value, whereArgs...); err != nil {
action = "delete old policies"
goto ROLLBACK
}
stmt, err = tx.PreparexContext(p.ctx, p.SqlInsertRow)
if err != nil {
action = "preparex context"
goto ROLLBACK
}
for _, policy := range newPolicies {
arg := p.GenArgs(ptype, policy)
if _, err = stmt.ExecContext(p.ctx, arg...); err != nil {
action = "stmt exec context"
goto ROLLBACK
}
}
if err = stmt.Close(); err != nil {
action = "stmt close"
goto ROLLBACK
}
if err = tx.Commit(); err != nil {
action = "commit"
goto ROLLBACK
}
oldPolicies = make([][]string, 0, len(oldRows))
for _, rule := range oldRows {
oldPolicies = append(oldPolicies, []string{rule.PType, rule.V0, rule.V1, rule.V2, rule.V3, rule.V4, rule.V5})
}
return
ROLLBACK:
if err1 := tx.Rollback(); err1 != nil {
err = fmt.Errorf("%s err: %v, rollback err: %v", action, err, err1)
}
return
}
// loadPolicyLine load a policy line to model.
func (SqlAdapter) loadPolicyLine(line *SqlCasbinRule, model model.Model) {
if line == nil {
return
}
var lineBuf bytes.Buffer
lineBuf.Grow(64)
lineBuf.WriteString(line.PType)
args := [6]string{line.V0, line.V1, line.V2, line.V3, line.V4, line.V5}
for _, arg := range args {
if arg != "" {
lineBuf.WriteByte(',')
lineBuf.WriteString(arg)
}
}
persist.LoadPolicyLine(lineBuf.String(), model)
}
// genArgs generate args from ptype and rule.
func (SqlAdapter) GenArgs(ptype string, rule []string) []interface{} {
l := len(rule)
args := make([]interface{}, maxParamLength)
args[0] = ptype
for idx := 0; idx < l; idx++ {
args[idx+1] = rule[idx]
}
for idx := l + 1; idx < maxParamLength; idx++ {
args[idx] = ""
}
return args
}

View File

@ -0,0 +1,466 @@
//
// adapter_sqlx_test.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcasbin_test
import (
"os"
"strings"
"testing"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/util"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"hexq.cn/tiglog/golib/gcasbin"
)
const (
rbacModelFile = "testdata/rbac_model.conf"
rbacPolicyFile = "testdata/rbac_policy.csv"
)
var (
dataSourceNames = map[string]string{
// "sqlite3": ":memory:",
// "mysql": "root:@tcp(127.0.0.1:3306)/sqlx_adapter_test",
"postgres": os.Getenv("DB_DSN"),
// "sqlserver": "sqlserver://sa:YourPassword@127.0.0.1:1433?database=sqlx_adapter_test&connection+timeout=30",
}
lines = []gcasbin.SqlCasbinRule{
{PType: "p", V0: "alice", V1: "data1", V2: "read"},
{PType: "p", V0: "bob", V1: "data2", V2: "read"},
{PType: "p", V0: "bob", V1: "data2", V2: "write"},
{PType: "p", V0: "data2_admin", V1: "data1", V2: "read", V3: "test1", V4: "test2", V5: "test3"},
{PType: "p", V0: "data2_admin", V1: "data2", V2: "write", V3: "test1", V4: "test2", V5: "test3"},
{PType: "p", V0: "data1_admin", V1: "data2", V2: "write"},
{PType: "g", V0: "alice", V1: "data2_admin"},
{PType: "g", V0: "bob", V1: "data2_admin", V2: "test"},
{PType: "g", V0: "bob", V1: "data1_admin", V2: "test2", V3: "test3", V4: "test4", V5: "test5"},
}
filter = gcasbin.SqlFilter{
PType: []string{"p"},
V0: []string{"bob", "data2_admin"},
V1: []string{"data1", "data2"},
V2: []string{"read", "write"},
V3: []string{"test1"},
V4: []string{"test2"},
V5: []string{"test3"},
}
)
func TestSqlAdapters(t *testing.T) {
for key, value := range dataSourceNames {
t.Logf("-------------------- test [%s] start, dataSourceName: [%s]", key, value)
db, err := sqlx.Connect(key, value)
if err != nil {
t.Fatalf("sqlx.Connect failed, err: %v", err)
}
t.Log("---------- testTableName start")
testTableName(t, db)
t.Log("---------- testTableName finished")
t.Log("---------- testSQL start")
testSQL(t, db, "sqlxadapter_sql")
t.Log("---------- testSQL finished")
t.Log("---------- testSaveLoad start")
testSaveLoad(t, db, "sqlxadapter_save_load")
t.Log("---------- testSaveLoad finished")
t.Log("---------- testAutoSave start")
testAutoSave(t, db, "sqlxadapter_auto_save")
t.Log("---------- testAutoSave finished")
t.Log("---------- testFilteredSqlPolicy start")
testFilteredSqlPolicy(t, db, "sqlxadapter_filtered_policy")
t.Log("---------- testFilteredSqlPolicy finished")
// t.Log("---------- testUpdateSqlPolicy start")
// testUpdateSqlPolicy(t, db, "sqladapter_filtered_policy")
// t.Log("---------- testUpdateSqlPolicy finished")
// t.Log("---------- testUpdateSqlPolicies start")
// testUpdateSqlPolicies(t, db, "sqladapter_filtered_policy")
// t.Log("---------- testUpdateSqlPolicies finished")
// t.Log("---------- testUpdateFilteredSqlPolicies start")
// testUpdateFilteredSqlPolicies(t, db, "sqladapter_filtered_policy")
// t.Log("---------- testUpdateFilteredSqlPolicies finished")
}
}
func testTableName(t *testing.T, db *sqlx.DB) {
_, err := gcasbin.NewSqlAdapter(db, "")
if err != nil {
t.Fatalf("NewAdapter failed, err: %v", err)
}
}
func testSQL(t *testing.T, db *sqlx.DB, tableName string) {
var err error
logErr := func(action string) {
if err != nil {
t.Errorf("%s test failed, err: %v", action, err)
}
}
equalValue := func(line1, line2 gcasbin.SqlCasbinRule) bool {
if line1.PType != line2.PType ||
line1.V0 != line2.V0 ||
line1.V1 != line2.V1 ||
line1.V2 != line2.V2 ||
line1.V3 != line2.V3 ||
line1.V4 != line2.V4 ||
line1.V5 != line2.V5 {
return false
}
return true
}
var a *gcasbin.SqlAdapter
a, err = gcasbin.NewSqlAdapter(db, tableName)
logErr("NewSqlAdapter")
// createTable test has passed when adapter create
// err = a.CreateTable()
// logErr("createTable")
if b := a.IsTableExist(); b == false {
t.Fatal("isTableExist test failed")
}
rules := make([][]interface{}, len(lines))
for idx, rule := range lines {
args := a.GenArgs(rule.PType, []string{rule.V0, rule.V1, rule.V2, rule.V3, rule.V4, rule.V5})
rules[idx] = args
}
err = a.TruncateAndInsertRows(rules)
logErr("truncateAndInsertRows")
err = a.DeleteAllAndInsertRows(rules)
logErr("truncateAndInsertRows")
err = a.DeleteRows(a.SqlDeleteByArgs, "g")
logErr("deleteRows sqlDeleteByArgs g")
err = a.DeleteRows(a.SqlDeleteAll)
logErr("deleteRows sqlDeleteAll")
_ = a.TruncateAndInsertRows(rules)
records, err := a.SelectRows(a.SqlSelectAll)
logErr("selectRows sqlSelectAll")
for idx, record := range records {
line := lines[idx]
if !equalValue(*record, line) {
t.Fatalf("selectRows records test not equal, query record: %+v, need record: %+v", record, line)
}
}
records, err = a.SelectWhereIn(&filter)
logErr("selectWhereIn")
i := 3
for _, record := range records {
line := lines[i]
if !equalValue(*record, line) {
t.Fatalf("selectWhereIn records test not equal, query record: %+v, need record: %+v", record, line)
}
i++
}
err = a.TruncateTable()
logErr("truncateTable")
}
func initSqlPolicy(t *testing.T, db *sqlx.DB, tableName string) {
// Because the DB is empty at first,
// so we need to load the policy from the file adapter (.CSV) first.
e, _ := casbin.NewEnforcer(rbacModelFile, rbacPolicyFile)
a, err := gcasbin.NewSqlAdapter(db, tableName)
if err != nil {
t.Fatal("NewAdapter test failed, err: ", err)
}
// This is a trick to save the current policy to the DB.
// We can't call e.SavePolicy() because the adapter in the enforcer is still the file adapter.
// The current policy means the policy in the Casbin enforcer (aka in memory).
err = a.SavePolicy(e.GetModel())
if err != nil {
t.Fatal("SavePolicy test failed, err: ", err)
}
// Clear the current policy.
e.ClearPolicy()
testGetSqlPolicy(t, e, [][]string{})
// Load the policy from DB.
err = a.LoadPolicy(e.GetModel())
if err != nil {
t.Fatal("LoadPolicy test failed, err: ", err)
}
testGetSqlPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}})
}
func testSaveLoad(t *testing.T, db *sqlx.DB, tableName string) {
// Initialize some policy in DB.
initSqlPolicy(t, db, tableName)
// Note: you don't need to look at the above code
// if you already have a working DB with policy inside.
// Now the DB has policy, so we can provide a normal use case.
// Create an adapter and an enforcer.
// NewEnforcer() will load the policy automatically.
a, _ := gcasbin.NewSqlAdapter(db, tableName)
e, _ := casbin.NewEnforcer(rbacModelFile, a)
testGetSqlPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}})
}
func testAutoSave(t *testing.T, db *sqlx.DB, tableName string) {
// Initialize some policy in DB.
initSqlPolicy(t, db, tableName)
// Note: you don't need to look at the above code
// if you already have a working DB with policy inside.
// Now the DB has policy, so we can provide a normal use case.
// Create an adapter and an enforcer.
// NewEnforcer() will load the policy automatically.
a, _ := gcasbin.NewSqlAdapter(db, tableName)
e, _ := casbin.NewEnforcer(rbacModelFile, a)
// AutoSave is enabled by default.
// Now we disable it.
e.EnableAutoSave(false)
var err error
logErr := func(action string) {
if err != nil {
t.Errorf("%s test failed, err: %v", action, err)
}
}
// Because AutoSave is disabled, the policy change only affects the policy in Casbin enforcer,
// it doesn't affect the policy in the storage.
_, err = e.AddPolicy("alice", "data1", "write")
logErr("AddPolicy1")
// Reload the policy from the storage to see the effect.
err = e.LoadPolicy()
logErr("LoadPolicy1")
// This is still the original policy.
testGetSqlPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}})
_, err = e.AddPolicies([][]string{{"alice_1", "data_1", "read_1"}, {"bob_1", "data_1", "write_1"}})
logErr("AddPolicies1")
// Reload the policy from the storage to see the effect.
err = e.LoadPolicy()
logErr("LoadPolicy2")
// This is still the original policy.
testGetSqlPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}})
// Now we enable the AutoSave.
e.EnableAutoSave(true)
// Because AutoSave is enabled, the policy change not only affects the policy in Casbin enforcer,
// but also affects the policy in the storage.
_, err = e.AddPolicy("alice", "data1", "write")
logErr("AddPolicy2")
// Reload the policy from the storage to see the effect.
err = e.LoadPolicy()
logErr("LoadPolicy3")
// The policy has a new rule: {"alice", "data1", "write"}.
testGetSqlPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}, {"alice", "data1", "write"}})
_, err = e.AddPolicies([][]string{{"alice_2", "data_2", "read_2"}, {"bob_2", "data_2", "write_2"}})
logErr("AddPolicies2")
// Reload the policy from the storage to see the effect.
err = e.LoadPolicy()
logErr("LoadPolicy4")
// This is still the original policy.
testGetSqlPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}, {"alice", "data1", "write"},
{"alice_2", "data_2", "read_2"}, {"bob_2", "data_2", "write_2"}})
_, err = e.RemovePolicies([][]string{{"alice_2", "data_2", "read_2"}, {"bob_2", "data_2", "write_2"}})
logErr("RemovePolicies")
err = e.LoadPolicy()
logErr("LoadPolicy5")
testGetSqlPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}, {"alice", "data1", "write"}})
// Remove the added rule.
_, err = e.RemovePolicy("alice", "data1", "write")
logErr("RemovePolicy")
err = e.LoadPolicy()
logErr("LoadPolicy6")
testGetSqlPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}})
// Remove "data2_admin" related policy rules via a filter.
// Two rules: {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"} are deleted.
_, err = e.RemoveFilteredPolicy(0, "data2_admin")
logErr("RemoveFilteredPolicy")
err = e.LoadPolicy()
logErr("LoadPolicy7")
testGetSqlPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}})
}
func testFilteredSqlPolicy(t *testing.T, db *sqlx.DB, tableName string) {
// Initialize some policy in DB.
initSqlPolicy(t, db, tableName)
// Note: you don't need to look at the above code
// if you already have a working DB with policy inside.
// Now the DB has policy, so we can provide a normal use case.
// Create an adapter and an enforcer.
// NewEnforcer() will load the policy automatically.
a, _ := gcasbin.NewSqlAdapter(db, tableName)
e, _ := casbin.NewEnforcer(rbacModelFile, a)
// Now set the adapter
e.SetAdapter(a)
var err error
logErr := func(action string) {
if err != nil {
t.Errorf("%s test failed, err: %v", action, err)
}
}
// Load only alice's policies
err = e.LoadFilteredPolicy(&gcasbin.SqlFilter{V0: []string{"alice"}})
logErr("LoadFilteredPolicy alice")
testGetSqlPolicy(t, e, [][]string{{"alice", "data1", "read"}})
// Load only bob's policies
err = e.LoadFilteredPolicy(&gcasbin.SqlFilter{V0: []string{"bob"}})
logErr("LoadFilteredPolicy bob")
testGetSqlPolicy(t, e, [][]string{{"bob", "data2", "write"}})
// Load policies for data2_admin
err = e.LoadFilteredPolicy(&gcasbin.SqlFilter{V0: []string{"data2_admin"}})
logErr("LoadFilteredPolicy data2_admin")
testGetSqlPolicy(t, e, [][]string{{"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}})
// Load policies for alice and bob
err = e.LoadFilteredPolicy(&gcasbin.SqlFilter{V0: []string{"alice", "bob"}})
logErr("LoadFilteredPolicy alice bob")
testGetSqlPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}})
_, err = e.AddPolicy("bob", "data1", "write", "test1", "test2", "test3")
logErr("AddPolicy")
err = e.LoadFilteredPolicy(&filter)
logErr("LoadFilteredPolicy filter")
testGetSqlPolicy(t, e, [][]string{{"bob", "data1", "write", "test1", "test2", "test3"}})
}
func testUpdateSqlPolicy(t *testing.T, db *sqlx.DB, tableName string) {
// Initialize some policy in DB.
initSqlPolicy(t, db, tableName)
a, _ := gcasbin.NewSqlAdapter(db, tableName)
e, _ := casbin.NewEnforcer(rbacModelFile, a)
e.EnableAutoSave(true)
e.UpdatePolicy([]string{"alice", "data1", "read"}, []string{"alice", "data1", "write"})
e.LoadPolicy()
testGetSqlPolicy(t, e, [][]string{{"alice", "data1", "write"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}})
}
func testUpdateSqlPolicies(t *testing.T, db *sqlx.DB, tableName string) {
// Initialize some policy in DB.
initSqlPolicy(t, db, tableName)
a, _ := gcasbin.NewSqlAdapter(db, tableName)
e, _ := casbin.NewEnforcer(rbacModelFile, a)
e.EnableAutoSave(true)
e.UpdatePolicies([][]string{{"alice", "data1", "write"}, {"bob", "data2", "write"}}, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "read"}})
e.LoadPolicy()
testGetSqlPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "read"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}})
}
func testUpdateFilteredSqlPolicies(t *testing.T, db *sqlx.DB, tableName string) {
// Initialize some policy in DB.
initSqlPolicy(t, db, tableName)
a, _ := gcasbin.NewSqlAdapter(db, tableName)
e, _ := casbin.NewEnforcer(rbacModelFile, a)
e.EnableAutoSave(true)
e.UpdateFilteredPolicies([][]string{{"alice", "data1", "write"}}, 0, "alice", "data1", "read")
e.UpdateFilteredPolicies([][]string{{"bob", "data2", "read"}}, 0, "bob", "data2", "write")
e.LoadPolicy()
testGetSqlPolicyWithoutOrder(t, e, [][]string{{"alice", "data1", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}, {"bob", "data2", "read"}})
}
func testGetSqlPolicy(t *testing.T, e *casbin.Enforcer, res [][]string) {
t.Helper()
myRes := e.GetPolicy()
t.Log("Policy: ", myRes)
m := make(map[string]struct{}, len(myRes))
for _, record := range myRes {
key := strings.Join(record, ",")
m[key] = struct{}{}
}
for _, record := range res {
key := strings.Join(record, ",")
if _, ok := m[key]; !ok {
t.Error("Policy: \n", myRes, ", supposed to be \n", res)
break
}
}
}
func testGetSqlPolicyWithoutOrder(t *testing.T, e *casbin.Enforcer, res [][]string) {
myRes := e.GetPolicy()
// log.Print("Policy: \n", myRes)
if !arraySqlEqualsWithoutOrder(myRes, res) {
t.Error("Policy: \n", myRes, ", supposed to be \n", res)
}
}
func arraySqlEqualsWithoutOrder(a [][]string, b [][]string) bool {
if len(a) != len(b) {
return false
}
mapA := make(map[int]string)
mapB := make(map[int]string)
order := make(map[int]struct{})
l := len(a)
for i := 0; i < l; i++ {
mapA[i] = util.ArrayToString(a[i])
mapB[i] = util.ArrayToString(b[i])
}
for i := 0; i < l; i++ {
for j := 0; j < l; j++ {
if _, ok := order[j]; ok {
if j == l-1 {
return false
} else {
continue
}
}
if mapA[i] == mapB[j] {
order[j] = struct{}{}
break
} else if j == l-1 {
return false
}
}
}
return true
}

8
gcasbin/casbin.go Normal file
View File

@ -0,0 +1,8 @@
//
// casbin.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcasbin

17
gcasbin/middleware.go Normal file
View File

@ -0,0 +1,17 @@
//
// middleware.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcasbin
import "github.com/gin-gonic/gin"
func GinCasbin() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
}
}

18
gcasbin/readme.adoc Normal file
View File

@ -0,0 +1,18 @@
= 授权
:author: tiglog
:experimental:
:toc: left
:toclevels: 3
:toc-title: 目录
:sectnums:
:icons: font
:!webfonts:
:autofit-option:
:source-highlighter: rouge
:rouge-style: github
:source-linenums-option:
:revdate: 2022-12-01
:imagesdir: ./img

18
gcasbin/testdata/model.conf vendored Normal file
View File

@ -0,0 +1,18 @@
# Request definition
[request_definition]
r = sub, obj, act
# Policy definition
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
# Policy effect
[policy_effect]
e = some(where (p.eft == allow))
# Matchers
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

3
gcasbin/testdata/policy.csv vendored Normal file
View File

@ -0,0 +1,3 @@
p, admin, data, read
p, admin, data, write
g, bob, admin
1 p, admin, data, read
2 p, admin, data, write
3 g, bob, admin

14
gcasbin/testdata/rbac_model.conf vendored Normal file
View File

@ -0,0 +1,14 @@
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

5
gcasbin/testdata/rbac_policy.csv vendored Normal file
View File

@ -0,0 +1,5 @@
p, alice, data1, read
p, bob, data2, write
p, data2_admin, data2, read
p, data2_admin, data2, write
g, alice, data2_admin
1 p, alice, data1, read
2 p, bob, data2, write
3 p, data2_admin, data2, read
4 p, data2_admin, data2, write
5 g, alice, data2_admin

View File

@ -0,0 +1,14 @@
[request_definition]
r = tenant, sub, obj, act, service
[policy_definition]
p =tenant, sub, obj, act, service, eft
[role_definition]
g = _, _
[policy_effect]
e = priority(p.eft) || deny
[matchers]
m = r.tenant == p.tenant && g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*") && (r.service == p.service || p.service == "*")

13
gconfig/auth.go Normal file
View File

@ -0,0 +1,13 @@
//
// auth.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gconfig
type AuthConfig struct {
TokenTtl int `yaml:"token_ttl"`
RefreshTtl int `yaml:"refresh_ttl"`
}

60
gconfig/config.go Normal file
View File

@ -0,0 +1,60 @@
//
// config.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gconfig
import (
"bytes"
"io/ioutil"
"text/template"
"hexq.cn/tiglog/golib/gfile"
"gopkg.in/yaml.v2"
"hexq.cn/tiglog/golib/helper"
)
type BaseConfig struct {
BaseDir string
Http HttpConfig `json:"http" yaml:"http"`
Auth AuthConfig `json:"auth" yaml:"auth"`
Param ParamConfig `json:"param" yaml:"param"`
Db DbConfig `json:"db" yaml:"db"`
Mongo MongoConfig `json:"mongo" yaml:"mongo"`
Redis RedisConfig `json:"redis" yaml:"redis"`
}
func (c *BaseConfig) LoadParams() {
if c.BaseDir == "" {
c.BaseDir = "./etc"
}
c.Param.Load(c.BaseDir + "/params.yaml")
}
func (c *BaseConfig) ParseAppConfig() {
buf := c.GetData("/app.yaml", true)
err := yaml.Unmarshal(buf.Bytes(), c)
helper.CheckErr(err)
}
func (c *BaseConfig) GetData(fname string, must bool) *bytes.Buffer {
fp := c.BaseDir + fname
if !gfile.Exists(fp) {
if must {
panic("配置文件" + fp + "不存在")
} else {
return nil
}
}
dat, err := ioutil.ReadFile(fp)
helper.CheckErr(err)
tpl, err := template.New("config").Parse(string(dat))
helper.CheckErr(err)
buf := new(bytes.Buffer)
tpl.Execute(buf, c.Param.Params)
return buf
}

56
gconfig/db.go Normal file
View File

@ -0,0 +1,56 @@
//
// db.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gconfig
import "fmt"
type DbConfig struct {
Type string `yaml:"type"`
Host string `yaml:"host"`
Username string `yaml:"user"`
Password string `yaml:"pass"`
Port int `yaml:"port"`
Name string `yaml:"name"`
}
type MongoConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Username string `yaml:"user"`
Password string `yaml:"pass"`
Name string `yaml:"name"`
PoolSize int `yaml:"pool_size"`
}
func (c *DbConfig) GetUri() string {
switch c.Type {
case "postgres":
return fmt.Sprintf("%s://host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", c.Type, c.Host, c.Port, c.Username, c.Password, c.Name)
case "mysql":
return fmt.Sprintf("%s://%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=Local", c.Type, c.Username, c.Password, c.Host, c.Port, c.Name)
}
return ""
}
func (c *MongoConfig) GetUri() string {
if c.Host == "" {
return ""
}
if c.Username == "" {
return fmt.Sprintf("mongodb://%s:%d/%s", c.Host, c.Port, c.Name)
} else {
return fmt.Sprintf("mongodb://%s:%s@%s:%d/%s", c.Username, c.Password, c.Host, c.Port, c.Name)
}
}
type RedisConfig struct {
Addr string `yaml:"addr"`
Username string `yaml:"user"`
Password string `yaml:"pass"`
Database int `yaml:"db"`
}

30
gconfig/http.go Normal file
View File

@ -0,0 +1,30 @@
//
// http.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gconfig
import "path/filepath"
type HttpConfig struct {
Addr string `yaml:"addr"`
Env string `yaml:"env"`
Debug bool `yaml:"debug"`
Storage string `yaml:"storage"` // 存储文件的目录。如果不是绝对路径,前面的 "./" 也不需要
}
func (c HttpConfig) GetBaseDir() string {
dir, _ := filepath.Abs("./")
return dir
}
// 全路径的 storage dir
func (c HttpConfig) GetStorageDir() string {
if c.Storage[0] == '/' {
return c.Storage
}
return filepath.Join(c.GetBaseDir(), "/", c.Storage)
}

33
gconfig/param.go Normal file
View File

@ -0,0 +1,33 @@
//
// param.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gconfig
import (
"io/ioutil"
"gopkg.in/yaml.v2"
"hexq.cn/tiglog/golib/gfile"
)
type ParamConfig struct {
Params map[string]any
}
func (c *ParamConfig) Load(fp string) {
if !gfile.Exists(fp) {
panic("配置文件 " + fp + " 不存在")
}
dat, err := ioutil.ReadFile(fp)
if err != nil {
panic(err)
}
err = yaml.Unmarshal(dat, &c.Params)
if err != nil {
panic(err)
}
}

0
gconfig/testdata/app.yaml vendored Normal file
View File

0
gconfig/testdata/params.yaml vendored Normal file
View File

24
gconsts/err_code.go Normal file
View File

@ -0,0 +1,24 @@
//
// err_code.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gconsts
const (
ErrCodeNone = 0 // 正确的结果
ErrCodeBadRequest = 40000 // 无效的请求
ErrCodeValidateFail = 40001 // 验证失败
ErrCodeNoLogin = 40100 // 没有认证
ErrCodeNoToken = 40101 // 没有带 token
ErrCodeExpiredToken = 40102 // token 过期
ErrCodeInvalidToken = 40103 // 1无效的 token
ErrCodeNoPermission = 40300 // 没有权限
ErrCodePageNotFound = 40400 // 页面不存在
ErrCodeEntryNotFound = 40401 // 对象不存在
ErrCodeResNotFound = 40402 // 资源不存在
ErrCodeInternal = 50000 // 服务内部错误
)

13
gdb/mgodb/bson.go Normal file
View File

@ -0,0 +1,13 @@
//
// bson.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package mgodb
import "go.mongodb.org/mongo-driver/bson"
type M = bson.M
type D = bson.D

33
gdb/mgodb/error.go Normal file
View File

@ -0,0 +1,33 @@
//
// error.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package mgodb
import "go.mongodb.org/mongo-driver/mongo"
var (
// ErrNilDocument is returned when a nil document is passed to a CRUD method.
ErrNilDocument = mongo.ErrNilDocument
// ErrNilValue is returned when a nil value is passed to a CRUD method.
ErrNilValue = mongo.ErrNilValue
// ErrNoDocuments is returned by SingleResult methods when the operation that
// created the SingleResult did not return any documents.
ErrNoDocuments = mongo.ErrNoDocuments
)
func IsNoDocuments(err error) bool {
return err == mongo.ErrNoDocuments
}
func IsNilValue(err error) bool {
return err == mongo.ErrNilValue
}
func IsNilDocument(err error) bool {
return err == mongo.ErrNilDocument
}

99
gdb/mgodb/mongo.go Normal file
View File

@ -0,0 +1,99 @@
//
// mongo.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package mgodb
import (
"context"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
var Mm *sMongoManager
type sMongoManager struct {
cli *mongo.Client
name string
poolSize int
uri string
}
func Init(uri, dbName string, poolSize int) {
Mm = &sMongoManager{
name: dbName,
uri: uri,
poolSize: poolSize,
}
}
func (s *sMongoManager) SetDb(name string) {
s.name = name
}
func (s *sMongoManager) Connect() error {
var err error
clientOptions := options.Client().ApplyURI(s.uri)
clientOptions.SetMaxPoolSize(uint64(s.poolSize))
// 连接到MongoDB
s.cli, err = mongo.Connect(context.TODO(), clientOptions)
if err != nil {
return err
}
// 检查连接
err = s.cli.Ping(context.TODO(), nil)
if err != nil {
return err
}
return nil
}
func (s *sMongoManager) Client() (*mongo.Client, error) {
if s.cli == nil {
err := s.Connect()
if err != nil {
return nil, err
}
}
return s.cli, nil
}
func (s *sMongoManager) Db() *mongo.Database {
c, err := s.Client()
if err != nil {
panic(err)
}
return c.Database(s.name)
}
func (s *sMongoManager) Collection(name string) *mongo.Collection {
c, err := s.Client()
if err != nil {
panic(err)
}
return c.Database(s.name).Collection(name)
}
func NewCtx() context.Context {
return context.Background()
}
func NewTtlCtx(ttl time.Duration) (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), ttl)
}
func (s *sMongoManager) Close() error {
if s.cli == nil {
return nil
}
err := s.cli.Disconnect(NewCtx())
if err != nil {
return err
}
return nil
}

38
gdb/mgodb/mongo_test.go Normal file
View File

@ -0,0 +1,38 @@
//
// mongo_test.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package mgodb_test
import (
"os"
"testing"
"go.mongodb.org/mongo-driver/bson"
"hexq.cn/tiglog/golib/gdb/mgodb"
"hexq.cn/tiglog/golib/gtest"
)
func initMM() {
url := os.Getenv("MONGO_URL")
mgodb.Init(url, "test", 8)
}
func TestConnect(t *testing.T) {
initMM()
err := mgodb.Mm.Connect()
gtest.NoError(t, err)
}
func TestListDatabases(t *testing.T) {
initMM()
cli, err := mgodb.Mm.Client()
gtest.NoError(t, err)
gtest.NotNil(t, cli)
names, err := cli.ListDatabaseNames(nil, bson.D{})
gtest.NoError(t, err)
gtest.Greater(t, 0, names)
}

180
gdb/sqldb/base_test.go Normal file
View File

@ -0,0 +1,180 @@
//
// base_test.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package sqldb_test
import (
"database/sql"
"fmt"
"os"
"strings"
"testing"
_ "github.com/lib/pq"
"hexq.cn/tiglog/golib/gdb/sqldb"
// _ "github.com/go-sql-driver/mysql"
)
type Schema struct {
create string
drop string
}
var defaultSchema = Schema{
create: `
CREATE TABLE person (
id serial,
first_name text,
last_name text,
email text,
added_at int default 0,
PRIMARY KEY (id)
);
CREATE TABLE place (
country text,
city text NULL,
telcode integer
);
CREATE TABLE capplace (
country text,
city text NULL,
telcode integer
);
CREATE TABLE nullperson (
first_name text NULL,
last_name text NULL,
email text NULL
);
CREATE TABLE employees (
name text,
id integer,
boss_id integer
);
`,
drop: `
drop table person;
drop table place;
drop table capplace;
drop table nullperson;
drop table employees;
`,
}
type Person struct {
Id int64 `db:"id"`
FirstName string `db:"first_name"`
LastName string `db:"last_name"`
Email string `db:"email"`
AddedAt int64 `db:"added_at"`
}
type Person2 struct {
FirstName sql.NullString `db:"first_name"`
LastName sql.NullString `db:"last_name"`
Email sql.NullString
}
type Place struct {
Country string
City sql.NullString
TelCode int
}
type PlacePtr struct {
Country string
City *string
TelCode int
}
type PersonPlace struct {
Person
Place
}
type PersonPlacePtr struct {
*Person
*Place
}
type EmbedConflict struct {
FirstName string `db:"first_name"`
Person
}
type SliceMember struct {
Country string
City sql.NullString
TelCode int
People []Person `db:"-"`
Addresses []Place `db:"-"`
}
func loadDefaultFixture(db *sqldb.Engine, t *testing.T) {
tx := db.MustBegin()
s1 := "INSERT INTO person (first_name, last_name, email) VALUES (?, ?, ?)"
tx.MustExec(db.Rebind(s1), "Jason", "Moiron", "jmoiron@jmoiron.net")
s1 = "INSERT INTO person (first_name, last_name, email) VALUES (?, ?, ?)"
tx.MustExec(db.Rebind(s1), "John", "Doe", "johndoeDNE@gmail.net")
s1 = "INSERT INTO place (country, city, telcode) VALUES (?, ?, ?)"
tx.MustExec(db.Rebind(s1), "United States", "New York", "1")
s1 = "INSERT INTO place (country, telcode) VALUES (?, ?)"
tx.MustExec(db.Rebind(s1), "Hong Kong", "852")
s1 = "INSERT INTO place (country, telcode) VALUES (?, ?)"
tx.MustExec(db.Rebind(s1), "Singapore", "65")
s1 = "INSERT INTO capplace (country, telcode) VALUES (?, ?)"
tx.MustExec(db.Rebind(s1), "Sarf Efrica", "27")
s1 = "INSERT INTO employees (name, id) VALUES (?, ?)"
tx.MustExec(db.Rebind(s1), "Peter", "4444")
s1 = "INSERT INTO employees (name, id, boss_id) VALUES (?, ?, ?)"
tx.MustExec(db.Rebind(s1), "Joe", "1", "4444")
s1 = "INSERT INTO employees (name, id, boss_id) VALUES (?, ?, ?)"
tx.MustExec(db.Rebind(s1), "Martin", "2", "4444")
tx.Commit()
}
func MultiExec(e *sqldb.Engine, query string) {
stmts := strings.Split(query, ";\n")
if len(strings.Trim(stmts[len(stmts)-1], " \n\t\r")) == 0 {
stmts = stmts[:len(stmts)-1]
}
for _, s := range stmts {
_, err := e.Exec(s)
if err != nil {
fmt.Println(err, s)
}
}
}
func RunDbTest(t *testing.T, test func(db *sqldb.Engine, t *testing.T)) {
// 先初始化数据库
url := os.Getenv("DB_URL")
var db = sqldb.New(url)
// 再注册清空数据库
defer func() {
MultiExec(db, defaultSchema.drop)
}()
// 再加入一些数据
MultiExec(db, defaultSchema.create)
loadDefaultFixture(db, t)
// 最后测试
test(db, t)
}

58
gdb/sqldb/db.go Normal file
View File

@ -0,0 +1,58 @@
//
// db.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package sqldb
import (
"database/sql"
"errors"
"strings"
"github.com/jmoiron/sqlx"
)
var Db *Engine
type Engine struct {
*sqlx.DB
}
var ErrNoRows = sql.ErrNoRows
type DbOption struct {
Url string
MaxOpenConns int
MaxIdleConns int
}
// mysql://[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]
// pgsql://host=X.X.X.X port=54321 user=postgres password=admin123 dbname=postgres sslmode=disable"
func NewWithOption(opt *DbOption) *Engine {
urls := strings.Split(opt.Url, "://")
if len(urls) != 2 {
panic(errors.New("wrong database url:" + opt.Url))
}
dbx, err := sqlx.Open(urls[0], urls[1])
if err != nil {
panic(err)
}
dbx.SetMaxIdleConns(opt.MaxIdleConns)
dbx.SetMaxOpenConns(opt.MaxOpenConns)
err = dbx.Ping()
if err != nil {
panic(err)
}
Db = &Engine{
dbx,
}
return Db
}
func New(url string) *Engine {
opt := &DbOption{Url: url, MaxOpenConns: 256, MaxIdleConns: 2}
return NewWithOption(opt)
}

220
gdb/sqldb/db_func.go Normal file
View File

@ -0,0 +1,220 @@
//
// db_func.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package sqldb
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/jmoiron/sqlx"
)
func (e *Engine) Begin() (*sqlx.Tx, error) {
return e.Beginx()
}
// 插入一条记录
func (e *Engine) NamedInsertRecord(opt *QueryOption, arg interface{}) (int64, error) { // {{{
if len(opt.fields) == 0 {
return 0, errors.New("empty fields")
}
var tmp = make([]string, 0)
for _, field := range opt.fields {
tmp = append(tmp, fmt.Sprintf(":%s", field))
}
fields_str := strings.Join(opt.fields, ",")
fields_pl := strings.Join(tmp, ",")
sql := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", opt.table, fields_str, fields_pl)
if e.DriverName() == "postgres" {
sql += " returning id"
}
// sql = e.Rebind(sql)
stmt, err := e.PrepareNamed(sql)
if err != nil {
return 0, err
}
var id int64
err = stmt.Get(&id, arg)
if err != nil {
return 0, err
}
return id, err
} // }}}
// 插入一条记录
func (e *Engine) InsertRecord(opt *QueryOption) (int64, error) { // {{{
if len(opt.fields) == 0 {
return 0, errors.New("empty fields")
}
fields_str := strings.Join(opt.fields, ",")
fields_pl := strings.TrimRight(strings.Repeat("?,", len(opt.fields)), ",")
sql := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s);", opt.table, fields_str, fields_pl)
if e.DriverName() == "postgres" {
sql += " returning id"
}
sql = e.Rebind(sql)
result, err := e.Exec(sql, opt.args...)
if err != nil {
return 0, err
}
return result.LastInsertId()
} // }}}
// 查询一条记录
// dest 目标对象
// table 查询表
// query 查询条件
// args bindvars
func (e *Engine) GetRecord(dest interface{}, opt *QueryOption) error { // {{{
if opt.query == "" {
return errors.New("empty query")
}
opt.query = "WHERE " + opt.query
sql := fmt.Sprintf("SELECT * FROM %s %s limit 1", opt.table, opt.query)
sql = e.Rebind(sql)
err := e.Get(dest, sql, opt.args...)
if err != nil {
return err
}
return nil
} // }}}
// 查询多条记录
// dest 目标变量
// opt 查询对象
// args bindvars
func (e *Engine) GetRecords(dest interface{}, opt *QueryOption) error { // {{{
var tmp = []string{}
if opt.query != "" {
tmp = append(tmp, "where", opt.query)
}
if opt.sort != "" {
tmp = append(tmp, "order by", opt.sort)
}
if opt.offset > 0 {
tmp = append(tmp, "offset", strconv.Itoa(opt.offset))
}
if opt.limit > 0 {
tmp = append(tmp, "limit", strconv.Itoa(opt.limit))
}
sql := fmt.Sprintf("select * from %s %s", opt.table, strings.Join(tmp, " "))
sql = e.Rebind(sql)
return e.Select(dest, sql, opt.args...)
} // }}}
// 更新一条记录
// table 待处理的表
// set 需要设置的语句, eg: age=:age
// query 查询语句,不能为空,确保误更新所有记录
// arg 值
func (e *Engine) NamedUpdateRecords(opt *QueryOption, arg interface{}) (int64, error) { // {{{
if opt.set == "" || opt.query == "" {
return 0, errors.New("empty set or query")
}
sql := fmt.Sprintf("update %s set %s where %s", opt.table, opt.set, opt.query)
result, err := e.NamedExec(sql, arg)
if err != nil {
return 0, err
}
rows, err := result.RowsAffected()
if err != nil {
return 0, err
}
return rows, nil
} // }}}
func (e *Engine) UpdateRecords(opt *QueryOption) (int64, error) { // {{{
if opt.set == "" || opt.query == "" {
return 0, errors.New("empty set or query")
}
sql := fmt.Sprintf("update %s set %s where %s", opt.table, opt.set, opt.query)
sql = e.Rebind(sql)
result, err := e.Exec(sql, opt.args...)
if err != nil {
return 0, err
}
rows, err := result.RowsAffected()
if err != nil {
return 0, err
}
return rows, nil
} // }}}
// 删除若干条记录
// opt 的 query 不能为空
// arg bindvars
func (e *Engine) NamedDeleteRecords(opt *QueryOption, arg interface{}) (int64, error) { // {{{
if opt.query == "" {
return 0, errors.New("emtpy query")
}
sql := fmt.Sprintf("delete from %s where %s", opt.table, opt.query)
result, err := e.NamedExec(sql, arg)
if err != nil {
return 0, err
}
rows, err := result.RowsAffected()
if err != nil {
return 0, err
}
return rows, nil
} // }}}
func (e *Engine) DeleteRecords(opt *QueryOption) (int64, error) {
if opt.query == "" {
return 0, errors.New("emtpy query")
}
sql := fmt.Sprintf("delete from %s where %s", opt.table, opt.query)
sql = e.Rebind(sql)
result, err := e.Exec(sql, opt.args...)
if err != nil {
return 0, err
}
rows, err := result.RowsAffected()
if err != nil {
return 0, err
}
return rows, nil
}
func (e *Engine) CountRecords(opt *QueryOption) (int, error) {
sql := fmt.Sprintf("select count(*) from %s where %s", opt.table, opt.query)
sql = e.Rebind(sql)
var num int
err := e.Get(&num, sql, opt.args...)
if err != nil {
return 0, err
}
return num, nil
}
// var levels = []int{4, 6, 7}
// query, args, err := sqlx.In("SELECT * FROM users WHERE level IN (?);", levels)
// sqlx.In returns queries with the `?` bindvar, we can rebind it for our backend
// query = db.Rebind(query)
// rows, err := db.Query(query, args...)
func (e *Engine) In(query string, args ...interface{}) (string, []interface{}, error) {
return sqlx.In(query, args...)
}
func IsNoRows(err error) bool {
return err == ErrNoRows
}
// 把 fields 转换为 field1=:field1, field2=:field2, ..., fieldN=:fieldN
func GetSetString(fields []string) string {
items := []string{}
for _, field := range fields {
if field == "id" {
continue
}
items = append(items, fmt.Sprintf("%s=:%s", field, field))
}
return strings.Join(items, ",")
}

75
gdb/sqldb/db_func_opt.go Normal file
View File

@ -0,0 +1,75 @@
//
// db_func_opt.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package sqldb
type QueryOption struct {
table string
query string
set string
fields []string
sort string
offset int
limit int
args []any
joins []string
}
func NewQueryOption(table string) *QueryOption {
return &QueryOption{
table: table,
fields: []string{"*"},
offset: 0,
limit: 0,
args: make([]any, 0),
joins: make([]string, 0),
}
}
func (opt *QueryOption) Query(query string) *QueryOption {
opt.query = query
return opt
}
func (opt *QueryOption) Fields(args []string) *QueryOption {
opt.fields = args
return opt
}
func (opt *QueryOption) Select(cols ...string) *QueryOption {
opt.fields = cols
return opt
}
func (opt *QueryOption) Offset(offset int) *QueryOption {
opt.offset = offset
return opt
}
func (opt *QueryOption) Limit(limit int) *QueryOption {
opt.limit = limit
return opt
}
func (opt *QueryOption) Sort(sort string) *QueryOption {
opt.sort = sort
return opt
}
func (opt *QueryOption) Set(set string) *QueryOption {
opt.set = set
return opt
}
func (opt *QueryOption) Args(args ...any) *QueryOption {
opt.args = args
return opt
}
func (opt *QueryOption) Join(table string, cond string) *QueryOption {
opt.joins = append(opt.joins, "join "+table+" on "+cond)
return opt
}
func (opt *QueryOption) LeftJoin(table string, cond string) *QueryOption {
opt.joins = append(opt.joins, "left join "+table+" on "+cond)
return opt
}
func (opt *QueryOption) RightJoin(table string, cond string) *QueryOption {
opt.joins = append(opt.joins, "right join "+table+" on "+cond)
return opt
}

114
gdb/sqldb/db_func_test.go Normal file
View File

@ -0,0 +1,114 @@
//
// db_func_test.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package sqldb_test
import (
"testing"
"time"
"hexq.cn/tiglog/golib/gdb/sqldb"
"hexq.cn/tiglog/golib/gtest"
)
// 经过测试,发现数据库里面使用 time 类型容易出现 timezone 不一致的情况
// 在存入数据库时,可能会导致时区丢失
// 因此,为了更好的兼容性,使用 int 时间戳会更合适
func dbFuncTest(db *sqldb.Engine, t *testing.T) {
var err error
fields := []string{"first_name", "last_name", "email"}
p := &Person{
FirstName: "三",
LastName: "张",
Email: "zs@foo.com",
}
// InsertRecord 的用法
opt := sqldb.NewQueryOption("person").Fields(fields)
rows, err := db.NamedInsertRecord(opt, p)
gtest.Nil(t, err)
gtest.True(t, rows > 0)
// fmt.Println(rows)
// GetRecord 的用法
var p3 Person
opt = sqldb.NewQueryOption("person").Query("email=?").Args("zs@foo.com")
err = db.GetRecord(&p3, opt)
// fmt.Println(p3)
gtest.Equal(t, "张", p3.LastName)
gtest.Equal(t, "三", p3.FirstName)
gtest.Equal(t, int64(0), p3.AddedAt)
gtest.Nil(t, err)
p2 := &Person{
FirstName: "四",
LastName: "李",
Email: "ls@foo.com",
AddedAt: time.Now().Unix(),
}
fields2 := append(fields, "added_at")
opt = sqldb.NewQueryOption("person").Fields(fields2)
_, err = db.NamedInsertRecord(opt, p2)
gtest.Nil(t, err)
var p4 Person
opt = sqldb.NewQueryOption("person")
err = db.GetRecord(&p4, opt)
gtest.NotNil(t, err)
gtest.Equal(t, "", p4.FirstName)
opt = sqldb.NewQueryOption("person").Query("first_name=?").Args("四")
err = db.GetRecord(&p4, opt)
gtest.Nil(t, err)
gtest.Equal(t, time.Now().Unix(), p4.AddedAt)
gtest.Equal(t, "ls@foo.com", p4.Email)
// GetRecords
var ps []Person
opt = sqldb.NewQueryOption("person").Query("id > ?").Args(0)
err = db.GetRecords(&ps, opt)
gtest.Nil(t, err)
gtest.Greater(t, int64(1), ps)
var ps2 []Person
opt = sqldb.NewQueryOption("person").Query("id=?").Args(1)
err = db.GetRecords(&ps2, opt)
gtest.Equal(t, 1, len(ps2))
if len(ps2) > 1 {
gtest.Equal(t, int64(1), ps2[0].Id)
}
// DeleteRecords
opt = sqldb.NewQueryOption("person").Query("id=?").Args(2)
n, err := db.DeleteRecords(opt)
gtest.Nil(t, err)
gtest.Greater(t, int64(0), n)
// UpdateRecords
opt = sqldb.NewQueryOption("person").Set("first_name=?").Query("email=?").Args("哈哈", "zs@foo.com")
n, err = db.UpdateRecords(opt)
gtest.Nil(t, err)
gtest.Greater(t, int64(0), n)
// NamedUpdateRecords
var p5 = ps[0]
p5.FirstName = "中华人民共和国"
opt = sqldb.NewQueryOption("person").Set("first_name=:first_name").Query("email=:email")
n, err = db.NamedUpdateRecords(opt, p5)
gtest.Nil(t, err)
gtest.Greater(t, int64(0), n)
var p6 Person
opt = sqldb.NewQueryOption("person").Query("first_name=?").Args(p5.FirstName)
err = db.GetRecord(&p6, opt)
gtest.Nil(t, err)
gtest.Greater(t, int64(0), p6.Id)
gtest.Equal(t, p6.FirstName, p5.FirstName)
}
func TestFunc(t *testing.T) {
RunDbTest(t, dbFuncTest)
}

20
gdb/sqldb/db_model.go Normal file
View File

@ -0,0 +1,20 @@
//
// db_model.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package sqldb
// TODO 暂时不好实现,以后再说
type Model struct {
db *Engine
}
func NewModel() *Model {
return &Model{
db: Db,
}
}

322
gdb/sqldb/db_query.go Normal file
View File

@ -0,0 +1,322 @@
//
// db_query.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package sqldb
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/jmoiron/sqlx"
)
type Query struct {
db *Engine
table string
fields []string
wheres []string // 不能太复杂
joins []string
orderBy string
groupBy string
offset int
limit int
}
func NewQueryBuild(table string, db *Engine) *Query {
return &Query{
db: db,
table: table,
fields: []string{},
wheres: []string{},
joins: []string{},
offset: 0,
limit: 0,
}
}
func (q *Query) Table(table string) *Query {
q.table = table
return q
}
// 设置 select fields
func (q *Query) Select(fields ...string) *Query {
q.fields = fields
return q
}
// 增加一个 select field
func (q *Query) AddFields(fields ...string) *Query {
q.fields = append(q.fields, fields...)
return q
}
func (q *Query) Where(query string) *Query {
q.wheres = []string{query}
return q
}
func (q *Query) AndWhere(query string) *Query {
q.wheres = append(q.wheres, "and "+query)
return q
}
func (q *Query) OrWhere(query string) *Query {
q.wheres = append(q.wheres, "or "+query)
return q
}
func (q *Query) Join(table string, on string) *Query {
var join = "join " + table
if on != "" {
join = join + " on " + on
}
q.joins = append(q.joins, join)
return q
}
func (q *Query) LeftJoin(table string, on string) *Query {
var join = "left join " + table
if on != "" {
join = join + " on " + on
}
q.joins = append(q.joins, join)
return q
}
func (q *Query) RightJoin(table string, on string) *Query {
var join = "right join " + table
if on != "" {
join = join + " on " + on
}
q.joins = append(q.joins, join)
return q
}
func (q *Query) InnerJoin(table string, on string) *Query {
var join = "inner join " + table
if on != "" {
join = join + " on " + on
}
q.joins = append(q.joins, join)
return q
}
func (q *Query) OrderBy(order string) *Query {
q.orderBy = order
return q
}
func (q *Query) GroupBy(group string) *Query {
q.groupBy = group
return q
}
func (q *Query) Offset(offset int) *Query {
q.offset = offset
return q
}
func (q *Query) Limit(limit int) *Query {
q.limit = limit
return q
}
// returningId postgres 数据库返回 LastInsertId 处理
// TODO returningId 暂时不处理
func (q *Query) getInsertSql(named, returningId bool) string {
fields_str := strings.Join(q.fields, ",")
var pl string
if named {
var tmp []string
for _, field := range q.fields {
tmp = append(tmp, ":"+field)
}
pl = strings.Join(tmp, ",")
} else {
pl = strings.Repeat("?,", len(q.fields))
pl = strings.TrimRight(pl, ",")
}
sql := fmt.Sprintf("insert into %s (%s) values (%s);", q.table, fields_str, pl)
sql = q.db.Rebind(sql)
// fmt.Println(sql)
return sql
}
// return RowsAffected, error
func (q *Query) Insert(args ...interface{}) (int64, error) {
if len(q.fields) == 0 {
return 0, errors.New("empty fields")
}
sql := q.getInsertSql(false, false)
result, err := q.db.Exec(sql, args...)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
// return RowsAffected, error
func (q *Query) NamedInsert(arg interface{}) (int64, error) {
if len(q.fields) == 0 {
return 0, errors.New("empty fields")
}
sql := q.getInsertSql(true, false)
result, err := q.db.NamedExec(sql, arg)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
func (q *Query) getQuerySql() string {
var (
fields_str string = "*"
join_str string
where_str string
offlim string
)
if len(q.fields) > 0 {
fields_str = strings.Join(q.fields, ",")
}
if len(q.joins) > 0 {
join_str = strings.Join(q.joins, " ")
}
if len(q.wheres) > 0 {
where_str = "where " + strings.Join(q.wheres, " ")
}
if q.offset > 0 {
offlim = " offset " + strconv.Itoa(q.offset)
}
if q.limit > 0 {
offlim = " limit " + strconv.Itoa(q.limit)
}
// select fields from table t join where groupby orderby offset limit
sql := fmt.Sprintf("select %s from %s t %s %s %s %s%s", fields_str, q.table, join_str, where_str, q.groupBy, q.orderBy, offlim)
return sql
}
func (q *Query) One(dest interface{}, args ...interface{}) error {
q.Limit(1)
sql := q.getQuerySql()
sql = q.db.Rebind(sql)
return q.db.Get(dest, sql, args...)
}
func (q *Query) NamedOne(dest interface{}, arg interface{}) error {
q.Limit(1)
sql := q.getQuerySql()
rows, err := q.db.NamedQuery(sql, arg)
if err != nil {
return err
}
if rows.Next() {
return rows.Scan(dest)
}
return errors.New("nr") // no record
}
func (q *Query) All(dest interface{}, args ...interface{}) error {
sql := q.getQuerySql()
sql = q.db.Rebind(sql)
return q.db.Select(dest, sql, args...)
}
// 为了省内存,直接返回迭代器
func (q *Query) NamedAll(dest interface{}, arg interface{}) (*sqlx.Rows, error) {
sql := q.getQuerySql()
return q.db.NamedQuery(sql, arg)
}
// set age=? / age=:age
func (q *Query) NamedUpdate(set string, arg interface{}) (int64, error) {
var where_str string
if len(q.wheres) > 0 {
where_str = strings.Join(q.wheres, " ")
}
if set == "" || where_str == "" {
return 0, errors.New("empty set or where")
}
// update table t where
sql := fmt.Sprintf("update %s t set %s where %s", q.table, set, where_str)
sql = q.db.Rebind(sql)
result, err := q.db.NamedExec(sql, arg)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
// 顺序容易弄反,记得先是 set 的参数,再是 where 里面的参数
func (q *Query) Update(set string, args ...interface{}) (int64, error) {
var where_str string
if len(q.wheres) > 0 {
where_str = strings.Join(q.wheres, " ")
}
if set == "" || where_str == "" {
return 0, errors.New("empty set or where")
}
// update table t where
sql := fmt.Sprintf("update %s t set %s where %s", q.table, set, where_str)
sql = q.db.Rebind(sql)
result, err := q.db.Exec(sql, args...)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
// 普通的删除
func (q *Query) Delete(args ...interface{}) (int64, error) {
var where_str string
if len(q.wheres) == 0 {
return 0, errors.New("missing where clause")
}
where_str = strings.Join(q.wheres, " ")
sql := fmt.Sprintf("delete from %s where %s", q.table, where_str)
sql = q.db.Rebind(sql)
result, err := q.db.Exec(sql, args...)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
func (q *Query) NamedDelete(arg interface{}) (int64, error) {
if len(q.wheres) == 0 {
return 0, errors.New("missing where clause")
}
var where_str string
where_str = strings.Join(q.wheres, " ")
sql := fmt.Sprintf("delete from %s where %s", q.table, where_str)
sql = q.db.Rebind(sql)
result, err := q.db.NamedExec(sql, arg)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
func (q *Query) Count(args ...interface{}) (int64, error) {
var where_str string
if len(q.wheres) > 0 {
where_str = " where " + strings.Join(q.wheres, " ")
}
sql := fmt.Sprintf("select count(1) as num from %s t%s", q.table, where_str)
sql = q.db.Rebind(sql)
var num int64
err := q.db.Get(&num, sql, args...)
return num, err
}

109
gdb/sqldb/db_query_test.go Normal file
View File

@ -0,0 +1,109 @@
//
// db_query_test.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package sqldb_test
import (
"testing"
"time"
"hexq.cn/tiglog/golib/gtest"
"hexq.cn/tiglog/golib/gdb/sqldb"
)
func dbQueryTest(db *sqldb.Engine, t *testing.T) {
query := sqldb.NewQueryBuild("person", db)
// query one
var p1 Person
query.Where("id=?")
err := query.One(&p1, 1)
gtest.Nil(t, err)
gtest.Equal(t, int64(1), p1.Id)
// query all
var ps1 []Person
query = sqldb.NewQueryBuild("person", db)
query.Where("id > ?")
err = query.All(&ps1, 1)
gtest.Nil(t, err)
gtest.True(t, len(ps1) > 0)
// fmt.Println(ps1)
if len(ps1) > 0 {
var val int64 = 2
gtest.Equal(t, val, ps1[0].Id)
}
// insert
query = sqldb.NewQueryBuild("person", db)
query.AddFields("first_name", "last_name", "email")
id, err := query.Insert("三", "张", "zs@bar.com")
gtest.Nil(t, err)
gtest.Greater(t, int64(0), id)
// fmt.Println(id)
// named insert
query = sqldb.NewQueryBuild("person", db)
query.AddFields("first_name", "last_name", "email")
row, err := query.NamedInsert(&Person{
FirstName: "四",
LastName: "李",
Email: "ls@bar.com",
AddedAt: time.Now().Unix(),
})
gtest.Nil(t, err)
gtest.Equal(t, int64(1), row)
// update
query = sqldb.NewQueryBuild("person", db)
query.Where("email=?")
n, err := query.Update("first_name=?", "哈哈", "ls@bar.com")
gtest.Nil(t, err)
gtest.Equal(t, int64(1), n)
// named update map
query = sqldb.NewQueryBuild("person", db)
query.Where("email=:email")
n, err = query.NamedUpdate("first_name=:first_name", map[string]interface{}{
"email": "ls@bar.com",
"first_name": "中华人民共和国",
})
gtest.Nil(t, err)
gtest.Equal(t, int64(1), n)
// named update struct
query = sqldb.NewQueryBuild("person", db)
query.Where("email=:email")
var p = &Person{
Email: "ls@bar.com",
LastName: "中华人民共和国,救民于水火",
}
n, err = query.NamedUpdate("last_name=:last_name", p)
gtest.Nil(t, err)
gtest.Equal(t, int64(1), n)
// count
query = sqldb.NewQueryBuild("person", db)
n, err = query.Count()
gtest.Nil(t, err)
// fmt.Println(n)
gtest.Greater(t, int64(0), n)
// delete
query = sqldb.NewQueryBuild("person", db)
n, err = query.Delete()
gtest.NotNil(t, err)
gtest.Equal(t, int64(0), n)
n, err = query.Where("id=?").Delete(2)
gtest.Nil(t, err)
gtest.Equal(t, int64(1), n)
}
func TestQuery(t *testing.T) {
RunDbTest(t, dbQueryTest)
}

133
gfile/file_copy.go Normal file
View File

@ -0,0 +1,133 @@
//
// file_copy.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gfile
import (
"errors"
"io"
"io/ioutil"
"os"
"path/filepath"
)
// Copy file/directory from `src` to `dst`.
//
// If `src` is file, it calls CopyFile to implements copy feature,
// or else it calls CopyDir.
func Copy(src string, dst string) error {
if src == "" {
return errors.New("source path cannot be empty")
}
if dst == "" {
return errors.New("destination path cannot be empty")
}
if IsFile(src) {
return CopyFile(src, dst)
}
return CopyDir(src, dst)
}
// CopyFile copies the contents of the file named `src` to the file named
// by `dst`. The file will be created if it does not exist. If the
// destination file exists, all it's contents will be replaced by the contents
// of the source file. The file mode will be copied from the source and
// the copied data is synced/flushed to stable storage.
// Thanks: https://gist.github.com/r0l1/92462b38df26839a3ca324697c8cba04
func CopyFile(src, dst string) (err error) {
if src == "" {
return errors.New("source file cannot be empty")
}
if dst == "" {
return errors.New("destination file cannot be empty")
}
// If src and dst are the same path, it does nothing.
if src == dst {
return nil
}
in, err := os.Open(src)
if err != nil {
return
}
defer func() {
if e := in.Close(); e != nil {
err = e
}
}()
out, err := os.Create(dst)
if err != nil {
return
}
defer func() {
if e := out.Close(); e != nil {
err = e
}
}()
if _, err = io.Copy(out, in); err != nil {
return
}
if err = out.Sync(); err != nil {
return
}
err = os.Chmod(dst, DefaultPermCopy)
if err != nil {
return
}
return
}
// CopyDir recursively copies a directory tree, attempting to preserve permissions.
//
// Note that, the Source directory must exist and symlinks are ignored and skipped.
func CopyDir(src string, dst string) (err error) {
if src == "" {
return errors.New("source directory cannot be empty")
}
if dst == "" {
return errors.New("destination directory cannot be empty")
}
// If src and dst are the same path, it does nothing.
if src == dst {
return nil
}
src = filepath.Clean(src)
dst = filepath.Clean(dst)
si, err := os.Stat(src)
if err != nil {
return err
}
if !si.IsDir() {
return errors.New("source is not a directory")
}
if !Exists(dst) {
if err = os.MkdirAll(dst, DefaultPermCopy); err != nil {
return
}
}
entries, err := ioutil.ReadDir(src)
if err != nil {
return
}
for _, entry := range entries {
srcPath := filepath.Join(src, entry.Name())
dstPath := filepath.Join(dst, entry.Name())
if entry.IsDir() {
if err = CopyDir(srcPath, dstPath); err != nil {
return
}
} else {
// Skip symlinks.
if entry.Mode()&os.ModeSymlink != 0 {
continue
}
if err = CopyFile(srcPath, dstPath); err != nil {
return
}
}
}
return
}

81
gfile/file_home.go Normal file
View File

@ -0,0 +1,81 @@
//
// file_home.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gfile
import (
"bytes"
"errors"
"os"
"os/exec"
"os/user"
"runtime"
"strings"
)
// Home returns absolute path of current user's home directory.
// The optional parameter `names` specifies the sub-folders/sub-files,
// which will be joined with current system separator and returned with the path.
func Home(names ...string) (string, error) {
path, err := getHomePath()
if err != nil {
return "", err
}
for _, name := range names {
path += Separator + name
}
return path, nil
}
// getHomePath returns absolute path of current user's home directory.
func getHomePath() (string, error) {
u, err := user.Current()
if nil == err {
return u.HomeDir, nil
}
if runtime.GOOS == "windows" {
return homeWindows()
}
return homeUnix()
}
// homeUnix retrieves and returns the home on unix system.
func homeUnix() (string, error) {
if home := os.Getenv("HOME"); home != "" {
return home, nil
}
var stdout bytes.Buffer
cmd := exec.Command("sh", "-c", "eval echo ~$USER")
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return "", err
}
result := strings.TrimSpace(stdout.String())
if result == "" {
return "", errors.New("blank output when reading home directory")
}
return result, nil
}
// homeWindows retrieves and returns the home on windows system.
func homeWindows() (string, error) {
var (
drive = os.Getenv("HOMEDRIVE")
path = os.Getenv("HOMEPATH")
home = drive + path
)
if drive == "" || path == "" {
home = os.Getenv("USERPROFILE")
}
if home == "" {
return "", errors.New("environment keys HOMEDRIVE, HOMEPATH and USERPROFILE are empty")
}
return home, nil
}

177
gfile/file_path.go Normal file
View File

@ -0,0 +1,177 @@
//
// file_path.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gfile
import (
"os"
"path/filepath"
"strings"
)
const (
// Separator for file system.
// It here defines the separator as variable
// to allow it modified by developer if necessary.
Separator = string(filepath.Separator)
// DefaultPermOpen is the default perm for file opening.
DefaultPermOpen = os.FileMode(0655)
// DefaultPermCopy is the default perm for file/folder copy.
DefaultPermCopy = os.FileMode(0755)
)
// Exists checks whether given `path` exist.
func Exists(path string) bool {
if stat, err := os.Stat(path); stat != nil && !os.IsNotExist(err) {
return true
}
return false
}
// IsDir checks whether given `path` a directory.
// Note that it returns false if the `path` does not exist.
func IsDir(path string) bool {
s, err := os.Stat(path)
if err != nil {
return false
}
return s.IsDir()
}
// Pwd returns absolute path of current working directory.
// Note that it returns an empty string if retrieving current
// working directory failed.
func Pwd() string {
path, err := os.Getwd()
if err != nil {
return ""
}
return path
}
// Chdir changes the current working directory to the named directory.
// If there is an error, it will be of type *PathError.
func Chdir(dir string) (err error) {
err = os.Chdir(dir)
return
}
// IsFile checks whether given `path` a file, which means it's not a directory.
// Note that it returns false if the `path` does not exist.
func IsFile(path string) bool {
s, err := os.Stat(path)
if err != nil {
return false
}
return !s.IsDir()
}
// DirNames returns sub-file names of given directory `path`.
// Note that the returned names are NOT absolute paths.
func DirNames(path string) ([]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
list, err := f.Readdirnames(-1)
_ = f.Close()
if err != nil {
return nil, err
}
return list, nil
}
// IsReadable checks whether given `path` is readable.
func IsReadable(path string) bool {
result := true
file, err := os.OpenFile(path, os.O_RDONLY, DefaultPermOpen)
if err != nil {
result = false
}
file.Close()
return result
}
// Name returns the last element of path without file extension.
// Example:
// /var/www/file.js -> file
// file.js -> file
func Name(path string) string {
base := filepath.Base(path)
if i := strings.LastIndexByte(base, '.'); i != -1 {
return base[:i]
}
return base
}
// Dir returns all but the last element of path, typically the path's directory.
// After dropping the final element, Dir calls Clean on the path and trailing
// slashes are removed.
// If the `path` is empty, Dir returns ".".
// If the `path` is ".", Dir treats the path as current working directory.
// If the `path` consists entirely of separators, Dir returns a single separator.
// The returned path does not end in a separator unless it is the root directory.
func Dir(path string) string {
if path == "." {
p, _ := filepath.Abs(path)
return filepath.Dir(p)
}
return filepath.Dir(path)
}
// IsEmpty checks whether the given `path` is empty.
// If `path` is a folder, it checks if there's any file under it.
// If `path` is a file, it checks if the file size is zero.
//
// Note that it returns true if `path` does not exist.
func IsEmpty(path string) bool {
stat, err := os.Stat(path)
if err != nil {
return true
}
if stat.IsDir() {
file, err := os.Open(path)
if err != nil {
return true
}
defer file.Close()
names, err := file.Readdirnames(-1)
if err != nil {
return true
}
return len(names) == 0
} else {
return stat.Size() == 0
}
}
// Ext returns the file name extension used by path.
// The extension is the suffix beginning at the final dot
// in the final element of path; it is empty if there is
// no dot.
// Note: the result contains symbol '.'.
// Eg:
// main.go => .go
// api.json => .json
func Ext(path string) string {
ext := filepath.Ext(path)
if p := strings.IndexByte(ext, '?'); p != -1 {
ext = ext[0:p]
}
return ext
}
// ExtName is like function Ext, which returns the file name extension used by path,
// but the result does not contain symbol '.'.
// Eg:
// main.go => go
// api.json => json
func ExtName(path string) string {
return strings.TrimLeft(Ext(path), ".")
}

132
gfile/file_size.go Normal file
View File

@ -0,0 +1,132 @@
//
// file_size.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gfile
import (
"fmt"
"os"
"strconv"
"strings"
)
// Size returns the size of file specified by `path` in byte.
func Size(path string) int64 {
s, e := os.Stat(path)
if e != nil {
return 0
}
return s.Size()
}
// SizeFormat returns the size of file specified by `path` in format string.
func SizeFormat(path string) string {
return FormatSize(Size(path))
}
// ReadableSize formats size of file given by `path`, for more human readable.
func ReadableSize(path string) string {
return FormatSize(Size(path))
}
// StrToSize converts formatted size string to its size in bytes.
func StrToSize(sizeStr string) int64 {
i := 0
for ; i < len(sizeStr); i++ {
if sizeStr[i] == '.' || (sizeStr[i] >= '0' && sizeStr[i] <= '9') {
continue
} else {
break
}
}
var (
unit = sizeStr[i:]
number, _ = strconv.ParseFloat(sizeStr[:i], 64)
)
if unit == "" {
return int64(number)
}
switch strings.ToLower(unit) {
case "b", "bytes":
return int64(number)
case "k", "kb", "ki", "kib", "kilobyte":
return int64(number * 1024)
case "m", "mb", "mi", "mib", "mebibyte":
return int64(number * 1024 * 1024)
case "g", "gb", "gi", "gib", "gigabyte":
return int64(number * 1024 * 1024 * 1024)
case "t", "tb", "ti", "tib", "terabyte":
return int64(number * 1024 * 1024 * 1024 * 1024)
case "p", "pb", "pi", "pib", "petabyte":
return int64(number * 1024 * 1024 * 1024 * 1024 * 1024)
case "e", "eb", "ei", "eib", "exabyte":
return int64(number * 1024 * 1024 * 1024 * 1024 * 1024 * 1024)
case "z", "zb", "zi", "zib", "zettabyte":
return int64(number * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024)
case "y", "yb", "yi", "yib", "yottabyte":
return int64(number * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024)
case "bb", "brontobyte":
return int64(number * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024)
}
return -1
}
// FormatSize formats size `raw` for more manually readable.
func FormatSize(raw int64) string {
var r float64 = float64(raw)
var t float64 = 1024
var d float64 = 1
if r < t {
return fmt.Sprintf("%.2fB", r/d)
}
d *= 1024
t *= 1024
if r < t {
return fmt.Sprintf("%.2fK", r/d)
}
d *= 1024
t *= 1024
if r < t {
return fmt.Sprintf("%.2fM", r/d)
}
d *= 1024
t *= 1024
if r < t {
return fmt.Sprintf("%.2fG", r/d)
}
d *= 1024
t *= 1024
if r < t {
return fmt.Sprintf("%.2fT", r/d)
}
d *= 1024
t *= 1024
if r < t {
return fmt.Sprintf("%.2fP", r/d)
}
d *= 1024
t *= 1024
if r < t {
return fmt.Sprintf("%.2fE", r/d)
}
d *= 1024
t *= 1024
if r < t {
return fmt.Sprintf("%.2fZ", r/d)
}
d *= 1024
t *= 1024
if r < t {
return fmt.Sprintf("%.2fY", r/d)
}
d *= 1024
t *= 1024
if r < t {
return fmt.Sprintf("%.2fBB", r/d)
}
return "TooLarge"
}

40
gfile/file_time.go Normal file
View File

@ -0,0 +1,40 @@
//
// file_time.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gfile
import (
"os"
"time"
)
// MTime returns the modification time of file given by `path` in second.
func MTime(path string) time.Time {
s, e := os.Stat(path)
if e != nil {
return time.Time{}
}
return s.ModTime()
}
// MTimestamp returns the modification time of file given by `path` in second.
func MTimestamp(path string) int64 {
mtime := MTime(path)
if mtime.IsZero() {
return -1
}
return mtime.Unix()
}
// MTimestampMilli returns the modification time of file given by `path` in millisecond.
func MTimestampMilli(path string) int64 {
mtime := MTime(path)
if mtime.IsZero() {
return -1
}
return mtime.UnixNano() / 1000000
}

68
go.mod Normal file
View File

@ -0,0 +1,68 @@
module hexq.cn/tiglog/golib
go 1.20
require (
github.com/casbin/casbin/v2 v2.70.0
github.com/gin-gonic/gin v1.9.1
github.com/go-redis/redis/v8 v8.11.5
github.com/go-sql-driver/mysql v1.7.1
github.com/hibiken/asynq v0.24.1
github.com/jmoiron/sqlx v1.3.5
github.com/lib/pq v1.10.9
github.com/mattn/go-runewidth v0.0.14
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/rs/xid v1.5.0
github.com/rs/zerolog v1.29.1
go.mongodb.org/mongo-driver v1.11.7
golang.org/x/crypto v0.10.0
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/google/uuid v1.2.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/redis/go-redis/v9 v9.0.3 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.1 // indirect
github.com/xdg-go/stringprep v1.0.3 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

223
go.sum Normal file
View File

@ -0,0 +1,223 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/casbin/casbin/v2 v2.70.0 h1:CuoWeWpMj6GsXf5K1npAKHEMb+9k9QE/Mo7cVZmSJ98=
github.com/casbin/casbin/v2 v2.70.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hibiken/asynq v0.24.1 h1:+5iIEAyA9K/lcSPvx3qoPtsKJeKI5u9aOIvUmSsazEw=
github.com/hibiken/asynq v0.24.1/go.mod h1:u5qVeSbrnfT+vtG5Mq8ZPzQu/BmCKMHvTGb91uy9Tts=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.0.3 h1:+7mmR26M0IvyLxGZUHxu4GiBkJkVDid0Un+j4ScYu4k=
github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.mongodb.org/mongo-driver v1.11.7 h1:LIwYxASDLGUg/8wOhgOOZhX8tQa/9tgZPgzZoVqJvcs=
go.mongodb.org/mongo-driver v1.11.7/go.mod h1:G9TgswdsWjX4tmDA5zfs2+6AEPpYJwqblyjsfuh8oXY=
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

49
gqueue/queue.go Normal file
View File

@ -0,0 +1,49 @@
//
// queue.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gqueue
import (
"sync"
"github.com/hibiken/asynq"
)
var onceCli sync.Once
var onceSvc sync.Once
var redisOpt asynq.RedisClientOpt
func Init(addr, username, password string, database int) {
redisOpt = asynq.RedisClientOpt{
Addr: addr,
Username: username,
Password: password,
DB: database,
}
}
var cli *asynq.Client
func Client() *asynq.Client {
onceCli.Do(func() {
cli = asynq.NewClient(redisOpt)
})
return cli
}
var svc *asynq.Server
func Server() *asynq.Server {
onceSvc.Do(func() {
svc = asynq.NewServer(
redisOpt,
asynq.Config{Concurrency: 10},
)
})
return svc
}

32
gqueue/queue_test.go Normal file
View File

@ -0,0 +1,32 @@
//
// queue_test.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gqueue_test
import (
"os"
"testing"
"hexq.cn/tiglog/golib/gqueue"
"hexq.cn/tiglog/golib/gtest"
)
func TestClient(t *testing.T) {
gqueue.Init(os.Getenv("REDIS_ADDR"), os.Getenv("REDIS_USERNAME"), os.Getenv("REDIS_PASSWORD"), 0)
c1 := gqueue.Client()
c2 := gqueue.Client()
gtest.NotNil(t, c1)
gtest.Equal(t, c1, c2)
}
func TestServer(t *testing.T) {
gqueue.Init(os.Getenv("REDIS_ADDR"), os.Getenv("REDIS_USERNAME"), os.Getenv("REDIS_PASSWORD"), 0)
s1 := gqueue.Server()
s2 := gqueue.Server()
gtest.NotNil(t, s1)
gtest.Equal(t, s1, s2)
}

179
gtest/test.go Normal file
View File

@ -0,0 +1,179 @@
//
// test.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gtest
import (
"reflect"
"strings"
"testing"
)
func Equal(t *testing.T, expected, val interface{}) {
if val != expected {
t.Errorf("Expected [%v] (type %v), but got [%v] (type %v)", expected, reflect.TypeOf(expected), val, reflect.TypeOf(val))
}
}
func NotEqual(t *testing.T, expected, val interface{}) {
if val == expected {
t.Errorf("Expected not [%v] (type %v), but got [%v] (type %v)", expected, reflect.TypeOf(expected), val, reflect.TypeOf(val))
}
}
func True(t *testing.T, val interface{}) {
if val != true {
t.Errorf("Expected true, but got [%v] (type %v)", val, reflect.TypeOf(val))
}
}
func False(t *testing.T, val interface{}) {
if val != false {
t.Errorf("Expected false, but got [%v] (type %v)", val, reflect.TypeOf(val))
}
}
func MsgNil(t *testing.T, val interface{}) {
t.Errorf("Expected nil, but got [%v] (type %v)", val, reflect.TypeOf(val))
}
func MsgNotNil(t *testing.T, val interface{}) {
t.Errorf("Expected not nil, but got [%v] (type %v)", val, reflect.TypeOf(val))
}
func MsgExpect(t *testing.T, expect string, val string) {
t.Errorf("Expected %s, but got [%v] (type %v)", expect, val, reflect.TypeOf(val))
}
// 有些情况下不能使用
func Nil(t *testing.T, val interface{}) {
if val != nil {
t.Errorf("Expected nil, but got [%v] (type %v)", val, reflect.TypeOf(val))
}
}
// 有些情况下不能使用
func NotNil(t *testing.T, val interface{}) {
if val == nil {
t.Errorf("Expected not nil, but got [%v] (type %v)", val, reflect.TypeOf(val))
}
}
func IsString(t *testing.T, val interface{}) {
_, ok := val.(string)
if !ok {
t.Errorf("Expected a string, but got [%v] (type %v)", val, reflect.TypeOf(val))
}
}
func IsBool(t *testing.T, val interface{}) {
_, ok := val.(bool)
if !ok {
t.Errorf("Expected a bool, but got [%v] (type %v)", val, reflect.TypeOf(val))
}
}
// val 要大于 base
func Greater(t *testing.T, base int64, val interface{}) {
v := reflect.ValueOf(val)
switch v.Kind() {
case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8:
if v.Int() <= base {
t.Errorf("Expected greater than %d, but got %v", base, v)
}
return
case reflect.Chan, reflect.Map, reflect.Slice:
if int64(v.Len()) <= base {
t.Errorf("Expected greater than %d, but got %d (%v)", base, v.Len(), v)
}
return
}
t.Errorf("Expected a num or countable val, but got [%v] (type %v)", val, reflect.TypeOf(val))
}
// val 要小于 base
func Less(t *testing.T, base int64, val interface{}) {
v := reflect.ValueOf(val)
switch v.Kind() {
case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8:
if v.Int() >= base {
t.Errorf("Expected less than %d, but got %v", base, v)
}
return
case reflect.Chan, reflect.Map, reflect.Slice:
if int64(v.Len()) >= base {
t.Errorf("Expected less than %d, but got %d (%v)", base, v.Len(), v)
}
return
}
t.Errorf("Expected a num or countable val, but got [%v] (type %v)", val, reflect.TypeOf(val))
}
func StartWith(t *testing.T, e string, val string) {
if !strings.HasPrefix(val, e) {
t.Errorf("Expected a string has prefix %s, but got %s", e, val)
}
}
func EndWith(t *testing.T, e string, val string) {
if !strings.HasSuffix(val, e) {
t.Errorf("Expected a string has suffix %s, but got %s", e, val)
}
}
func ContainWith(t *testing.T, e string, val string) {
if !strings.Contains(val, e) {
t.Errorf("Expected a string contain %s, but got %s", e, val)
}
}
func Error(t *testing.T, err error) {
if err == nil {
t.Errorf("Expected an error, but got nil")
}
}
// 不希望是一个 error
func NoError(t *testing.T, err error) {
if err != nil {
t.Errorf("Received unexpected error:\n%+v", err)
}
}
func Empty(t *testing.T, val interface{}) {
if !isEmpty(val) {
t.Errorf("Should be empty, but got [%v] (type %v)", val, reflect.TypeOf(val))
}
}
func NotEmpty(t *testing.T, val interface{}) {
if isEmpty(val) {
t.Errorf("should be not emtpy, but got [%v] (type %v)", val, reflect.TypeOf(val))
}
}
func isEmpty(object interface{}) bool {
if object == nil {
return true
}
objValue := reflect.ValueOf(object)
switch objValue.Kind() {
case reflect.Chan, reflect.Map, reflect.Slice:
return objValue.Len() == 0
case reflect.Ptr:
if objValue.IsNil() {
return true
}
deref := objValue.Elem().Interface()
return isEmpty(deref)
default:
zero := reflect.Zero(objValue.Type())
return reflect.DeepEqual(object, zero.Interface())
}
}

117
helper/conv_helper.go Normal file
View File

@ -0,0 +1,117 @@
//
// conv_helper.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package helper
import (
"strconv"
)
func AnyToInt(val any, dv int) (int, error) {
switch val.(type) {
case int:
rv, _ := val.(int)
return rv, nil
case int8:
rv, _ := val.(int8)
return int(rv), nil
case int16:
rv, _ := val.(int16)
return int(rv), nil
case int32:
rv, _ := val.(int32)
return int(rv), nil
case int64:
rv, _ := val.(int64)
return int(rv), nil
case string:
sv, _ := val.(string)
rv, err := strconv.Atoi(sv)
if err != nil {
return dv, err
}
return rv, nil
case float32:
rv, _ := val.(float32)
return int(rv), nil
case float64:
rv, _ := val.(float64)
return int(rv), nil
}
return dv, nil
}
func AnyToFloat(val any, dv float64) (float64, error) {
switch val.(type) {
case int:
iv, _ := val.(int)
return float64(iv), nil
case int8:
iv, _ := val.(int8)
return float64(iv), nil
case int16:
iv, _ := val.(int16)
return float64(iv), nil
case int32:
iv, _ := val.(int32)
return float64(iv), nil
case int64:
iv, _ := val.(int64)
return float64(iv), nil
case string:
sv, _ := val.(string)
return strconv.ParseFloat(sv, 64)
case float32:
fv, _ := val.(float32)
return float64(fv), nil
case float64:
fv, _ := val.(float64)
return fv, nil
}
return dv, nil
}
// 只处理简单数据类型
func AnyToString(val any, dv string) (string, error) {
switch val.(type) {
case string:
sv, _ := val.(string)
return sv, nil
case int:
iv, _ := val.(int)
return strconv.Itoa(iv), nil
case int8:
iv, _ := val.(int8)
return strconv.FormatInt(int64(iv), 10), nil
case int16:
iv, _ := val.(int16)
return strconv.FormatInt(int64(iv), 10), nil
case int32:
iv, _ := val.(int32)
return strconv.FormatInt(int64(iv), 10), nil
case int64:
iv, _ := val.(int64)
return strconv.FormatInt(iv, 10), nil
case float32:
fv, _ := val.(float32)
return strconv.FormatFloat(float64(fv), 'f', -1, 64), nil
case float64:
fv, _ := val.(float64)
return strconv.FormatFloat(fv, 'f', -1, 64), nil
case bool:
bv, _ := val.(bool)
if bv {
return "true", nil
}
return "false", nil
}
return dv, nil
}

156
helper/conv_helper_test.go Normal file
View File

@ -0,0 +1,156 @@
//
// conv_helper_test.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package helper_test
import (
"fmt"
"testing"
"hexq.cn/tiglog/golib/gtest"
"hexq.cn/tiglog/golib/helper"
)
func TestAnyToInt(t *testing.T) {
var i1 int8 = 11
v1, err := helper.AnyToInt(i1, 0)
gtest.NoError(t, err)
gtest.Equal(t, 11, v1)
fmt.Printf("v1 %d type is %T\n", v1, v1)
var i2 int16 = 22
v2, err := helper.AnyToInt(i2, 0)
gtest.NoError(t, err)
gtest.Equal(t, 22, v2)
fmt.Printf("v2 %d type is %T\n", v2, v2)
var i3 int32 = 33
v3, err := helper.AnyToInt(i3, 0)
gtest.NoError(t, err)
gtest.Equal(t, 33, v3)
fmt.Printf("v3 %d type is %T\n", v3, v3)
var i4 int64 = 44
v4, err := helper.AnyToInt(i4, 0)
gtest.NoError(t, err)
gtest.Equal(t, 44, v4)
fmt.Printf("v4 %d type is %T\n", v4, v4)
var i5 int = 55
v5, err := helper.AnyToInt(i5, 0)
gtest.NoError(t, err)
gtest.Equal(t, 55, v5)
fmt.Printf("v5 %d type is %T\n", v5, v5)
var i6 string = "66"
v6, err := helper.AnyToInt(i6, 0)
gtest.NoError(t, err)
gtest.Equal(t, 66, v6)
fmt.Printf("v6 %d type is %T\n", v6, v6)
var i7 float32 = 77
v7, err := helper.AnyToInt(i7, 0)
gtest.NoError(t, err)
gtest.Equal(t, 77, v7)
fmt.Printf("v7 %d type is %T\n", v7, v7)
var i8 float64 = 88.8
v8, err := helper.AnyToInt(i8, 0)
gtest.NoError(t, err)
gtest.Equal(t, 88, v8)
fmt.Printf("v8 %d type is %T\n", v8, v8)
}
func TestAnyToFloat(t *testing.T) {
var f1 int8 = 11
v1, err := helper.AnyToFloat(f1, 0)
gtest.NoError(t, err)
gtest.Equal(t, float64(11), v1)
fmt.Printf("v1 %f type is %T\n", v1, v1)
var f2 int16 = 22
v2, err := helper.AnyToFloat(f2, 0)
gtest.NoError(t, err)
gtest.Equal(t, float64(22), v2)
fmt.Printf("v2 %f type is %T\n", v2, v2)
var f3 int32 = 33
v3, err := helper.AnyToFloat(f3, 0)
gtest.NoError(t, err)
gtest.Equal(t, float64(33), v3)
fmt.Printf("v3 %f type is %T\n", v3, v3)
var f4 int64 = 44
v4, err := helper.AnyToFloat(f4, 0)
gtest.NoError(t, err)
gtest.Equal(t, float64(44), v4)
fmt.Printf("v4 %f type is %T\n", v4, v4)
var f5 int = 55
v5, err := helper.AnyToFloat(f5, 0)
gtest.NoError(t, err)
gtest.Equal(t, float64(55), v5)
fmt.Printf("v5 %f type is %T\n", v5, v5)
var f6 string = "66"
v6, err := helper.AnyToFloat(f6, 0)
gtest.NoError(t, err)
gtest.Equal(t, float64(66), v6)
fmt.Printf("v6 %f type is %T\n", v6, v6)
var f7 float32 = 77
v7, err := helper.AnyToFloat(f7, 0)
gtest.NoError(t, err)
gtest.Equal(t, float64(77), v7)
gtest.Equal(t, 77.0, v7)
fmt.Printf("v7 %f type is %T\n", v7, v7)
var f8 float64 = 88.8
v8, err := helper.AnyToFloat(f8, 0)
gtest.NoError(t, err)
gtest.Equal(t, 88.8, v8)
fmt.Printf("v8 %f type is %T\n", v8, v8)
}
func TestAnyToString(t *testing.T) {
var s1 int8 = 11
v1, err := helper.AnyToString(s1, "")
gtest.NoError(t, err)
gtest.Equal(t, "11", v1)
fmt.Printf("v1 %s type is %T\n", v1, v1)
var s2 int16 = 22
v2, err := helper.AnyToString(s2, "")
gtest.NoError(t, err)
gtest.Equal(t, "22", v2)
fmt.Printf("v2 %s type is %T\n", v2, v2)
var s5 int = 55
v5, err := helper.AnyToString(s5, "")
gtest.NoError(t, err)
gtest.Equal(t, "55", v5)
fmt.Printf("v5 %s type is %T\n", v5, v5)
var s6 string = "66"
v6, err := helper.AnyToString(s6, "")
gtest.NoError(t, err)
gtest.Equal(t, "66", v6)
fmt.Printf("v6 %s type is %T\n", v6, v6)
var s7 float32 = 77
v7, err := helper.AnyToString(s7, "")
gtest.NoError(t, err)
gtest.Equal(t, "77", v7)
fmt.Printf("v7 %s type is %T\n", v7, v7)
var s8 float64 = 88.8
v8, err := helper.AnyToString(s8, "")
gtest.NoError(t, err)
gtest.Equal(t, "88.8", v8)
fmt.Printf("v8 %s type is %T\n", v8, v8)
}

14
helper/error_helper.go Normal file
View File

@ -0,0 +1,14 @@
//
// error_helper.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package helper
func CheckErr(err error) {
if err != nil {
panic(err)
}
}

42
helper/http_helper.go Normal file
View File

@ -0,0 +1,42 @@
//
// http_helper.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package helper
import (
"bytes"
"encoding/json"
"errors"
"net/http"
)
func RequestJson(url string, data []byte) (*http.Response, error) {
res, err := http.Post(url, "application/json", bytes.NewBuffer(data))
if err != nil {
return nil, err
}
return res, nil
}
func NotifyBack(url string, vals map[string]any, errmsg string) (*http.Response, error) {
if url == "" {
return nil, errors.New("no url")
}
vals["msg"] = errmsg
if errmsg == "" {
vals["stat"] = "ok"
} else {
vals["stat"] = "fail"
}
body, _ := json.Marshal(vals)
res, err := http.Post(url, "application/json", bytes.NewReader(body))
if err != nil {
return nil, err
}
return res, nil
}

59
helper/resp_helper.go Normal file
View File

@ -0,0 +1,59 @@
//
// resp_helper.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package helper
import (
"net/http"
"github.com/gin-gonic/gin"
)
func RenderJson(c *gin.Context, code int, msg string, data interface{}) {
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": msg,
"data": data,
})
}
// 成功时,返回的 code 为 0
func RenderOk(c *gin.Context, data interface{}) {
RenderJson(c, 0, "success", data)
}
// 失败时,返回的 code 为指定的 code
// 一般会比 http status code 要详细一点
func RenderFail(c *gin.Context, code int, msg string) {
RenderJson(c, code, msg, nil)
}
// 成功时,返回自定义消息和数据
func RenderSuccess(c *gin.Context, msg string, data interface{}) {
RenderJson(c, 0, msg, data)
}
func RenderClientError(c *gin.Context, err error) {
RenderJson(c, http.StatusBadRequest, err.Error(), nil)
}
func RenderServerError(c *gin.Context, err error) {
RenderJson(c, http.StatusInternalServerError, err.Error(), nil)
}
func RenderClientFail(c *gin.Context, msg string) {
RenderJson(c, http.StatusBadRequest, msg, nil)
}
func RenderServerFail(c *gin.Context, msg string) {
RenderJson(c, http.StatusInternalServerError, msg, nil)
}
// 未发现的各种情况
func RenderNotFound(c *gin.Context, msg string) {
RenderJson(c, http.StatusNotFound, msg, nil)
}

41
helper/slice_helper.go Normal file
View File

@ -0,0 +1,41 @@
//
// slice_helper.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package helper
func InStringSlice(need string, haystack []string) bool {
for _, e := range haystack {
if e == need {
return true
}
}
return false
}
func InIntSlice(need int, haystack []int) bool {
for _, e := range haystack {
if e == need {
return true
}
}
return false
}
func IsAnySlice(v interface{}) bool {
_, ok := v.([]interface{})
return ok
}
func IsStringSlice(v interface{}) bool {
_, ok := v.([]string)
return ok
}
func IsIntSlice(v interface{}) bool {
_, ok := v.([]int)
return ok
}

120
helper/str_helper.go Normal file
View File

@ -0,0 +1,120 @@
//
// str_helper.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package helper
import (
"math/rand"
"time"
"unicode"
"github.com/rs/xid"
"hexq.cn/tiglog/golib/crypto/gmd5"
)
// 是否是字符串
func IsString(v interface{}) bool {
_, ok := v.(string)
return ok
}
// 随机字符串
func RandString(n int) string {
letterRunes := []rune("1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
rand.Seed(time.Now().UnixNano())
b := make([]rune, n)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}
// 首字母大写
func UcFirst(str string) string {
for _, v := range str {
u := string(unicode.ToUpper(v))
return u + str[len(u):]
}
return ""
}
// 首字母小写
func LcFirst(str string) string {
for _, v := range str {
u := string(unicode.ToLower(v))
return u + str[len(u):]
}
return ""
}
// 乱序字符串
func Shuffle(str string) string {
if str == "" {
return str
}
runes := []rune(str)
index := 0
for i := len(runes) - 1; i > 0; i-- {
index = rand.Intn(i + 1)
if i != index {
runes[i], runes[index] = runes[index], runes[i]
}
}
return string(runes)
}
// 反序字符串
func Reverse(str string) string {
n := len(str)
runes := make([]rune, n)
for _, r := range str {
n--
runes[n] = r
}
return string(runes[n:])
}
// 唯一字符串
// 返回字符串长度为 20
func GenId() string {
guid := xid.New()
return guid.String()
}
// 可变长度的唯一字符串
// 长度太短,可能就不唯一了
// 长度大于等于 16 为最佳
// 长度小于20时为 GenId 的值 md5 后的前缀因此理论上前6位也能在大多数情况
// 下唯一
func Uniq(l int) string {
if l <= 0 {
panic("wrong length param")
}
ret := GenId()
hl := len(ret)
if l < hl {
t, err := gmd5.EncryptString(ret)
if err != nil {
return ret[hl-l:]
}
return t[:l]
}
mhac_len := 6
pl := len(ret)
var hash string
for l > pl {
hash = GenId()
hash = hash[mhac_len:]
ret += hash
pl += len(hash)
}
// log.Println("ret=", ret, ", pl=", pl, ", l=", l)
return ret[0:l]
}

120
helper/str_helper_test.go Normal file
View File

@ -0,0 +1,120 @@
//
// str_helper_test.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package helper_test
import (
"fmt"
"testing"
"hexq.cn/tiglog/golib/gtest"
"hexq.cn/tiglog/golib/helper"
)
func TestIsString(t *testing.T) {
v1 := 111
r1 := helper.IsString(v1)
gtest.False(t, r1)
v2 := "hello"
r2 := helper.IsString(v2)
gtest.True(t, r2)
}
func TestRandString(t *testing.T) {
r1 := helper.RandString(10)
fmt.Println(r1)
gtest.NotEqual(t, "", r1)
gtest.Equal(t, 10, len(r1))
}
func TestUcFirst(t *testing.T) {
v1 := "hello"
r1 := helper.UcFirst(v1)
gtest.Equal(t, "Hello", r1)
v2 := "hello world"
r2 := helper.UcFirst(v2)
gtest.Equal(t, "Hello world", r2)
v3 := "helloWorld"
r3 := helper.UcFirst(v3)
gtest.Equal(t, "HelloWorld", r3)
}
func TestGenId(t *testing.T) {
s1 := helper.GenId()
s2 := helper.GenId()
s3 := helper.GenId()
s4 := helper.GenId()
fmt.Println("gen id: ", s4)
gtest.NotNil(t, s1)
gtest.NotNil(t, s2)
gtest.NotNil(t, s3)
gtest.NotNil(t, s4)
gtest.NotEqual(t, s1, s2)
gtest.NotEqual(t, s1, s3)
gtest.NotEqual(t, s1, s4)
// fmt.Println(s1)
// fmt.Println(s2)
// fmt.Println(s3)
// fmt.Println(s4)
}
func TestUniq(t *testing.T) {
s1 := helper.Uniq(1)
fmt.Println("s1=", s1)
gtest.True(t, 1 == len(s1))
s12 := helper.Uniq(6)
s13 := helper.Uniq(6)
s14 := helper.Uniq(6)
s15 := helper.Uniq(6)
fmt.Println("s12..15", s12, s13, s14, s15)
gtest.NotNil(t, s12)
gtest.NotNil(t, s13)
gtest.NotNil(t, s14)
gtest.NotNil(t, s15)
gtest.NotEqual(t, s12, s13)
gtest.NotEqual(t, s12, s14)
gtest.NotEqual(t, s12, s15)
s2 := helper.Uniq(16)
s3 := helper.Uniq(16)
s4 := helper.Uniq(16)
s5 := helper.Uniq(16)
gtest.NotNil(t, s2)
gtest.NotNil(t, s3)
gtest.NotNil(t, s4)
gtest.NotNil(t, s5)
gtest.NotEqual(t, s2, s3)
gtest.NotEqual(t, s2, s4)
gtest.NotEqual(t, s2, s5)
s6 := helper.Uniq(32)
fmt.Println("s6=", s6)
s7 := helper.Uniq(32)
s8 := helper.Uniq(32)
s9 := helper.Uniq(32)
gtest.NotNil(t, s6)
gtest.NotNil(t, s7)
gtest.NotNil(t, s8)
gtest.NotNil(t, s9)
// fmt.Println("s6789=", s6, s7, s8, s9)
s60 := helper.Uniq(64)
fmt.Println("s60=", s60)
s70 := helper.Uniq(64)
s80 := helper.Uniq(64)
s90 := helper.Uniq(64)
gtest.NotNil(t, s60)
gtest.NotNil(t, s70)
gtest.NotNil(t, s80)
gtest.NotNil(t, s90)
// fmt.Println(s60, s70, s80, s90)
}

26
helper/time_helper.go Normal file
View File

@ -0,0 +1,26 @@
//
// time_helper.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package helper
import "time"
func Format(ts int64, layout string) string {
return time.Unix(ts, 0).Format(layout)
}
func FormatDate(ts int64) string {
return Format(ts, "2006-01-02")
}
func FormatDt(ts int64) string {
return Format(ts, "2006-01-02 15:04")
}
func FormatDateTime(ts int64) string {
return Format(ts, "2006-01-02 15:04:05")
}

55
logger/access.go Normal file
View File

@ -0,0 +1,55 @@
//
// access.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package logger
import (
"os"
"sync"
"time"
"github.com/natefinch/lumberjack"
"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
)
var access_once sync.Once
var access_log *zerolog.Logger
var access_log_path = "./var/log/access.log"
func SetupAccessLogFile(path string) {
access_log_path = path
}
func Access() *zerolog.Logger {
access_once.Do(func() {
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
zerolog.TimeFieldFormat = time.RFC3339Nano
fileLogger := &lumberjack.Logger{
Filename: access_log_path,
MaxSize: 5, //
MaxBackups: 10,
MaxAge: 14,
Compress: true,
}
output := zerolog.MultiLevelWriter(os.Stderr, fileLogger)
l := zerolog.New(output).
Level(zerolog.InfoLevel).
With().
Timestamp().
Logger()
access_log = &l
})
return access_log
}

55
logger/console.go Normal file
View File

@ -0,0 +1,55 @@
//
// console.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package logger
import (
"os"
"sync"
"time"
"github.com/natefinch/lumberjack"
"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
)
var console_once sync.Once
var console_log *zerolog.Logger
var console_log_path = "./var/log/console.log"
func SetupConsoleLogFile(path string) {
access_log_path = path
}
func Console() *zerolog.Logger {
console_once.Do(func() {
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
zerolog.TimeFieldFormat = time.RFC3339Nano
fileLogger := &lumberjack.Logger{
Filename: console_log_path,
MaxSize: 5, //
MaxBackups: 10,
MaxAge: 14,
Compress: true,
}
output := zerolog.MultiLevelWriter(os.Stderr, fileLogger)
l := zerolog.New(output).
Level(zerolog.InfoLevel).
With().
Timestamp().
Logger()
console_log = &l
})
return console_log
}

83
logger/log.go Normal file
View File

@ -0,0 +1,83 @@
//
// log.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package logger
import (
"os"
"sync"
"time"
"github.com/natefinch/lumberjack"
"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
)
var once sync.Once
var log_path = "./var/log/app.log"
var log_level = zerolog.InfoLevel
func SetLogPath(path string) {
log_path = path
}
func SetLogLevel(level zerolog.Level) {
log_level = level
}
var log *zerolog.Logger
func Get() *zerolog.Logger {
once.Do(func() {
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
zerolog.TimeFieldFormat = time.RFC3339Nano
fileLogger := &lumberjack.Logger{
Filename: log_path,
MaxSize: 5, //
MaxBackups: 10,
MaxAge: 14,
Compress: true,
}
output := zerolog.MultiLevelWriter(os.Stderr, fileLogger)
l := zerolog.New(output).
Level(log_level).
With().
Timestamp().
Logger()
log = &l
})
return log
}
func Debug(msg string) {
Get().Debug().Msg(msg)
}
func Debugf(format string, v ...interface{}) {
Get().Debug().Msgf(format, v...)
}
func Info(msg string) {
Get().Info().Msg(msg)
}
func Infof(format string, v ...interface{}) {
Get().Info().Msgf(format, v...)
}
func Warn(msg string) {
Get().Warn().Msg(msg)
}
func Warnf(format string, v ...interface{}) {
Get().Warn().Msgf(format, v...)
}
func Error(msg string) {
Get().Error().Msg(msg)
}
func Errorf(format string, v ...interface{}) {
Get().Error().Msgf(format, v...)
}

22
logger/log_test.go Normal file
View File

@ -0,0 +1,22 @@
//
// log_test.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package logger_test
import (
"testing"
"hexq.cn/tiglog/golib/gtest"
"hexq.cn/tiglog/golib/logger"
)
func TestLogToFile(t *testing.T) {
// logger.SetupLog("./var/log/test.log", zerolog.DebugLevel)
var log = logger.Get()
gtest.NotNil(t, log)
log.Log().Msg("hello world")
}

55
logger/recover.go Normal file
View File

@ -0,0 +1,55 @@
//
// recover.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package logger
import (
"os"
"sync"
"time"
"github.com/natefinch/lumberjack"
"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
)
var recover_once sync.Once
var recover_log *zerolog.Logger
var recover_log_path = "./var/log/recover.log"
func SetupRecoverLogFile(path string) {
recover_log_path = path
}
func Recover() *zerolog.Logger {
recover_once.Do(func() {
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
zerolog.TimeFieldFormat = time.RFC3339Nano
fileLogger := &lumberjack.Logger{
Filename: recover_log_path,
MaxSize: 5, //
MaxBackups: 10,
MaxAge: 14,
Compress: true,
}
output := zerolog.MultiLevelWriter(os.Stderr, fileLogger)
l := zerolog.New(output).
// Level(zerolog.InfoLevel).
With().
Timestamp().
Logger()
recover_log = &l
})
return recover_log
}

55
logger/work.go Normal file
View File

@ -0,0 +1,55 @@
//
// work.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package logger
import (
"os"
"sync"
"time"
"github.com/natefinch/lumberjack"
"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
)
var work_once sync.Once
var work_log *zerolog.Logger
var work_log_path = "./var/log/work.log"
func SetupWorkLogFile(path string) {
access_log_path = path
}
func Work() *zerolog.Logger {
work_once.Do(func() {
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
zerolog.TimeFieldFormat = time.RFC3339Nano
fileLogger := &lumberjack.Logger{
Filename: work_log_path,
MaxSize: 5, //
MaxBackups: 10,
MaxAge: 14,
Compress: true,
}
output := zerolog.MultiLevelWriter(os.Stderr, fileLogger)
l := zerolog.New(output).
Level(zerolog.InfoLevel).
With().
Timestamp().
Logger()
work_log = &l
})
return work_log
}

66
middleware/access_log.go Normal file
View File

@ -0,0 +1,66 @@
//
// access_log.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package middleware
import (
"bytes"
"io"
"io/ioutil"
"time"
"github.com/gin-gonic/gin"
"hexq.cn/tiglog/golib/logger"
)
func GinLogger() gin.HandlerFunc {
log := logger.Access()
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
raw := c.Request.URL.RawQuery
latency := time.Since(start)
// 处理请求
c.Next()
clientIP := c.ClientIP()
method := c.Request.Method
statusCode := c.Writer.Status()
comment := c.Errors.ByType(gin.ErrorTypePrivate).String()
if raw != "" {
path = path + "?" + raw
}
l := log.Log()
if comment != "" {
l = log.Error()
}
var buf bytes.Buffer
tee := io.TeeReader(c.Request.Body, &buf)
requestBody, _ := ioutil.ReadAll(tee)
c.Request.Body = ioutil.NopCloser(&buf)
l.
Str("proto", c.Request.Proto).
Str("server_name", c.Request.Host).
Str("content_type", c.Request.Header.Get("Content-Type")).
Str("user_agent", c.Request.UserAgent()).
Str("method", method).
Str("path", path).
Int("status_code", statusCode).
Str("client_ip", clientIP).
Dur("latency", latency)
if gin.IsDebugging() {
l.Str("content", string(requestBody))
}
l.Msg(comment)
}
}

36
middleware/cors.go Normal file
View File

@ -0,0 +1,36 @@
//
// cors.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
"hexq.cn/tiglog/golib/helper"
)
func NewCors(origins []string) gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
if helper.InStringSlice(origin, origins) {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Headers", "Content-Type,X-CSRF-Token, Authorization")
c.Header("Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE,PUT")
c.Header("Access-Control-Expose-Headers", "Content-Length, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true")
}
// 放行所有OPTIONS方法
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
// 处理请求
c.Next()
}
}

104
middleware/cors_test.go Normal file
View File

@ -0,0 +1,104 @@
//
// cors_test.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package middleware_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"hexq.cn/tiglog/golib/gtest"
"hexq.cn/tiglog/golib/middleware"
)
func newTestRouter(origins []string) *gin.Engine {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(middleware.NewCors(origins))
router.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "get")
})
router.POST("/", func(c *gin.Context) {
c.String(http.StatusOK, "post")
})
router.PATCH("/", func(c *gin.Context) {
c.String(http.StatusOK, "patch")
})
return router
}
func performRequest(r http.Handler, method, origin string) *httptest.ResponseRecorder {
return performRequestWithHeaders(r, method, origin, http.Header{})
}
func performRequestWithHeaders(r http.Handler, method, origin string, header http.Header) *httptest.ResponseRecorder {
req, _ := http.NewRequestWithContext(context.Background(), method, "/", nil)
// From go/net/http/request.go:
// For incoming requests, the Host header is promoted to the
// Request.Host field and removed from the Header map.
req.Host = header.Get("Host")
header.Del("Host")
if len(origin) > 0 {
header.Set("Origin", origin)
}
req.Header = header
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
return w
}
func TestPassesAllowOrigins(t *testing.T) {
router := newTestRouter([]string{"http://google.com"})
// no CORS request, origin == ""
w := performRequest(router, "GET", "")
gtest.Equal(t, "get", w.Body.String())
gtest.Empty(t, w.Header().Get("Access-Control-Allow-Origin"))
gtest.Empty(t, w.Header().Get("Access-Control-Allow-Credentials"))
gtest.Empty(t, w.Header().Get("Access-Control-Expose-Headers"))
// no CORS request, origin == host
h := http.Header{}
h.Set("Host", "facebook.com")
w = performRequestWithHeaders(router, "GET", "http://facebook.com", h)
gtest.Equal(t, "get", w.Body.String())
gtest.Empty(t, w.Header().Get("Access-Control-Allow-Origin"))
gtest.Empty(t, w.Header().Get("Access-Control-Allow-Credentials"))
gtest.Empty(t, w.Header().Get("Access-Control-Expose-Headers"))
// allowed CORS request
w = performRequest(router, "GET", "http://google.com")
gtest.Equal(t, "get", w.Body.String())
gtest.Equal(t, "http://google.com", w.Header().Get("Access-Control-Allow-Origin"))
gtest.Equal(t, "true", w.Header().Get("Access-Control-Allow-Credentials"))
// deny CORS request
w = performRequest(router, "GET", "https://google.com")
// gtest.Equal(t, http.StatusForbidden, w.Code)
gtest.Empty(t, w.Header().Get("Access-Control-Allow-Origin"))
gtest.Empty(t, w.Header().Get("Access-Control-Allow-Credentials"))
gtest.Empty(t, w.Header().Get("Access-Control-Expose-Headers"))
// allowed CORS prefligh request
w = performRequest(router, "OPTIONS", "http://google.com")
gtest.Equal(t, http.StatusNoContent, w.Code)
gtest.Equal(t, "http://google.com", w.Header().Get("Access-Control-Allow-Origin"))
gtest.Equal(t, "true", w.Header().Get("Access-Control-Allow-Credentials"))
// gtest.Equal(t, "GET,POST,PUT,HEAD", w.Header().Get("Access-Control-Allow-Methods"))
// deny CORS prefligh request
w = performRequest(router, "OPTIONS", "http://example.com")
// gtest.Equal(t, http.StatusForbidden, w.Code)
gtest.Empty(t, w.Header().Get("Access-Control-Allow-Origin"))
gtest.Empty(t, w.Header().Get("Access-Control-Allow-Credentials"))
gtest.Empty(t, w.Header().Get("Access-Control-Allow-Methods"))
gtest.Empty(t, w.Header().Get("Access-Control-Allow-Headers"))
gtest.Empty(t, w.Header().Get("Access-Control-Max-Age"))
}

54
middleware/recover_log.go Normal file
View File

@ -0,0 +1,54 @@
//
// recover_log.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package middleware
import (
"net"
"net/http"
"net/http/httputil"
"os"
"strings"
"github.com/gin-gonic/gin"
"hexq.cn/tiglog/golib/logger"
)
func GinRecover() gin.HandlerFunc {
var log = logger.Recover()
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") ||
strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
httpRequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
log.Error().Err(err.(error)).Str("request", string(httpRequest)).Msg(c.Request.URL.Path)
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) // nolint: errcheck
c.Abort()
return
}
var er = err.(error)
log.Error().Stack().Str("request", string(httpRequest)).Err(er).Msg("Recovery from panic")
c.AbortWithError(http.StatusInternalServerError, er)
}
}()
c.Next()
}
}

4
readme.md Normal file
View File

@ -0,0 +1,4 @@
说明
====
lib of go web project.

61
storage/adapter_local.go Normal file
View File

@ -0,0 +1,61 @@
//
// adapter_local.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package storage
import (
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"time"
)
type LocalStorage struct {
rootDir string
}
func NewLocalStorage(dir string) *LocalStorage {
return &LocalStorage{rootDir: dir}
}
func (s *LocalStorage) GetName() string {
return "local"
}
func (s *LocalStorage) Upload(upfile *multipart.FileHeader) (string, error) {
now := time.Now()
path := fmt.Sprintf("%d/%d/%d", now.Year(), now.Month(), now.Day())
fp := filepath.Join(s.rootDir, path)
if _, err := os.Stat(fp); err != nil {
os.MkdirAll(fp, 0755)
}
outfp := filepath.Join(fp, upfile.Filename)
fd, err := os.Create(outfp)
if err != nil {
return "", err
}
defer fd.Close()
ifd, err := upfile.Open()
if err != nil {
return "", err
}
defer ifd.Close()
io.Copy(fd, ifd)
return filepath.Join(path, upfile.Filename), nil
}
func (s *LocalStorage) GetBaseDir() string {
return s.rootDir
}
func (s *LocalStorage) GetFullPath(path string) string {
return filepath.Join(s.GetBaseDir(), path)
}

17
storage/readme.adoc Normal file
View File

@ -0,0 +1,17 @@
= 存储
:author: tiglog
:experimental:
:toc: left
:toclevels: 3
:toc-title: 目录
:sectnums:
:icons: font
:!webfonts:
:autofit-option:
:source-highlighter: rouge
:rouge-style: github
:source-linenums-option:
:revdate: 2022-11-27
:imagesdir: ./img
存储实现。

16
storage/storage.go Normal file
View File

@ -0,0 +1,16 @@
//
// storage.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package storage
type Engine struct {
IStorageAdapter
}
func NewStorage(adapter IStorageAdapter) *Engine {
return &Engine{adapter}
}

View File

@ -0,0 +1,17 @@
//
// storage_contact.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package storage
import "mime/multipart"
type IStorageAdapter interface {
GetName() string
Upload(upfile *multipart.FileHeader) (string, error)
GetBaseDir() string
GetFullPath(path string) string
}

View File

@ -0,0 +1,78 @@
//
// storage_local_test.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package storage_test
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"hexq.cn/tiglog/golib/gtest"
"hexq.cn/tiglog/golib/storage"
)
func getLocalStorage() *storage.Engine {
ls := storage.NewLocalStorage("/tmp")
return storage.NewStorage(ls)
}
func TestNewStorage(t *testing.T) {
s := getLocalStorage()
name := s.GetName()
gtest.Equal(t, "local", name)
}
func getFileheader() (*multipart.FileHeader, error) {
path := "testdata/hello.txt"
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("my_file", filepath.Base(path))
if err != nil {
writer.Close()
return nil, err
}
io.Copy(part, file)
writer.Close()
req := httptest.NewRequest("POST", "/upload", body)
req.Header.Set("Content-Type", writer.FormDataContentType())
_, h, err := req.FormFile("my_file")
if err != nil {
return nil, err
}
return h, nil
}
func TestUpload(t *testing.T) {
s := getLocalStorage()
upfile, err := getFileheader()
gtest.Nil(t, err)
path, err2 := s.Upload(upfile)
gtest.Nil(t, err2)
fmt.Println("path is ", path)
gtest.NotEqual(t, "", path)
tmp := strings.Split(path, "/")
fmt.Println(tmp)
if len(tmp) > 1 {
dir := filepath.Join(s.GetBaseDir(), tmp[0])
os.RemoveAll(dir)
}
}

1
storage/testdata/hello.txt vendored Normal file
View File

@ -0,0 +1 @@
hello world