├── config └── config.go ├── .travis.yml ├── gbb.json ├── .gitignore ├── view ├── json │ └── json.go ├── viewer.go ├── viewer_test.go ├── csv │ └── csv.go ├── txt │ └── txt.go ├── yaml │ └── yaml.go ├── md │ └── md.go └── xlsx │ └── xlsx.go ├── Makefile ├── go.mod ├── model ├── repo.go └── mysql │ └── repo.go ├── main.go ├── README.md ├── go.sum └── LICENSE /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Config 配置信息 4 | type Config struct { 5 | Host string 6 | Port int 7 | Socket string 8 | Username string 9 | Password string 10 | DB string 11 | Tables []string 12 | Viewer string 13 | Output string 14 | Debug bool 15 | Sorted bool 16 | } 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | sudo: false 4 | 5 | go: 6 | - master 7 | 8 | before_install: 9 | - go get -t -v ./... 10 | 11 | script: 12 | - GO111MODULE=on GOOS=darwin go build 13 | - GO111MODULE=on GOOS=windows go build 14 | - GO111MODULE=on GOOS=linux go build 15 | - go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... 16 | 17 | after_success: 18 | - bash <(curl -s https://codecov.io/bash) 19 | -------------------------------------------------------------------------------- /gbb.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.6.1", 3 | "tool": "go install -v -trimpath -ldflags='-s -w'", 4 | "importpath": "github.com/voidint/tsdump/build", 5 | "variables": [ 6 | { 7 | "variable": "Built", 8 | "value": "{{.Date}}" 9 | }, 10 | { 11 | "variable": "GitCommit", 12 | "value": "$(git rev-parse HEAD|cut -c 1-8)" 13 | }, 14 | { 15 | "variable": "GitBranch", 16 | "value": "$(git symbolic-ref --short -q HEAD)" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | *.db 6 | 7 | # Folders 8 | _obj 9 | _test 10 | bin 11 | pkg 12 | doc/doc 13 | src/cmd/cloudboot-license-create-tool/ 14 | .vscode 15 | .idea/ 16 | .cache 17 | dist 18 | tsdump 19 | debug 20 | 21 | # Architecture specific extensions/prefixes 22 | *.[568vq] 23 | [568vq].out 24 | 25 | *.cgo1.go 26 | *.cgo2.c 27 | _cgo_defun.c 28 | _cgo_gotypes.go 29 | _cgo_export.* 30 | 31 | _testmain.go 32 | 33 | *.exe 34 | .DS_Store 35 | 36 | # Front 37 | node_modules/ 38 | 39 | 40 | # vim 41 | *.sw[op] 42 | -------------------------------------------------------------------------------- /view/json/json.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | 7 | "github.com/voidint/tsdump/model" 8 | "github.com/voidint/tsdump/view" 9 | ) 10 | 11 | func init() { 12 | view.Register(Name, NewView()) 13 | } 14 | 15 | const ( 16 | // Name 视图名称 17 | Name = "json" 18 | ) 19 | 20 | // View JSON视图 21 | type View struct { 22 | } 23 | 24 | // NewView 返回JSON视图实例 25 | func NewView() view.Viewer { 26 | return new(View) 27 | } 28 | 29 | // Do 将数据库元数据以JSON视图形式输出。 30 | func (v *View) Do(items []model.DB, out io.Writer) error { 31 | enc := json.NewEncoder(out) 32 | enc.SetIndent(" ", " ") 33 | return enc.Encode(items) 34 | } 35 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO = CGO_ENABLED=0 GO111MODULE=on GOPROXY=https://goproxy.cn,direct go 2 | BUILD_DATE := $(shell date '+%Y-%m-%d %H:%M:%S') 3 | GIT_BRANCH := $(shell git symbolic-ref --short -q HEAD) 4 | GIT_COMMIT_HASH := $(shell git rev-parse HEAD|cut -c 1-8) 5 | GO_FLAGS := -v -ldflags="-X 'github.com/voidint/tsdump/build.Built=$(BUILD_DATE)' -X 'github.com/voidint/tsdump/build.GitCommit=$(GIT_COMMIT_HASH)' -X 'github.com/voidint/tsdump/build.GitBranch=$(GIT_BRANCH)'" 6 | 7 | all: install test 8 | 9 | build: 10 | $(GO) build $(GO_FLAGS) 11 | 12 | install: build 13 | $(GO) install $(GO_FLAGS) 14 | 15 | test: 16 | $(GO) test -v ./... 17 | 18 | clean: 19 | $(GO) clean -x 20 | 21 | .PHONY: all build install test clean -------------------------------------------------------------------------------- /view/viewer.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | 7 | "github.com/voidint/tsdump/model" 8 | ) 9 | 10 | var pool = make(map[string]Viewer) 11 | var mux sync.Mutex 12 | 13 | // Register 注册视图 14 | func Register(name string, viewer Viewer) { 15 | mux.Lock() 16 | defer mux.Unlock() 17 | if viewer == nil { 18 | panic("Register viewer is nil") 19 | } 20 | if _, dup := pool[name]; dup { 21 | panic("Register called twice for viewer " + name) 22 | } 23 | pool[name] = viewer 24 | } 25 | 26 | // Registered 返回已注册的视图名称列表 27 | func Registered() (names []string) { 28 | mux.Lock() 29 | defer mux.Unlock() 30 | 31 | for key := range pool { 32 | names = append(names, key) 33 | } 34 | return names 35 | } 36 | 37 | // SelectViewer 根据视图名称从已注册的视图中进行查找,若指定视图不存在,则返回nil。 38 | func SelectViewer(name string) Viewer { 39 | mux.Lock() 40 | defer mux.Unlock() 41 | 42 | for key := range pool { 43 | if key == name { 44 | return pool[key] 45 | } 46 | } 47 | return nil 48 | } 49 | 50 | // Viewer 数据视图 51 | type Viewer interface { 52 | Do(items []model.DB, out io.Writer) error 53 | } 54 | -------------------------------------------------------------------------------- /view/viewer_test.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | 7 | "github.com/voidint/tsdump/model" 8 | 9 | . "github.com/smartystreets/goconvey/convey" 10 | ) 11 | 12 | var ( 13 | Name = "test" 14 | ) 15 | 16 | type testView struct{} 17 | 18 | func (v *testView) Do(items []model.DB, out io.Writer) error { 19 | return nil 20 | } 21 | 22 | func clearPool() { 23 | mux.Lock() 24 | defer mux.Unlock() 25 | for k := range pool { 26 | delete(pool, k) 27 | } 28 | } 29 | 30 | func TestRegister(t *testing.T) { 31 | Convey("视图注册", t, func() { 32 | defer clearPool() 33 | 34 | Register(Name, new(testView)) 35 | So(SelectViewer(Name), ShouldNotBeNil) 36 | 37 | So(func() { Register(Name, nil) }, ShouldPanicWith, "Register viewer is nil") 38 | So(func() { Register(Name, new(testView)) }, ShouldPanicWith, "Register called twice for viewer "+Name) 39 | }) 40 | } 41 | 42 | func TestRegistered(t *testing.T) { 43 | Convey("返回已注册视图名称", t, func() { 44 | defer clearPool() 45 | 46 | So(Registered(), ShouldBeEmpty) 47 | 48 | Register(Name, new(testView)) 49 | So(len(Registered()), ShouldEqual, 1) 50 | So(Registered()[0], ShouldEqual, Name) 51 | }) 52 | } 53 | 54 | func TestSelectViewer(t *testing.T) { 55 | Convey("根据名称查找视图", t, func() { 56 | defer clearPool() 57 | 58 | So(SelectViewer(""), ShouldBeNil) 59 | Register(Name, new(testView)) 60 | So(SelectViewer(Name), ShouldNotBeNil) 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/voidint/tsdump 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/go-sql-driver/mysql v1.7.1 7 | github.com/olekukonko/tablewriter v0.0.5 8 | github.com/smartystreets/goconvey v1.8.0 9 | github.com/tealeg/xlsx/v3 v3.3.0 10 | github.com/urfave/cli v1.22.14 11 | golang.org/x/crypto v0.10.0 12 | gopkg.in/yaml.v2 v2.4.0 13 | xorm.io/xorm v1.0.4 14 | ) 15 | 16 | require ( 17 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 18 | github.com/frankban/quicktest v1.14.5 // indirect 19 | github.com/golang/snappy v0.0.1 // indirect 20 | github.com/google/btree v1.1.2 // indirect 21 | github.com/google/go-cmp v0.5.9 // indirect 22 | github.com/gopherjs/gopherjs v1.17.2 // indirect 23 | github.com/jtolds/gls v4.20.0+incompatible // indirect 24 | github.com/kr/pretty v0.3.1 // indirect 25 | github.com/kr/text v0.2.0 // indirect 26 | github.com/mattn/go-runewidth v0.0.14 // indirect 27 | github.com/peterbourgon/diskv/v3 v3.0.1 // indirect 28 | github.com/rivo/uniseg v0.4.4 // indirect 29 | github.com/rogpeppe/fastuuid v1.2.0 // indirect 30 | github.com/rogpeppe/go-internal v1.10.0 // indirect 31 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 32 | github.com/shabbyrobe/xmlwriter v0.0.0-20230525083848-85336ec334fa // indirect 33 | github.com/smartystreets/assertions v1.13.1 // indirect 34 | github.com/syndtr/goleveldb v1.0.0 // indirect 35 | golang.org/x/sys v0.9.0 // indirect 36 | golang.org/x/term v0.9.0 // indirect 37 | golang.org/x/text v0.10.0 // indirect 38 | xorm.io/builder v0.3.7 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /model/repo.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrDBNotFound 数据库不存在 7 | ErrDBNotFound = errors.New("db not found") 8 | ) 9 | 10 | // DB 数据库实例元信息 11 | type DB struct { 12 | Name string `json:"name,omitempty"` 13 | CharSet string `json:"charset,omitempty"` 14 | Collation string `json:"collation,omitempty"` 15 | Tables []Table `json:"tables,omitempty"` 16 | Extra map[string]string `json:"extra,omitempty"` 17 | } 18 | 19 | // Table 表元信息 20 | type Table struct { 21 | DB string `json:"-"` 22 | Name string `json:"name,omitempty"` 23 | Collation string `json:"collation,omitempty"` 24 | Comment string `json:"comment,omitempty"` 25 | Columns []Column `json:"columns,omitempty"` 26 | Extra map[string]string `json:"extra,omitempty"` 27 | } 28 | 29 | // Column 列元信息 30 | type Column struct { 31 | DB string `json:"-"` 32 | Table string `json:"-"` 33 | Name string `json:"name,omitempty"` 34 | Default string `json:"default,omitempty"` 35 | Nullable string `json:"nullable,omitempty"` 36 | DataType string `json:"data_type,omitempty"` 37 | Key string `json:"key,omitempty"` 38 | CharSet string `json:"charset,omitempty"` 39 | Collation string `json:"collation,omitempty"` 40 | Comment string `json:"comment,omitempty"` 41 | Extra map[string]string `json:"extra,omitempty"` 42 | } 43 | 44 | // IRepo 数据库元信息查询接口 45 | type IRepo interface { 46 | // GetDBs 查询数据库元信息 47 | GetDBs(cond *DB, lazy bool) ([]DB, error) 48 | // GetTables 查询表元信息 49 | GetTables(cond *Table) ([]Table, error) 50 | // GetColumns 查询列元信息 51 | GetColumns(cond *Column) ([]Column, error) 52 | } 53 | -------------------------------------------------------------------------------- /view/csv/csv.go: -------------------------------------------------------------------------------- 1 | package csv 2 | 3 | import ( 4 | "encoding/csv" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/voidint/tsdump/model" 9 | "github.com/voidint/tsdump/view" 10 | ) 11 | 12 | func init() { 13 | view.Register(Name, NewView()) 14 | } 15 | 16 | const ( 17 | // Name 视图名称 18 | Name = "csv" 19 | ) 20 | 21 | // View CSV视图 22 | type View struct { 23 | } 24 | 25 | // NewView 返回CSV视图实例 26 | func NewView() view.Viewer { 27 | return new(View) 28 | } 29 | 30 | // Do 将数据库元数据以CSV视图形式输出。 31 | func (v *View) Do(items []model.DB, out io.Writer) (err error) { 32 | for i := range items { 33 | if err = v.renderDB(&items[i], out); err != nil { 34 | return err 35 | } 36 | fmt.Fprintf(out, "\n\n\n") 37 | for j := range items[i].Tables { 38 | fmt.Fprintf(out, "TABLE: %s,%s\n", 39 | items[i].Tables[j].Name, 40 | items[i].Tables[j].Comment, 41 | ) 42 | if err = v.renderTable(&items[i].Tables[j], out); err != nil { 43 | return err 44 | } 45 | fmt.Fprintf(out, "\n\n\n") 46 | } 47 | } 48 | return nil 49 | } 50 | 51 | func (v *View) renderDB(db *model.DB, out io.Writer) error { 52 | rows := [][]string{ 53 | []string{"Database", "Character Set", "Collation"}, 54 | []string{db.Name, db.CharSet, db.Collation}, 55 | } 56 | return csv.NewWriter(out).WriteAll(rows) 57 | } 58 | 59 | func (v *View) renderTable(table *model.Table, out io.Writer) error { 60 | rows := make([][]string, 0, len(table.Columns)+1) 61 | rows = append(rows, []string{"Column", "Data Type", "Nullable", "Key", "Default", "Character Set", "Collation", "Comment"}) 62 | for i := range table.Columns { 63 | rows = append(rows, []string{ 64 | table.Columns[i].Name, 65 | table.Columns[i].DataType, 66 | table.Columns[i].Nullable, 67 | table.Columns[i].Key, 68 | table.Columns[i].Default, 69 | table.Columns[i].CharSet, 70 | table.Columns[i].Collation, 71 | table.Columns[i].Comment, 72 | }) 73 | } 74 | return csv.NewWriter(out).WriteAll(rows) 75 | } 76 | -------------------------------------------------------------------------------- /view/txt/txt.go: -------------------------------------------------------------------------------- 1 | package txt 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/olekukonko/tablewriter" 8 | "github.com/voidint/tsdump/model" 9 | "github.com/voidint/tsdump/view" 10 | ) 11 | 12 | func init() { 13 | view.Register(Name, NewView()) 14 | } 15 | 16 | const ( 17 | // Name 视图名称 18 | Name = "txt" 19 | ) 20 | 21 | // View Text视图 22 | type View struct { 23 | } 24 | 25 | // NewView 返回Text视图实例 26 | func NewView() view.Viewer { 27 | return new(View) 28 | } 29 | 30 | // Do 将数据库元数据以Text视图形式输出。 31 | func (v *View) Do(items []model.DB, out io.Writer) error { 32 | for i := range items { 33 | v.renderDB(&items[i], out) 34 | fmt.Fprintln(out) 35 | for j := range items[i].Tables { 36 | fmt.Fprintf(out, "TABLE:\t%s\t%s\n", 37 | items[i].Tables[j].Name, 38 | items[i].Tables[j].Comment, 39 | ) 40 | v.renderTable(&items[i].Tables[j], out) 41 | fmt.Fprintln(out) 42 | } 43 | } 44 | return nil 45 | } 46 | 47 | func (v *View) renderDB(db *model.DB, out io.Writer) { 48 | rows := [][]string{[]string{db.Name, db.CharSet, db.Collation}} 49 | 50 | t := tablewriter.NewWriter(out) 51 | t.SetHeader([]string{"Database", "Character Set", "Collation"}) 52 | t.SetCenterSeparator("|") 53 | t.AppendBulk(rows) 54 | t.Render() 55 | } 56 | 57 | func (v *View) renderTable(table *model.Table, out io.Writer) { 58 | rows := make([][]string, 0, len(table.Columns)) 59 | for i := range table.Columns { 60 | rows = append(rows, []string{ 61 | table.Columns[i].Name, 62 | table.Columns[i].DataType, 63 | table.Columns[i].Nullable, 64 | table.Columns[i].Key, 65 | table.Columns[i].Default, 66 | table.Columns[i].CharSet, 67 | table.Columns[i].Collation, 68 | table.Columns[i].Comment, 69 | }) 70 | } 71 | 72 | t := tablewriter.NewWriter(out) 73 | t.SetHeader([]string{"Column", "Data Type", "Nullable", "Key", "Default", "Character Set", "Collation", "Comment"}) 74 | t.SetCenterSeparator("|") 75 | t.AppendBulk(rows) 76 | t.Render() 77 | } 78 | -------------------------------------------------------------------------------- /view/yaml/yaml.go: -------------------------------------------------------------------------------- 1 | package xlsx 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/voidint/tsdump/model" 7 | "github.com/voidint/tsdump/view" 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | func init() { 12 | view.Register(Name, NewView()) 13 | } 14 | 15 | const ( 16 | // Name 视图名称 17 | Name = "yaml" 18 | ) 19 | 20 | // View Markdown视图 21 | type View struct { 22 | } 23 | 24 | // NewView 返回Markdown视图实例 25 | func NewView() view.Viewer { 26 | return new(View) 27 | } 28 | 29 | type Column struct { 30 | Name string `yaml:"name"` 31 | Type string `yaml:"type"` 32 | Comment string `yaml:"comment"` 33 | Default string `yaml:"default"` 34 | } 35 | 36 | type Table struct { 37 | Name string `yaml:"name"` 38 | Columns []Column `yaml:"columns"` 39 | } 40 | 41 | type Database struct { 42 | Name string `yaml:"name"` 43 | Tables []*Table `yaml:"tables"` 44 | } 45 | 46 | // Do 将数据库元数据以JSON视图形式输出。 47 | func (v *View) Do(items []model.DB, out io.Writer) error { 48 | ds := map[string]*Database{} 49 | for _, db := range items { 50 | d := ds[db.Name] 51 | if d == nil { 52 | d = &Database{ 53 | Name: db.Name, 54 | } 55 | } 56 | 57 | found := false 58 | for _, table := range db.Tables { 59 | // find table 60 | var t *Table 61 | for _, tt := range d.Tables { 62 | if tt.Name == table.Name { 63 | t = tt 64 | found = true 65 | break 66 | } 67 | } 68 | if t == nil { 69 | t = &Table{ 70 | Name: table.Name, 71 | } 72 | } 73 | 74 | var tableColumns []Column 75 | for _, c := range table.Columns { 76 | cc := Column{ 77 | Name: c.Name, 78 | Type: c.DataType, 79 | Comment: c.Comment, 80 | Default: c.Default, 81 | } 82 | tableColumns = append(tableColumns, cc) 83 | } 84 | 85 | t.Columns = append(t.Columns, tableColumns...) 86 | 87 | if !found { 88 | d.Tables = append(d.Tables, t) 89 | } 90 | } 91 | 92 | ds[db.Name] = d 93 | } 94 | 95 | enc := yaml.NewEncoder(out) 96 | return enc.Encode(ds) 97 | } 98 | -------------------------------------------------------------------------------- /view/md/md.go: -------------------------------------------------------------------------------- 1 | package md 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/olekukonko/tablewriter" 8 | "github.com/voidint/tsdump/model" 9 | "github.com/voidint/tsdump/view" 10 | ) 11 | 12 | func init() { 13 | view.Register(Name, NewView()) 14 | } 15 | 16 | const ( 17 | // Name 视图名称 18 | Name = "md" 19 | ) 20 | 21 | // View Markdown视图 22 | type View struct { 23 | } 24 | 25 | // NewView 返回Markdown视图实例 26 | func NewView() view.Viewer { 27 | return new(View) 28 | } 29 | 30 | // Do 将数据库元数据以Markdown视图形式输出。 31 | func (v *View) Do(items []model.DB, out io.Writer) error { 32 | for i := range items { 33 | v.renderDB(&items[i], out) 34 | fmt.Fprintln(out) 35 | for j := range items[i].Tables { 36 | fmt.Fprintf(out, "### `%s`\n%s\n\n", 37 | items[i].Tables[j].Name, 38 | items[i].Tables[j].Comment, 39 | ) 40 | v.renderTable(&items[i].Tables[j], out) 41 | fmt.Fprintln(out) 42 | } 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func (v *View) renderDB(db *model.DB, out io.Writer) { 49 | rows := [][]string{[]string{db.Name, db.CharSet, db.Collation}} 50 | 51 | t := tablewriter.NewWriter(out) 52 | t.SetHeader([]string{"Database", "Character Set", "Collation"}) 53 | t.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) 54 | t.SetCenterSeparator("|") 55 | t.AppendBulk(rows) 56 | t.Render() 57 | } 58 | 59 | func (v *View) renderTable(table *model.Table, out io.Writer) { 60 | rows := make([][]string, 0, len(table.Columns)) 61 | for i := range table.Columns { 62 | rows = append(rows, []string{ 63 | table.Columns[i].Name, 64 | table.Columns[i].DataType, 65 | table.Columns[i].Nullable, 66 | table.Columns[i].Key, 67 | table.Columns[i].Default, 68 | table.Columns[i].CharSet, 69 | table.Columns[i].Collation, 70 | table.Columns[i].Comment, 71 | }) 72 | } 73 | 74 | t := tablewriter.NewWriter(out) 75 | t.SetHeader([]string{"Column", "Data Type", "Nullable", "Key", "Default", "Character Set", "Collation", "Comment"}) 76 | t.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) 77 | t.SetCenterSeparator("|") 78 | t.AppendBulk(rows) 79 | t.Render() 80 | } 81 | -------------------------------------------------------------------------------- /view/xlsx/xlsx.go: -------------------------------------------------------------------------------- 1 | package xlsx 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "unicode/utf8" 8 | 9 | "github.com/tealeg/xlsx/v3" 10 | "github.com/voidint/tsdump/model" 11 | "github.com/voidint/tsdump/view" 12 | ) 13 | 14 | func init() { 15 | view.Register(Name, NewView()) 16 | } 17 | 18 | const ( 19 | // Name 视图名称 20 | Name = "xlsx" 21 | ) 22 | 23 | // View Markdown视图 24 | type View struct { 25 | } 26 | 27 | // NewView 返回Markdown视图实例 28 | func NewView() view.Viewer { 29 | return new(View) 30 | } 31 | 32 | var ( 33 | headerStyle *xlsx.Style 34 | rowStyle *xlsx.Style 35 | ) 36 | 37 | func init() { 38 | headerStyle = xlsx.NewStyle() 39 | headerStyle.Border = *xlsx.NewBorder("thin", "thin", "thin", "thin") 40 | headerStyle.ApplyBorder = true 41 | headerStyle.Font = *xlsx.NewFont(12, "Verdana") 42 | headerStyle.ApplyFont = true 43 | 44 | rowStyle = xlsx.NewStyle() 45 | rowStyle.Border = *xlsx.NewBorder("thin", "thin", "thin", "thin") 46 | rowStyle.ApplyBorder = true 47 | rowStyle.Font = *xlsx.NewFont(10, "Verdana") 48 | rowStyle.ApplyFont = true 49 | } 50 | 51 | var headerDefs = []struct { 52 | Title string 53 | Width float64 54 | }{ 55 | {Title: "Column", Width: 30}, 56 | {Title: "Data Type", Width: 25}, 57 | {Title: "Nullable", Width: 10}, 58 | {Title: "Key", Width: 6}, 59 | {Title: "Default", Width: 10}, 60 | {Title: "Character Set", Width: 15}, 61 | {Title: "Collation", Width: 20}, 62 | {Title: "Comment", Width: 50}, 63 | } 64 | 65 | // Do 将数据库元数据以Excel视图形式输出。 66 | func (v *View) Do(items []model.DB, out io.Writer) error { 67 | if len(items) <= 0 { 68 | return nil 69 | } 70 | f := xlsx.NewFile() 71 | for _, table := range items[0].Tables { 72 | sheetName := table.Name 73 | 74 | runeLength := utf8.RuneCountInString(sheetName) 75 | if runeLength > 31 { 76 | fmt.Fprintf(os.Stderr, "The sheet name is too long, rename %q to %q\n", sheetName, sheetName[0:31]) 77 | sheetName = sheetName[0:31] 78 | } 79 | 80 | sheet, err := f.AddSheet(sheetName) 81 | if err != nil { 82 | fmt.Println(err) 83 | continue 84 | } 85 | firstCell := sheet.AddRow().AddCell() 86 | firstCell.SetString(fmt.Sprintf("Table: %s(%s)", table.Name, table.Comment)) 87 | firstCell.Merge(7, 0) 88 | 89 | header := sheet.AddRow() 90 | for i, def := range headerDefs { 91 | col := xlsx.NewColForRange(i, i) 92 | col.SetWidth(def.Width) 93 | sheet.SetColParameters(col) 94 | 95 | cell := header.AddCell() 96 | cell.SetString(def.Title) 97 | cell.SetStyle(headerStyle) 98 | } 99 | 100 | for _, c := range table.Columns { 101 | row := sheet.AddRow() 102 | c0 := row.AddCell() 103 | c0.SetString(c.Name) 104 | c0.SetStyle(rowStyle) 105 | 106 | c1 := row.AddCell() 107 | c1.SetString(c.DataType) 108 | c1.SetStyle(rowStyle) 109 | 110 | c2 := row.AddCell() 111 | c2.SetString(c.Nullable) 112 | c2.SetStyle(rowStyle) 113 | 114 | c3 := row.AddCell() 115 | c3.SetString(c.Key) 116 | c3.SetStyle(rowStyle) 117 | 118 | c4 := row.AddCell() 119 | c4.SetString(c.Default) 120 | c4.SetStyle(rowStyle) 121 | 122 | c5 := row.AddCell() 123 | c5.SetString(c.CharSet) 124 | c5.SetStyle(rowStyle) 125 | 126 | c6 := row.AddCell() 127 | c6.SetString(c.Collation) 128 | c6.SetStyle(rowStyle) 129 | 130 | c7 := row.AddCell() 131 | c7.SetString(c.Comment) 132 | c7.SetStyle(rowStyle) 133 | } 134 | } 135 | defer func() { 136 | for i := range f.Sheets { 137 | f.Sheets[i].Close() 138 | } 139 | }() 140 | return f.Write(out) 141 | } 142 | -------------------------------------------------------------------------------- /model/mysql/repo.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/voidint/tsdump/config" 7 | "github.com/voidint/tsdump/model" 8 | "xorm.io/xorm" 9 | ) 10 | 11 | // Repo MySQL的model.IRepo接口实现 12 | type Repo struct { 13 | engine *xorm.Engine 14 | } 15 | 16 | func genDataSource(c *config.Config, params string) (dataSource string) { 17 | if c.Socket == "" { 18 | // use tcp protocol 19 | if c.Password == "" { 20 | return fmt.Sprintf("%s@tcp(%s:%d)/information_schema?%s", c.Username, c.Host, c.Port, params) 21 | } 22 | return fmt.Sprintf("%s:%s@tcp(%s:%d)/information_schema?%s", c.Username, c.Password, c.Host, c.Port, params) 23 | } 24 | // use unix domain socket protocol 25 | if c.Password == "" { 26 | return fmt.Sprintf("%s@unix(%s)/information_schema?%s", c.Username, c.Socket, params) 27 | } 28 | return fmt.Sprintf("%s:%s@unix(%s)/information_schema?%s", c.Username, c.Password, c.Socket, params) 29 | } 30 | 31 | // NewRepo 实例化 32 | func NewRepo(c *config.Config) (model.IRepo, error) { 33 | engine, err := xorm.NewEngine("mysql", genDataSource(c, "charset=utf8&parseTime=true&loc=Local")) 34 | if err != nil { 35 | return nil, err 36 | } 37 | engine.ShowSQL(c.Debug) 38 | return &Repo{ 39 | engine: engine, 40 | }, nil 41 | } 42 | 43 | type schema struct { 44 | Name string `xorm:"'SCHEMA_NAME'"` 45 | CharSet string `xorm:"'DEFAULT_CHARACTER_SET_NAME'"` 46 | Collation string `xorm:"'DEFAULT_COLLATION_NAME'"` 47 | } 48 | 49 | func (schema) TableName() string { 50 | return "SCHEMATA" 51 | } 52 | 53 | func (repo *Repo) getSchemas(cond *schema) (items []schema, err error) { 54 | if err = repo.engine.Find(&items, cond); err != nil { 55 | return nil, err 56 | } 57 | 58 | return items, nil 59 | } 60 | 61 | type table struct { 62 | Schema string `xorm:"'TABLE_SCHEMA'"` 63 | Name string `xorm:"'TABLE_NAME'"` 64 | Collation string `xorm:"'TABLE_COLLATION'"` 65 | Comment string `xorm:"'TABLE_COMMENT'"` 66 | } 67 | 68 | func (table) TableName() string { 69 | return "TABLES" 70 | } 71 | 72 | func (repo *Repo) getTables(cond *table) (items []table, err error) { 73 | if err = repo.engine.Find(&items, cond); err != nil { 74 | return nil, err 75 | } 76 | return items, nil 77 | } 78 | 79 | type column struct { 80 | Schema string `xorm:"'TABLE_SCHEMA'"` 81 | Table string `xorm:"'TABLE_NAME'"` 82 | Name string `xorm:"'COLUMN_NAME'"` 83 | Default string `xorm:"'COLUMN_DEFAULT'"` 84 | Nullable string `xorm:"'IS_NULLABLE'"` 85 | DataType string `xorm:"'COLUMN_TYPE'"` 86 | Key string `xorm:"'COLUMN_KEY'"` 87 | CharSet string `xorm:"'CHARACTER_SET_NAME'"` 88 | Collation string `xorm:"'COLLATION_NAME'"` 89 | Comment string `xorm:"'COLUMN_COMMENT'"` 90 | } 91 | 92 | func (column) TableName() string { 93 | return "COLUMNS" 94 | } 95 | 96 | func (repo *Repo) getColumns(cond *column) (items []column, err error) { 97 | if err = repo.engine.Find(&items, cond); err != nil { 98 | return nil, err 99 | } 100 | return items, nil 101 | } 102 | 103 | // GetDBs 按条件查询数据库信息 104 | func (repo *Repo) GetDBs(cond *model.DB, lazy bool) (items []model.DB, err error) { 105 | var sCond schema 106 | if cond != nil { 107 | sCond.Name = cond.Name 108 | sCond.CharSet = cond.CharSet 109 | sCond.Collation = cond.Collation 110 | } 111 | schemas, err := repo.getSchemas(&sCond) 112 | 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | if len(schemas) <= 0 { 118 | return nil, model.ErrDBNotFound 119 | } 120 | 121 | for i := range schemas { 122 | var tables []model.Table 123 | if !lazy { 124 | tables, err = repo.GetTables(&model.Table{ 125 | DB: schemas[i].Name, 126 | }) 127 | if err != nil { 128 | return nil, err 129 | } 130 | } 131 | items = append(items, model.DB{ 132 | Name: schemas[i].Name, 133 | CharSet: schemas[i].CharSet, 134 | Collation: schemas[i].Collation, 135 | Tables: tables, 136 | }) 137 | } 138 | 139 | return items, nil 140 | } 141 | 142 | // GetTables 按条件查询表信息 143 | func (repo *Repo) GetTables(cond *model.Table) (items []model.Table, err error) { 144 | var tCond table 145 | if cond != nil { 146 | tCond.Schema = cond.DB 147 | tCond.Name = cond.Name 148 | tCond.Collation = cond.Collation 149 | tCond.Comment = cond.Comment 150 | } 151 | 152 | tables, err := repo.getTables(&tCond) 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | for i := range tables { 158 | cols, err := repo.GetColumns(&model.Column{ 159 | DB: tables[i].Schema, 160 | Table: tables[i].Name, 161 | }) 162 | if err != nil { 163 | return nil, err 164 | } 165 | items = append(items, model.Table{ 166 | DB: tables[i].Schema, 167 | Name: tables[i].Name, 168 | Collation: tables[i].Collation, 169 | Comment: tables[i].Comment, 170 | Columns: cols, 171 | }) 172 | } 173 | return items, nil 174 | } 175 | 176 | // GetColumns 按条件查询列信息 177 | func (repo *Repo) GetColumns(cond *model.Column) (items []model.Column, err error) { 178 | var cCond column 179 | if cond != nil { 180 | cCond.Schema = cond.DB 181 | cCond.Table = cond.Table 182 | cCond.Name = cond.Name 183 | cCond.Default = cond.Default 184 | cCond.Nullable = cond.Nullable 185 | cCond.CharSet = cond.CharSet 186 | cCond.Collation = cond.Collation 187 | cCond.DataType = cond.DataType 188 | cCond.Key = cond.Key 189 | cCond.Comment = cond.Comment 190 | } 191 | cols, err := repo.getColumns(&cCond) 192 | if err != nil { 193 | return nil, err 194 | } 195 | 196 | for i := range cols { 197 | items = append(items, model.Column{ 198 | DB: cols[i].Schema, 199 | Table: cols[i].Table, 200 | Name: cols[i].Name, 201 | Default: cols[i].Default, 202 | Nullable: cols[i].Nullable, 203 | DataType: cols[i].DataType, 204 | Key: cols[i].Key, 205 | CharSet: cols[i].CharSet, 206 | Collation: cols[i].Collation, 207 | Comment: cols[i].Comment, 208 | }) 209 | } 210 | return items, nil 211 | } 212 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "os/user" 8 | "sort" 9 | "strings" 10 | "time" 11 | 12 | "github.com/urfave/cli" 13 | "github.com/voidint/tsdump/build" 14 | "github.com/voidint/tsdump/config" 15 | "github.com/voidint/tsdump/model" 16 | "github.com/voidint/tsdump/model/mysql" 17 | "github.com/voidint/tsdump/view" 18 | "github.com/voidint/tsdump/view/txt" 19 | "golang.org/x/crypto/ssh/terminal" 20 | 21 | _ "github.com/go-sql-driver/mysql" 22 | _ "github.com/voidint/tsdump/view/csv" 23 | _ "github.com/voidint/tsdump/view/json" 24 | _ "github.com/voidint/tsdump/view/md" 25 | _ "github.com/voidint/tsdump/view/txt" 26 | _ "github.com/voidint/tsdump/view/xlsx" 27 | _ "github.com/voidint/tsdump/view/yaml" 28 | ) 29 | 30 | var ( 31 | username string 32 | c config.Config 33 | out io.Writer = os.Stdout 34 | ) 35 | 36 | func init() { 37 | cli.HelpFlag = cli.BoolFlag{ 38 | Name: "help", 39 | Usage: "show help", 40 | } 41 | 42 | cli.AppHelpTemplate = fmt.Sprintf(`NAME: 43 | {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} 44 | 45 | USAGE: 46 | {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[OPTIONS]{{end}} [database [table ...]]{{end}}{{if .Version}}{{if not .HideVersion}} 47 | 48 | VERSION: 49 | %s{{end}}{{end}}{{if .Description}} 50 | 51 | DESCRIPTION: 52 | {{.Description}}{{end}}{{if len .Authors}} 53 | 54 | AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: 55 | {{range $index, $author := .Authors}}{{if $index}} 56 | {{end}}{{$author}}{{end}}{{end}}{{if .VisibleFlags}} 57 | 58 | OPTIONS: 59 | {{range $index, $option := .VisibleFlags}}{{if $index}} 60 | {{end}}{{$option}}{{end}}{{end}}{{if .Copyright}} 61 | 62 | COPYRIGHT: 63 | {{.Copyright}}{{end}} 64 | `, build.ShortVersion) 65 | 66 | u, err := user.Current() 67 | if err == nil { 68 | username = u.Username 69 | } 70 | } 71 | 72 | func main() { 73 | now := time.Now() 74 | app := cli.NewApp() 75 | app.Name = "tsdump" 76 | app.Usage = "Database table structure dump tool." 77 | app.Version = build.Version() 78 | app.Copyright = fmt.Sprintf("Copyright (c) 2017-%d, voidint. All rights reserved.", now.Year()) 79 | app.Authors = []cli.Author{ 80 | cli.Author{ 81 | Name: "voidint", 82 | Email: "voidint@126.com", 83 | }, 84 | } 85 | 86 | app.Flags = []cli.Flag{ 87 | cli.BoolFlag{ 88 | Name: "D, debug", 89 | Usage: "enable debug mode", 90 | Destination: &c.Debug, 91 | }, 92 | cli.StringFlag{ 93 | Name: "h, host", 94 | Value: "127.0.0.1", 95 | Usage: "connect to host", 96 | Destination: &c.Host, 97 | }, 98 | cli.IntFlag{ 99 | Name: "P, port", 100 | Value: 3306, 101 | Usage: "port number to use for connection", 102 | Destination: &c.Port, 103 | }, 104 | cli.StringFlag{ 105 | Name: "S, socket", 106 | Usage: "socket file to use for connection", 107 | Destination: &c.Socket, 108 | }, 109 | cli.StringFlag{ 110 | Name: "u, user", 111 | Value: username, 112 | Usage: "user for login if not current user", 113 | Destination: &c.Username, 114 | }, 115 | cli.StringFlag{ 116 | Name: "p, password", 117 | Usage: "password to use when connecting to server. If password is not given it's solicited on the tty.", 118 | Destination: &c.Password, 119 | }, 120 | cli.StringFlag{ 121 | Name: "V, viewer", 122 | Value: txt.Name, 123 | Usage: fmt.Sprintf( 124 | "output viewer. Optional values: %s", 125 | strings.Join(view.Registered(), "|"), 126 | ), 127 | Destination: &c.Viewer, 128 | }, 129 | cli.StringFlag{ 130 | Name: "o, output", 131 | Usage: "write to a file, instead of STDOUT", 132 | Destination: &c.Output, 133 | }, 134 | cli.BoolFlag{ 135 | Name: "s, sorted", 136 | Usage: "sort table columns", 137 | Destination: &c.Sorted, 138 | }, 139 | } 140 | 141 | app.Before = func(ctx *cli.Context) (err error) { 142 | if args := ctx.Args(); len(args) > 0 { 143 | c.DB = args.First() 144 | c.Tables = args.Tail() 145 | } 146 | 147 | if c.Password == "" { 148 | if c.Password, err = readPassword("Enter Password: "); err != nil { 149 | return cli.NewExitError(fmt.Sprintf("[tsdump] %s", err.Error()), 1) 150 | } 151 | } 152 | return nil 153 | } 154 | 155 | app.Action = func(ctx *cli.Context) (err error) { 156 | repo, err := mysql.NewRepo(&c) 157 | if err != nil { 158 | return cli.NewExitError(fmt.Sprintf("[tsdump] %s", err.Error()), 1) 159 | } 160 | 161 | // Get db and table metadata 162 | dbs, err := getMetadata(repo, c.DB, c.Tables...) 163 | if err != nil { 164 | return cli.NewExitError(fmt.Sprintf("[tsdump] %s", err.Error()), 1) 165 | } 166 | 167 | if c.Sorted { 168 | sortedDBs(dbs) 169 | } 170 | 171 | if c.Output != "" { 172 | var f *os.File 173 | if f, err = os.Create(c.Output); err != nil { 174 | return cli.NewExitError(fmt.Sprintf("[tsdump] %s", err.Error()), 1) 175 | } 176 | defer f.Close() 177 | out = f 178 | } 179 | 180 | // Output as target viewer 181 | v := view.SelectViewer(c.Viewer) 182 | if v == nil { 183 | return cli.NewExitError(fmt.Sprintf("[tsdump] unsupported viewer: %q", c.Viewer), 1) 184 | } 185 | if err = v.Do(dbs, out); err != nil { 186 | return cli.NewExitError(fmt.Sprintf("[tsdump] %s", err.Error()), 1) 187 | } 188 | return nil 189 | } 190 | 191 | if err := app.Run(os.Args); err != nil { 192 | fmt.Fprintln(os.Stderr, fmt.Sprintf("[tsdump] %s", err.Error())) 193 | os.Exit(1) 194 | } 195 | } 196 | 197 | // readPassword 从stdin读取密码 198 | func readPassword(prompt string) (passwd string, err error) { 199 | state, err := terminal.MakeRaw(int(os.Stdin.Fd())) 200 | if err != nil { 201 | return "", err 202 | } 203 | defer terminal.Restore(int(os.Stdin.Fd()), state) 204 | return terminal.NewTerminal(os.Stdin, "").ReadPassword(prompt) 205 | } 206 | 207 | // getMetadata 根据目标数据库名和表名,返回目标数据库及其表的元数据。 208 | func getMetadata(repo model.IRepo, db string, tables ...string) (dbs []model.DB, err error) { 209 | if db == "" && len(tables) > 0 { 210 | panic("unreachable") 211 | } 212 | 213 | // 获取所有数据库下的表 214 | if db == "" { 215 | return repo.GetDBs(nil, false) 216 | } 217 | 218 | // 获取单个数据库下的表 219 | if len(tables) == 0 { 220 | return repo.GetDBs(&model.DB{ 221 | Name: db, 222 | }, false) 223 | } 224 | 225 | // 获取单个数据库下的若干表 226 | dbs, err = repo.GetDBs(&model.DB{ 227 | Name: c.DB, 228 | }, true) 229 | if err != nil { 230 | return nil, err 231 | } 232 | 233 | for i := range dbs { 234 | for j := range tables { 235 | tables, err := repo.GetTables(&model.Table{ 236 | DB: dbs[i].Name, 237 | Name: tables[j], 238 | }) 239 | if err != nil { 240 | return nil, err 241 | } 242 | dbs[i].Tables = append(dbs[i].Tables, tables...) 243 | } 244 | } 245 | return dbs, nil 246 | } 247 | 248 | func sortedDBs(dbs []model.DB) { 249 | for i := range dbs { 250 | sortedTables(dbs[i].Tables) 251 | } 252 | 253 | sort.Slice(dbs, func(i, j int) bool { 254 | return dbs[i].Name < dbs[j].Name 255 | }) 256 | } 257 | 258 | func sortedTables(tables []model.Table) { 259 | for i := range tables { 260 | sortedColumns(tables[i].Columns) 261 | } 262 | sort.Slice(tables, func(i, j int) bool { 263 | return tables[i].Name < tables[j].Name 264 | }) 265 | } 266 | 267 | func sortedColumns(columns []model.Column) { 268 | sort.Slice(columns, func(i, j int) bool { 269 | return columns[i].Name < columns[j].Name 270 | }) 271 | } 272 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tsdump 2 | [![Build Status](https://travis-ci.org/voidint/tsdump.svg?branch=master)](https://travis-ci.org/voidint/tsdump) 3 | [![codecov](https://codecov.io/gh/voidint/tsdump/branch/master/graph/badge.svg)](https://codecov.io/gh/voidint/tsdump) 4 | [![codebeat badge](https://codebeat.co/badges/99dc335b-fd8a-4280-acf1-0eeb04a059e3)](https://codebeat.co/projects/github-com-voidint-tsdump-master) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/voidint/tsdump)](https://goreportcard.com/report/github.com/voidint/tsdump) 6 | 7 | **注意:**`master`分支可能处于开发之中并**非稳定版本**,请通过tag下载稳定版本的源代码,或通过[release](https://github.com/voidint/tsdump/releases)下载已编译的二进制可执行文件。 8 | 9 | ## 目录 10 | - [特性](#特性) 11 | - [安装](#安装) 12 | - [基本使用](#基本使用) 13 | - [Changelog](#changelog) 14 | 15 | ## 特性 16 | - 支持将数据库(当前仅支持`MySQL`)及其表结构的元数据以`text`、`markdown`、`json`、`csv`、`xlsx`形式输出。 17 | 18 | 19 | ## 安装 20 | - 源代码安装 21 | ```shell 22 | $ GO111MODULE=on GOPROXY=https://goproxy.cn go install -v github.com/voidint/tsdump@v0.5.0 23 | ``` 24 | - 二进制安装 25 | 26 | [Download](https://github.com/voidint/tsdump/releases) 27 | 28 | ## 基本使用 29 | 30 | ```shell 31 | $ tsdump --help 32 | NAME: 33 | tsdump - Database table structure dump tool. 34 | 35 | USAGE: 36 | tsdump [OPTIONS] [database [table ...]] 37 | 38 | VERSION: 39 | 0.5.0 40 | 41 | AUTHOR: 42 | voidint 43 | 44 | OPTIONS: 45 | -D, --debug enable debug mode 46 | -h value, --host value connect to host (default: "127.0.0.1") 47 | -P value, --port value port number to use for connection (default: 3306) 48 | -S value, --socket value socket file to use for connection 49 | -u value, --user value user for login if not current user (default: "voidint") 50 | -p value, --password value password to use when connecting to server. If password is not given it's solicited on the tty. 51 | -V value, --viewer value output viewer. Optional values: csv|json|md|xlsx|txt (default: "txt") 52 | -o value, --output value write to a file, instead of STDOUT 53 | -s, --sorted sort table columns 54 | --help show help 55 | --version, -v print the version 56 | 57 | COPYRIGHT: 58 | Copyright (c) 2017-2021, voidint. All rights reserved. 59 | ``` 60 | 61 | - 使用`root`用户创建一个名为`mydb`的数据库实例,以及一张`student`的表。 62 | ```SQL 63 | CREATE DATABASE IF NOT EXISTS `mydb` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; 64 | 65 | USE `mydb`; 66 | 67 | CREATE TABLE `student` ( 68 | `sno` char(8) NOT NULL COMMENT '学号', 69 | `sname` varchar(255) NOT NULL COMMENT '姓名', 70 | `gender` char(2) DEFAULT NULL COMMENT '性别', 71 | `native` char(20) DEFAULT NULL COMMENT '籍贯', 72 | `birthday` datetime DEFAULT NULL COMMENT '出生日期', 73 | `dno` char(6) DEFAULT NULL COMMENT '所在院系', 74 | `spno` char(8) DEFAULT NULL COMMENT '专业代码', 75 | `classno` char(4) DEFAULT NULL COMMENT '班级号', 76 | `entime` date DEFAULT NULL COMMENT '入校时间', 77 | `home` varchar(40) DEFAULT NULL COMMENT '家庭住址', 78 | `tell` varchar(40) DEFAULT NULL COMMENT '联系电话', 79 | PRIMARY KEY (`sno`) 80 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生信息表'; 81 | ``` 82 | 83 | - 将目标数据库及其所有表的表结构数据以表格形式输出到console 84 | ```shell 85 | $ tsdump -h 127.0.0.1 -P 3307 -u root mydb 86 | Enter Password: 87 | |----------|---------------|--------------------| 88 | | DATABASE | CHARACTER SET | COLLATION | 89 | |----------|---------------|--------------------| 90 | | mydb | utf8mb4 | utf8mb4_general_ci | 91 | |----------|---------------|--------------------| 92 | 93 | TABLE: student 学生信息表 94 | |----------|--------------|----------|-----|---------|---------------|--------------------|----------| 95 | | COLUMN | DATA TYPE | NULLABLE | KEY | DEFAULT | CHARACTER SET | COLLATION | COMMENT | 96 | |----------|--------------|----------|-----|---------|---------------|--------------------|----------| 97 | | sno | char(8) | NO | PRI | | utf8mb4 | utf8mb4_general_ci | 学号 | 98 | | sname | varchar(255) | NO | | | utf8mb4 | utf8mb4_general_ci | 姓名 | 99 | | gender | char(2) | YES | | | utf8mb4 | utf8mb4_general_ci | 性别 | 100 | | native | char(20) | YES | | | utf8mb4 | utf8mb4_general_ci | 籍贯 | 101 | | birthday | datetime | YES | | | | | 出生日期 | 102 | | dno | char(6) | YES | | | utf8mb4 | utf8mb4_general_ci | 所在院系 | 103 | | spno | char(8) | YES | | | utf8mb4 | utf8mb4_general_ci | 专业代码 | 104 | | classno | char(4) | YES | | | utf8mb4 | utf8mb4_general_ci | 班级号 | 105 | | entime | date | YES | | | | | 入校时间 | 106 | | home | varchar(40) | YES | | | utf8mb4 | utf8mb4_general_ci | 家庭住址 | 107 | | tell | varchar(40) | YES | | | utf8mb4 | utf8mb4_general_ci | 联系电话 | 108 | |----------|--------------|----------|-----|---------|---------------|--------------------|----------| 109 | ``` 110 | 111 | - 将目标数据库下目标表的表结构数据输出到markdown文件 112 | ```shell 113 | $ tsdump -h 127.0.0.1 -P 3307 -u root -V md -o ./student.md mydb student 114 | ``` 115 | 116 | output: 117 | 118 | | DATABASE | CHARACTER SET | COLLATION | 119 | |----------|---------------|--------------------| 120 | | mydb | utf8mb4 | utf8mb4_general_ci | 121 | 122 | ### `student` 123 | 学生信息表 124 | 125 | | COLUMN | DATA TYPE | NULLABLE | KEY | DEFAULT | CHARACTER SET | COLLATION | COMMENT | 126 | |----------|--------------|----------|-----|---------|---------------|--------------------|----------| 127 | | sno | char(8) | NO | PRI | | utf8mb4 | utf8mb4_general_ci | 学号 | 128 | | sname | varchar(255) | NO | | | utf8mb4 | utf8mb4_general_ci | 姓名 | 129 | | gender | char(2) | YES | | | utf8mb4 | utf8mb4_general_ci | 性别 | 130 | | native | char(20) | YES | | | utf8mb4 | utf8mb4_general_ci | 籍贯 | 131 | | birthday | datetime | YES | | | | | 出生日期 | 132 | | dno | char(6) | YES | | | utf8mb4 | utf8mb4_general_ci | 所在院系 | 133 | | spno | char(8) | YES | | | utf8mb4 | utf8mb4_general_ci | 专业代码 | 134 | | classno | char(4) | YES | | | utf8mb4 | utf8mb4_general_ci | 班级号 | 135 | | entime | date | YES | | | | | 入校时间 | 136 | | home | varchar(40) | YES | | | utf8mb4 | utf8mb4_general_ci | 家庭住址 | 137 | | tell | varchar(40) | YES | | | utf8mb4 | utf8mb4_general_ci | 联系电话 | 138 | 139 | - 将用户权限范围内数据库及其表结构数据输出到csv文件 140 | ```shell 141 | $ tsdump -h 127.0.0.1 -P 3307 -u root -V csv > ./mydb.csv 142 | ``` 143 | 144 | - 将目标数据库及其所有表的表结构数据输出到JSON文件 145 | ```shell 146 | $ tsdump -h 127.0.0.1 -P 3307 -u root -V json mydb > mydb.json 147 | ``` 148 | 149 | 150 | ## Changelog 151 | ### 0.5.0 - 2021/11/16 152 | - 支持以`xlsx`视图方式导出表结构数据。[#27](https://github.com/voidint/tsdump/issues/27) 153 | - 新增`-s`选项以支持对数据库、表、字段按字典序排列。[#28](https://github.com/voidint/tsdump/issues/28) 154 | 155 | ### 0.4.2 - 2020/05/22 156 | - 更新依赖(xorm)避免`go get`编译错误 157 | 158 | ### 0.4.1 - 2020/01/05 159 | - 修订版权信息 160 | 161 | ### 0.4.0 - 2018/03/25 162 | - 支持`UNIX Domain Socket`方式连接数据库。[#18](https://github.com/voidint/tsdump/issues/18) 163 | - 优化JSON视图输出格式。[#19](https://github.com/voidint/tsdump/issues/19) 164 | 165 | ### 0.3.0 - 2018/01/05 166 | - 支持通过`-p`选项指定数据库登录密码。[#16](https://github.com/voidint/tsdump/issues/16) 167 | - `Fixbug`: 标准输出重定向后获得的内容中包含有`Enter Password:`字样。[#17](https://github.com/voidint/tsdump/issues/17) 168 | 169 | ### 0.2.0 - 2018/01/01 170 | - 支持从stdin console中读取数据库登录密码。[#5](https://github.com/voidint/tsdump/issues/5) 171 | - `Fixbug`: 修正help信息。[#6](https://github.com/voidint/tsdump/issues/6) 172 | - 支持命令行参数指定目标数据库和表。[#12](https://github.com/voidint/tsdump/issues/12) 173 | - 支持通过`-h`选项指定主机名。[#14](https://github.com/voidint/tsdump/issues/14) 174 | 175 | ### 0.1.0 - 2017/12/31 176 | - 支持以`csv`视图方式导出表结构数据。[#1](https://github.com/voidint/tsdump/issues/1) 177 | - 支持以`markdown`视图方式导出表结构数据。[#2](https://github.com/voidint/tsdump/issues/2) 178 | - 支持以`text`视图方式导出表结构数据。[#3](https://github.com/voidint/tsdump/issues/3) 179 | - 支持以`json`视图方式导出表结构数据。[#4](https://github.com/voidint/tsdump/issues/4) 180 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= 2 | gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= 3 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 4 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 5 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 6 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 8 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 13 | github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= 14 | github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 15 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 16 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 17 | github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= 18 | github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 19 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 20 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 21 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 22 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 23 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 24 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 25 | github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= 26 | github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 27 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 28 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 29 | github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= 30 | github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= 31 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 32 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 33 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 34 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 35 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 36 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 37 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 38 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 39 | github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 40 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 41 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= 42 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 43 | github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA= 44 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= 45 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 46 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 47 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 48 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 49 | github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= 50 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 51 | github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= 52 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 53 | github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= 54 | github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= 55 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 56 | github.com/pkg/profile v1.5.0 h1:042Buzk+NhDI+DeSAA62RwJL8VAuZUMQZUjCsRz1Mug= 57 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 58 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 59 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 60 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 61 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 62 | github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= 63 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 64 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 65 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 66 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 67 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 68 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 69 | github.com/shabbyrobe/xmlwriter v0.0.0-20230525083848-85336ec334fa h1:qSlczoKLzv10e3zbEqwwDvjhai++MGDuns4ZoOKaE+U= 70 | github.com/shabbyrobe/xmlwriter v0.0.0-20230525083848-85336ec334fa/go.mod h1:tKYSeHyJGYz7eoZMlzrRDQSfdYPYt0UduMr8b97Mmaw= 71 | github.com/smartystreets/assertions v1.13.1 h1:Ef7KhSmjZcK6AVf9YbJdvPYG9avaF0ZxudX+ThRdWfU= 72 | github.com/smartystreets/assertions v1.13.1/go.mod h1:cXr/IwVfSo/RbCSPhoAPv73p3hlSdrBH/b3SdnW/LMY= 73 | github.com/smartystreets/goconvey v1.8.0 h1:Oi49ha/2MURE0WexF052Z0m+BNSGirfjg5RL+JXWq3w= 74 | github.com/smartystreets/goconvey v1.8.0/go.mod h1:EdX8jtrTIj26jmjCOVNMVSIYAtgexqXKHOXW2Dx9JLg= 75 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 76 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 77 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 78 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 79 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 80 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 81 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 82 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 83 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 84 | github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= 85 | github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= 86 | github.com/tealeg/xlsx/v3 v3.3.0 h1:GTm5dBwjHIclUGP8nSdxZ4WDAe0op9Y8lVdGnM/81/s= 87 | github.com/tealeg/xlsx/v3 v3.3.0/go.mod h1:89pBNWeVVSonnnrL2V2SjIvdel0DU8XDi7W0XsNSzfk= 88 | github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= 89 | github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= 90 | github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= 91 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 92 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 93 | golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= 94 | golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= 95 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 96 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 97 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 98 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 99 | golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= 100 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 101 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 102 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 103 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 104 | golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= 105 | golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 106 | golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= 107 | golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= 108 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 109 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 110 | golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= 111 | golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 112 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 113 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 114 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= 115 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 116 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 117 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 118 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 119 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 120 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 121 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 122 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 123 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 124 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 125 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 126 | xorm.io/builder v0.3.7 h1:2pETdKRK+2QG4mLX4oODHEhn5Z8j1m8sXa7jfu+/SZI= 127 | xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= 128 | xorm.io/xorm v1.0.4 h1:UBXA4I3NhiyjXfPqxXUkS2t5hMta9SSPATeMMaZg9oA= 129 | xorm.io/xorm v1.0.4/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4= 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. --------------------------------------------------------------------------------