init repo
This commit is contained in:
commit
f21ddce0c8
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal 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
59
console/cli_about.go
Normal 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
142
console/cli_air.go
Normal 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
28
console/cli_base.go
Normal 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
25
console/cli_contract.go
Normal 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
47
console/cli_help.go
Normal 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
42
console/cli_list.go
Normal 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
96
console/console.go
Normal 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)
|
||||
}
|
||||
}
|
53
console/tablewriter/csv.go
Normal file
53
console/tablewriter/csv.go
Normal 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
1057
console/tablewriter/table.go
Normal file
File diff suppressed because it is too large
Load Diff
143
console/tablewriter/table_with_color.go
Normal file
143
console/tablewriter/table_with_color.go
Normal 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
|
||||
}
|
93
console/tablewriter/util.go
Normal file
93
console/tablewriter/util.go
Normal 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
|
||||
}
|
99
console/tablewriter/wrap.go
Normal file
99
console/tablewriter/wrap.go
Normal 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
172
crypto/gaes/aes.go
Normal 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
91
crypto/gmd5/md5.go
Normal 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
47
crypto/gsha1/sha1.go
Normal 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
121
encoding/gbase64/base64.go
Normal 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
87
encoding/gurl/url.go
Normal 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
20
gauth/auth.go
Normal 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
25
gauth/helper.go
Normal 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
19
gauth/middleware.go
Normal 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
17
gauth/readme.adoc
Normal 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
10
gcache/adapter_file.go
Normal 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
10
gcache/adapter_local.go
Normal 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
10
gcache/adapter_redis.go
Normal 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
16
gcache/cache.go
Normal 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
18
gcache/cache_contract.go
Normal 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
54
gcache/readme.adoc
Normal 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
172
gcasbin/adapter_redis.go
Normal 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)
|
||||
}
|
160
gcasbin/adapter_redis_test.go
Normal file
160
gcasbin/adapter_redis_test.go
Normal 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
749
gcasbin/adapter_sqlx.go
Normal 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
|
||||
}
|
466
gcasbin/adapter_sqlx_test.go
Normal file
466
gcasbin/adapter_sqlx_test.go
Normal 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
8
gcasbin/casbin.go
Normal 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
17
gcasbin/middleware.go
Normal 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
18
gcasbin/readme.adoc
Normal 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
18
gcasbin/testdata/model.conf
vendored
Normal 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
3
gcasbin/testdata/policy.csv
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
p, admin, data, read
|
||||
p, admin, data, write
|
||||
g, bob, admin
|
|
14
gcasbin/testdata/rbac_model.conf
vendored
Normal file
14
gcasbin/testdata/rbac_model.conf
vendored
Normal 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
5
gcasbin/testdata/rbac_policy.csv
vendored
Normal 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
|
|
14
gcasbin/testdata/rbac_tenant_service.conf
vendored
Normal file
14
gcasbin/testdata/rbac_tenant_service.conf
vendored
Normal 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
13
gconfig/auth.go
Normal 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
60
gconfig/config.go
Normal 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
56
gconfig/db.go
Normal 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
30
gconfig/http.go
Normal 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
33
gconfig/param.go
Normal 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
0
gconfig/testdata/app.yaml
vendored
Normal file
0
gconfig/testdata/params.yaml
vendored
Normal file
0
gconfig/testdata/params.yaml
vendored
Normal file
24
gconsts/err_code.go
Normal file
24
gconsts/err_code.go
Normal 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
13
gdb/mgodb/bson.go
Normal 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
33
gdb/mgodb/error.go
Normal 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
99
gdb/mgodb/mongo.go
Normal 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
38
gdb/mgodb/mongo_test.go
Normal 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
180
gdb/sqldb/base_test.go
Normal 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
58
gdb/sqldb/db.go
Normal 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&...¶mN=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
220
gdb/sqldb/db_func.go
Normal 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
75
gdb/sqldb/db_func_opt.go
Normal 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
114
gdb/sqldb/db_func_test.go
Normal 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
20
gdb/sqldb/db_model.go
Normal 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
322
gdb/sqldb/db_query.go
Normal 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
109
gdb/sqldb/db_query_test.go
Normal 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
133
gfile/file_copy.go
Normal 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
81
gfile/file_home.go
Normal 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
177
gfile/file_path.go
Normal 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
132
gfile/file_size.go
Normal 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
40
gfile/file_time.go
Normal 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
68
go.mod
Normal 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
223
go.sum
Normal 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
49
gqueue/queue.go
Normal 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
32
gqueue/queue_test.go
Normal 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
179
gtest/test.go
Normal 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
117
helper/conv_helper.go
Normal 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
156
helper/conv_helper_test.go
Normal 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
14
helper/error_helper.go
Normal 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
42
helper/http_helper.go
Normal 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
59
helper/resp_helper.go
Normal 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
41
helper/slice_helper.go
Normal 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
120
helper/str_helper.go
Normal 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
120
helper/str_helper_test.go
Normal 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
26
helper/time_helper.go
Normal 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
55
logger/access.go
Normal 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
55
logger/console.go
Normal 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
83
logger/log.go
Normal 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
22
logger/log_test.go
Normal 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
55
logger/recover.go
Normal 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
55
logger/work.go
Normal 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
66
middleware/access_log.go
Normal 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
36
middleware/cors.go
Normal 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
104
middleware/cors_test.go
Normal 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
54
middleware/recover_log.go
Normal 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()
|
||||
}
|
||||
|
||||
}
|
61
storage/adapter_local.go
Normal file
61
storage/adapter_local.go
Normal 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
17
storage/readme.adoc
Normal 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
16
storage/storage.go
Normal 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}
|
||||
}
|
17
storage/storage_contract.go
Normal file
17
storage/storage_contract.go
Normal 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
|
||||
}
|
78
storage/storage_local_test.go
Normal file
78
storage/storage_local_test.go
Normal 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
1
storage/testdata/hello.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
hello world
|
Loading…
Reference in New Issue
Block a user