├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── build ├── build.go ├── build_test.go ├── repo.go └── task.go ├── config.go ├── db.go ├── deploy ├── deploy.go ├── server.go └── task.go ├── docs ├── .nojekyll ├── 2.0 │ └── .nojekyll ├── assets │ ├── css │ │ └── app.css │ └── img │ │ ├── favicon.ico │ │ ├── gitee-logo-black.svg │ │ ├── logo-blue.svg │ │ ├── logo.svg │ │ └── wechat-donate.png ├── docs │ ├── _404.md │ ├── _sidebar.md │ ├── architecture.md │ ├── assets │ │ ├── css │ │ │ └── app.css │ │ └── img │ │ │ ├── syncd-member-add.png │ │ │ ├── syncd-principle.png │ │ │ ├── syncd-project-add.png │ │ │ ├── syncd-server-add.png │ │ │ ├── syncd-server-group-add.png │ │ │ └── syncd-space-add.png │ ├── build.md │ ├── donate.md │ ├── help.md │ ├── home.md │ ├── include │ │ └── footer.md │ ├── index.html │ ├── install.md │ ├── project.md │ ├── server.md │ ├── setting.md │ ├── troubleinstall.md │ └── troubleuse.md ├── index.html └── install.sh ├── go.mod ├── go.sum ├── model ├── deploy_apply.go ├── deploy_build.go ├── deploy_task.go ├── model.go ├── project.go ├── project_member.go ├── project_space.go ├── server.go ├── server_group.go ├── user.go ├── user_role.go └── user_token.go ├── module ├── deploy │ ├── apply.go │ ├── build.go │ └── deploy.go ├── project │ ├── member.go │ ├── project.go │ └── space.go ├── server │ ├── group.go │ └── server.go └── user │ ├── login.go │ ├── priv.go │ ├── role.go │ ├── token.go │ └── user.go ├── public ├── css │ ├── app.d770df8f.css │ ├── chunk-21c9e7bf.a3c25090.css │ ├── chunk-8a44c3e0.cb38e73a.css │ ├── chunk-e3695f98.b86ea360.css │ └── chunk-vendors.9c8e3772.css ├── favicon.ico ├── fonts │ ├── element-icons.2fad952a.woff │ └── element-icons.6f0a7632.ttf ├── img │ └── login_bg.193d505f.jpg ├── index.html └── js │ ├── app.2bf0ccd2.js │ ├── app.2bf0ccd2.js.map │ ├── chunk-162ff1ff.15a23fae.js │ ├── chunk-162ff1ff.15a23fae.js.map │ ├── chunk-21c9e7bf.ec2f8115.js │ ├── chunk-21c9e7bf.ec2f8115.js.map │ ├── chunk-22f331f4.0eddfbce.js │ ├── chunk-22f331f4.0eddfbce.js.map │ ├── chunk-232f6874.c45db812.js │ ├── chunk-232f6874.c45db812.js.map │ ├── chunk-29553eaa.4ebf8cb5.js │ ├── chunk-29553eaa.4ebf8cb5.js.map │ ├── chunk-2d0f0051.d23388af.js │ ├── chunk-2d0f0051.d23388af.js.map │ ├── chunk-58cbcdef.2c344c6b.js │ ├── chunk-58cbcdef.2c344c6b.js.map │ ├── chunk-5a77cfd8.4e40248e.js │ ├── chunk-5a77cfd8.4e40248e.js.map │ ├── chunk-768b7ab8.2e39c37f.js │ ├── chunk-768b7ab8.2e39c37f.js.map │ ├── chunk-7ef915ac.e5c53307.js │ ├── chunk-7ef915ac.e5c53307.js.map │ ├── chunk-8a44c3e0.5374e90e.js │ ├── chunk-8a44c3e0.5374e90e.js.map │ ├── chunk-a101e242.0f232132.js │ ├── chunk-a101e242.0f232132.js.map │ ├── chunk-e3695f98.4489b617.js │ ├── chunk-e3695f98.4489b617.js.map │ ├── chunk-f4b66f66.eb08a10d.js │ ├── chunk-f4b66f66.eb08a10d.js.map │ ├── chunk-vendors.512af33a.js │ └── chunk-vendors.512af33a.js.map ├── render └── render.go ├── resource └── sql │ └── syncd_v2.0.0.sql ├── router ├── common │ ├── hook.go │ └── inspace.go ├── deploy │ ├── apply.go │ ├── build.go │ ├── deploy.go │ └── mail.go ├── middleware │ └── api_priv.go ├── project │ ├── member.go │ ├── project.go │ └── space.go ├── route │ ├── api │ │ └── api.go │ └── route.go ├── server │ ├── group.go │ └── server.go └── user │ ├── login.go │ ├── my.go │ ├── role.go │ └── user.go ├── script └── install.sh ├── sendmail.go ├── syncd.go ├── syncd.ini ├── syncd └── main.go ├── util ├── command │ ├── command.go │ ├── command_test.go │ ├── task.go │ └── task_test.go ├── goaes │ └── aes.go ├── gofile │ └── file.go ├── gois │ └── is.go ├── golog │ ├── file.go │ └── log.go ├── gopath │ └── path.go ├── goslice │ └── slice.go └── gostring │ └── string.go └── web ├── .browserslistrc ├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── api │ ├── deploy.js │ ├── login.js │ ├── project.js │ ├── server.js │ └── user.js ├── asset │ ├── login_bg.jpg │ ├── logo.png │ └── logo.svg ├── component │ └── ScrollBar │ │ └── index.vue ├── lang │ ├── en.js │ ├── index.js │ └── zh_cn.js ├── lib │ ├── code.js │ ├── data.js │ ├── fetch.js │ ├── priv.js │ └── util.js ├── main.js ├── router │ └── index.js ├── scss │ └── app.scss ├── store │ ├── index.js │ └── module │ │ └── account.js └── view │ ├── Dashboard.vue │ ├── Layer.vue │ ├── Login.vue │ ├── deploy │ ├── Apply.vue │ ├── Deploy.vue │ └── Release.vue │ ├── project │ ├── Member.vue │ ├── Project.vue │ └── Space.vue │ ├── server │ ├── Group.vue │ └── Server.vue │ └── user │ ├── Group.vue │ └── User.vue ├── vue.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # dir 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - 1.11.x 5 | 6 | script: go get github.com/dreamans/syncd/syncd -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.12-alpine3.10 AS build 2 | COPY . /usr/local/src 3 | WORKDIR /usr/local/src 4 | RUN apk --no-cache add build-base && make 5 | 6 | FROM alpine:3.10 7 | WORKDIR /syncd 8 | COPY --from=build /usr/local/src/output /syncd 9 | EXPOSE 8878 10 | CMD [ "/syncd/bin/syncd" ] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 syncd 4 | Copyright (c) 2018 dreamans 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO_CMD=go 2 | SYNCD_BIN=syncd_bin 3 | SYNCD_BIN_PATH=./output/bin 4 | SYNCD_ETC_PATH=./output/etc 5 | SYNCD_PUBLIC_PATH=./output/public 6 | SYNCD_LOG_PATH=./output/log 7 | SYNCD_RES_PATH=./output/resource 8 | 9 | .PHONY: all 10 | all: clean build install 11 | 12 | .PHONY: linux 13 | linux: clean build-linux install 14 | 15 | .PHONY: build 16 | build: 17 | @echo "build syncd start >>>" 18 | GOPROXY=https://goproxy.io $(GO_CMD) mod tidy 19 | $(GO_CMD) build -o $(SYNCD_BIN) ./syncd/main.go 20 | @echo ">>> build syncd complete" 21 | 22 | .PHONY: install 23 | install: 24 | @echo "install syncd start >>>" 25 | mkdir -p $(SYNCD_BIN_PATH) 26 | mv $(SYNCD_BIN) $(SYNCD_BIN_PATH)/syncd 27 | mkdir -p $(SYNCD_ETC_PATH) 28 | cp ./syncd.ini $(SYNCD_ETC_PATH) 29 | cp -r ./public $(SYNCD_PUBLIC_PATH) 30 | mkdir -p $(SYNCD_LOG_PATH) 31 | cp -r ./resource $(SYNCD_RES_PATH) 32 | @echo ">>> install syncd complete" 33 | 34 | .PHONY: clean 35 | clean: 36 | @echo "clean start >>>" 37 | rm -fr ./output 38 | rm -f $(SYNCD_BIN) 39 | @echo ">>> clean complete" 40 | 41 | .PHONY: build-linux 42 | build-linux: 43 | @echo "build-linux start >>>" 44 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO_CMD) build -o $(SYNCD_BIN) ./syncd/main.go 45 | @echo ">>> build-linux complete" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

Syncd - 自动化部署工具

5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

14 |

Syncd是一款开源的代码部署工具,它具有简单、高效、易用等特点,可以提高团队的工作效率。

15 |

16 |

码云GVP项目

17 | 18 | ## 文档 19 | 20 | [查看文档](https://syncd.cc) 21 | 22 | ## 特性 23 | 24 | - Go语言开发,编译简单、运行高效 25 | - Web界面访问,交互友好 26 | - 权限模型灵活自由 27 | - 支持自定义构建 28 | - 支持Git仓库 29 | - 支持分支、Tag上线 30 | - 部署Hook支持,可扩展性强 31 | - 完善的上线工作流 32 | - 邮件通知机制 33 | 34 | ## 项目地址 35 | 36 | Github: https://github.com/dreamans/syncd 37 | 38 | Gitee: https://gitee.com/dreamans/syncd 39 | 40 | ## 获取帮助 41 | 42 | Syncd使用交流QQ群①: 725302833 43 | 44 | ## 捐赠 45 | 46 | Syncd的发展离不开您的支持,[前往捐赠](https://syncd.cc/docs/#donate.md) 47 | 48 | ## LICENSE 49 | 50 | 本项目采用 MIT 开源授权许可证,完整的授权说明已放置在 LICENSE 文件中。 -------------------------------------------------------------------------------- /build/build.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package build 6 | 7 | import ( 8 | "time" 9 | "fmt" 10 | 11 | "github.com/dreamans/syncd/util/gostring" 12 | "github.com/dreamans/syncd/util/command" 13 | "github.com/dreamans/syncd/util/gofile" 14 | "github.com/dreamans/syncd/util/gopath" 15 | ) 16 | 17 | type Build struct { 18 | repo *Repo 19 | local string 20 | tmp string 21 | packFile string 22 | scriptFile string 23 | task *command.Task 24 | result *Result 25 | } 26 | 27 | const ( 28 | STATUS_INIT = 1 29 | STATUS_ING = 2 30 | STATUS_DONE = 3 31 | STATUS_FAILED = 4 32 | ) 33 | 34 | const ( 35 | COMMAND_TIMEOUT = 86400 36 | ) 37 | 38 | func NewBuild(repo *Repo, local, tmp, packFile, scripts string) (*Build, error) { 39 | build := &Build{ 40 | repo: repo, 41 | local: local, 42 | tmp: tmp, 43 | packFile: packFile, 44 | result: &Result{ 45 | status: STATUS_INIT, 46 | }, 47 | } 48 | if err := build.createScriptFile(scripts); err != nil { 49 | return build, err 50 | } 51 | build.initBuildTask() 52 | 53 | return build, nil 54 | } 55 | 56 | func (b *Build) createScriptFile(scripts string) error { 57 | b.scriptFile = gostring.JoinStrings(b.tmp, "/", gostring.StrRandom(24), ".sh") 58 | s := gostring.JoinStrings( 59 | "#!/bin/bash\n\n", 60 | "#--------- build scripts env ---------\n", 61 | fmt.Sprintf("env_workspace=%s\n", b.local), 62 | fmt.Sprintf("env_pack_file=%s\n", b.packFile), 63 | scripts, 64 | ) 65 | if err := gofile.CreateFile(b.scriptFile, []byte(s), 0744); err != nil { 66 | return err 67 | } 68 | return nil 69 | } 70 | 71 | func (b *Build) initBuildTask() { 72 | cmds := b.repo.Fetch() 73 | cmds = append(cmds, []string{ 74 | "echo \"Now is\" `date`", 75 | "echo \"Run user is\" `whoami`", 76 | fmt.Sprintf("rm -f %s", b.packFile), 77 | fmt.Sprintf("/bin/bash -c %s", b.scriptFile), 78 | fmt.Sprintf("rm -f %s", b.scriptFile), 79 | fmt.Sprintf("rm -fr %s", b.local), 80 | "echo \"Compile completed\" `date`", 81 | }...) 82 | b.task = command.NewTask(cmds, COMMAND_TIMEOUT) 83 | } 84 | 85 | func (b *Build) Run() { 86 | b.result.status = STATUS_ING 87 | b.result.stime = int(time.Now().Unix()) 88 | b.task.Run() 89 | if err := b.task.GetError(); err != nil { 90 | b.result.status = STATUS_FAILED 91 | b.result.err = err 92 | } else { 93 | b.result.status = STATUS_DONE 94 | } 95 | b.result.etime = int(time.Now().Unix()) 96 | } 97 | 98 | func (b *Build) Result() *Result { 99 | return b.result 100 | } 101 | 102 | func (b *Build) Output() []*command.TaskResult{ 103 | return b.task.Result() 104 | } 105 | 106 | func (b *Build) PackFile() string { 107 | return b.packFile 108 | } 109 | 110 | func (b *Build) PackRealFile() string { 111 | if gopath.Exists(b.packFile) { 112 | return b.packFile 113 | } 114 | return "" 115 | } 116 | 117 | func (b *Build) Terminate() { 118 | if b.task != nil { 119 | b.task.Terminate() 120 | } 121 | } 122 | 123 | type Result struct { 124 | err error 125 | status int 126 | stime int 127 | etime int 128 | } 129 | 130 | func (r *Result) During() int { 131 | return r.etime - r.stime 132 | } 133 | 134 | func (r *Result) Status() int { 135 | return r.status 136 | } 137 | 138 | func (r *Result) GetError() error { 139 | return r.err 140 | } -------------------------------------------------------------------------------- /build/build_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package build 6 | 7 | import ( 8 | "testing" 9 | "time" 10 | "fmt" 11 | 12 | "github.com/dreamans/syncd/util/command" 13 | ) 14 | 15 | func TestBuild(t *testing.T) { 16 | local := "/tmp/laravel" 17 | tmp := "/tmp" 18 | packFile := "/tmp/laravel.tar.gz" 19 | repo := NewRepo("git@gitee.com:mirrors/laravel.git", local) 20 | scripts := ` 21 | cd ${env_workspace} 22 | tar --exclude=.git --exclude=.gitignore -zcvf ${env_pack_file} * 23 | ` 24 | build, err := NewBuild(repo, local, tmp, packFile, scripts) 25 | if err != nil { 26 | t.Errorf("create build task failed: %s", err.Error()) 27 | } 28 | NewTask(1, build, func(id int, result *Result, taskResult []*command.TaskResult) { 29 | fmt.Println(id) 30 | fmt.Println(result) 31 | for _, t := range taskResult { 32 | fmt.Println(t) 33 | } 34 | }) 35 | 36 | time.Sleep(2 * time.Second) 37 | 38 | result, taskResult, err := StatusTask(1) 39 | 40 | fmt.Println(result) 41 | for _, t := range taskResult { 42 | fmt.Println(t) 43 | } 44 | 45 | time.Sleep(10 * time.Second) 46 | 47 | } -------------------------------------------------------------------------------- /build/repo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package build 6 | 7 | import ( 8 | "fmt" 9 | ) 10 | 11 | type Repo struct { 12 | url string 13 | branch string 14 | commit string 15 | local string 16 | } 17 | 18 | func NewRepo(url, local string) *Repo { 19 | repo := &Repo{ 20 | url: url, 21 | local: local, 22 | } 23 | return repo 24 | } 25 | 26 | func (r *Repo) SetBranch(branch string) { 27 | r.branch = branch 28 | } 29 | 30 | func (r *Repo) SetCommit(version string) { 31 | r.commit = version 32 | } 33 | 34 | func (r *Repo) Fetch() []string { 35 | cmds := []string{ 36 | fmt.Sprintf("rm -fr %s", r.local), 37 | fmt.Sprintf("/usr/bin/env git clone -q %s %s", r.url, r.local), 38 | } 39 | if r.branch != "" { 40 | cmds = append(cmds, fmt.Sprintf("cd %s && /usr/bin/env git checkout -q %s", r.local, r.branch)) 41 | } 42 | if r.commit != "" { 43 | cmds = append(cmds, fmt.Sprintf("cd %s && /usr/bin/env git reset -q --hard %s", r.local, r.commit)) 44 | } 45 | return cmds 46 | } -------------------------------------------------------------------------------- /build/task.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package build 6 | 7 | import ( 8 | "sync" 9 | "errors" 10 | "fmt" 11 | 12 | "github.com/dreamans/syncd/util/command" 13 | ) 14 | 15 | type buildTask struct { 16 | builds map[int]*Build 17 | mu sync.Mutex 18 | } 19 | 20 | type CallbackFn func(int, string, *Result, []*command.TaskResult) 21 | 22 | var task = &buildTask{ 23 | builds: make(map[int]*Build), 24 | } 25 | 26 | func NewTask(id int, build *Build, fn CallbackFn) error { 27 | if exists := task.exists(id); exists { 28 | return errors.New(fmt.Sprintf("build task [id: %d] have exists", id)) 29 | } 30 | task.append(id, build) 31 | go func() { 32 | build.Run() 33 | task.remove(id) 34 | if fn != nil { 35 | fn(id, build.PackRealFile(), build.Result(), build.Output()) 36 | } 37 | }() 38 | return nil 39 | } 40 | 41 | func StopTask(id int) { 42 | task.stop(id) 43 | } 44 | 45 | func StatusTask(id int) (*Result, []*command.TaskResult, error) { 46 | build, exists := task.get(id) 47 | if !exists { 48 | return nil, nil, errors.New(fmt.Sprintf("build task [id: %d] not exists", id)) 49 | } 50 | return build.Result(), build.Output(), nil 51 | } 52 | 53 | func (t *buildTask) exists(id int) bool { 54 | t.mu.Lock() 55 | defer t.mu.Unlock() 56 | _, exists := t.builds[id] 57 | return exists 58 | } 59 | 60 | func (t *buildTask) append(id int, build *Build) { 61 | t.mu.Lock() 62 | defer t.mu.Unlock() 63 | t.builds[id] = build 64 | } 65 | 66 | func (t *buildTask) remove(id int) { 67 | t.mu.Lock() 68 | defer t.mu.Unlock() 69 | delete(t.builds, id) 70 | } 71 | 72 | func (t *buildTask) get(id int) (*Build, bool) { 73 | t.mu.Lock() 74 | defer t.mu.Unlock() 75 | build, exists := t.builds[id] 76 | return build, exists 77 | } 78 | 79 | func (t *buildTask) stop(id int) { 80 | t.mu.Lock() 81 | defer t.mu.Unlock() 82 | build, exists := t.builds[id] 83 | if exists { 84 | build.Terminate() 85 | } 86 | } -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package syncd 6 | 7 | type ( 8 | Config struct { 9 | Serve *ServeConfig 10 | Db *DbConfig 11 | Log *LogConfig 12 | Syncd *SyncdConfig 13 | Mail *MailConfig 14 | } 15 | 16 | SyncdConfig struct { 17 | LocalSpace string 18 | RemoteSpace string 19 | Cipher string 20 | AppHost string 21 | } 22 | 23 | LogConfig struct { 24 | Path string 25 | } 26 | 27 | MailConfig struct { 28 | Enable int 29 | Smtp string 30 | Port int 31 | User string 32 | Pass string 33 | } 34 | 35 | ServeConfig struct { 36 | Addr string 37 | FeServeEnable int 38 | ReadTimeout int 39 | WriteTimeout int 40 | IdleTimeout int 41 | } 42 | 43 | DbConfig struct { 44 | Unix string 45 | Host string 46 | Port int 47 | Charset string 48 | User string 49 | Pass string 50 | DbName string 51 | TablePrefix string 52 | MaxIdleConns int 53 | MaxOpenConns int 54 | ConnMaxLifeTime int 55 | } 56 | ) 57 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package syncd 6 | 7 | import ( 8 | "fmt" 9 | "time" 10 | "errors" 11 | 12 | "github.com/jinzhu/gorm" 13 | _ "github.com/go-sql-driver/mysql" 14 | ) 15 | 16 | type DB struct { 17 | DbHandler *gorm.DB 18 | cfg *DbConfig 19 | } 20 | 21 | func NewDatabase(cfg *DbConfig) *DB { 22 | return &DB{ 23 | cfg: cfg, 24 | } 25 | } 26 | 27 | func (db *DB) Open() error { 28 | c, err := gorm.Open("mysql", db.parseConnConfig()) 29 | if err != nil { 30 | return errors.New(fmt.Sprintf("mysql connect failed, %s", err.Error())) 31 | } 32 | 33 | c.LogMode(false) 34 | c.DB().SetMaxIdleConns(db.cfg.MaxIdleConns) 35 | c.DB().SetMaxOpenConns(db.cfg.MaxOpenConns) 36 | c.DB().SetConnMaxLifetime(time.Second * time.Duration(db.cfg.ConnMaxLifeTime)) 37 | 38 | db.DbHandler = c 39 | return nil 40 | } 41 | 42 | func (db *DB) Close() { 43 | db.DbHandler.Close() 44 | } 45 | 46 | func (db *DB) parseConnConfig() string { 47 | var connHost string 48 | if db.cfg.Unix != "" { 49 | connHost = fmt.Sprintf("unix(%s)", db.cfg.Unix) 50 | } else { 51 | connHost = fmt.Sprintf("tcp(%s:%d)", db.cfg.Host, db.cfg.Port) 52 | } 53 | s := fmt.Sprintf("%s:%s@%s/%s?charset=%s&parseTime=True&loc=Local", db.cfg.User, db.cfg.Pass, connHost, db.cfg.DbName, db.cfg.Charset) 54 | return s 55 | } 56 | -------------------------------------------------------------------------------- /deploy/deploy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package deploy 6 | 7 | import ( 8 | "sync" 9 | ) 10 | 11 | const ( 12 | STATUS_INIT = 0 13 | STATUS_ING = 1 14 | STATUS_DONE = 2 15 | STATUS_FAILED = 3 16 | ) 17 | 18 | const ( 19 | DEPLOY_PARALLEL = 1 20 | DEPLOY_SERIAL = 2 21 | ) 22 | 23 | type Deploy struct { 24 | ID int 25 | User string 26 | PreCmd string 27 | PostCmd string 28 | DeployPath string 29 | DeployTmpPath string 30 | PackFile string 31 | srvs []*Server 32 | status int 33 | wg sync.WaitGroup 34 | } 35 | 36 | type DeployResult struct { 37 | ID int 38 | Status int 39 | ServerRest []*ServerResult 40 | } 41 | 42 | func (d *Deploy) AddServer(id int, addr string, port int) { 43 | srv := &Server{ 44 | ID: id, 45 | Addr: addr, 46 | User: d.User, 47 | Port: port, 48 | PreCmd: d.PreCmd, 49 | PostCmd: d.PostCmd, 50 | PackFile: d.PackFile, 51 | DeployTmpPath: d.DeployTmpPath, 52 | DeployPath: d.DeployPath, 53 | } 54 | NewServer(srv) 55 | d.srvs = append(d.srvs, srv) 56 | } 57 | 58 | func (d *Deploy) Parallel() { 59 | if d.status == STATUS_FAILED { 60 | return 61 | } 62 | d.status = STATUS_ING 63 | status := STATUS_DONE 64 | for _, srv := range d.srvs { 65 | d.wg.Add(1) 66 | go func() { 67 | if d.status == STATUS_ING { 68 | srv.Deploy() 69 | if srv.Result().Status == STATUS_FAILED { 70 | status = STATUS_FAILED 71 | } 72 | } 73 | defer d.wg.Done() 74 | }() 75 | } 76 | d.wg.Wait() 77 | d.status = status 78 | } 79 | 80 | func (d *Deploy) Serial() { 81 | if d.status == STATUS_FAILED { 82 | return 83 | } 84 | d.status = STATUS_ING 85 | status := STATUS_DONE 86 | for _, srv := range d.srvs { 87 | if d.status == STATUS_ING { 88 | srv.Deploy() 89 | if srv.Result().Status == STATUS_FAILED { 90 | status = STATUS_FAILED 91 | } 92 | } 93 | } 94 | d.status = status 95 | } 96 | 97 | func (d *Deploy) Result() ([]*ServerResult, int) { 98 | var rest []*ServerResult 99 | for _, srv := range d.srvs { 100 | rest = append(rest, srv.Result()) 101 | } 102 | return rest, d.status 103 | } 104 | 105 | func (d *Deploy) Terminate() { 106 | d.status = STATUS_FAILED 107 | for _, srv := range d.srvs { 108 | srv.Terminate() 109 | } 110 | } -------------------------------------------------------------------------------- /deploy/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package deploy 6 | 7 | import ( 8 | "fmt" 9 | "path" 10 | 11 | "github.com/dreamans/syncd/util/command" 12 | ) 13 | 14 | const ( 15 | COMMAND_TIMEOUT = 3600 16 | ) 17 | 18 | type Server struct { 19 | ID int 20 | Addr string 21 | User string 22 | Port int 23 | PreCmd string 24 | PostCmd string 25 | Key string 26 | PackFile string 27 | DeployTmpPath string 28 | DeployPath string 29 | task *command.Task 30 | result *ServerResult 31 | } 32 | 33 | type ServerResult struct { 34 | ID int 35 | TaskResult []*command.TaskResult 36 | Status int 37 | Error error 38 | } 39 | 40 | func NewServer(srv *Server) { 41 | srv.result = &ServerResult{ 42 | ID: srv.ID, 43 | Status: STATUS_INIT, 44 | } 45 | srv.task = command.NewTask( 46 | srv.deployCmd(), 47 | COMMAND_TIMEOUT, 48 | ) 49 | } 50 | 51 | func (srv *Server) Deploy() { 52 | srv.result.Status = STATUS_ING 53 | srv.task.Run() 54 | if err := srv.task.GetError(); err != nil { 55 | srv.result.Error = err 56 | srv.result.Status = STATUS_FAILED 57 | } else { 58 | srv.result.Status = STATUS_DONE 59 | } 60 | } 61 | 62 | func (srv *Server) Terminate() { 63 | if srv.result.Status == STATUS_ING { 64 | srv.task.Terminate() 65 | } 66 | } 67 | 68 | func (srv *Server) Result() *ServerResult { 69 | srv.result.TaskResult = srv.task.Result() 70 | return srv.result 71 | } 72 | 73 | func (srv *Server) deployCmd() []string { 74 | var ( 75 | useCustomKey, useSshPort, useScpPort string 76 | ) 77 | if srv.Key != "" { 78 | useCustomKey = fmt.Sprintf("-i %s", srv.Key) 79 | } 80 | if srv.Port != 0 { 81 | useSshPort = fmt.Sprintf("-p %d", srv.Port) 82 | useScpPort = fmt.Sprintf(" -P %d", srv.Port) 83 | } 84 | var cmds []string 85 | if srv.PackFile == "" { 86 | cmds = append(cmds, "echo 'packfile empty' && exit 1") 87 | } 88 | 89 | cmds = append(cmds, []string{ 90 | fmt.Sprintf( 91 | "/usr/bin/env ssh -o StrictHostKeyChecking=no %s %s %s@%s 'mkdir -p %s; mkdir -p %s'", 92 | useCustomKey, 93 | useSshPort, 94 | srv.User, 95 | srv.Addr, 96 | srv.DeployTmpPath, 97 | srv.DeployPath, 98 | ), 99 | fmt.Sprintf( 100 | "/usr/bin/env scp -o StrictHostKeyChecking=no -q %s %s %s %s@%s:%s/", 101 | useCustomKey, 102 | useScpPort, 103 | srv.PackFile, 104 | srv.User, 105 | srv.Addr, 106 | srv.DeployTmpPath, 107 | ), 108 | }...) 109 | if srv.PreCmd != "" { 110 | cmds = append( 111 | cmds, 112 | fmt.Sprintf( 113 | "/usr/bin/env ssh -o StrictHostKeyChecking=no %s %s %s@%s '%s'", 114 | useCustomKey, 115 | useSshPort, 116 | srv.User, 117 | srv.Addr, 118 | srv.PreCmd, 119 | ), 120 | ) 121 | } 122 | packFileName := path.Base(srv.PackFile) 123 | cmds = append( 124 | cmds, 125 | fmt.Sprintf( 126 | "/usr/bin/env ssh -o StrictHostKeyChecking=no %s %s %s@%s 'cd %s; tar -zxf %s -C %s; rm -f %s'", 127 | useCustomKey, 128 | useSshPort, 129 | srv.User, 130 | srv.Addr, 131 | srv.DeployTmpPath, 132 | packFileName, 133 | srv.DeployPath, 134 | packFileName, 135 | ), 136 | ) 137 | if srv.PostCmd != "" { 138 | cmds = append( 139 | cmds, 140 | fmt.Sprintf("/usr/bin/env ssh -o StrictHostKeyChecking=no %s %s %s@%s '%s'", 141 | useCustomKey, 142 | useSshPort, 143 | srv.User, 144 | srv.Addr, 145 | srv.PostCmd, 146 | ), 147 | ) 148 | } 149 | return cmds 150 | } -------------------------------------------------------------------------------- /deploy/task.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package deploy 6 | 7 | import ( 8 | "sync" 9 | "errors" 10 | "fmt" 11 | ) 12 | 13 | type deployTask struct { 14 | deploys map[int][]*Deploy 15 | mu sync.Mutex 16 | } 17 | 18 | type CallbackFn func(int, int, int, []*ServerResult) 19 | 20 | type TaskCallbackFn func(int, int) 21 | 22 | var task = &deployTask{ 23 | deploys: map[int][]*Deploy{}, 24 | } 25 | 26 | func NewTask(id, mode int, deploys []*Deploy, startFn, finishFn CallbackFn, taskFn TaskCallbackFn) error { 27 | if exists := task.exists(id); exists { 28 | return errors.New(fmt.Sprintf("deploy task [id: %d] have exists", id)) 29 | } 30 | task.append(id, deploys) 31 | go func() { 32 | taskStatus := STATUS_DONE 33 | for _, deploy := range deploys { 34 | if startFn != nil { 35 | rest, status := deploy.Result() 36 | startFn(id, deploy.ID, status, rest) 37 | } 38 | switch mode { 39 | case DEPLOY_PARALLEL: 40 | deploy.Parallel() 41 | default: 42 | deploy.Serial() 43 | } 44 | resultList, status := deploy.Result() 45 | if finishFn != nil { 46 | finishFn(id, deploy.ID, status, resultList) 47 | } 48 | if status == STATUS_FAILED { 49 | taskStatus = STATUS_FAILED 50 | } 51 | } 52 | task.remove(id) 53 | if taskFn != nil { 54 | taskFn(id, taskStatus) 55 | } 56 | }() 57 | return nil 58 | } 59 | 60 | func StopTask(id int) { 61 | task.stop(id) 62 | } 63 | 64 | func ExistsTask(id int) bool { 65 | return task.exists(id) 66 | } 67 | 68 | func StatusTask(id int) []*DeployResult { 69 | deploys, exists := task.get(id) 70 | if !exists { 71 | return nil 72 | } 73 | rests := []*DeployResult{} 74 | for _, deploy := range deploys { 75 | rest, s := deploy.Result() 76 | rests = append(rests, &DeployResult{ 77 | ID: deploy.ID, 78 | Status: s, 79 | ServerRest: rest, 80 | }) 81 | } 82 | return rests 83 | } 84 | 85 | func (t *deployTask) exists(id int) bool { 86 | t.mu.Lock() 87 | defer t.mu.Unlock() 88 | _, exists := t.deploys[id] 89 | return exists 90 | } 91 | 92 | func (t *deployTask) append(id int, deploys []*Deploy) { 93 | t.mu.Lock() 94 | defer t.mu.Unlock() 95 | t.deploys[id] = deploys 96 | } 97 | 98 | func (t *deployTask) remove(id int) { 99 | t.mu.Lock() 100 | defer t.mu.Unlock() 101 | delete(t.deploys, id) 102 | } 103 | 104 | func (t *deployTask) get(id int) ([]*Deploy, bool) { 105 | t.mu.Lock() 106 | defer t.mu.Unlock() 107 | deploys, exists := t.deploys[id] 108 | return deploys, exists 109 | } 110 | 111 | func (t *deployTask) stop(id int) { 112 | t.mu.Lock() 113 | defer t.mu.Unlock() 114 | deploys, exists := t.deploys[id] 115 | if exists { 116 | for _, deploy := range deploys { 117 | deploy.Terminate() 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamans/syncd/71876dc6d3e7caa40e44a33ad47f8aa392881d40/docs/.nojekyll -------------------------------------------------------------------------------- /docs/2.0/.nojekyll: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/assets/css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: rgba(0,0,0,.87); 3 | } 4 | a { 5 | color: #409EFF; 6 | text-decoration: none; 7 | } 8 | a:hover { 9 | color: #66b1ff; 10 | } 11 | .bg-gray { 12 | background-color: #f4f4f4; 13 | } 14 | .bg-dark { 15 | background-color: #171f26; 16 | } 17 | .text-center { 18 | text-align: center; 19 | } 20 | .wrap { 21 | max-width: 1200px; 22 | margin: 0 auto; 23 | } 24 | .icon-large { 25 | color: #666; 26 | font-size: 36px; 27 | } 28 | .iconfont.left { 29 | margin-right: 6px; 30 | } 31 | .header-container { 32 | position: relative; 33 | padding: 20px 0; 34 | color: #fff; 35 | } 36 | .header-container .nav { 37 | color: #fff; 38 | display:flex; 39 | } 40 | .header-container .nav .logo img { 41 | height: 30px; 42 | } 43 | .header-container .nav .nav-list { 44 | display: flex; 45 | align-items: center; 46 | margin-left: 30px; 47 | } 48 | .header-container .nav-link { 49 | color: #fff; 50 | text-decoration: none; 51 | opacity: .7; 52 | padding: 0 15px; 53 | } 54 | .header-container .nav-link:hover { 55 | opacity: 1; 56 | color: #409EFF; 57 | } 58 | .header-container .nav-social { 59 | padding: 0 5px; 60 | } 61 | .header-container .banner { 62 | height: 450px; 63 | opacity: .7; 64 | overflow: hidden; 65 | } 66 | .header-container .banner .h { 67 | font-weight: 300; 68 | } 69 | .header-container .banner .tit { 70 | font-size: 50px; 71 | padding-top: 100px; 72 | } 73 | .header-container .banner .subtit { 74 | font-size: 20px; 75 | } 76 | .header-container .banner .start { 77 | padding: 60px 0; 78 | text-align: center; 79 | } 80 | .header-container .banner .start-link { 81 | background: #283542; 82 | padding: 15px 20px; 83 | margin: 0 10px; 84 | width: 90px; 85 | opacity: 1; 86 | } 87 | .header-container .banner .start-link.main{ 88 | background: #1989fa; 89 | } 90 | .header-container .banner .start-link:hover { 91 | color: #fff; 92 | background: #409EFF; 93 | } 94 | .section-container { 95 | padding: 36px 0; 96 | } 97 | .features-list { 98 | padding: 0; 99 | overflow: hidden; 100 | display: flex; 101 | justify-content: space-between; 102 | } 103 | .features-list:not(:first-child) { 104 | margin-top: 48px; 105 | } 106 | .features-list li { 107 | list-style: none; 108 | display: flex; 109 | flex-direction: column; 110 | align-items: center; 111 | width: 280px; 112 | } 113 | .features-list .desc { 114 | margin-top: 24px; 115 | } 116 | .features-tit { 117 | padding: 15px 0; 118 | font-size: 30px; 119 | position: relative; 120 | } 121 | .features-tit:after { 122 | content: ''; 123 | width: 50px; 124 | height: 2px; 125 | background-color: #333; 126 | position: absolute; 127 | bottom: 0; 128 | left: 50%; 129 | transform: translateX(-50%); 130 | } 131 | .quick-shell { 132 | padding: 12px; 133 | border-radius: 4px; 134 | background: #282a36; 135 | color: #fff; 136 | width: 720px; 137 | margin: 32px auto; 138 | } 139 | .quick-shell pre { 140 | margin: 0; 141 | padding: 0; 142 | line-height: 1.6; 143 | } 144 | .quick-internet { 145 | color: #fff; 146 | opacity: .8; 147 | } 148 | .license-text { 149 | width: 500px; 150 | line-height: 1.8; 151 | margin: 0 auto; 152 | } 153 | .license-text .logo-large { 154 | margin: 80px auto; 155 | } 156 | .footer-copyright p{ 157 | color: #a3a4a9; 158 | } 159 | .footer-copyright p { 160 | margin: 0; 161 | padding: 3px 0; 162 | } 163 | .footer-copyright a { 164 | color: #a3a4a9; 165 | } 166 | .footer-copyright a:hover { 167 | color: #fff; 168 | } -------------------------------------------------------------------------------- /docs/assets/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamans/syncd/71876dc6d3e7caa40e44a33ad47f8aa392881d40/docs/assets/img/favicon.ico -------------------------------------------------------------------------------- /docs/assets/img/gitee-logo-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | logo 5 | Created with Sketch. 6 | 7 | 8 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/assets/img/wechat-donate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamans/syncd/71876dc6d3e7caa40e44a33ad47f8aa392881d40/docs/assets/img/wechat-donate.png -------------------------------------------------------------------------------- /docs/docs/_404.md: -------------------------------------------------------------------------------- 1 | Coming Soon... -------------------------------------------------------------------------------- /docs/docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * 快速开始 2 | * [概述](/) 3 | * [运行原理](architecture.md) 4 | * [编译安装](install.md) 5 | * [服务配置](setting.md) 6 | * [获取帮助](help.md) 7 | * [安装问题列表](troubleinstall.md) 8 | * 使用方法 9 | * [项目配置](project.md) 10 | * [集群配置](server.md) 11 | * [构建配置](build.md) 12 | * [使用问题列表](troubleuse.md) 13 | * 其他 14 | * [捐赠](donate.md) -------------------------------------------------------------------------------- /docs/docs/architecture.md: -------------------------------------------------------------------------------- 1 | # 运行原理 2 | 3 | ## 原理 4 | 5 | Syncd通过 `git-ssh` 方式创建仓库本地副本,通过运行项目构建脚本来生成可发布的代码包,使用 `scp` 串行/并行地向生产服务器分发并解包部署。 6 | 7 | Syncd与生产服务器通过ssh交互,因此Syncd运行用户的 ssh-key 必须加入到目标主机的用户ssh-key信任列表中。 8 | 9 | 10 | 11 | ## Git 12 | 13 | Syncd目前仅支持Git仓库部署,并且通过 `git-ssh` 方式从仓库中拉取指定tag(分支)代码。 14 | 15 | ## 构建 16 | 17 | 部署过程中,在拉取git仓库到本地后会运行项目的自定义构建脚本,编译成可上线的软件包。 18 | 19 | 在这一环节中可运行: 20 | - 单元测试,如 `go test`, `php phpunit` 21 | - 下载依赖,如 `go: glide install`, `php: composer install` 22 | - 编译软件包,如 `js: npm build`, `go: go build xx.go`, `java: mvn ...` 23 | 24 | ## 分发 25 | 26 | 通过 `scp` 命令分发软件包到各机房生产服务器的临时目录, 远程执行 `pre-deploy` 配置的命令, 执行完毕后解压缩软件包到目标目录,然后执行 `post-deploy` 命令。 27 | 28 | 分发上线过程是集群之间串行执行,集群内部可串行可并行。 29 | 30 | ## SSH信任 31 | 32 | 生产服务器与部署服务器之间通过ssh-key建立信任,配置方法请参考 [秘钥配置](server.md?id=秘钥配置) 33 | 34 | [filename](include/footer.md ':include') -------------------------------------------------------------------------------- /docs/docs/assets/css/app.css: -------------------------------------------------------------------------------- 1 | .app-logo { 2 | margin: 50px auto 0; 3 | width: 300px; 4 | display: block; 5 | } 6 | .app-donate { 7 | width: 220px; 8 | border: 1px solid #dedede; 9 | padding: 8px; 10 | border-radius: 5px; 11 | } 12 | .app-donate .tit { 13 | text-align: center; 14 | display: block; 15 | padding: 10px 0; 16 | } 17 | .app-image { 18 | border: 1px solid #dedede; 19 | border-radius: 5px; 20 | } 21 | .app-img-eg { 22 | width: 800px; 23 | } 24 | .app-logo-img { 25 | width: 60%; 26 | } -------------------------------------------------------------------------------- /docs/docs/assets/img/syncd-member-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamans/syncd/71876dc6d3e7caa40e44a33ad47f8aa392881d40/docs/docs/assets/img/syncd-member-add.png -------------------------------------------------------------------------------- /docs/docs/assets/img/syncd-principle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamans/syncd/71876dc6d3e7caa40e44a33ad47f8aa392881d40/docs/docs/assets/img/syncd-principle.png -------------------------------------------------------------------------------- /docs/docs/assets/img/syncd-project-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamans/syncd/71876dc6d3e7caa40e44a33ad47f8aa392881d40/docs/docs/assets/img/syncd-project-add.png -------------------------------------------------------------------------------- /docs/docs/assets/img/syncd-server-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamans/syncd/71876dc6d3e7caa40e44a33ad47f8aa392881d40/docs/docs/assets/img/syncd-server-add.png -------------------------------------------------------------------------------- /docs/docs/assets/img/syncd-server-group-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamans/syncd/71876dc6d3e7caa40e44a33ad47f8aa392881d40/docs/docs/assets/img/syncd-server-group-add.png -------------------------------------------------------------------------------- /docs/docs/assets/img/syncd-space-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamans/syncd/71876dc6d3e7caa40e44a33ad47f8aa392881d40/docs/docs/assets/img/syncd-space-add.png -------------------------------------------------------------------------------- /docs/docs/build.md: -------------------------------------------------------------------------------- 1 | # 构建配置 2 | 3 | ## 配置说明 4 | 5 | 通过项目列表中的 `构建设置` 来编辑构建脚本。脚本需在上线单中手动触发,系统会使用 `/bin/bash -c {脚本文件}` 执行。 6 | 7 | ## 构建脚本支持的变量 8 | 9 | - **${env_workspace}** 10 | 11 | 代码仓库本地副本目录 12 | 13 | - **${env_pack_file}** 14 | 15 | 打包文件绝对地址,构建完成后将需要部署到线上的代码打包到此文件中,必须使用 `tar -zcf` 命令进行打包。 16 | 17 | 部署模块会将此压缩包分发到目标主机并解压缩到指定目录,请按照要求打包,否则会部署失败。 18 | 19 | ## 简单构建示例 20 | 21 | ```shell 22 | cd ${env_workspace} 23 | tar --exclude='.git' -zcvf ${env_pack_file} * 24 | ``` 25 | 26 | ## Laravel构建示例 27 | 28 | ```shell 29 | cd ${env_workspace} 30 | composer install 31 | tar --exclude='.git' -zcvf ${env_pack_file} * 32 | ``` 33 | 34 | ## 前端项目构建示例 35 | 36 | ```shell 37 | cd ${env_workspace} 38 | yarn 39 | yarn build 40 | cd ./dist 41 | tar -zcvf ${env_pack_file} * 42 | ``` 43 | 44 | ## Java项目构建示例 45 | 46 | 待补充 47 | 48 | ## Syncd构建示例 49 | 50 | ```shell 51 | cd ${env_workspace} 52 | make 53 | cd ./output 54 | tar -zcvf ${env_pack_file} * 55 | ``` 56 | 57 | [filename](include/footer.md ':include') -------------------------------------------------------------------------------- /docs/docs/donate.md: -------------------------------------------------------------------------------- 1 | # 捐赠我们 2 | 3 | Syncd 一直在致力于为企业和个人提供简洁高效的持续集成工具,您的帮助是对我们最大的支持和动力! 4 | 5 | ### 扫码捐赠 6 | 7 |
8 | 9 | 微信扫码捐赠 10 |
11 | 12 | ### 捐赠列表 13 | 14 | | Name | Date | Amount | Via | Note | 15 | | ------------ | ---------- | -------- | ------ | -------------------- | 16 | | *🍉 | 2020-03-28 | ¥50.00 | Wechat | | 17 | | W*l | 2020-03-10 | ¥666.00 | Wechat | | 18 | | lizhibo | 2019-04-11 | ¥68.88 | Wechat | | 19 | | k8svip | 2019-04-01 | ¥100.00 | Wechat | | 20 | 21 | [filename](include/footer.md ':include') 22 | -------------------------------------------------------------------------------- /docs/docs/help.md: -------------------------------------------------------------------------------- 1 | # 获取帮助 2 | 3 | Syncd使用交流QQ群①: 725302833 4 | 5 | [filename](include/footer.md ':include') -------------------------------------------------------------------------------- /docs/docs/home.md: -------------------------------------------------------------------------------- 1 | # Syncd - 自动化部署工具 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 欢迎使用 Syncd,本文档将帮助你快速上手。如果您在使用过程中遇到问题,请查看 [问题解答](troubleshooting.md) 中的解答,或者在 [GitHub Issue](https://github.com/dreamans/syncd/issues) 上提问。 12 | 13 | ## 什么是 Syncd? 14 | 15 | Syncd是一款开源的代码部署工具,它具有简单、高效、易用等特点,可以提高团队的工作效率。 16 | 17 | 码云GVP项目 18 | 19 | [![Fork me on Gitee](https://gitee.com/logo-black.svg)](https://gitee.com/dreamans/syncd) 20 | 21 | ## 特性 22 | 23 | - Go语言开发,编译简单、运行高效 24 | - Web界面访问,交互友好 25 | - 权限模型灵活自由 26 | - 支持自定义构建 27 | - 支持Git仓库 28 | - 支持分支、Tag上线 29 | - 部署Hook支持,可扩展性强 30 | - 完善的上线工作流 31 | - 邮件通知机制 32 | 33 | ## 项目地址 34 | 35 | Github: https://github.com/dreamans/syncd 36 | 37 | Gitee: https://gitee.com/dreamans/syncd 38 | 39 | ## 捐赠 40 | 41 | Syncd的发展离不开您的支持,[前往捐赠](donate.md) 42 | 43 | ## LICENSE 44 | 45 | 本项目采用 MIT 开源授权许可证,完整的授权说明已放置在 LICENSE 文件中。 46 | 47 | [filename](include/footer.md ':include') -------------------------------------------------------------------------------- /docs/docs/include/footer.md: -------------------------------------------------------------------------------- 1 | ---- 2 |

文档最后更新时间: {docsify-updated}

-------------------------------------------------------------------------------- /docs/docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 文档 | Syncd 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 |
22 | 35 | 36 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /docs/docs/install.md: -------------------------------------------------------------------------------- 1 | # 安装 2 | 3 | 安装 Syncd 只需几分钟时间,若您在安装过程中遇到问题或无法找到解决方式,请[提交Issue](https://github.com/dreamans/syncd/issues),我们会尽力解决您的问题。 4 | 5 | ## 环境需求 6 | 7 | **操作系统** 8 | 9 | Linux / macOS + Bash. 需要注意的是Syncd不支持Win系统。 10 | 11 | **Go 编译环境** 12 | 13 | Syncd依赖 `Go1.11+` 编译环境,可前往[官方网站](https://golang.org/dl/) 或 [国内镜像](https://golang.google.cn/dl/) 下载安装。 14 | 15 | **MySQL** 16 | 17 | MySQL 5.6+ 18 | 19 | **Git** 20 | 21 | 升级操作系统Git到最新版本。 22 | 23 | ## 编译Syncd 24 | 25 | 执行以下命令 26 | ```shell 27 | $ curl https://syncd.cc/install.sh | bash 28 | ``` 29 | 30 | 若编译程序未报错并且在当前目录中生成 `syncd-deploy` 子目录,则表示安装成功。 31 | 32 | syncd-deploy目录结构 33 | 34 | ```shell 35 | syncd-deploy // syncd-deploy可修改为任意其他目录名称 36 | ├── bin // bin目录存放Syncd的可执行文件 37 | │   └── syncd 38 | ├── etc // bin/syncd 程序运行时若不指定配置文件,则会在etc目录中查找syncd.ini作为默认配置 39 | │   └── syncd.ini 40 | ├── log 41 | ├── public // 静态资源目录 42 | ├── css 43 | ├── favicon.ico 44 | ├── fonts 45 | ├── img 46 | ├── index.html 47 | └── js 48 | └── resource // 资源目录 49 | └── sql 50 | ``` 51 | 52 | > 生成的 syncd-deploy 目录可拷贝或移动到你想要的地方,但不要试图将此目录拷贝到其他服务器上运行,会造成不可预料的结果。 53 | 54 | ## 导入数据库 55 | 56 | Syncd依赖的MySQL数据表结构存在于 `syncd-deploy/resource/sql/syncd_{版本号}.sql` 中 57 | 58 | ``` 59 | $ cd ./syncd-deploy 60 | $ mysql --default-character-set=utf8mb4 < ./resource/sql/syncd_{版本号}.sql # 导入MySQL表结构 61 | ``` 62 | 63 | ## 配置文件 64 | 65 | 配置文件位置: `syncd-deploy/etc/syncd.ini` 66 | 67 | 修改数据库连接信息(查看[配置项](setting.md)详细文档) 68 | 69 | ```ini 70 | [database] 71 | host = 127.0.0.1 72 | port = 3306 73 | user = syncd 74 | password = syncd 75 | dbname = syncd 76 | ``` 77 | 78 | ## 运行程序 79 | 80 | ```shell 81 | $ cd syncd-deploy 82 | $ ./bin/syncd 83 | 84 | _____ __ __ ____ _____ ____/ / 85 | / ___/ / / / / / __ \ / ___/ / __ / 86 | (__ ) / /_/ / / / / / / /__ / /_/ / 87 | /____/ \__, / /_/ /_/ \___/ \__,_/ 88 | /____/ 89 | 90 | Service: syncd 91 | Version: v2.0.0 92 | Config Loaded: ./etc/syncd.ini 93 | Log: stdout 94 | Mail Enable: 0 95 | HTTP Service: :8878 96 | Start Running... 97 | ``` 98 | 99 | 打开浏览器,访问 `http://localhost:8878` 100 | 101 | 初始账号: 102 | ``` 103 | 用户名: syncd 104 | 密码: 111111 105 | ``` 106 | **!!!登录后尽快修改默认密码** 107 | 108 | [filename](include/footer.md ':include') -------------------------------------------------------------------------------- /docs/docs/project.md: -------------------------------------------------------------------------------- 1 | # 项目配置 2 | 3 | ## 项目空间 4 | 5 | 项目空间是项目的基本组织单元,是进行项目和多用户隔离和访问控制的主要边界。 6 | 7 | 项目 -> 空间管理 -> 新增项目空间 8 | 9 | 10 | 11 | ## 项目管理 12 | 13 | 项目 -> 项目管理 -> [切换项目空间] -> 新增项目 14 | 15 | 16 | 17 | ## 成员管理 18 | 19 | 只有将用户添加到该项目空间,成为空间成员后才具有相应的权限。 20 | 21 | 项目 -> 成员管理 -> [切换项目空间] -> 添加新成员 22 | 23 | 在 `添加新成员` 输入框中输入待加入成员的关键词,选中用户后点击添加。 24 | 25 | 26 | 27 | [filename](include/footer.md ':include') -------------------------------------------------------------------------------- /docs/docs/server.md: -------------------------------------------------------------------------------- 1 | # 集群配置 2 | 3 | ## 添加集群 4 | 5 | 在创建项目前,需要先添加好项目所需的服务器集群,新建集群步骤如下: 6 | 7 | 服务器 -> 集群管理 -> 新增集群 8 | 9 | 按实际情况输入集群名称,如图: 10 | 11 | 12 | 13 | ## 添加服务器 14 | 15 | 在新建的集群中添加服务器信息,步骤如下: 16 | 17 | 服务器 -> 服务器管理 -> 新增服务器 18 | 19 | 20 | 21 | ## 秘钥配置 22 | 23 | 由于部署服务器(Syncd服务所在的服务器)与生产服务器(代码部署目标机)之间通过ssh协议通信,所以需要将部署服务器的公钥 (一般在这里: `$HOME/.ssh/id_rsa.pub`)加入到生产机的信任列表中(一般在这里 `$HOME/.ssh/authorized_keys`) 24 | 25 | 可使用 `ssh-copy-id` 命令添加,或手动拷贝。 26 | 27 | 设置完成后不要忘记进行测试连通性 `ssh {生产机用户名}@{生产机地址}` 28 | 29 | > 注意:部署服务器的$HOME必须与运行Syncd的用户一致,否则ssh的免密登录将无效。 30 | 31 | [filename](include/footer.md ':include') -------------------------------------------------------------------------------- /docs/docs/setting.md: -------------------------------------------------------------------------------- 1 | # 配置参考 2 | 3 | Syncd默认的配置文件位置为 `output/etc/syncd.ini`,也可以在运行时使用 `-c` 参数指定配置文件,如: 4 | 5 | ``` 6 | $ bin/syncd -c [path/syncd.ini] 7 | ``` 8 | 9 | ## 配置项说明 10 | 11 | ## syncd 12 | 13 | #### app_host 14 | 15 | - Type: `string` 16 | - Default: `http://localhost:8878` 17 | 18 | 项目访问域名, 结尾不要加 `/`,主要是在邮件中使用。 19 | 20 | #### local_space 21 | 22 | - Type: `string` 23 | - Default: `/tmp/syncd_data` 24 | 25 | 本地工作目录,Syncd所需的临时文件和编译打包归档文件都会在此目录中。 26 | 27 | #### remote_space 28 | 29 | - Type: `string` 30 | - Default: `~/.syncd` 31 | 32 | 目标主机临时工作目录,用来临时存放需要部署的打包文件。 33 | 34 | #### cipher_key 35 | 36 | - Type: `string` 37 | - Default: `pG1L62EM0cPIIOwusQsbcV8Cs6j/M1RxLoXIZylWUC4=` 38 | 39 | AES加密/解密使用的私钥,秘钥需要进行base64编码 40 | 41 | ## serve 42 | 43 | #### addr 44 | 45 | - Type: `string` 46 | - Default: `:8878` 47 | 48 | HTTP服务监听的端口,Syncd默认监听8878端口 49 | 50 | #### fe_serve_enable 51 | 52 | - Type: `int` 53 | - Default: `1` 54 | 55 | 是否开启前端资源服务,开启后Syncd前端资源将不再依赖nginx等web服务。 56 | 57 | 可选值: 58 | - 1 开启 59 | - 0 关闭 60 | 61 | #### read_timeout 62 | 63 | - Type: `int` 64 | - Default: `300` 65 | 66 | 读超时时间设置, 单位秒。 67 | 68 | #### write_timeout 69 | 70 | - Type: `int` 71 | - Default: `300` 72 | 73 | 写超时时间设置, 单位秒。 74 | 75 | 76 | #### idle_timeout 77 | 78 | - Type: `int` 79 | - Default: `300` 80 | 81 | 空闲连接超时设置, 单位秒。 82 | 83 | ## database 84 | 85 | #### unix 86 | 87 | - Type: `string` 88 | - Default: ` ` 89 | 90 | 以 Unix Socket 方式连接MySQL。 91 | 92 | #### max_idle_conns 93 | 94 | - Type: `int` 95 | - Default: `100` 96 | 97 | 最大空闲连接数。 98 | 99 | #### max_open_conns 100 | 101 | - Type: `int` 102 | - Default: `200` 103 | 104 | MySQL最大连接数。 105 | 106 | #### conn_max_life_time 107 | 108 | - Type: `int` 109 | - Default: `500` 110 | 111 | 最长连接生命周期,单位秒。 112 | 113 | #### host 114 | 115 | - Type: `string` 116 | - Default: `127.0.0.1` 117 | 118 | MySQL连接主机名。 119 | 120 | #### port 121 | 122 | - Type: `int` 123 | - Default: `3306` 124 | 125 | MySQL连接端口。 126 | 127 | #### user 128 | 129 | - Type: `string` 130 | - Default: `syncd` 131 | 132 | MySQL连接用户名。 133 | 134 | #### password 135 | 136 | - Type: `string` 137 | - Default: `syncd` 138 | 139 | MySQL连接密码。 140 | 141 | #### dbname 142 | 143 | - Type: `string` 144 | - Default: `syncd` 145 | 146 | Syncd数据库名。 147 | 148 | ## log 149 | 150 | #### path 151 | 152 | - Type: `string` 153 | - Default: `stdout` 154 | 155 | 错误日志输出路径。 156 | 157 | 可选的值: 158 | 159 | - stdout 打印到标准输出 160 | - /path/file 输出到文件 161 | 162 | ## mail 163 | 164 | #### enable 165 | 166 | - Type: `int` 167 | - Default: `0` 168 | 169 | 是否开启邮件发送功能。 170 | 171 | #### smtp_host 172 | 173 | #### smtp_port 174 | 175 | #### smtp_user 176 | 177 | #### smtp_pass 178 | 179 | [filename](include/footer.md ':include') -------------------------------------------------------------------------------- /docs/docs/troubleinstall.md: -------------------------------------------------------------------------------- 1 | # 安装问题列表 2 | 3 | [filename](include/footer.md ':include') -------------------------------------------------------------------------------- /docs/docs/troubleuse.md: -------------------------------------------------------------------------------- 1 | # 使用问题列表 2 | 3 | #### 部署失败:[cmd] $ echo 'packfile empty' && exit 1 X 4 | 5 | 原因:构建脚本中未打包出tgz文件,检查是否使用 `tar -zcf` 命令进行打包。 6 | 7 | [filename](include/footer.md ':include') -------------------------------------------------------------------------------- /docs/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | syncd_remote_repo=https://github.com/dreamans/syncd.git 4 | syncd_repo_path=$HOME/.syncd-repo 5 | syncd_install_path=$( cd `dirname $0`; pwd )/syncd-deploy 6 | 7 | checkCommand() { 8 | type $1 > /dev/null 2>&1 9 | if [ $? -ne 0 ]; then 10 | echo "error: $1 must be installed" 11 | echo "install exit" 12 | exit 1 13 | fi 14 | } 15 | 16 | checkCommand "go" 17 | checkCommand "git" 18 | checkCommand "make" 19 | 20 | if [ -d ${syncd_install_path} ];then 21 | syncd_install_path=${syncd_install_path}-$( date +%Y%m%d%H%M%S ) 22 | fi 23 | 24 | rm -fr ${syncd_repo_path} 25 | git clone ${syncd_remote_repo} ${syncd_repo_path} 26 | 27 | cd ${syncd_repo_path} 28 | make 29 | 30 | rm -fr ${syncd_install_path} 31 | cp -r ${syncd_repo_path}/output ${syncd_install_path} 32 | 33 | rm -fr ${syncd_repo_path} 34 | 35 | cat << EOF 36 | 37 | Installing Syncd Path: ${syncd_install_path} 38 | Install complete. 39 | 40 | EOF -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dreamans/syncd 2 | 3 | go 1.16 4 | 5 | require ( 6 | cloud.google.com/go v0.36.0 // indirect 7 | github.com/Unknwon/goconfig v0.0.0-20181105214110-56bd8ab18619 8 | github.com/denisenkom/go-mssqldb v0.0.0-20190204142019-df6d76eb9289 // indirect 9 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect 10 | github.com/gin-gonic/gin v1.4.0 11 | github.com/go-sql-driver/mysql v1.4.1 12 | github.com/gofrs/uuid v3.2.0+incompatible // indirect 13 | github.com/jinzhu/gorm v1.9.2 14 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect 15 | github.com/jinzhu/now v1.0.0 // indirect 16 | github.com/lib/pq v1.0.0 // indirect 17 | github.com/mattn/go-sqlite3 v1.10.0 // indirect 18 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 19 | github.com/modern-go/reflect2 v1.0.1 // indirect 20 | github.com/smartystreets/goconvey v0.0.0-20190222223459-a17d461953aa // indirect 21 | github.com/stretchr/testify v1.3.0 // indirect 22 | github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 // indirect 23 | google.golang.org/appengine v1.4.0 // indirect 24 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect 25 | gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 26 | gopkg.in/go-playground/validator.v8 v8.18.2 // indirect 27 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df 28 | gopkg.in/yaml.v2 v2.2.2 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /model/deploy_apply.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package model 6 | 7 | import( 8 | "time" 9 | ) 10 | 11 | type DeployApply struct { 12 | ID int `gorm:"primary_key"` 13 | SpaceId int `gorm:"type:int(11);not null;default:0"` 14 | ProjectId int `gorm:"type:int(11);not null;default:0"` 15 | Name string `gorm:"type:varchar(100);not null;default:''"` 16 | Description string `gorm:"type:varchar(500);not null;default:''"` 17 | BranchName string `gorm:"type:varchar(100);not null;default:''"` 18 | CommitVersion string `gorm:"type:varchar(50);not null;default:''"` 19 | AuditStatus int `gorm:"type:int(11);not null;default:0"` 20 | AuditRefusalReasion string `gorm:"type:varchar(500);not null;default:''"` 21 | Status int `gorm:"type:int(11);not null;default:0"` 22 | UserId int `gorm:"type:int(11);not null;default:0"` 23 | RollbackId int `gorm:"type:int(11);not null;default:0"` 24 | RollbackApplyId int `gorm:"type:int(11);not null;default:0"` 25 | IsRollbackApply int `gorm:"type:int(11);not null;default:0"` 26 | Ctime int `gorm:"type:int(11);not null;default:0"` 27 | } 28 | 29 | func (m *DeployApply) TableName() string { 30 | return "syd_deploy_apply" 31 | } 32 | 33 | func (m *DeployApply) Create() bool { 34 | m.Ctime = int(time.Now().Unix()) 35 | return Create(m) 36 | } 37 | 38 | func (m *DeployApply) Update() bool { 39 | return UpdateByPk(m) 40 | } 41 | 42 | func (m *DeployApply) UpdateByFields(data map[string]interface{}, query QueryParam) bool { 43 | return Update(m, data, query) 44 | } 45 | 46 | func (m *DeployApply) List(query QueryParam) ([]DeployApply, bool) { 47 | var data []DeployApply 48 | ok := GetMulti(&data, query) 49 | return data, ok 50 | } 51 | 52 | func (m *DeployApply) Count(query QueryParam) (int, bool) { 53 | var count int 54 | ok := Count(m, &count, query) 55 | return count, ok 56 | } 57 | 58 | func (m *DeployApply) Delete() bool { 59 | return DeleteByPk(m) 60 | } 61 | 62 | func (m *DeployApply) Get(id int) bool { 63 | return GetByPk(m, id) 64 | } 65 | -------------------------------------------------------------------------------- /model/deploy_build.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package model 6 | 7 | import( 8 | "time" 9 | ) 10 | 11 | type DeployBuild struct { 12 | ID int `gorm:"primary_key"` 13 | ApplyId int `gorm:"type:int(11);not null;default:0"` 14 | StartTime int `gorm:"type:int(11);not null;default:0"` 15 | FinishTime int `gorm:"type:int(11);not null;default:0"` 16 | Status int `gorm:"type:int(11);not null;default:0"` 17 | Tar string `gorm:"type:varchar(2000);not null;default:''"` 18 | Output string `gorm:"type:text;not null"` 19 | Errmsg string `gorm:"type:text;not null"` 20 | Ctime int `gorm:"type:int(11);not null;default:0"` 21 | } 22 | 23 | func (m *DeployBuild) TableName() string { 24 | return "syd_deploy_build" 25 | } 26 | 27 | func (m *DeployBuild) GetByApplyId(id int) bool { 28 | return GetOne(m, QueryParam{ 29 | Where: []WhereParam{ 30 | WhereParam{ 31 | Field: "apply_id", 32 | Prepare: id, 33 | }, 34 | }, 35 | }) 36 | } 37 | 38 | func (m *DeployBuild) UpdateByFields(data map[string]interface{}, query QueryParam) bool { 39 | return Update(m, data, query) 40 | } 41 | 42 | func (m *DeployBuild) Create() bool { 43 | m.Ctime = int(time.Now().Unix()) 44 | return Create(m) 45 | } 46 | 47 | func (m *DeployBuild) Delete() bool { 48 | return DeleteByPk(m) 49 | } 50 | -------------------------------------------------------------------------------- /model/deploy_task.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package model 6 | 7 | import( 8 | "time" 9 | ) 10 | 11 | type DeployTask struct { 12 | ID int `gorm:"primary_key"` 13 | ApplyId int `gorm:"type:int(11);not null;default:0"` 14 | GroupId int `gorm:"type:int(11);not null;default:0"` 15 | Status int `gorm:"type:int(11);not null;default:0"` 16 | Content string `gorm:"type:text;not null"` 17 | Ctime int `gorm:"type:int(11);not null;default:0"` 18 | } 19 | 20 | func (m *DeployTask) TableName() string { 21 | return "syd_deploy_task" 22 | } 23 | 24 | func (m *DeployTask) List(query QueryParam) ([]DeployTask, bool) { 25 | var data []DeployTask 26 | ok := GetMulti(&data, query) 27 | return data, ok 28 | } 29 | 30 | func (m *DeployTask) GetByApplyId(id int) bool { 31 | return GetOne(m, QueryParam{ 32 | Where: []WhereParam{ 33 | WhereParam{ 34 | Field: "apply_id", 35 | Prepare: id, 36 | }, 37 | }, 38 | }) 39 | } 40 | 41 | func (m *DeployTask) UpdateByFields(data map[string]interface{}, query QueryParam) bool { 42 | return Update(m, data, query) 43 | } 44 | 45 | func (m *DeployTask) Create() bool { 46 | m.Ctime = int(time.Now().Unix()) 47 | return Create(m) 48 | } 49 | 50 | func (m *DeployTask) Delete(query QueryParam) bool { 51 | return Delete(m, query) 52 | } 53 | -------------------------------------------------------------------------------- /model/model.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package model 6 | 7 | import ( 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/jinzhu/gorm" 12 | "github.com/dreamans/syncd" 13 | ) 14 | 15 | type WhereParam struct { 16 | Field string 17 | Tag string 18 | Prepare interface{} 19 | } 20 | 21 | type QueryParam struct { 22 | Fields string 23 | Offset int 24 | Limit int 25 | Order string 26 | Where []WhereParam 27 | } 28 | 29 | func Create(model interface{}) bool { 30 | db := syncd.App.DB.DbHandler.Create(model) 31 | if err := db.Error; err != nil { 32 | syncd.App.Logger.Warning("mysql execute error: %s, sql [%v]", err.Error(), db.QueryExpr()) 33 | return false 34 | } 35 | return true 36 | } 37 | 38 | func GetMulti(model interface{}, query QueryParam) bool { 39 | db := syncd.App.DB.DbHandler.Offset(query.Offset) 40 | if query.Limit > 0 { 41 | db = db.Limit(query.Limit) 42 | } 43 | if query.Fields != "" { 44 | db = db.Select(query.Fields) 45 | } 46 | if query.Order != "" { 47 | db = db.Order(query.Order) 48 | } 49 | db = parseWhereParam(db, query.Where) 50 | db.Find(model) 51 | if err := db.Error; err != nil { 52 | syncd.App.Logger.Warning("mysql query error: %s, sql[%v]", err.Error(), db.QueryExpr()) 53 | return false 54 | } 55 | return true 56 | } 57 | 58 | func Count(model interface{}, count *int, query QueryParam) bool { 59 | db := syncd.App.DB.DbHandler.Model(model) 60 | db = parseWhereParam(db, query.Where) 61 | db = db.Count(count) 62 | if err := db.Error; err != nil { 63 | syncd.App.Logger.Warning("mysql query error: %s, sql[%v]", err.Error(), db.QueryExpr()) 64 | return false 65 | } 66 | return true 67 | } 68 | 69 | func Delete(model interface{}, query QueryParam) bool { 70 | if len(query.Where) == 0 { 71 | syncd.App.Logger.Warning("mysql query error: delete failed, where conditions cannot be empty") 72 | return false 73 | } 74 | db := syncd.App.DB.DbHandler.Model(model) 75 | db = parseWhereParam(db, query.Where) 76 | db = db.Delete(model) 77 | if err := db.Error; err != nil { 78 | syncd.App.Logger.Warning("mysql query error: %s, sql[%v]", err.Error(), db.QueryExpr()) 79 | return false 80 | } 81 | return true 82 | } 83 | 84 | func DeleteByPk(model interface{}) bool { 85 | db := syncd.App.DB.DbHandler.Model(model) 86 | db.Delete(model) 87 | if err := db.Error; err != nil { 88 | syncd.App.Logger.Warning("mysql query error: %s, sql[%v]", err.Error(), db.QueryExpr()) 89 | return false 90 | } 91 | return true 92 | } 93 | 94 | func GetOne(model interface{}, query QueryParam) bool { 95 | db := syncd.App.DB.DbHandler.Model(model) 96 | if query.Fields != "" { 97 | db = db.Select(query.Fields) 98 | } 99 | db = parseWhereParam(db, query.Where) 100 | db = db.First(model) 101 | if err := db.Error; err != nil && !db.RecordNotFound() { 102 | syncd.App.Logger.Warning("mysql query error: %s, sql[%v]", err.Error(), db.QueryExpr()) 103 | return false 104 | } 105 | return true 106 | } 107 | 108 | func GetByPk(model interface{}, id interface{}) bool { 109 | db := syncd.App.DB.DbHandler.Model(model) 110 | db.First(model, id) 111 | if err := db.Error; err != nil && !db.RecordNotFound() { 112 | syncd.App.Logger.Warning("mysql query error: %s sql[%v]", err.Error(), db.QueryExpr()) 113 | return false 114 | } 115 | return true 116 | } 117 | 118 | func UpdateByPk(model interface{}) bool { 119 | db := syncd.App.DB.DbHandler.Model(model) 120 | db = db.Updates(model) 121 | if err := db.Error; err != nil { 122 | syncd.App.Logger.Warning("mysql query error: %s, sql[%v]", err.Error(), db.QueryExpr()) 123 | return false 124 | } 125 | return true 126 | } 127 | 128 | func Update(model interface{}, data interface{}, query QueryParam) bool { 129 | db := syncd.App.DB.DbHandler.Model(model) 130 | db = parseWhereParam(db, query.Where) 131 | db = db.Updates(data) 132 | if err := db.Error; err != nil { 133 | syncd.App.Logger.Warning("mysql query error: %s, sql[%v]", err.Error(), db.QueryExpr()) 134 | return false 135 | } 136 | return true 137 | } 138 | 139 | func parseWhereParam(db *gorm.DB, where []WhereParam) *gorm.DB { 140 | if len(where) == 0 { 141 | return db 142 | } 143 | var ( 144 | plain []string 145 | prepare []interface{} 146 | ) 147 | for _, w := range where { 148 | tag := w.Tag 149 | if tag == "" { 150 | tag = "=" 151 | } 152 | var plainFmt string 153 | switch tag { 154 | case "IN": 155 | plainFmt = fmt.Sprintf("%s IN (?)", w.Field) 156 | default: 157 | plainFmt = fmt.Sprintf("%s %s ?", w.Field, tag) 158 | } 159 | plain = append(plain, plainFmt) 160 | prepare = append(prepare, w.Prepare) 161 | } 162 | return db.Where(strings.Join(plain, " AND "), prepare...) 163 | } 164 | -------------------------------------------------------------------------------- /model/project.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package model 6 | 7 | import( 8 | "time" 9 | ) 10 | 11 | type Project struct { 12 | ID int `gorm:"primary_key"` 13 | SpaceId int `gorm:"type:int(11);not null;default:0"` 14 | Name string `gorm:"type:varchar(100);not null;default:''"` 15 | Description string `gorm:"type:varchar(500);not null;default:''"` 16 | NeedAudit int `gorm:"type:int(11);not null;default:0"` 17 | Status int `gorm:"type:int(11);not null;default:0"` 18 | RepoUrl string `gorm:"type:varchar(500);not null;default:''"` 19 | DeployMode int `gorm:"type:int(11);not null;default:0"` 20 | RepoBranch string `gorm:"type:varchar(100);not null;default:''"` 21 | OnlineCluster string `gorm:"type:varchar(1000);not null;default:''"` 22 | DeployUser string `gorm:"type:varchar(100);not null;default:''"` 23 | DeployPath string `gorm:"type:varchar(500);not null;default:''"` 24 | BuildScript string `gorm:"type:text;not null"` 25 | BuildHookScript string `gorm:"type:text;not null"` 26 | DeployHookScript string `gorm:"type:text;not null"` 27 | PreDeployCmd string `gorm:"type:text;not null"` 28 | AfterDeployCmd string `gorm:"type:text;not null"` 29 | AuditNotice string `gorm:"type:varchar(2000);not null;default:''"` 30 | DeployNotice string `gorm:"type:varchar(2000);not null;default:''"` 31 | Ctime int `gorm:"type:int(11);not null;default:0"` 32 | } 33 | 34 | func (m *Project) TableName() string { 35 | return "syd_project" 36 | } 37 | 38 | func (m *Project) Create() bool { 39 | m.Ctime = int(time.Now().Unix()) 40 | return Create(m) 41 | } 42 | 43 | func (m *Project) Update() bool { 44 | return UpdateByPk(m) 45 | } 46 | 47 | func (m *Project) UpdateByFields(data map[string]interface{}, query QueryParam) bool { 48 | return Update(m, data, query) 49 | } 50 | 51 | func (m *Project) List(query QueryParam) ([]Project, bool) { 52 | var data []Project 53 | ok := GetMulti(&data, query) 54 | return data, ok 55 | } 56 | 57 | func (m *Project) Count(query QueryParam) (int, bool) { 58 | var count int 59 | ok := Count(m, &count, query) 60 | return count, ok 61 | } 62 | 63 | func (m *Project) Delete() bool { 64 | return DeleteByPk(m) 65 | } 66 | 67 | func (m *Project) Get(id int) bool { 68 | return GetByPk(m, id) 69 | } 70 | -------------------------------------------------------------------------------- /model/project_member.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package model 6 | 7 | import( 8 | "time" 9 | ) 10 | 11 | type ProjectMember struct { 12 | ID int `gorm:"primary_key"` 13 | SpaceId int `gorm:"type:int(11);not null;default:0"` 14 | UserId int `gorm:"type:int(11);not null;default:0"` 15 | Ctime int `gorm:"type:int(11);not null;default:0"` 16 | } 17 | 18 | func (m *ProjectMember) TableName() string { 19 | return "syd_project_member" 20 | } 21 | 22 | func (m *ProjectMember) Create() bool { 23 | m.Ctime = int(time.Now().Unix()) 24 | return Create(m) 25 | } 26 | 27 | func (m *ProjectMember) Update() bool { 28 | return UpdateByPk(m) 29 | } 30 | 31 | func (m *ProjectMember) List(query QueryParam) ([]ProjectMember, bool) { 32 | var data []ProjectMember 33 | ok := GetMulti(&data, query) 34 | return data, ok 35 | } 36 | 37 | func (m *ProjectMember) Count(query QueryParam) (int, bool) { 38 | var count int 39 | ok := Count(m, &count, query) 40 | return count, ok 41 | } 42 | 43 | func (m *ProjectMember) Delete() bool { 44 | return DeleteByPk(m) 45 | } 46 | 47 | func (m *ProjectMember) Get(id int) bool { 48 | return GetByPk(m, id) 49 | } 50 | -------------------------------------------------------------------------------- /model/project_space.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package model 6 | 7 | import( 8 | "time" 9 | ) 10 | 11 | type ProjectSpace struct { 12 | ID int `gorm:"primary_key"` 13 | Name string `gorm:"type:varchar(100);not null;default:''"` 14 | Description string `gorm:"type:varchar(2000);not null;default:''"` 15 | Ctime int `gorm:"type:int(11);not null;default:0"` 16 | } 17 | 18 | func (m *ProjectSpace) TableName() string { 19 | return "syd_project_space" 20 | } 21 | 22 | func (m *ProjectSpace) Create() bool { 23 | m.Ctime = int(time.Now().Unix()) 24 | return Create(m) 25 | } 26 | 27 | func (m *ProjectSpace) Update() bool { 28 | return UpdateByPk(m) 29 | } 30 | 31 | func (m *ProjectSpace) List(query QueryParam) ([]ProjectSpace, bool) { 32 | var data []ProjectSpace 33 | ok := GetMulti(&data, query) 34 | return data, ok 35 | } 36 | 37 | func (m *ProjectSpace) Count(query QueryParam) (int, bool) { 38 | var count int 39 | ok := Count(m, &count, query) 40 | return count, ok 41 | } 42 | 43 | func (m *ProjectSpace) Delete() bool { 44 | return DeleteByPk(m) 45 | } 46 | 47 | func (m *ProjectSpace) Get(id int) bool { 48 | return GetByPk(m, id) 49 | } 50 | -------------------------------------------------------------------------------- /model/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package model 6 | 7 | import( 8 | "time" 9 | ) 10 | 11 | type Server struct { 12 | ID int `gorm:"primary_key"` 13 | GroupId int `gorm:"type:int(11);not null;default:0"` 14 | Name string `gorm:"type:varchar(100);not null;default:''"` 15 | Ip string `gorm:"type:varchar(100);not null;default:''"` 16 | SSHPort int `gorm:"type:int(11);not null;default:0"` 17 | Ctime int `gorm:"type:int(11);not null;default:0"` 18 | } 19 | 20 | func (m *Server) TableName() string { 21 | return "syd_server" 22 | } 23 | 24 | func (m *Server) Create() bool { 25 | m.Ctime = int(time.Now().Unix()) 26 | return Create(m) 27 | } 28 | 29 | func (m *Server) Update() bool { 30 | return UpdateByPk(m) 31 | } 32 | 33 | func (m *Server) List(query QueryParam) ([]Server, bool) { 34 | var data []Server 35 | ok := GetMulti(&data, query) 36 | return data, ok 37 | } 38 | 39 | func (m *Server) Count(query QueryParam) (int, bool) { 40 | var count int 41 | ok := Count(m, &count, query) 42 | return count, ok 43 | } 44 | 45 | func (m *Server) Delete() bool { 46 | return DeleteByPk(m) 47 | } 48 | 49 | func (m *Server) Get(id int) bool { 50 | return GetByPk(m, id) 51 | } 52 | -------------------------------------------------------------------------------- /model/server_group.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package model 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | type ServerGroup struct { 12 | ID int `gorm:"primary_key"` 13 | Name string `gorm:"type:varchar(100);not null;default:''"` 14 | Ctime int `gorm:"type:int(11);not null;default:0"` 15 | } 16 | 17 | func (m *ServerGroup) TableName() string { 18 | return "syd_server_group" 19 | } 20 | 21 | func (m *ServerGroup) Create() bool { 22 | m.Ctime = int(time.Now().Unix()) 23 | return Create(m) 24 | } 25 | 26 | func (m *ServerGroup) Update() bool { 27 | return UpdateByPk(m) 28 | } 29 | 30 | func (m *ServerGroup) List(query QueryParam) ([]ServerGroup, bool) { 31 | var data []ServerGroup 32 | ok := GetMulti(&data, query) 33 | return data, ok 34 | } 35 | 36 | func (m *ServerGroup) Count(query QueryParam) (int, bool) { 37 | var count int 38 | ok := Count(m, &count, query) 39 | return count, ok 40 | } 41 | 42 | func (m *ServerGroup) Delete() bool { 43 | return DeleteByPk(m) 44 | } 45 | 46 | func (m *ServerGroup) Get(id int) bool { 47 | return GetByPk(m, id) 48 | } 49 | -------------------------------------------------------------------------------- /model/user.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package model 6 | 7 | import( 8 | "time" 9 | ) 10 | 11 | type User struct { 12 | ID int `gorm:"primary_key"` 13 | RoleId int `gorm:"type:int(11);not null;default:0"` 14 | Username string `gorm:"type:varchar(20);not null;default:''"` 15 | Password string `gorm:"type:char(32);not null;default:''"` 16 | Salt string `gorm:"type:char(10);not null;default:''"` 17 | Truename string `gorm:"type:varchar(20);not null;default:''"` 18 | Mobile string `gorm:"type:varchar(20);not null;default:''"` 19 | Email string `gorm:"type:varchar(500);not null;default:''"` 20 | Status int `gorm:"type:int(11);not null;default:0"` 21 | LastLoginTime int `gorm:"type:int(11);not null;default:0"` 22 | LastLoginIp string `gorm:"type:varchar(50);not null;default:''"` 23 | Ctime int `gorm:"type:int(11);not null;default:0"` 24 | } 25 | 26 | func (m *User) TableName() string { 27 | return "syd_user" 28 | } 29 | 30 | func (m *User) Create() bool { 31 | m.Ctime = int(time.Now().Unix()) 32 | return Create(m) 33 | } 34 | 35 | func (m *User) UpdateByFields(data map[string]interface{}, query QueryParam) bool { 36 | return Update(m, data, query) 37 | } 38 | 39 | func (m *User) List(query QueryParam) ([]User, bool) { 40 | var data []User 41 | ok := GetMulti(&data, query) 42 | return data, ok 43 | } 44 | 45 | func (m *User) Count(query QueryParam) (int, bool) { 46 | var count int 47 | ok := Count(m, &count, query) 48 | return count, ok 49 | } 50 | 51 | func (m *User) Delete() bool { 52 | return DeleteByPk(m) 53 | } 54 | 55 | func (m *User) Get(id int) bool { 56 | return GetByPk(m, id) 57 | } 58 | 59 | func (m *User) GetOne(query QueryParam) bool { 60 | return GetOne(m, query) 61 | } 62 | -------------------------------------------------------------------------------- /model/user_role.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package model 6 | 7 | import( 8 | "time" 9 | ) 10 | 11 | type UserRole struct { 12 | ID int `gorm:"primary_key"` 13 | Name string `gorm:"type:varchar(100);not null;default:''"` 14 | Privilege string `gorm:"type:varchar(2000);not null;default:''"` 15 | Ctime int `gorm:"type:int(11);not null;default:0"` 16 | } 17 | 18 | func (m *UserRole) TableName() string { 19 | return "syd_user_role" 20 | } 21 | 22 | func (m *UserRole) Create() bool { 23 | m.Ctime = int(time.Now().Unix()) 24 | return Create(m) 25 | } 26 | 27 | func (m *UserRole) Update() bool { 28 | return UpdateByPk(m) 29 | } 30 | 31 | func (m *UserRole) List(query QueryParam) ([]UserRole, bool) { 32 | var data []UserRole 33 | ok := GetMulti(&data, query) 34 | return data, ok 35 | } 36 | 37 | func (m *UserRole) Count(query QueryParam) (int, bool) { 38 | var count int 39 | ok := Count(m, &count, query) 40 | return count, ok 41 | } 42 | 43 | func (m *UserRole) Delete() bool { 44 | return DeleteByPk(m) 45 | } 46 | 47 | func (m *UserRole) Get(id int) bool { 48 | return GetByPk(m, id) 49 | } 50 | -------------------------------------------------------------------------------- /model/user_token.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package model 6 | 7 | import( 8 | "time" 9 | ) 10 | 11 | type UserToken struct { 12 | ID int `gorm:"primary_key"` 13 | UserId int `gorm:"type:int(11);not null;default:0"` 14 | Token string `gorm:"type:varchar(100);not null;default:''"` 15 | Expire int `gorm:"type:int(11);not null;default:0"` 16 | Ctime int `gorm:"type:int(11);not null;default:0"` 17 | } 18 | 19 | func (m *UserToken) TableName() string { 20 | return "syd_user_token" 21 | } 22 | 23 | func (m *UserToken) Create() bool { 24 | m.Ctime = int(time.Now().Unix()) 25 | return Create(m) 26 | } 27 | 28 | func (m *UserToken) UpdateByFields(data map[string]interface{}, query QueryParam) bool { 29 | return Update(m, data, query) 30 | } 31 | 32 | func (m *UserToken) Update() bool { 33 | return UpdateByPk(m) 34 | } 35 | 36 | func (m *UserToken) List(query QueryParam) ([]UserToken, bool) { 37 | var data []UserToken 38 | ok := GetMulti(&data, query) 39 | return data, ok 40 | } 41 | 42 | func (m *UserToken) Count(query QueryParam) (int, bool) { 43 | var count int 44 | ok := Count(m, &count, query) 45 | return count, ok 46 | } 47 | 48 | func (m *UserToken) Delete() bool { 49 | return DeleteByPk(m) 50 | } 51 | 52 | func (m *UserToken) DeleteByFields(query QueryParam) bool { 53 | return Delete(m, query) 54 | } 55 | 56 | func (m *UserToken) Get(id int) bool { 57 | return GetByPk(m, id) 58 | } 59 | 60 | func (m *UserToken) GetOne(query QueryParam) bool { 61 | return GetOne(m, query) 62 | } 63 | -------------------------------------------------------------------------------- /module/deploy/build.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package deploy 6 | 7 | import ( 8 | "errors" 9 | "time" 10 | 11 | "github.com/dreamans/syncd/model" 12 | ) 13 | 14 | type Build struct{ 15 | ID int `json:"id"` 16 | ApplyId int `json:"apply_id"` 17 | StartTime int `json:"start_time"` 18 | FinishTime int `json:"finish_time"` 19 | Status int `json:"status"` 20 | Tar string `json:"tar"` 21 | Output string `json:"Output"` 22 | Errmsg string `json:"errmsg"` 23 | Ctime int `json:"ctime"` 24 | } 25 | 26 | const ( 27 | BUILD_STATUS_NONE = 0 28 | BUILD_STATUS_START = 1 29 | BUILD_STATUS_SUCCESS = 2 30 | BUILD_STATUS_FAILED = 3 31 | ) 32 | 33 | func (b *Build) Create() error { 34 | build := &model.DeployBuild{ 35 | ApplyId: b.ApplyId, 36 | Status: b.Status, 37 | StartTime: int(time.Now().Unix()), 38 | } 39 | if ok := build.Create(); !ok { 40 | return errors.New("create deploy build failed") 41 | } 42 | return nil 43 | } 44 | 45 | func (b *Build) CreateFull() error { 46 | build := &model.DeployBuild{ 47 | ApplyId: b.ApplyId, 48 | StartTime: b.StartTime, 49 | FinishTime: b.FinishTime, 50 | Status: b.Status, 51 | Tar: b.Tar, 52 | Output: b.Output, 53 | Errmsg: b.Errmsg, 54 | } 55 | if ok := build.Create(); !ok { 56 | return errors.New("create deploy build failed") 57 | } 58 | return nil 59 | } 60 | 61 | func (b *Build) Detail() error { 62 | build := &model.DeployBuild{} 63 | if ok := build.GetByApplyId(b.ApplyId); !ok { 64 | return errors.New("get deploy build detail failed") 65 | } 66 | if build.ID == 0 { 67 | build.Status = BUILD_STATUS_NONE 68 | return nil 69 | } 70 | b.ID = build.ID 71 | b.Status = build.Status 72 | b.StartTime = build.StartTime 73 | b.FinishTime = build.FinishTime 74 | b.Tar = build.Tar 75 | b.Output = build.Output 76 | b.Errmsg = build.Errmsg 77 | b.Ctime = build.Ctime 78 | 79 | return nil 80 | } 81 | 82 | func (b *Build) Exists() (bool, error) { 83 | if err := b.Detail(); err != nil { 84 | return false, err 85 | } 86 | if b.ID == 0 { 87 | return false, nil 88 | } 89 | return true, nil 90 | } 91 | 92 | func (b *Build) Finish() error { 93 | build := &model.DeployBuild{} 94 | updateData := map[string]interface{}{ 95 | "status": b.Status, 96 | "tar": b.Tar, 97 | "output": b.Output, 98 | "finish_time": int(time.Now().Unix()), 99 | "errmsg": b.Errmsg, 100 | } 101 | if ok := build.UpdateByFields(updateData, model.QueryParam{ 102 | Where:[]model.WhereParam{ 103 | model.WhereParam{ 104 | Field: "apply_id", 105 | Prepare: b.ApplyId, 106 | }, 107 | }, 108 | }); !ok { 109 | return errors.New("update deploy build failed") 110 | } 111 | return nil 112 | } 113 | 114 | func (b *Build) Delete() error { 115 | build := &model.DeployBuild{ 116 | ID: b.ID, 117 | } 118 | if ok := build.Delete(); !ok { 119 | return errors.New("remove deploy build failed") 120 | } 121 | return nil 122 | } 123 | -------------------------------------------------------------------------------- /module/deploy/deploy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package deploy 6 | 7 | import ( 8 | "errors" 9 | 10 | "github.com/dreamans/syncd/model" 11 | ) 12 | 13 | type Deploy struct{ 14 | ID int `json:"id"` 15 | ApplyId int `json:"apply_id"` 16 | GroupId int `json:"group_id"` 17 | Status int `json:"status"` 18 | Content string `json:"content"` 19 | Ctime int `json:"ctime"` 20 | } 21 | 22 | const ( 23 | DEPLOY_STATUS_NONE = 0 24 | DEPLOY_STATUS_START = 1 25 | DEPLOY_STATUS_SUCCESS = 2 26 | DEPLOY_STATUS_FAILED = 3 27 | ) 28 | 29 | func (d *Deploy) TaskList() ([]Deploy, error) { 30 | dt := &model.DeployTask{} 31 | list, ok := dt.List(model.QueryParam{ 32 | Fields: "id, apply_id, group_id, status, content, ctime", 33 | Where: []model.WhereParam{ 34 | model.WhereParam{ 35 | Field: "apply_id", 36 | Prepare: d.ApplyId, 37 | }, 38 | }, 39 | }) 40 | if !ok { 41 | return nil, errors.New("get deploy task list failed") 42 | } 43 | var ( 44 | deployList []Deploy 45 | ) 46 | for _, l := range list { 47 | deployList = append(deployList, Deploy{ 48 | ID: l.ID, 49 | ApplyId: l.ApplyId, 50 | GroupId: l.GroupId, 51 | Status: l.Status, 52 | Content: l.Content, 53 | Ctime: l.Ctime, 54 | }) 55 | } 56 | return deployList, nil 57 | } 58 | 59 | func (d *Deploy) Create() error { 60 | deploy := model.DeployTask{ 61 | ApplyId: d.ApplyId, 62 | GroupId: d.GroupId, 63 | Status: d.Status, 64 | } 65 | if ok := deploy.Create(); !ok { 66 | return errors.New("create deploy task failed") 67 | } 68 | return nil 69 | } 70 | 71 | func (d *Deploy) UpdateStatus() error { 72 | dt := &model.DeployTask{} 73 | updateData := map[string]interface{}{ 74 | "status": d.Status, 75 | } 76 | if ok := dt.UpdateByFields(updateData, model.QueryParam{ 77 | Where: []model.WhereParam{ 78 | model.WhereParam{ 79 | Field: "apply_id", 80 | Prepare: d.ApplyId, 81 | }, 82 | model.WhereParam{ 83 | Field: "group_id", 84 | Prepare: d.GroupId, 85 | }, 86 | }, 87 | }); !ok { 88 | return errors.New("update deploy task result failed") 89 | } 90 | return nil 91 | } 92 | 93 | func (d *Deploy) UpdateResult() error { 94 | dt := &model.DeployTask{} 95 | updateData := map[string]interface{}{ 96 | "status": d.Status, 97 | "content": d.Content, 98 | } 99 | if ok := dt.UpdateByFields(updateData, model.QueryParam{ 100 | Where: []model.WhereParam{ 101 | model.WhereParam{ 102 | Field: "apply_id", 103 | Prepare: d.ApplyId, 104 | }, 105 | model.WhereParam{ 106 | Field: "group_id", 107 | Prepare: d.GroupId, 108 | }, 109 | }, 110 | }); !ok { 111 | return errors.New("update deploy task result failed") 112 | } 113 | return nil 114 | } 115 | 116 | func (d *Deploy) DeleteByApplyId() error { 117 | dep := &model.DeployTask{ 118 | ApplyId: d.ApplyId, 119 | } 120 | if ok := dep.Delete(model.QueryParam{ 121 | Where: []model.WhereParam{ 122 | model.WhereParam{ 123 | Field: "apply_id", 124 | Prepare: d.ApplyId, 125 | }, 126 | }, 127 | }); !ok { 128 | return errors.New("remove deploy task failed") 129 | } 130 | return nil 131 | } -------------------------------------------------------------------------------- /module/project/space.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package project 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | 11 | "github.com/dreamans/syncd/model" 12 | ) 13 | 14 | type Space struct { 15 | ID int `json:"id"` 16 | Name string `json:"name"` 17 | Description string `json:"description"` 18 | Ctime int `json:"ctime"` 19 | } 20 | 21 | func SpaceListByIds(spaceIds []int) ([]Space, error) { 22 | s := &Space{} 23 | return s.List(spaceIds, "", 0, 999) 24 | } 25 | 26 | func (s *Space) Delete() error { 27 | space := &model.ProjectSpace{ 28 | ID: s.ID, 29 | } 30 | if ok := space.Delete(); !ok { 31 | return errors.New("delete project space failed") 32 | } 33 | return nil 34 | } 35 | 36 | func (s *Space) Detail() error { 37 | space := &model.ProjectSpace{} 38 | if ok := space.Get(s.ID); !ok { 39 | return errors.New("get project space detail failed") 40 | } 41 | if space.ID == 0 { 42 | return errors.New("project space detail not exists") 43 | } 44 | 45 | s.ID = space.ID 46 | s.Name = space.Name 47 | s.Description = space.Description 48 | s.Ctime = space.Ctime 49 | 50 | return nil 51 | } 52 | 53 | func (s *Space) CreateOrUpdate() error { 54 | space := &model.ProjectSpace{ 55 | ID: s.ID, 56 | Name: s.Name, 57 | Description: s.Description, 58 | } 59 | if space.ID == 0 { 60 | if ok := space.Create(); !ok { 61 | return errors.New("project space create failed") 62 | } 63 | s.ID = space.ID 64 | } else { 65 | if ok := space.Update(); !ok { 66 | return errors.New("project space update failed") 67 | } 68 | } 69 | return nil 70 | } 71 | 72 | func (s *Space) List(spaceIds []int, keyword string, offset, limit int) ([]Space, error) { 73 | space := &model.ProjectSpace{} 74 | list, ok := space.List(model.QueryParam{ 75 | Fields: "id, name, description, ctime", 76 | Offset: offset, 77 | Limit: limit, 78 | Order: "id DESC", 79 | Where: s.parseWhereConds(spaceIds, keyword), 80 | }) 81 | if !ok { 82 | return nil, errors.New("get project space list failed") 83 | } 84 | 85 | var spaceList []Space 86 | for _, l := range list { 87 | spaceList = append(spaceList, Space{ 88 | ID: l.ID, 89 | Name: l.Name, 90 | Description: l.Description, 91 | Ctime: l.Ctime, 92 | }) 93 | } 94 | return spaceList, nil 95 | } 96 | 97 | func (s *Space) Total(spaceIds []int, keyword string) (int, error) { 98 | space := &model.ProjectSpace{} 99 | total, ok := space.Count(model.QueryParam{ 100 | Where: s.parseWhereConds(spaceIds, keyword), 101 | }) 102 | if !ok { 103 | return 0, errors.New("get project space count failed") 104 | } 105 | return total, nil 106 | } 107 | 108 | func (s *Space) parseWhereConds(spaceIds []int, keyword string) []model.WhereParam { 109 | var where []model.WhereParam 110 | if keyword != "" { 111 | where = append(where, model.WhereParam{ 112 | Field: "name", 113 | Tag: "LIKE", 114 | Prepare: fmt.Sprintf("%%%s%%", keyword), 115 | }) 116 | } 117 | where = append(where, model.WhereParam{ 118 | Field: "id", 119 | Tag: "IN", 120 | Prepare: spaceIds, 121 | }) 122 | return where 123 | } 124 | -------------------------------------------------------------------------------- /module/server/group.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package server 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | 11 | "github.com/dreamans/syncd/util/gois" 12 | "github.com/dreamans/syncd/model" 13 | ) 14 | 15 | type Group struct { 16 | ID int `json:"id"` 17 | Name string `json:"name"` 18 | Ctime int `json:"ctime"` 19 | } 20 | 21 | func GroupGetMapByIds(ids []int) (map[int]Group, error) { 22 | if len(ids) == 0 { 23 | return nil, nil 24 | } 25 | group := &model.ServerGroup{} 26 | groupList, ok := group.List(model.QueryParam{ 27 | Where: []model.WhereParam{ 28 | model.WhereParam{ 29 | Field: "id", 30 | Tag: "IN", 31 | Prepare: ids, 32 | }, 33 | }, 34 | }) 35 | if !ok { 36 | return nil, errors.New("get server group list failed") 37 | } 38 | groupMap := make(map[int]Group) 39 | for _, l := range groupList{ 40 | groupMap[l.ID] = Group{ 41 | ID: l.ID, 42 | Name: l.Name, 43 | Ctime: l.Ctime, 44 | } 45 | } 46 | return groupMap, nil 47 | } 48 | 49 | func (g *Group) Create() error { 50 | serverGroup := model.ServerGroup{ 51 | Name: g.Name, 52 | } 53 | if ok := serverGroup.Create(); !ok { 54 | return errors.New("create server group data failed") 55 | } 56 | return nil 57 | } 58 | 59 | func (g *Group) Update() error { 60 | serverGroup := model.ServerGroup{ 61 | ID: g.ID, 62 | Name: g.Name, 63 | } 64 | if ok := serverGroup.Update(); !ok { 65 | return errors.New("update server group data failed") 66 | } 67 | return nil 68 | } 69 | 70 | func (g *Group) List(keyword string, offset, limit int) ([]Group, error) { 71 | group := model.ServerGroup{} 72 | list, ok := group.List(model.QueryParam{ 73 | Fields: "id, name, ctime", 74 | Offset: offset, 75 | Limit: limit, 76 | Order: "id DESC", 77 | Where: g.parseWhereConds(keyword), 78 | }) 79 | if !ok { 80 | return nil, errors.New("get server group list failed") 81 | } 82 | 83 | var groupList []Group 84 | for _, l := range list { 85 | groupList = append(groupList, Group{ 86 | ID: l.ID, 87 | Name: l.Name, 88 | Ctime: l.Ctime, 89 | }) 90 | } 91 | return groupList, nil 92 | } 93 | 94 | func (g *Group) Total(keyword string) (int, error) { 95 | group := model.ServerGroup{} 96 | total, ok := group.Count(model.QueryParam{ 97 | Where: g.parseWhereConds(keyword), 98 | }) 99 | if !ok { 100 | return 0, errors.New("get server group count failed") 101 | } 102 | return total, nil 103 | } 104 | 105 | func (g *Group) Delete() error { 106 | group := &model.ServerGroup{ 107 | ID: g.ID, 108 | } 109 | if ok := group.Delete(); !ok { 110 | return errors.New("delete server group failed") 111 | } 112 | return nil 113 | } 114 | 115 | func (g *Group) Detail() error { 116 | group := model.ServerGroup{} 117 | if ok := group.Get(g.ID); !ok { 118 | return errors.New("get server group detail failed") 119 | } 120 | if group.ID == 0 { 121 | return errors.New("server group not exists") 122 | } 123 | 124 | g.ID = group.ID 125 | g.Name = group.Name 126 | g.Ctime = group.Ctime 127 | 128 | return nil 129 | } 130 | 131 | func (g *Group) parseWhereConds(keyword string) []model.WhereParam { 132 | var where []model.WhereParam 133 | if keyword != "" { 134 | if gois.IsInteger(keyword) { 135 | where = append(where, model.WhereParam{ 136 | Field: "id", 137 | Prepare: keyword, 138 | }) 139 | } else { 140 | where = append(where, model.WhereParam{ 141 | Field: "name", 142 | Tag: "LIKE", 143 | Prepare: fmt.Sprintf("%%%s%%", keyword), 144 | }) 145 | } 146 | } 147 | return where 148 | } 149 | -------------------------------------------------------------------------------- /module/server/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package server 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | 11 | "github.com/dreamans/syncd/util/gois" 12 | "github.com/dreamans/syncd/model" 13 | ) 14 | 15 | type Server struct { 16 | ID int `json:"id"` 17 | GroupId int `json:"group_id"` 18 | GroupName string `json:"group_name"` 19 | Name string `json:"name"` 20 | Ip string `json:"ip"` 21 | SSHPort int `json:"ssh_port"` 22 | Ctime int `json:"ctime"` 23 | } 24 | 25 | func ServerGetListByGroupIds(groupIds []int) ([]Server, error){ 26 | server := &model.Server{} 27 | list, ok := server.List(model.QueryParam{ 28 | Fields: "id, group_id, name, ip, ssh_port, ctime", 29 | Where: []model.WhereParam{ 30 | model.WhereParam{ 31 | Field: "group_id", 32 | Tag: "IN", 33 | Prepare: groupIds, 34 | }, 35 | }, 36 | }) 37 | if !ok { 38 | return nil, errors.New("get server list failed") 39 | } 40 | serList := []Server{} 41 | for _, s := range list { 42 | serList = append(serList, Server{ 43 | ID: s.ID, 44 | GroupId: s.GroupId, 45 | Name: s.Name, 46 | Ip: s.Ip, 47 | SSHPort: s.SSHPort, 48 | }) 49 | } 50 | return serList, nil 51 | } 52 | 53 | func (s *Server) CreateOrUpdate() error { 54 | server := &model.Server{ 55 | ID: s.ID, 56 | GroupId: s.GroupId, 57 | Name: s.Name, 58 | Ip: s.Ip, 59 | SSHPort: s.SSHPort, 60 | } 61 | if server.ID == 0 { 62 | if ok := server.Create(); !ok { 63 | return errors.New("create server failed") 64 | } 65 | } else { 66 | if ok := server.Update(); !ok { 67 | return errors.New("update server failed") 68 | } 69 | } 70 | return nil 71 | } 72 | 73 | func (s *Server) List(keyword string, offset, limit int) ([]Server, error) { 74 | server := &model.Server{} 75 | list, ok := server.List(model.QueryParam{ 76 | Fields: "id, group_id, name, ip, ssh_port, ctime", 77 | Offset: offset, 78 | Limit: limit, 79 | Order: "id DESC", 80 | Where: s.parseWhereConds(keyword), 81 | }) 82 | if !ok { 83 | return nil, errors.New("get server list failed") 84 | } 85 | 86 | var ( 87 | serverList []Server 88 | groupIds []int 89 | ) 90 | for _, l := range list { 91 | serverList = append(serverList, Server{ 92 | ID: l.ID, 93 | GroupId: l.GroupId, 94 | Name: l.Name, 95 | Ip: l.Ip, 96 | SSHPort: l.SSHPort, 97 | Ctime: l.Ctime, 98 | }) 99 | groupIds = append(groupIds, l.GroupId) 100 | } 101 | 102 | groupMap, err := GroupGetMapByIds(groupIds) 103 | if err != nil { 104 | return nil, errors.New("get server group map failed") 105 | } 106 | for k, l := range serverList { 107 | if g, exists := groupMap[l.GroupId]; exists { 108 | serverList[k].GroupName = g.Name 109 | } 110 | } 111 | 112 | return serverList, nil 113 | } 114 | 115 | func (s *Server) Total(keyword string) (int, error) { 116 | server := model.Server{} 117 | total, ok := server.Count(model.QueryParam{ 118 | Where: s.parseWhereConds(keyword), 119 | }) 120 | if !ok { 121 | return 0, errors.New("get server count failed") 122 | } 123 | return total, nil 124 | } 125 | 126 | func (s *Server) Delete() error { 127 | server := &model.Server{ 128 | ID: s.ID, 129 | } 130 | if ok := server.Delete(); !ok { 131 | return errors.New("delete server failed") 132 | } 133 | return nil 134 | } 135 | 136 | func (s *Server) Detail() error { 137 | server := &model.Server{} 138 | if ok := server.Get(s.ID); !ok { 139 | return errors.New("get server detail failed") 140 | } 141 | if server.ID == 0 { 142 | return errors.New("server not exists") 143 | } 144 | 145 | s.ID = server.ID 146 | s.GroupId = server.GroupId 147 | s.Name = server.Name 148 | s.Ip = server.Ip 149 | s.SSHPort = server.SSHPort 150 | s.Ctime = server.Ctime 151 | 152 | return nil 153 | } 154 | 155 | func (s *Server) parseWhereConds(keyword string) []model.WhereParam { 156 | var where []model.WhereParam 157 | if keyword != "" { 158 | if gois.IsInteger(keyword) { 159 | where = append(where, model.WhereParam{ 160 | Field: "id", 161 | Prepare: keyword, 162 | }) 163 | } else { 164 | if gois.IsIp(keyword) { 165 | where = append(where, model.WhereParam{ 166 | Field: "ip", 167 | Prepare: keyword, 168 | }) 169 | } else { 170 | where = append(where, model.WhereParam{ 171 | Field: "name", 172 | Tag: "LIKE", 173 | Prepare: fmt.Sprintf("%%%s%%", keyword), 174 | }) 175 | } 176 | } 177 | } 178 | return where 179 | } 180 | -------------------------------------------------------------------------------- /module/user/login.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package user 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "time" 11 | "strings" 12 | 13 | "github.com/dreamans/syncd" 14 | "github.com/dreamans/syncd/util/gostring" 15 | "github.com/dreamans/syncd/util/goaes" 16 | ) 17 | 18 | type Login struct { 19 | UserId int 20 | RoleId int 21 | Username string 22 | Password string 23 | Email string 24 | Truename string 25 | Mobile string 26 | Token string 27 | } 28 | 29 | func (login *Login) Logout() error { 30 | token := &Token{ 31 | UserId: login.UserId, 32 | } 33 | return token.DeleteByUserId() 34 | } 35 | 36 | func (login *Login) Login() error { 37 | u := &User{} 38 | if login.Username != "" { 39 | u.Username = login.Username 40 | } 41 | if login.Email != "" { 42 | u.Email = login.Email 43 | } 44 | if err := u.Detail(); err != nil { 45 | return errors.New("username or password incorrect") 46 | } 47 | loginPassword := gostring.StrMd5(gostring.JoinStrings(login.Password, u.Salt)) 48 | if u.Password != loginPassword { 49 | return errors.New("password incorrect") 50 | } 51 | 52 | if u.Status != 1 { 53 | return errors.New("user is locked") 54 | } 55 | 56 | login.UserId = u.ID 57 | if err := login.createToken(); err != nil { 58 | return errors.New("token create failed") 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (login *Login) ValidateToken() error { 65 | authTokenBytes, err := gostring.Base64UrlDecode(login.Token) 66 | if err != nil { 67 | return errors.New("token check failed, can not decode raw token") 68 | } 69 | tokenValBytes, err := goaes.Decrypt(syncd.App.CipherKey, authTokenBytes) 70 | if err != nil { 71 | return errors.New("token check failed, can not decrypt raw token") 72 | } 73 | tokenArr := strings.Split(string(tokenValBytes), "\t") 74 | if len(tokenArr) != 2 { 75 | return errors.New("token check failed, len wrong") 76 | } 77 | token := &Token{ 78 | UserId: gostring.Str2Int(tokenArr[0]), 79 | Token: tokenArr[1], 80 | } 81 | if ok := token.ValidateToken(); !ok { 82 | return errors.New("token check failed, maybe your account is logged in on another device or token expired") 83 | } 84 | 85 | //get user detail 86 | user := &User{ 87 | ID: token.UserId, 88 | } 89 | if err := user.Detail(); err != nil { 90 | return errors.New("token check failed, user detail get failed") 91 | } 92 | 93 | if user.Status != 1 { 94 | return errors.New("user is locked") 95 | } 96 | 97 | login.UserId = user.ID 98 | login.Username = user.Username 99 | login.Email = user.Email 100 | login.Truename = user.Truename 101 | login.Mobile = user.Mobile 102 | login.RoleId = user.RoleId 103 | 104 | return nil 105 | } 106 | 107 | func (login *Login) createToken() error { 108 | loginKey := gostring.StrRandom(40) 109 | loginRaw := fmt.Sprintf("%d\t%s", login.UserId, loginKey) 110 | var ( 111 | err error 112 | tokenBytes []byte 113 | ) 114 | tokenBytes, err = goaes.Encrypt(syncd.App.CipherKey, []byte(loginRaw)) 115 | if err != nil { 116 | return err 117 | } 118 | login.Token = gostring.Base64UrlEncode(tokenBytes) 119 | 120 | token := &Token{ 121 | UserId: login.UserId, 122 | Token: loginKey, 123 | Expire: int(time.Now().Unix()) + 86400 * 30, 124 | } 125 | if err := token.CreateOrUpdate(); err != nil { 126 | return err 127 | } 128 | return nil 129 | } 130 | 131 | -------------------------------------------------------------------------------- /module/user/role.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package user 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | 11 | "github.com/dreamans/syncd/util/gostring" 12 | "github.com/dreamans/syncd/util/gois" 13 | "github.com/dreamans/syncd/model" 14 | ) 15 | 16 | type Role struct { 17 | ID int `json:"id"` 18 | Name string `json:"name"` 19 | Privilege []int `json:"privilege"` 20 | Ctime int `json:"ctime"` 21 | } 22 | 23 | func RoleGetMapByIds(ids []int) (map[int]Role, error) { 24 | if len(ids) == 0 { 25 | return nil, nil 26 | } 27 | role := &model.UserRole{} 28 | list, ok := role.List(model.QueryParam{ 29 | Where: []model.WhereParam{ 30 | model.WhereParam{ 31 | Field: "id", 32 | Tag: "IN", 33 | Prepare: ids, 34 | }, 35 | }, 36 | }) 37 | if !ok { 38 | return nil, errors.New("get user role maps failed") 39 | } 40 | roleMap := make(map[int]Role) 41 | for _, l := range list { 42 | roleMap[l.ID] = Role{ 43 | ID: l.ID, 44 | Name: l.Name, 45 | } 46 | } 47 | return roleMap, nil 48 | } 49 | 50 | func (r *Role) Detail() error { 51 | role := &model.UserRole{} 52 | if ok := role.Get(r.ID); !ok { 53 | return errors.New("get user role detail failed") 54 | } 55 | if role.ID == 0 { 56 | return errors.New("user role not exists") 57 | } 58 | 59 | r.ID = role.ID 60 | r.Name = role.Name 61 | r.Privilege = gostring.StrSplit2IntSlice(role.Privilege, ",") 62 | r.Ctime = role.Ctime 63 | 64 | return nil 65 | } 66 | 67 | func (r *Role) CreateOrUpdate() error { 68 | role := &model.UserRole{ 69 | ID: r.ID, 70 | Name: r.Name, 71 | Privilege: gostring.JoinIntSlice2String(r.Privilege, ","), 72 | } 73 | if role.ID == 0 { 74 | if ok := role.Create(); !ok { 75 | return errors.New("create user role data failed") 76 | } 77 | } else { 78 | if ok := role.Update(); !ok { 79 | return errors.New("update user role failed") 80 | } 81 | } 82 | return nil 83 | } 84 | 85 | func (r *Role) List(keyword string, offset, limit int) ([]Role, error) { 86 | role := &model.UserRole{} 87 | list, ok := role.List(model.QueryParam{ 88 | Fields: "id, name, ctime", 89 | Offset: offset, 90 | Limit: limit, 91 | Order: "id ASC", 92 | Where: r.parseWhereConds(keyword), 93 | }) 94 | if !ok { 95 | return nil, errors.New("get user role list failed") 96 | } 97 | 98 | var roleList []Role 99 | for _, l := range list { 100 | roleList = append(roleList, Role{ 101 | ID: l.ID, 102 | Name: l.Name, 103 | Ctime: l.Ctime, 104 | }) 105 | } 106 | return roleList, nil 107 | } 108 | 109 | func (r *Role) Total(keyword string) (int, error) { 110 | role := &model.UserRole{} 111 | total, ok := role.Count(model.QueryParam{ 112 | Where: r.parseWhereConds(keyword), 113 | }) 114 | if !ok { 115 | return 0, errors.New("get user role count failed") 116 | } 117 | return total, nil 118 | } 119 | 120 | func (r *Role) Delete() error { 121 | role := &model.UserRole{ 122 | ID: r.ID, 123 | } 124 | if ok := role.Delete(); !ok { 125 | return errors.New("delete user role failed") 126 | } 127 | return nil 128 | } 129 | 130 | func (r *Role) parseWhereConds(keyword string) []model.WhereParam { 131 | var where []model.WhereParam 132 | if keyword != "" { 133 | if gois.IsInteger(keyword) { 134 | where = append(where, model.WhereParam{ 135 | Field: "id", 136 | Prepare: keyword, 137 | }) 138 | } else { 139 | where = append(where, model.WhereParam{ 140 | Field: "name", 141 | Tag: "LIKE", 142 | Prepare: fmt.Sprintf("%%%s%%", keyword), 143 | }) 144 | } 145 | } 146 | return where 147 | } 148 | -------------------------------------------------------------------------------- /module/user/token.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package user 6 | 7 | import ( 8 | "errors" 9 | "time" 10 | 11 | "github.com/dreamans/syncd/model" 12 | ) 13 | 14 | type Token struct { 15 | ID int 16 | UserId int 17 | Token string 18 | Expire int 19 | } 20 | 21 | func (t *Token) DeleteByUserId() error { 22 | token := &model.UserToken{} 23 | if ok := token.DeleteByFields(model.QueryParam{ 24 | Where: []model.WhereParam{ 25 | model.WhereParam{ 26 | Field: "user_id", 27 | Prepare: t.UserId, 28 | }, 29 | }, 30 | }); !ok { 31 | return errors.New("delete user token failed") 32 | } 33 | return nil 34 | } 35 | 36 | func (t *Token) CreateOrUpdate() error { 37 | token := &model.UserToken{} 38 | if ok := token.GetOne(model.QueryParam{ 39 | Where: []model.WhereParam{ 40 | model.WhereParam{ 41 | Field: "user_id", 42 | Prepare: t.UserId, 43 | }, 44 | }, 45 | }); !ok { 46 | return errors.New("get user token detail failed") 47 | } 48 | 49 | token.UserId = t.UserId 50 | token.Token = t.Token 51 | token.Expire = t.Expire 52 | 53 | if token.ID == 0 { 54 | if ok := token.Create(); !ok { 55 | return errors.New("user token create failed") 56 | } 57 | } else { 58 | if ok := token.Update(); !ok { 59 | return errors.New("user token update failed") 60 | } 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func (t *Token) ValidateToken() bool { 67 | if t.UserId == 0 || t.Token == "" { 68 | return false 69 | } 70 | token := &model.UserToken{} 71 | if ok := token.GetOne(model.QueryParam{ 72 | Where: []model.WhereParam{ 73 | model.WhereParam{ 74 | Field: "user_id", 75 | Prepare: t.UserId, 76 | }, 77 | model.WhereParam{ 78 | Field: "token", 79 | Prepare: t.Token, 80 | }, 81 | }, 82 | }); !ok { 83 | return false 84 | } 85 | if token.ID == 0 { 86 | return false 87 | } 88 | if token.Expire < int(time.Now().Unix()) { 89 | return false 90 | } 91 | return true 92 | } 93 | -------------------------------------------------------------------------------- /public/css/chunk-21c9e7bf.a3c25090.css: -------------------------------------------------------------------------------- 1 | .scroll-container[data-v-d88310be]{position:relative;width:100%;height:100%;overflow:hidden}.scroll-container .scroll-wrapper[data-v-d88310be]{position:absolute;width:100%} -------------------------------------------------------------------------------- /public/css/chunk-e3695f98.b86ea360.css: -------------------------------------------------------------------------------- 1 | .app-login{background-image:url(../img/login_bg.193d505f.jpg);background-position:50%;background-size:cover}.app-login,.app-login .app-login-inner{width:100%;height:100%}.app-login .login-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.app-login .login-container .login-box{margin-top:20vh;width:30vw}.app-login .login-container .login-box .login-title{font-weight:500;text-align:center;font-size:14px;margin-bottom:20px}.app-login .login-cpy{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;margin-top:30px;color:#fff}.app-login .login-cpy a{margin:0 5px;color:#fff} -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamans/syncd/71876dc6d3e7caa40e44a33ad47f8aa392881d40/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/element-icons.2fad952a.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamans/syncd/71876dc6d3e7caa40e44a33ad47f8aa392881d40/public/fonts/element-icons.2fad952a.woff -------------------------------------------------------------------------------- /public/fonts/element-icons.6f0a7632.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamans/syncd/71876dc6d3e7caa40e44a33ad47f8aa392881d40/public/fonts/element-icons.6f0a7632.ttf -------------------------------------------------------------------------------- /public/img/login_bg.193d505f.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamans/syncd/71876dc6d3e7caa40e44a33ad47f8aa392881d40/public/img/login_bg.193d505f.jpg -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | Syncd - 自动化发布系统
2 | -------------------------------------------------------------------------------- /public/js/chunk-2d0f0051.d23388af.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d0f0051"],{"9b1c":function(n,t,o){"use strict";o.r(t);var e=function(){var n=this,t=n.$createElement,o=n._self._c||t;return o("el-card",[o("p",[n._v("Dashboard Coming Soon...")]),o("p",{staticStyle:{"margin-top":"100px"}},[o("i",{staticClass:"iconfont icon-working",staticStyle:{"font-size":"48px",color:"#409EFF"}})])])},i=[],a=o("2877"),c={},s=Object(a["a"])(c,e,i,!1,null,null,null);s.options.__file="Dashboard.vue";t["default"]=s.exports}}]); 2 | //# sourceMappingURL=chunk-2d0f0051.d23388af.js.map -------------------------------------------------------------------------------- /public/js/chunk-2d0f0051.d23388af.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///./src/view/Dashboard.vue?7af1","webpack:///./src/view/Dashboard.vue"],"names":["render","_vm","this","_h","$createElement","_c","_self","_v","staticStyle","margin-top","staticClass","font-size","color","staticRenderFns","script","component","Object","componentNormalizer","options","__file","__webpack_exports__"],"mappings":"yHAAA,IAAAA,EAAA,WAA0B,IAAAC,EAAAC,KAAaC,EAAAF,EAAAG,eAA0BC,EAAAJ,EAAAK,MAAAD,IAAAF,EAAwB,OAAAE,EAAA,WAAAA,EAAA,KAAAJ,EAAAM,GAAA,8BAAAF,EAAA,KAA0EG,YAAA,CAAaC,aAAA,UAAsB,CAAAJ,EAAA,KAAUK,YAAA,wBAAAF,YAAA,CAAiDG,YAAA,OAAAC,MAAA,kBACjQC,EAAA,eCAAC,EAAA,GAKAC,EAAgBC,OAAAC,EAAA,KAAAD,CAChBF,EACEd,EACAa,GACF,EACA,KACA,KACA,MAIAE,EAAAG,QAAAC,OAAA,gBACeC,EAAA,WAAAL","file":"js/chunk-2d0f0051.d23388af.js","sourcesContent":["var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('el-card',[_c('p',[_vm._v(\"Dashboard Coming Soon...\")]),_c('p',{staticStyle:{\"margin-top\":\"100px\"}},[_c('i',{staticClass:\"iconfont icon-working\",staticStyle:{\"font-size\":\"48px\",\"color\":\"#409EFF\"}})])])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import { render, staticRenderFns } from \"./Dashboard.vue?vue&type=template&id=2a5e8ab0&\"\nvar script = {}\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\ncomponent.options.__file = \"Dashboard.vue\"\nexport default component.exports"],"sourceRoot":""} -------------------------------------------------------------------------------- /public/js/chunk-e3695f98.4489b617.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-e3695f98"],{"0da5":function(e,n,t){"use strict";var o=t("c47e"),r=t.n(o);r.a},"7ded":function(e,n,t){"use strict";t.d(n,"a",function(){return r}),t.d(n,"b",function(){return s}),t.d(n,"c",function(){return i});var o=t("ead3");function r(e){return Object(o["b"])("/login",e)}function s(){return Object(o["a"])("/login/status")}function i(){return Object(o["b"])("/logout")}},c47e:function(e,n,t){},dd1d:function(e,n,t){"use strict";t.r(n);var o=function(){var e=this,n=e.$createElement,t=e._self._c||n;return t("div",{staticClass:"app-login"},[t("div",{staticClass:"app-login-inner",style:{backgroundColor:e.bgColor}},[t("div",{staticClass:"login-container"},[t("el-card",{staticClass:"login-box"},[t("div",{staticClass:"login-title"},[e._v(e._s(e.$t("welcome_to_login_syncd")))]),t("el-form",{ref:"loginFormRef",staticClass:"login-form",attrs:{model:e.loginForm,rules:e.loginRules,size:"medium"},nativeOn:{keyup:function(n){return"button"in n||!e._k(n.keyCode,"enter",13,n.key,"Enter")?e.loginHandler(n):null}}},[t("el-form-item",{attrs:{prop:"username"}},[t("el-input",{attrs:{placeholder:e.$t("username_or_email"),"prefix-icon":"iconfont icon-user",autocomplete:"off"},model:{value:e.loginForm.username,callback:function(n){e.$set(e.loginForm,"username",n)},expression:"loginForm.username"}})],1),t("el-form-item",{attrs:{prop:"password"}},[t("el-input",{attrs:{placeholder:e.$t("password"),"prefix-icon":"iconfont icon-lock",type:"password",autocomplete:"off"},model:{value:e.loginForm.password,callback:function(n){e.$set(e.loginForm,"password",n)},expression:"loginForm.password"}})],1),t("el-form-item",[t("el-button",{staticStyle:{width:"100%"},attrs:{type:"primary"},on:{click:e.loginHandler}},[e._v(e._s(e.$t("login")))])],1)],1)],1)],1),t("div",{staticClass:"login-cpy"},[e._v("\n ©️ "+e._s((new Date).getFullYear())+" "),t("a",{attrs:{href:"https://github.com/dreamans/syncd",target:"_blank"}},[e._v("Syncd")]),e._v(". All Rights Reserved. MIT License.\n ")])])])},r=[],s=(t("cadf"),t("551c"),t("097d"),t("7ded")),i=t("1c06"),a={data:function(){return{loginLoadding:!1,loginForm:{username:"",password:""},loginRules:{username:[{required:!0,message:this.$t("please_input_loginname"),trigger:"blur"}],password:[{required:!0,message:this.$t("please_input_password"),trigger:"blur"},{min:6,max:20,message:this.$t("strlen_between",{min:6,max:20}),trigger:"blur"}]}}},computed:{bgColor:function(){var e=Math.min(Math.abs((new Date).getHours()-12)/10,.8);return"rgba(0, 0, 0, ".concat(e,")")}},methods:{loginHandler:function(){var e=this;this.$refs.loginFormRef.validate(function(n){if(!n)return!1;var t={username:e.loginForm.username,password:e.$root.Md5Sum(e.loginForm.password)};Object(s["a"])(t).then(function(n){e.$root.SetLoginToken(n.token),e.$router.push({name:"dashboard"})}).catch(function(n){n.code&&n.code==i["a"].CODE_ERR_LOGIN_FAILED&&e.$message.error("登录失败, 错误信息: "+n.message)})})}}},l=a,u=(t("0da5"),t("2877")),c=Object(u["a"])(l,o,r,!1,null,null,null);c.options.__file="Login.vue";n["default"]=c.exports}}]); 2 | //# sourceMappingURL=chunk-e3695f98.4489b617.js.map -------------------------------------------------------------------------------- /render/render.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package render 6 | 7 | import ( 8 | "net/http" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | const ( 14 | CODE_OK = 0 15 | CODE_ERR_SYSTEM = 1000 16 | CODE_ERR_APP = 1001 17 | CODE_ERR_PARAM = 1002 18 | CODE_ERR_DATA_REPEAT = 1003 19 | CODE_ERR_LOGIN_FAILED = 1004 20 | CODE_ERR_NO_LOGIN = 1005 21 | CODE_ERR_NO_PRIV = 1006 22 | CODE_ERR_TASK_ERROR = 1007 23 | CODE_ERR_USER_OR_PASS_WRONG = 1008 24 | CODE_ERR_NO_DATA = 1009 25 | ) 26 | 27 | func JSON(c *gin.Context, data interface{}) { 28 | c.JSON(http.StatusOK, gin.H{ 29 | "code": CODE_OK, 30 | "message": "success", 31 | "data": data, 32 | }) 33 | } 34 | 35 | func CustomerError(c *gin.Context, code int, message string) { 36 | c.JSON(http.StatusOK, gin.H{ 37 | "code": code, 38 | "message": message, 39 | }) 40 | } 41 | 42 | func RepeatError(c *gin.Context, message string) { 43 | c.JSON(http.StatusOK, gin.H{ 44 | "code": CODE_ERR_DATA_REPEAT, 45 | "message": message, 46 | }) 47 | } 48 | 49 | func NoDataError(c *gin.Context, message string) { 50 | c.JSON(http.StatusOK, gin.H{ 51 | "code": CODE_ERR_NO_DATA, 52 | "message": message, 53 | }) 54 | } 55 | 56 | func ParamError(c *gin.Context, message string) { 57 | c.JSON(http.StatusOK, gin.H{ 58 | "code": CODE_ERR_PARAM, 59 | "message": message, 60 | }) 61 | } 62 | 63 | func AppError(c *gin.Context, message string) { 64 | c.JSON(http.StatusOK, gin.H{ 65 | "code": CODE_ERR_APP, 66 | "message": message, 67 | }) 68 | } 69 | 70 | func Success(c *gin.Context) { 71 | c.JSON(http.StatusOK, gin.H{ 72 | "code": CODE_OK, 73 | "message": "success", 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /router/common/hook.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package common 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/dreamans/syncd" 11 | "github.com/dreamans/syncd/util/gofile" 12 | "github.com/dreamans/syncd/util/command" 13 | "github.com/dreamans/syncd/util/gostring" 14 | "github.com/dreamans/syncd/module/project" 15 | "github.com/dreamans/syncd/module/deploy" 16 | ) 17 | 18 | func HookBuild(applyId int) { 19 | apply := &deploy.Apply{ 20 | ID: applyId, 21 | } 22 | if err := apply.Detail(); err != nil { 23 | return 24 | } 25 | proj := &project.Project{ 26 | ID: apply.ProjectId, 27 | } 28 | if err := proj.Detail(); err != nil { 29 | return 30 | } 31 | if proj.BuildHookScript == "" { 32 | return 33 | } 34 | 35 | build := &deploy.Build{ 36 | ApplyId: applyId, 37 | } 38 | if err := build.Detail(); err != nil { 39 | return 40 | } 41 | 42 | var buildStatus int 43 | if build.Status == deploy.BUILD_STATUS_SUCCESS { 44 | buildStatus = 1 45 | } 46 | 47 | s := gostring.JoinStrings( 48 | "#!/bin/bash\n\n", 49 | "#--------- build hook scripts env ---------\n", 50 | fmt.Sprintf("env_apply_id=%d\n", apply.ID), 51 | fmt.Sprintf("env_apply_name='%s'\n", apply.Name), 52 | fmt.Sprintf("env_pack_file='%s'\n", build.Tar), 53 | fmt.Sprintf("env_build_output='%s'\n", build.Output), 54 | fmt.Sprintf("env_build_errmsg='%s'\n", build.Errmsg), 55 | fmt.Sprintf("env_build_status=%d\n", buildStatus), 56 | proj.BuildHookScript, 57 | ) 58 | hookCommandTaskRun(s, applyId) 59 | } 60 | 61 | func HookDeploy(applyId int) { 62 | apply := &deploy.Apply{ 63 | ID: applyId, 64 | } 65 | if err := apply.Detail(); err != nil { 66 | return 67 | } 68 | proj := &project.Project{ 69 | ID: apply.ProjectId, 70 | } 71 | if err := proj.Detail(); err != nil { 72 | return 73 | } 74 | if proj.DeployHookScript == "" { 75 | return 76 | } 77 | 78 | var deployStatus int 79 | if apply.Status == deploy.APPLY_STATUS_SUCCESS { 80 | deployStatus = 1 81 | } 82 | 83 | s := gostring.JoinStrings( 84 | "#!/bin/bash\n\n", 85 | "#--------- deploy hook scripts env ---------\n", 86 | fmt.Sprintf("env_apply_id=%d\n", apply.ID), 87 | fmt.Sprintf("env_apply_name='%s'\n", apply.Name), 88 | fmt.Sprintf("env_deploy_status=%d\n", deployStatus), 89 | proj.DeployHookScript, 90 | ) 91 | hookCommandTaskRun(s, applyId) 92 | } 93 | 94 | func hookCommandTaskRun(s string, applyId int) { 95 | scriptFile := gostring.JoinStrings(syncd.App.LocalTmpSpace, "/", gostring.StrRandom(24), ".sh") 96 | if err := gofile.CreateFile(scriptFile, []byte(s), 0744); err != nil { 97 | syncd.App.Logger.Error("hook script file create failed, err[%s], apply_id[%d]", err.Error(), applyId) 98 | return 99 | } 100 | cmds := []string{ 101 | fmt.Sprintf("/bin/bash -c %s", scriptFile), 102 | fmt.Sprintf("rm -f %s", scriptFile), 103 | } 104 | task := command.NewTask(cmds, 86400) 105 | task.Run() 106 | if err := task.GetError(); err != nil { 107 | syncd.App.Logger.Error("hook script run failed, err[%s], output[%s], apply_id[%d]", err.Error(), gostring.JsonEncode(task.Result()), applyId) 108 | } 109 | } -------------------------------------------------------------------------------- /router/common/inspace.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package common 6 | 7 | import ( 8 | "github.com/gin-gonic/gin" 9 | "github.com/dreamans/syncd/render" 10 | "github.com/dreamans/syncd/module/project" 11 | ) 12 | 13 | func InSpaceCheck(c *gin.Context, spaceId int) bool { 14 | member := &project.Member{ 15 | UserId: c.GetInt("user_id"), 16 | SpaceId: spaceId, 17 | } 18 | if in := member.MemberInSpace(); !in { 19 | render.CustomerError(c, render.CODE_ERR_NO_PRIV, "user is not in the project space") 20 | return false 21 | } 22 | return true 23 | } -------------------------------------------------------------------------------- /router/middleware/api_priv.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package middleware 6 | 7 | import ( 8 | "strings" 9 | "github.com/gin-gonic/gin" 10 | "github.com/dreamans/syncd/util/goslice" 11 | "github.com/dreamans/syncd/render" 12 | "github.com/dreamans/syncd/module/user" 13 | reqApi "github.com/dreamans/syncd/router/route/api" 14 | ) 15 | 16 | func ApiPriv() gin.HandlerFunc { 17 | return func(c *gin.Context) { 18 | token, _ := c.Cookie("_syd_identity") 19 | 20 | path := strings.TrimSpace(c.Request.URL.Path) 21 | if len(path) < 4 { 22 | respondWithError(c, render.CODE_ERR_NO_PRIV, "request path is too short") 23 | return 24 | } 25 | path = path[4:] 26 | 27 | if path == reqApi.LOGIN { 28 | c.Next() 29 | return 30 | } 31 | 32 | if token == "" { 33 | respondWithError(c, render.CODE_ERR_NO_LOGIN, "no login") 34 | return 35 | } 36 | 37 | login := &user.Login{ 38 | Token: token, 39 | } 40 | if err := login.ValidateToken(); err != nil { 41 | respondWithError(c, render.CODE_ERR_NO_LOGIN, err.Error()) 42 | return 43 | } 44 | 45 | //priv check 46 | role := &user.Role{ 47 | ID: login.RoleId, 48 | } 49 | if err := role.Detail(); err != nil { 50 | respondWithError(c, render.CODE_ERR_APP, err.Error()) 51 | return 52 | } 53 | 54 | c.Set("user_id", login.UserId) 55 | c.Set("username", login.Username) 56 | c.Set("email", login.Email) 57 | c.Set("truename", login.Truename) 58 | c.Set("mobile", login.Mobile) 59 | c.Set("role_name", role.Name) 60 | c.Set("privilege", role.Privilege) 61 | 62 | needLoginReq := []string{ 63 | reqApi.LOGIN_STATUS, 64 | reqApi.LOGOUT, 65 | reqApi.MY_USER_SETTING, 66 | reqApi.MY_USER_PASSWORD, 67 | } 68 | if goslice.InSliceString(path, needLoginReq) { 69 | c.Next() 70 | return 71 | } 72 | 73 | if pass := user.CheckHavePriv(path, role.Privilege); !pass { 74 | respondWithError(c, render.CODE_ERR_NO_PRIV, "no priv") 75 | return 76 | } 77 | 78 | c.Next() 79 | } 80 | } 81 | 82 | func respondWithError(c *gin.Context, code int, message string) { 83 | render.CustomerError(c, code, message) 84 | c.Abort() 85 | } 86 | -------------------------------------------------------------------------------- /router/project/member.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package project 6 | 7 | import ( 8 | "github.com/gin-gonic/gin" 9 | "github.com/dreamans/syncd/router/common" 10 | "github.com/dreamans/syncd/module/user" 11 | "github.com/dreamans/syncd/module/project" 12 | "github.com/dreamans/syncd/render" 13 | "github.com/dreamans/syncd/util/gostring" 14 | ) 15 | 16 | type MemberAddQueryBind struct { 17 | MemberId int `form:"member_id" binding:"required"` 18 | SpaceId int `form:"space_id" binding:"required"` 19 | } 20 | 21 | type MemberListQueryBind struct { 22 | SpaceId int `form:"space_id" binding:"required"` 23 | Offset int `form:"offset"` 24 | Limit int `form:"limit" binding:"required,gte=1,lte=999"` 25 | } 26 | 27 | func MemberRemove(c *gin.Context) { 28 | id := gostring.Str2Int(c.PostForm("id")) 29 | if id == 0 { 30 | render.ParamError(c, "id cannot be empty") 31 | return 32 | } 33 | 34 | member := &project.Member{ 35 | ID: id, 36 | } 37 | if err := member.Detail(); err != nil { 38 | render.AppError(c, err.Error()) 39 | return 40 | } 41 | 42 | if !common.InSpaceCheck(c, member.SpaceId) { 43 | return 44 | } 45 | 46 | if err := member.Delete(); err != nil { 47 | render.AppError(c, err.Error()) 48 | return 49 | } 50 | render.JSON(c, nil) 51 | } 52 | 53 | func MemberList(c *gin.Context) { 54 | var query MemberListQueryBind 55 | if err := c.ShouldBind(&query); err != nil { 56 | render.ParamError(c, err.Error()) 57 | return 58 | } 59 | 60 | if !common.InSpaceCheck(c, query.SpaceId) { 61 | return 62 | } 63 | 64 | m := &project.Member{} 65 | memberList, err := m.List(query.SpaceId, query.Offset, query.Limit) 66 | if err != nil { 67 | render.AppError(c, err.Error()) 68 | return 69 | } 70 | total, err := m.Total(query.SpaceId) 71 | if err != nil { 72 | render.AppError(c, err.Error()) 73 | return 74 | } 75 | 76 | render.JSON(c, gin.H{ 77 | "list": memberList, 78 | "total": total, 79 | }) 80 | } 81 | 82 | func MemberSearch(c *gin.Context) { 83 | keyword := c.Query("keyword") 84 | u := &user.User{} 85 | list, err := u.List(keyword, 0, 20) 86 | if err != nil { 87 | render.AppError(c, err.Error()) 88 | return 89 | } 90 | userList := []map[string]interface{}{} 91 | for _, l := range list { 92 | userList = append(userList, map[string]interface{}{ 93 | "id": l.ID, 94 | "username": l.Username, 95 | "email": l.Email, 96 | "status": l.Status, 97 | "role_name": l.RoleName, 98 | }) 99 | } 100 | render.JSON(c, userList) 101 | } 102 | 103 | func MemberAdd(c *gin.Context) { 104 | var query MemberAddQueryBind 105 | if err := c.ShouldBind(&query); err != nil { 106 | render.ParamError(c, err.Error()) 107 | return 108 | } 109 | 110 | if !common.InSpaceCheck(c, query.SpaceId) { 111 | return 112 | } 113 | 114 | var ( 115 | err error 116 | exists bool 117 | ) 118 | 119 | u := &user.User{ 120 | ID: query.MemberId, 121 | } 122 | exists , err = u.Exists() 123 | if err != nil { 124 | render.AppError(c, err.Error()) 125 | return 126 | } 127 | if !exists { 128 | render.NoDataError(c, "user not exists") 129 | return 130 | } 131 | 132 | member := project.Member{ 133 | UserId: query.MemberId, 134 | SpaceId: query.SpaceId, 135 | } 136 | exists, err = member.Exists() 137 | if err != nil { 138 | render.AppError(c, err.Error()) 139 | return 140 | } 141 | if exists { 142 | render.RepeatError(c, "user have exists in project space") 143 | return 144 | } 145 | if err := member.Create(); err != nil { 146 | render.AppError(c, err.Error()) 147 | return 148 | } 149 | 150 | render.Success(c) 151 | } -------------------------------------------------------------------------------- /router/project/space.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package project 6 | 7 | import ( 8 | "github.com/gin-gonic/gin" 9 | "github.com/dreamans/syncd/render" 10 | "github.com/dreamans/syncd/module/project" 11 | "github.com/dreamans/syncd/util/gostring" 12 | ) 13 | 14 | type SpaceFormBind struct { 15 | Name string `form:"name" binding:"required"` 16 | Description string `form:"description"` 17 | } 18 | 19 | func SpaceDelete(c *gin.Context) { 20 | id := gostring.Str2Int(c.PostForm("id")) 21 | if id == 0 { 22 | render.ParamError(c, "id cannot be empty") 23 | return 24 | } 25 | member := &project.Member{ 26 | UserId: c.GetInt("user_id"), 27 | SpaceId: id, 28 | } 29 | if in := member.MemberInSpace(); !in { 30 | render.CustomerError(c, render.CODE_ERR_NO_PRIV, "user is not in the project space") 31 | return 32 | } 33 | space := &project.Space{ 34 | ID: id, 35 | } 36 | if err := space.Delete(); err != nil { 37 | render.AppError(c, err.Error()) 38 | return 39 | } 40 | render.JSON(c, nil) 41 | } 42 | 43 | func SpaceDetail(c *gin.Context) { 44 | id := gostring.Str2Int(c.Query("id")) 45 | if id == 0 { 46 | render.ParamError(c, "id cannot be empty") 47 | return 48 | } 49 | member := &project.Member{ 50 | UserId: c.GetInt("user_id"), 51 | SpaceId: id, 52 | } 53 | if in := member.MemberInSpace(); !in { 54 | render.CustomerError(c, render.CODE_ERR_NO_PRIV, "user is not in the project space") 55 | return 56 | } 57 | space := &project.Space{ 58 | ID: id, 59 | } 60 | if err := space.Detail(); err != nil { 61 | render.AppError(c, err.Error()) 62 | return 63 | } 64 | render.JSON(c, space) 65 | } 66 | 67 | func SpaceList(c *gin.Context) { 68 | var query QueryBind 69 | if err := c.ShouldBind(&query); err != nil { 70 | render.ParamError(c, err.Error()) 71 | return 72 | } 73 | member := &project.Member{ 74 | UserId: c.GetInt("user_id"), 75 | } 76 | spaceIds, err := member.SpaceIdsByUserId() 77 | if err != nil { 78 | render.AppError(c, err.Error()) 79 | return 80 | } 81 | 82 | space := &project.Space{} 83 | list, err := space.List(spaceIds, query.Keyword, query.Offset, query.Limit) 84 | if err != nil { 85 | render.AppError(c, err.Error()) 86 | return 87 | } 88 | 89 | total, err := space.Total(spaceIds, query.Keyword) 90 | if err != nil { 91 | render.AppError(c, err.Error()) 92 | return 93 | } 94 | render.JSON(c, gin.H{ 95 | "list": list, 96 | "total": total, 97 | }) 98 | } 99 | 100 | func SpaceAdd(c *gin.Context) { 101 | spaceCreateOrUpdate(c, 0) 102 | } 103 | 104 | func SpaceUpdate(c *gin.Context) { 105 | id := gostring.Str2Int(c.PostForm("id")) 106 | if id == 0 { 107 | render.ParamError(c, "id cannot be empty") 108 | return 109 | } 110 | member := &project.Member{ 111 | UserId: c.GetInt("user_id"), 112 | SpaceId: id, 113 | } 114 | if in := member.MemberInSpace(); !in { 115 | render.CustomerError(c, render.CODE_ERR_NO_PRIV, "user is not in the project space") 116 | return 117 | } 118 | spaceCreateOrUpdate(c, id) 119 | } 120 | 121 | func spaceCreateOrUpdate(c *gin.Context, id int) { 122 | var spaceForm SpaceFormBind 123 | if err := c.ShouldBind(&spaceForm); err != nil { 124 | render.ParamError(c, err.Error()) 125 | return 126 | } 127 | space := &project.Space{ 128 | ID: id, 129 | Name: spaceForm.Name, 130 | Description: spaceForm.Description, 131 | } 132 | if err := space.CreateOrUpdate(); err != nil { 133 | render.AppError(c, err.Error()) 134 | return 135 | } 136 | if id == 0 { 137 | member := &project.Member{ 138 | UserId: c.GetInt("user_id"), 139 | SpaceId: space.ID, 140 | } 141 | if err := member.Create(); err != nil { 142 | render.AppError(c, err.Error()) 143 | return 144 | } 145 | } 146 | render.Success(c) 147 | } 148 | -------------------------------------------------------------------------------- /router/route/api/api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package api 6 | 7 | const ( 8 | LOGIN = "/login" 9 | LOGOUT = "/logout" 10 | LOGIN_STATUS = "/login/status" 11 | 12 | MY_USER_SETTING = "/user/my/setting" 13 | MY_USER_PASSWORD = "/user/my/password" 14 | 15 | SERVER_GROUP_ADD = "/server/group/add" 16 | SERVER_GROUP_LIST = "/server/group/list" 17 | SERVER_GROUP_DELETE = "/server/group/delete" 18 | SERVER_GROUP_DETAIL = "/server/group/detail" 19 | SERVER_GROUP_UPDATE = "/server/group/update" 20 | SERVER_ADD = "/server/add" 21 | SERVER_UPDATE = "/server/update" 22 | SERVER_LIST = "/server/list" 23 | SERVER_DELETE = "/server/delete" 24 | SERVER_DETAIL = "/server/detail" 25 | 26 | USER_ROLE_PRIV_LIST = "/user/role/privlist" 27 | USER_ROLE_ADD = "/user/role/add" 28 | USER_ROLE_UPDATE = "/user/role/update" 29 | USER_ROLE_LIST = "/user/role/list" 30 | USER_ROLE_DETAIL = "/user/role/detail" 31 | USER_ROLE_DELETE = "/user/role/delete" 32 | USER_ADD = "/user/add" 33 | USER_UPDATE = "/user/update" 34 | USER_LIST = "/user/list" 35 | USER_EXISTS = "/user/exists" 36 | USER_DETAIL = "/user/detail" 37 | USER_DELETE = "/user/delete" 38 | 39 | PROJECT_SPACE_ADD = "/project/space/add" 40 | PROJECT_SPACE_UPDATE = "/project/space/update" 41 | PROJECT_SPACE_LIST = "/project/space/list" 42 | PROJECT_SPACE_DETAIL = "/project/space/detail" 43 | PROJECT_SPACE_DELETE = "/project/space/delete" 44 | PROJECT_MEMBER_SEARCH = "/project/member/search" 45 | PROJECT_MEMBER_ADD = "/project/member/add" 46 | PROJECT_MEMBER_LIST = "/project/member/list" 47 | PROJECT_MEMBER_REMOVE = "/project/member/remove" 48 | PROJECT_ADD = "/project/add" 49 | PROJECT_UPDATE = "/project/update" 50 | PROJECT_LIST = "/project/list" 51 | PROJECT_SWITCHSTATUS = "/project/switchstatus" 52 | PROJECT_DETAIL = "/project/detail" 53 | PROJECT_DELETE = "/project/delete" 54 | PROJECT_BUILDSCRIPT = "/project/buildscript" 55 | PROJECT_HOOKSCRIPT = "/project/hookscript" 56 | 57 | DEPLOY_APPLY_PROJECT_DETAIL = "/deploy/apply/project/detail" 58 | DEPLOY_APPLY_SUBMIT = "/deploy/apply/submit" 59 | DEPLOY_APPLY_PROJECT_ALL = "/deploy/apply/project/all" 60 | DEPLOY_APPLY_LIST = "/deploy/apply/list" 61 | DEPLOY_APPLY_DETAIL = "/deploy/apply/detail" 62 | DEPLOY_APPLY_AUDIT = "/deploy/apply/audit" 63 | DEPLOY_APPLY_UPDATE = "/deploy/apply/update" 64 | DEPLOY_APPLY_DROP = "/deploy/apply/drop" 65 | DEPLOY_APPLY_ROLLBACK = "/deploy/apply/rollbacklist" 66 | DEPLOY_BUILD_START = "/deploy/build/start" 67 | DEPLOY_BUILD_STATUS = "/deploy/build/status" 68 | DEPLOY_BUILD_STOP = "/deploy/build/stop" 69 | DEPLOY_DEPLOY_START = "/deploy/deploy/start" 70 | DEPLOY_DEPLOY_STATUS = "/deploy/deploy/status" 71 | DEPLOY_DEPLOY_STOP = "/deploy/deploy/stop" 72 | DEPLOY_DEPLOY_ROLLBACK = "/deploy/deploy/rollback" 73 | ) 74 | -------------------------------------------------------------------------------- /router/server/group.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package server 6 | 7 | import ( 8 | "github.com/gin-gonic/gin" 9 | "github.com/dreamans/syncd/render" 10 | "github.com/dreamans/syncd/module/server" 11 | "github.com/dreamans/syncd/util/gostring" 12 | ) 13 | 14 | type GroupForm struct { 15 | ID int `form:"id"` 16 | Name string `form:"name" binding:"required"` 17 | } 18 | 19 | func GroupAdd(c *gin.Context) { 20 | var groupForm GroupForm 21 | if err := c.ShouldBind(&groupForm); err != nil { 22 | render.ParamError(c, err.Error()) 23 | return 24 | } 25 | group := &server.Group{ 26 | Name: groupForm.Name, 27 | } 28 | if err := group.Create(); err != nil { 29 | render.AppError(c, err.Error()) 30 | return 31 | } 32 | render.Success(c) 33 | } 34 | 35 | func GroupList(c *gin.Context) { 36 | var query QueryBind 37 | if err := c.ShouldBind(&query); err != nil { 38 | render.ParamError(c, err.Error()) 39 | return 40 | } 41 | group := &server.Group{} 42 | list, err := group.List(query.Keyword, query.Offset, query.Limit) 43 | if err != nil { 44 | render.AppError(c, err.Error()) 45 | return 46 | } 47 | total, err := group.Total(query.Keyword) 48 | if err != nil { 49 | render.AppError(c, err.Error()) 50 | return 51 | } 52 | render.JSON(c, gin.H{ 53 | "list": list, 54 | "total": total, 55 | }) 56 | } 57 | 58 | func GroupDelete(c *gin.Context) { 59 | id := gostring.Str2Int(c.PostForm("id")) 60 | if id == 0 { 61 | render.ParamError(c, "id cannot be empty") 62 | return 63 | } 64 | group := &server.Group{ 65 | ID: id, 66 | } 67 | if err := group.Delete(); err != nil { 68 | render.AppError(c, err.Error()) 69 | return 70 | } 71 | render.JSON(c, nil) 72 | } 73 | 74 | func GroupDetail(c *gin.Context) { 75 | id := gostring.Str2Int(c.Query("id")) 76 | if id == 0 { 77 | render.ParamError(c, "id cannot be empty") 78 | return 79 | } 80 | group := &server.Group{ 81 | ID: id, 82 | } 83 | if err := group.Detail(); err != nil { 84 | render.AppError(c, err.Error()) 85 | return 86 | } 87 | render.JSON(c, group) 88 | } 89 | 90 | func GroupUpdate(c *gin.Context) { 91 | var groupForm GroupForm 92 | if err := c.ShouldBind(&groupForm); err != nil { 93 | render.ParamError(c, err.Error()) 94 | return 95 | } 96 | if groupForm.ID == 0 { 97 | render.ParamError(c, "id cannot be empty") 98 | return 99 | } 100 | group := &server.Group{ 101 | ID: groupForm.ID, 102 | Name: groupForm.Name, 103 | } 104 | if err := group.Update(); err != nil { 105 | render.AppError(c, err.Error()) 106 | return 107 | } 108 | render.Success(c) 109 | } 110 | -------------------------------------------------------------------------------- /router/server/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package server 6 | 7 | import ( 8 | "github.com/gin-gonic/gin" 9 | "github.com/dreamans/syncd/render" 10 | "github.com/dreamans/syncd/module/server" 11 | "github.com/dreamans/syncd/util/gostring" 12 | ) 13 | 14 | type QueryBind struct { 15 | Keyword string `form:"keyword"` 16 | Offset int `form:"offset"` 17 | Limit int `form:"limit" binding:"required,gte=1,lte=999"` 18 | } 19 | 20 | type ServerForm struct { 21 | GroupId int `form:"group_id" binding:"required"` 22 | Name string `form:"name" binding:"required"` 23 | Ip string `form:"ip" binding:"required"` 24 | SSHPort int `form:"ssh_port" binding:"required,gte=1,lte=65535"` 25 | } 26 | 27 | func ServerAdd(c *gin.Context) { 28 | serverCreateOrUpdate(c, 0) 29 | } 30 | 31 | func ServerUpdate(c *gin.Context) { 32 | id := gostring.Str2Int(c.PostForm("id")) 33 | if id == 0 { 34 | render.ParamError(c, "id cannot be empty") 35 | return 36 | } 37 | serverCreateOrUpdate(c, id) 38 | } 39 | 40 | func serverCreateOrUpdate(c *gin.Context, id int) { 41 | var serverForm ServerForm 42 | if err := c.ShouldBind(&serverForm); err != nil { 43 | render.ParamError(c, err.Error()) 44 | return 45 | } 46 | server := &server.Server{ 47 | ID: id, 48 | GroupId: serverForm.GroupId, 49 | Name: serverForm.Name, 50 | Ip: serverForm.Ip, 51 | SSHPort: serverForm.SSHPort, 52 | } 53 | if err := server.CreateOrUpdate(); err != nil { 54 | render.AppError(c, err.Error()) 55 | return 56 | } 57 | render.Success(c) 58 | } 59 | 60 | func ServerList(c *gin.Context) { 61 | var query QueryBind 62 | if err := c.ShouldBind(&query); err != nil { 63 | render.ParamError(c, err.Error()) 64 | return 65 | } 66 | ser := &server.Server{} 67 | list, err := ser.List(query.Keyword, query.Offset, query.Limit) 68 | if err != nil { 69 | render.AppError(c, err.Error()) 70 | return 71 | } 72 | 73 | total, err := ser.Total(query.Keyword) 74 | if err != nil { 75 | render.AppError(c, err.Error()) 76 | return 77 | } 78 | render.JSON(c, gin.H{ 79 | "list": list, 80 | "total": total, 81 | }) 82 | } 83 | 84 | func ServerDelete(c *gin.Context) { 85 | id := gostring.Str2Int(c.PostForm("id")) 86 | if id == 0 { 87 | render.ParamError(c, "id cannot be empty") 88 | return 89 | } 90 | server := &server.Server{ 91 | ID: id, 92 | } 93 | if err := server.Delete(); err != nil { 94 | render.AppError(c, err.Error()) 95 | return 96 | } 97 | render.JSON(c, nil) 98 | } 99 | 100 | func ServerDetail(c *gin.Context) { 101 | id := gostring.Str2Int(c.Query("id")) 102 | if id == 0 { 103 | render.ParamError(c, "id cannot be empty") 104 | return 105 | } 106 | ser := &server.Server{ 107 | ID: id, 108 | } 109 | if err := ser.Detail(); err != nil { 110 | render.AppError(c, err.Error()) 111 | return 112 | } 113 | 114 | group := &server.Group{ 115 | ID: ser.GroupId, 116 | } 117 | if err := group.Detail(); err == nil { 118 | ser.GroupName = group.Name 119 | } 120 | 121 | render.JSON(c, ser) 122 | } 123 | -------------------------------------------------------------------------------- /router/user/login.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package user 6 | 7 | import ( 8 | "github.com/gin-gonic/gin" 9 | "github.com/dreamans/syncd/render" 10 | "github.com/dreamans/syncd/module/user" 11 | "github.com/dreamans/syncd/util/gois" 12 | 13 | ) 14 | 15 | type LoginBind struct { 16 | Username string `form:"username" binding:"required"` 17 | Password string `form:"password" binding:"required"` 18 | } 19 | 20 | func Logout(c *gin.Context) { 21 | login := &user.Login{ 22 | UserId: c.GetInt("user_id"), 23 | } 24 | if err := login.Logout(); err != nil { 25 | render.AppError(c, err.Error()) 26 | return 27 | } 28 | render.Success(c) 29 | } 30 | 31 | func Login(c *gin.Context) { 32 | var form LoginBind 33 | if err := c.ShouldBind(&form); err != nil { 34 | render.ParamError(c, err.Error()) 35 | return 36 | } 37 | 38 | login := &user.Login{ 39 | Password: form.Password, 40 | } 41 | if gois.IsEmail(form.Username) { 42 | login.Email = form.Username 43 | } else { 44 | login.Username = form.Username 45 | } 46 | 47 | if err := login.Login(); err != nil { 48 | render.CustomerError(c, render.CODE_ERR_LOGIN_FAILED , err.Error()) 49 | return 50 | } 51 | 52 | userInfo := map[string]interface{}{ 53 | "token": login.Token, 54 | } 55 | 56 | render.JSON(c, userInfo) 57 | } 58 | 59 | func LoginStatus(c *gin.Context) { 60 | privilege, _ := c.Get("privilege") 61 | render.JSON(c, gin.H{ 62 | "is_login": 1, 63 | "user_id": c.GetInt("user_id"), 64 | "username": c.GetString("username"), 65 | "email": c.GetString("email"), 66 | "truename": c.GetString("truename"), 67 | "mobile": c.GetString("mobile"), 68 | "role_name": c.GetString("role_name"), 69 | "privilege": privilege, 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /router/user/my.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package user 6 | 7 | import ( 8 | "github.com/gin-gonic/gin" 9 | "github.com/dreamans/syncd/render" 10 | "github.com/dreamans/syncd/module/user" 11 | "github.com/dreamans/syncd/util/gostring" 12 | ) 13 | 14 | type UserSettingForm struct { 15 | Truename string `form:"truename"` 16 | Mobile string `form:"mobile"` 17 | } 18 | 19 | type UserPasswordForm struct { 20 | Password string `form:"password"` 21 | NewPassword string `form:"new_password"` 22 | } 23 | 24 | func MyUserSetting(c *gin.Context) { 25 | var form UserSettingForm 26 | if err := c.ShouldBind(&form); err != nil { 27 | render.ParamError(c, err.Error()) 28 | return 29 | } 30 | 31 | u := &user.User{ 32 | ID: c.GetInt("user_id"), 33 | Truename: form.Truename, 34 | Mobile: form.Mobile, 35 | } 36 | if err := u.UserSettingUpdate(); err != nil { 37 | render.AppError(c, err.Error()) 38 | return 39 | } 40 | render.Success(c) 41 | } 42 | 43 | func MyUserPassword(c *gin.Context) { 44 | var form UserPasswordForm 45 | if err := c.ShouldBind(&form); err != nil { 46 | render.ParamError(c, err.Error()) 47 | return 48 | } 49 | user := &user.User{ 50 | ID: c.GetInt("user_id"), 51 | } 52 | if err := user.Detail(); err != nil { 53 | render.AppError(c, err.Error()) 54 | return 55 | } 56 | 57 | if user.Password != gostring.StrMd5(gostring.JoinStrings(form.Password, user.Salt)) { 58 | render.CustomerError(c, render.CODE_ERR_USER_OR_PASS_WRONG, "current password wrong") 59 | return 60 | } 61 | 62 | user.Password = form.NewPassword 63 | if err := user.UpdatePassword(); err != nil { 64 | render.AppError(c, err.Error()) 65 | return 66 | } 67 | 68 | render.Success(c) 69 | } 70 | -------------------------------------------------------------------------------- /router/user/role.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package user 6 | 7 | import ( 8 | "github.com/gin-gonic/gin" 9 | "github.com/dreamans/syncd/render" 10 | "github.com/dreamans/syncd/module/user" 11 | "github.com/dreamans/syncd/util/gostring" 12 | "github.com/dreamans/syncd/util/goslice" 13 | ) 14 | 15 | type RoleFormBind struct { 16 | ID int `form:"id"` 17 | Name string `form:"name" binding:"required"` 18 | Privilege []int `form:"privilege"` 19 | } 20 | 21 | func RolePrivList(c *gin.Context) { 22 | render.JSON(c, user.PrivList) 23 | } 24 | 25 | func RoleDelete(c *gin.Context) { 26 | id := gostring.Str2Int(c.PostForm("id")) 27 | if id == 0 { 28 | render.ParamError(c, "id cannot be empty") 29 | return 30 | } 31 | role := &user.Role{ 32 | ID: id, 33 | } 34 | if err := role.Delete(); err != nil { 35 | render.AppError(c, err.Error()) 36 | return 37 | } 38 | render.JSON(c, nil) 39 | } 40 | 41 | func RoleDetail(c *gin.Context) { 42 | id := gostring.Str2Int(c.Query("id")) 43 | if id == 0 { 44 | render.ParamError(c, "id cannot be empty") 45 | return 46 | } 47 | role := &user.Role{ 48 | ID: id, 49 | } 50 | if err := role.Detail(); err != nil { 51 | render.AppError(c, err.Error()) 52 | return 53 | } 54 | render.JSON(c, role) 55 | } 56 | 57 | func RoleList(c *gin.Context) { 58 | var query QueryBind 59 | if err := c.ShouldBind(&query); err != nil { 60 | render.ParamError(c, err.Error()) 61 | return 62 | } 63 | role := &user.Role{} 64 | list, err := role.List(query.Keyword, query.Offset, query.Limit) 65 | if err != nil { 66 | render.AppError(c, err.Error()) 67 | return 68 | } 69 | total, err := role.Total(query.Keyword) 70 | if err != nil { 71 | render.AppError(c, err.Error()) 72 | return 73 | } 74 | 75 | var roleList []map[string]interface{} 76 | for _, l := range list { 77 | roleList = append(roleList, map[string]interface{}{ 78 | "id": l.ID, 79 | "name": l.Name, 80 | "ctime": l.Ctime, 81 | }) 82 | } 83 | 84 | render.JSON(c, gin.H{ 85 | "list": roleList, 86 | "total": total, 87 | }) 88 | } 89 | 90 | func RoleAdd(c *gin.Context) { 91 | roleCreateOrUpdate(c, 0) 92 | } 93 | 94 | func RoleUpdate(c *gin.Context) { 95 | id := gostring.Str2Int(c.PostForm("id")) 96 | if id == 0 { 97 | render.ParamError(c, "id cannot be empty") 98 | return 99 | } 100 | roleCreateOrUpdate(c, id) 101 | } 102 | 103 | func roleCreateOrUpdate(c *gin.Context, id int) { 104 | var roleForm RoleFormBind 105 | if err := c.ShouldBind(&roleForm); err != nil { 106 | render.ParamError(c, err.Error()) 107 | return 108 | } 109 | role := user.Role{ 110 | ID: roleForm.ID, 111 | Name: roleForm.Name, 112 | Privilege: goslice.FilterSliceInt(roleForm.Privilege), 113 | } 114 | if err := role.CreateOrUpdate(); err != nil { 115 | render.AppError(c, err.Error()) 116 | return 117 | } 118 | render.Success(c) 119 | } 120 | -------------------------------------------------------------------------------- /script/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | syncd_remote_repo=https://github.com/dreamans/syncd.git 4 | syncd_repo_path=$HOME/.syncd-repo 5 | syncd_install_path=$( cd `dirname $0`; pwd )/syncd-deploy 6 | 7 | checkCommand() { 8 | type $1 > /dev/null 2>&1 9 | if [ $? -ne 0 ]; then 10 | echo "error: $1 must be installed" 11 | echo "install exit" 12 | exit 1 13 | fi 14 | } 15 | 16 | checkCommand "go" 17 | checkCommand "git" 18 | checkCommand "make" 19 | 20 | if [ -d ${syncd_install_path} ];then 21 | syncd_install_path=${syncd_install_path}-$( date +%Y%m%d%H%M%S ) 22 | fi 23 | 24 | rm -fr ${syncd_repo_path} 25 | git clone ${syncd_remote_repo} ${syncd_repo_path} 26 | 27 | cd ${syncd_repo_path} 28 | make 29 | 30 | rm -fr ${syncd_install_path} 31 | cp -r ${syncd_repo_path}/output ${syncd_install_path} 32 | 33 | rm -fr ${syncd_repo_path} 34 | 35 | cat << EOF 36 | 37 | Installing Syncd Path: ${syncd_install_path} 38 | Install complete. 39 | 40 | EOF -------------------------------------------------------------------------------- /sendmail.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package syncd 6 | 7 | import ( 8 | "strings" 9 | 10 | "gopkg.in/gomail.v2" 11 | ) 12 | 13 | type SendMail struct { 14 | Enable int 15 | Smtp string 16 | Port int 17 | User string 18 | Pass string 19 | dialer *gomail.Dialer 20 | } 21 | 22 | func NewSendMail(mail *SendMail) *SendMail { 23 | mail.dialer = gomail.NewPlainDialer(mail.Smtp, mail.Port, mail.User, mail.Pass) 24 | return mail 25 | } 26 | 27 | func (mail *SendMail) send(msg *SendMailMessage) error { 28 | if mail.Enable == 0 { 29 | return nil 30 | } 31 | msg.mail = mail 32 | m := msg.NewMessage() 33 | if err := mail.dialer.DialAndSend(m); err != nil { 34 | return err 35 | } 36 | return nil 37 | } 38 | 39 | func (mail *SendMail) Send(msg *SendMailMessage) { 40 | if err := mail.send(msg); err != nil { 41 | App.Logger.Error( 42 | "send mail failed, to[%s], cc[%s], subject[%s]", 43 | strings.Join(msg.To, ","), 44 | strings.Join(msg.Cc, ","), 45 | msg.Subject, 46 | ) 47 | } 48 | } 49 | 50 | func (mail *SendMail) AsyncSend(msg *SendMailMessage) { 51 | go mail.Send(msg) 52 | } 53 | 54 | type SendMailMessage struct { 55 | From string 56 | To []string 57 | Cc []string 58 | Subject string 59 | Body string 60 | Attach string 61 | mail *SendMail 62 | } 63 | 64 | func (m *SendMailMessage) NewMessage() *gomail.Message { 65 | mailMsg := gomail.NewMessage() 66 | if m.From == "" { 67 | mailMsg.SetHeader("From", m.mail.User) 68 | } else { 69 | mailMsg.SetHeader("From", m.From) 70 | } 71 | mailMsg.SetHeader("To", m.To...) 72 | if len(m.Cc) > 0 { 73 | mailMsg.SetHeader("Cc", m.Cc...) 74 | } 75 | mailMsg.SetHeader("Subject", m.Subject) 76 | mailMsg.SetBody("text/html", m.Body) 77 | if m.Attach != "" { 78 | mailMsg.Attach(m.Attach) 79 | } 80 | return mailMsg 81 | } -------------------------------------------------------------------------------- /syncd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package syncd 6 | 7 | import ( 8 | "errors" 9 | "encoding/base64" 10 | "fmt" 11 | "os" 12 | "io" 13 | 14 | "github.com/gin-gonic/gin" 15 | "github.com/dreamans/syncd/util/golog" 16 | "github.com/dreamans/syncd/util/gopath" 17 | ) 18 | 19 | var ( 20 | App *syncd 21 | ) 22 | 23 | const ( 24 | Version = "v2.0.0" 25 | ) 26 | 27 | func init() { 28 | App = newSyncd() 29 | } 30 | 31 | type syncd struct { 32 | Gin *gin.Engine 33 | DB *DB 34 | Logger *golog.Logger 35 | Mail *SendMail 36 | LocalSpace string 37 | LocalTmpSpace string 38 | LocalTarSpace string 39 | RemoteSpace string 40 | CipherKey []byte 41 | AppHost string 42 | FeServeEnable int 43 | config *Config 44 | } 45 | 46 | func newSyncd() *syncd { 47 | return &syncd{ 48 | Gin: gin.New(), 49 | } 50 | } 51 | 52 | func (s *syncd) Init(cfg *Config) error { 53 | s.config = cfg 54 | 55 | if err := s.registerOrm(); err != nil { 56 | return err 57 | } 58 | s.registerMail() 59 | s.registerLog() 60 | 61 | if err := s.initEnv(); err != nil { 62 | return err 63 | } 64 | return nil 65 | } 66 | 67 | func (s *syncd) Start() error { 68 | return s.Gin.Run(s.config.Serve.Addr) 69 | } 70 | 71 | func (s *syncd) registerOrm() error { 72 | s.DB = NewDatabase(s.config.Db) 73 | return s.DB.Open() 74 | } 75 | 76 | func (s *syncd) registerLog() { 77 | var loggerHandler io.Writer 78 | switch s.config.Log.Path { 79 | case "stdout": 80 | loggerHandler = os.Stdout 81 | case "stderr": 82 | loggerHandler = os.Stderr 83 | case "": 84 | loggerHandler = os.Stdout 85 | default: 86 | loggerHandler = golog.NewFileHandler(s.config.Log.Path) 87 | } 88 | s.Logger = golog.New(loggerHandler) 89 | } 90 | 91 | func (s *syncd) registerMail() { 92 | sendmail := &SendMail{ 93 | Enable: s.config.Mail.Enable, 94 | Smtp: s.config.Mail.Smtp, 95 | Port: s.config.Mail.Port, 96 | User: s.config.Mail.User, 97 | Pass: s.config.Mail.Pass, 98 | } 99 | s.Mail = NewSendMail(sendmail) 100 | } 101 | 102 | func (s *syncd) initEnv() error { 103 | s.AppHost = s.config.Syncd.AppHost 104 | s.FeServeEnable = s.config.Serve.FeServeEnable 105 | s.LocalSpace = s.config.Syncd.LocalSpace 106 | s.LocalTmpSpace = s.LocalSpace + "/tmp" 107 | s.LocalTarSpace = s.LocalSpace + "/tar" 108 | 109 | if err := gopath.CreatePath(s.LocalSpace); err != nil { 110 | return err 111 | } 112 | if err := gopath.CreatePath(s.LocalTmpSpace); err != nil { 113 | return err 114 | } 115 | if err := gopath.CreatePath(s.LocalTarSpace); err != nil { 116 | return err 117 | } 118 | 119 | s.RemoteSpace = s.config.Syncd.RemoteSpace 120 | if s.config.Syncd.Cipher == "" { 121 | return errors.New("syncd config 'Cipher' not setting") 122 | } 123 | dec, err := base64.StdEncoding.DecodeString(s.config.Syncd.Cipher) 124 | if err != nil { 125 | return errors.New(fmt.Sprintf("decode Cipher failed, %s", err.Error())) 126 | } 127 | s.CipherKey = dec 128 | 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /syncd.ini: -------------------------------------------------------------------------------- 1 | [syncd] 2 | ; 项目访问域名, 结尾不要加 `/` 3 | app_host = http://localhost:8878 4 | 5 | ; 工作空间 6 | local_space = /tmp/syncd_data 7 | 8 | ; 远端机器工作空间 9 | remote_space = ~/.syncd 10 | 11 | ; AES加密/解密使用的私钥 12 | ; 秘钥需要进行base64编码 13 | ; 16 => AES-128, 24 => AES-192, 32 => AES-256 14 | cipher_key = pG1L62EM0cPIIOwusQsbcV8Cs6j/M1RxLoXIZylWUC4= 15 | 16 | [serve] 17 | ; HTTP服务监听的端口 18 | addr = :8878 19 | 20 | ; 是否开启前端资源服务 21 | ; 开启后Syncd前端资源将不再依赖nginx等web服务 22 | ; 1 - 开启 23 | ; 0 - 关闭 24 | fe_serve_enable = 1 25 | 26 | ; 读超时时间设置, 单位秒 27 | read_timeout = 300 28 | 29 | ; 写超时时间设置, 单位秒 30 | write_timeout = 300 31 | 32 | ; 空闲连接超时设置, 单位秒 33 | idle_timeout = 300 34 | 35 | [database] 36 | ; 数据库连接信息 37 | ; 必须是utf8mb4编码 38 | ;unix = 39 | ;max_idle_conns = 100 40 | ;max_open_conns = 200 41 | ;conn_max_life_time = 500 42 | host = 127.0.0.1 43 | port = 3306 44 | user = syncd 45 | password = syncd 46 | dbname = syncd 47 | 48 | [log] 49 | ; 日志输出路径 50 | ; path = stdout - 打印到标准输出 51 | ; path = /path/file - 输出到文件 52 | path = stdout 53 | 54 | [mail] 55 | ; 是否开启邮件发送功能 56 | ; 0 - 关闭 57 | ; 1 - 开启 58 | enable = 0 59 | 60 | ; 邮件smtp配置 61 | smtp_host = smtp.exmail.qq.com 62 | smtp_port = 465 63 | smtp_user = 64 | smtp_pass = 65 | -------------------------------------------------------------------------------- /util/command/command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package command 6 | 7 | import ( 8 | "bytes" 9 | "os/exec" 10 | "time" 11 | "syscall" 12 | "errors" 13 | "fmt" 14 | "strings" 15 | ) 16 | 17 | const ( 18 | DEFAULT_RUM_TIMEOUT = 3600 19 | ) 20 | 21 | type Command struct { 22 | Cmd string 23 | Timeout time.Duration 24 | TerminateChan chan int 25 | Setpgid bool 26 | command *exec.Cmd 27 | stdout bytes.Buffer 28 | stderr bytes.Buffer 29 | } 30 | 31 | func NewCmd(c *Command) (*Command, error) { 32 | if c.Timeout == 0 * time.Second { 33 | c.Timeout = DEFAULT_RUM_TIMEOUT * time.Second 34 | } 35 | if c.TerminateChan == nil { 36 | c.TerminateChan = make(chan int) 37 | } 38 | cmd := exec.Command("/bin/bash", "-c", c.Cmd) 39 | if c.Setpgid { 40 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 41 | } 42 | cmd.Stderr = &c.stderr 43 | cmd.Stdout = &c.stdout 44 | c.command = cmd 45 | 46 | return c, nil 47 | } 48 | 49 | func (c *Command) Run() error { 50 | if err := c.command.Start(); err != nil { 51 | return err 52 | } 53 | 54 | errChan := make(chan error) 55 | go func(){ 56 | errChan <- c.command.Wait() 57 | defer close(errChan) 58 | }() 59 | 60 | var err error 61 | select { 62 | case err = <-errChan: 63 | case <-time.After(c.Timeout): 64 | err = c.terminate() 65 | if err == nil { 66 | err = errors.New(fmt.Sprintf("cmd run timeout, cmd [%s], time[%v]", c.Cmd, c.Timeout)) 67 | } 68 | case <-c.TerminateChan: 69 | err = c.terminate() 70 | if err == nil { 71 | err = errors.New(fmt.Sprintf("cmd is terminated, cmd [%s]", c.Cmd)) 72 | } 73 | } 74 | return err 75 | } 76 | 77 | func (c *Command) Stderr() string { 78 | return strings.TrimSpace(string(c.stderr.Bytes())) 79 | } 80 | 81 | func (c *Command) Stdout() string { 82 | return strings.TrimSpace(string(c.stdout.Bytes())) 83 | } 84 | 85 | func (c *Command) terminate() error { 86 | if c.Setpgid { 87 | return syscall.Kill(-c.command.Process.Pid, syscall.SIGKILL) 88 | } else { 89 | return syscall.Kill(c.command.Process.Pid, syscall.SIGKILL) 90 | } 91 | } -------------------------------------------------------------------------------- /util/command/command_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package command 6 | 7 | import ( 8 | "testing" 9 | "time" 10 | "strings" 11 | ) 12 | 13 | func TestCmdRun(t *testing.T) { 14 | c := &Command{ 15 | Cmd: "echo 'syncd'", 16 | Timeout: time.Second * 60, 17 | } 18 | var err error 19 | if c, err = NewCmd(c); err != nil { 20 | t.Error(err) 21 | } 22 | if err = c.Run(); err != nil { 23 | t.Error(err) 24 | } 25 | output := c.Stdout() 26 | if output != "syncd" { 27 | t.Errorf("cmd `echo syncd` output '%s' not eq 'syncd'", output) 28 | } 29 | } 30 | 31 | func TestCmdTimeout(t *testing.T) { 32 | c := &Command{ 33 | Cmd: "sleep 2", 34 | Timeout: time.Second * 1, 35 | } 36 | var err error 37 | if c, err = NewCmd(c); err != nil { 38 | t.Error(err) 39 | } 40 | err = c.Run() 41 | if err == nil { 42 | t.Error("cmd should run timeout and output errmsg, but err is nil") 43 | } 44 | if strings.Index(err.Error(), "cmd run timeout") == -1 { 45 | t.Errorf("cmd run timeout output '%s' prefix not eq 'cmd run timeout'", err.Error()) 46 | } 47 | } 48 | 49 | func TestCmdTerminate(t *testing.T) { 50 | tChan := make(chan int) 51 | c := &Command{ 52 | Cmd: "sleep 5", 53 | TerminateChan: tChan, 54 | } 55 | var err error 56 | if c, err = NewCmd(c); err != nil { 57 | t.Error(err) 58 | } 59 | go func() { 60 | err = c.Run() 61 | if err == nil { 62 | t.Error("cmd should be terminated and output errmsg, but err is nil") 63 | } 64 | if strings.Index(err.Error(), "cmd is terminated") == -1 { 65 | t.Errorf("cmd is terminated output '%s' prefix not eq 'cmd is terminated'", err.Error()) 66 | } 67 | }() 68 | 69 | tChan<- 1 70 | } 71 | 72 | -------------------------------------------------------------------------------- /util/command/task.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package command 6 | 7 | import ( 8 | "errors" 9 | "time" 10 | ) 11 | 12 | type Task struct { 13 | Commands []string 14 | done bool 15 | timeout int 16 | termChan chan int 17 | err error 18 | result []*TaskResult 19 | } 20 | 21 | type TaskResult struct { 22 | Cmd string `json:"cmd"` 23 | Stdout string `json:"stdout"` 24 | Stderr string `json:"stderr"` 25 | Success bool `json:"success"` 26 | } 27 | 28 | func NewTask(cmds []string, timeout int) *Task { 29 | return &Task{ 30 | Commands: cmds, 31 | termChan: make(chan int), 32 | timeout: timeout, 33 | } 34 | } 35 | 36 | func (t *Task) Run() { 37 | for _, cmd := range t.Commands { 38 | result, err := t.next(cmd) 39 | t.result = append(t.result, result) 40 | if err != nil { 41 | t.err = errors.New("task run failed, " + err.Error()) 42 | break 43 | } 44 | } 45 | t.done = true 46 | } 47 | 48 | func (t *Task) GetError() error { 49 | return t.err 50 | } 51 | 52 | func (t *Task) Result() []*TaskResult { 53 | return t.result 54 | } 55 | 56 | func (t *Task) next(cmd string) (*TaskResult, error) { 57 | result := &TaskResult{ 58 | Cmd: cmd, 59 | } 60 | command := &Command{ 61 | Cmd: cmd, 62 | Timeout: time.Second * time.Duration(t.timeout), 63 | TerminateChan: t.termChan, 64 | } 65 | var err error 66 | command, err = NewCmd(command) 67 | if err != nil { 68 | return result, err 69 | } 70 | if err := command.Run(); err != nil { 71 | result.Stderr = command.Stderr() 72 | return result, err 73 | } 74 | result.Stdout = command.Stdout() 75 | result.Success = true 76 | return result, nil 77 | } 78 | 79 | func (t *Task) Terminate() { 80 | if !t.done { 81 | t.termChan <- 1 82 | } 83 | } -------------------------------------------------------------------------------- /util/command/task_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package command 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func TestTaskRun(t *testing.T) { 12 | cmds := []string{ 13 | "echo 'syncd'", 14 | "whoami", 15 | "date", 16 | } 17 | task := TaskNew(cmds, 10) 18 | task.Run() 19 | if err := task.GetError(); err != nil { 20 | t.Errorf("cmd task running error: %s", err.Error()) 21 | } 22 | } -------------------------------------------------------------------------------- /util/goaes/aes.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // license at https://github.com/snail007/goproxy/blob/master/LICENSE 5 | 6 | package goaes 7 | 8 | import ( 9 | "bytes" 10 | "crypto/aes" 11 | "crypto/cipher" 12 | "crypto/rand" 13 | "errors" 14 | "io" 15 | ) 16 | 17 | func Encrypt(key []byte, text []byte) ([]byte, error) { 18 | block, err := aes.NewCipher(key) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | msg := pad(text) 24 | ciphertext := make([]byte, aes.BlockSize+len(msg)) 25 | iv := ciphertext[:aes.BlockSize] 26 | if _, err := io.ReadFull(rand.Reader, iv); err != nil { 27 | return nil, err 28 | } 29 | 30 | cfb := cipher.NewCFBEncrypter(block, iv) 31 | cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(msg)) 32 | 33 | return ciphertext, nil 34 | } 35 | 36 | func Decrypt(key []byte, text []byte) ([]byte, error) { 37 | block, err := aes.NewCipher(key) 38 | if err != nil { 39 | return nil, err 40 | } 41 | if (len(text) % aes.BlockSize) != 0 { 42 | return nil, errors.New("blocksize must be multipe of decoded message length") 43 | } 44 | iv := text[:aes.BlockSize] 45 | msg := text[aes.BlockSize:] 46 | 47 | cfb := cipher.NewCFBDecrypter(block, iv) 48 | cfb.XORKeyStream(msg, msg) 49 | 50 | unpadMsg, err := unpad(msg) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | return unpadMsg, nil 56 | } 57 | 58 | func pad(src []byte) []byte { 59 | padding := aes.BlockSize - len(src)%aes.BlockSize 60 | padtext := bytes.Repeat([]byte{byte(padding)}, padding) 61 | return append(src, padtext...) 62 | } 63 | 64 | func unpad(src []byte) ([]byte, error) { 65 | length := len(src) 66 | unpadding := int(src[length-1]) 67 | 68 | if unpadding > length { 69 | return nil, errors.New("unpad error. This could happen when incorrect encryption key is used") 70 | } 71 | 72 | return src[:(length - unpadding)], nil 73 | } -------------------------------------------------------------------------------- /util/gofile/file.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package gofile 6 | 7 | import ( 8 | "io/ioutil" 9 | "os" 10 | ) 11 | 12 | func CreateFile(filePath string, data []byte, perm os.FileMode) error { 13 | return ioutil.WriteFile(filePath, data, perm) 14 | } 15 | -------------------------------------------------------------------------------- /util/gois/is.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package gois 6 | 7 | import ( 8 | "strings" 9 | "net" 10 | "regexp" 11 | ) 12 | 13 | func IsInteger(val interface{}) bool { 14 | switch val.(type) { 15 | case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: 16 | case string: 17 | str := val.(string) 18 | if str == "" { 19 | return false 20 | } 21 | str = strings.TrimSpace(str) 22 | if str[0] == '-' || str[0] == '+' { 23 | if len(str) == 1 { 24 | return false 25 | } 26 | str = str[1:] 27 | } 28 | for _, v := range str { 29 | if v < '0' || v > '9' { 30 | return false 31 | } 32 | } 33 | } 34 | return true 35 | } 36 | 37 | func IsIp(s string) bool { 38 | ip := net.ParseIP(s) 39 | if ip == nil { 40 | return false 41 | } 42 | return true 43 | } 44 | 45 | func IsEmail(s string) bool { 46 | pattern := `^[0-9A-Za-z][\.\-_0-9A-Za-z]*\@[0-9A-Za-z\-]+(\.[0-9A-Za-z]+)+$` 47 | reg := regexp.MustCompile(pattern) 48 | return reg.MatchString(s) 49 | } 50 | -------------------------------------------------------------------------------- /util/golog/file.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package golog 6 | 7 | import ( 8 | "os" 9 | "sync" 10 | ) 11 | 12 | type FileHandler struct { 13 | filename string 14 | file *os.File 15 | mu sync.Mutex 16 | } 17 | 18 | func NewFileHandler(path string) *FileHandler { 19 | return &FileHandler{ 20 | filename: path, 21 | } 22 | } 23 | 24 | func (f *FileHandler) Write(p []byte) (n int, err error) { 25 | f.mu.Lock() 26 | defer f.mu.Unlock() 27 | 28 | if err = f.openFileHandler(); err != nil { 29 | return 0, err 30 | } 31 | 32 | return f.file.Write(p) 33 | } 34 | 35 | func (f *FileHandler) Close() error { 36 | f.mu.Lock() 37 | defer f.mu.Unlock() 38 | return f.close() 39 | } 40 | 41 | func (f *FileHandler) close() error { 42 | if f.file == nil { 43 | return nil 44 | } 45 | err := f.file.Close() 46 | f.file = nil 47 | return err 48 | } 49 | 50 | func (f *FileHandler) openFileHandler() error { 51 | file, err := os.OpenFile(f.filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0755) 52 | if err != nil { 53 | return err 54 | } 55 | f.file = file 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /util/golog/log.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package golog 6 | 7 | import ( 8 | "io" 9 | "os" 10 | "fmt" 11 | "time" 12 | "bytes" 13 | "strings" 14 | "sync" 15 | ) 16 | 17 | const ( 18 | LEVEL_DEBUG = iota 19 | LEVEL_INFO 20 | LEVEL_NOTICE 21 | LEVEL_WARNING 22 | LEVEL_ERROR 23 | LEVEL_PANIC 24 | ) 25 | 26 | var levelMessage = map[int]string{ 27 | LEVEL_DEBUG: "debug", 28 | LEVEL_INFO: "info", 29 | LEVEL_NOTICE: "notice", 30 | LEVEL_WARNING: "warning", 31 | LEVEL_ERROR: "error", 32 | LEVEL_PANIC: "panic", 33 | } 34 | 35 | type Logger struct { 36 | output io.Writer 37 | mu sync.Mutex 38 | } 39 | 40 | func New(out io.Writer) *Logger { 41 | logger := &Logger{ 42 | output: out, 43 | } 44 | return logger 45 | } 46 | 47 | var stdLog = New(os.Stderr) 48 | 49 | func (l *Logger) Output(level int, s string) error { 50 | levelMsg, _ := levelMessage[level] 51 | var buf bytes.Buffer 52 | buf.WriteByte('[') 53 | buf.WriteString(time.Now().Format("2006-01-02 15:04:05")) 54 | buf.WriteString("] ") 55 | buf.WriteString(strings.ToUpper(levelMsg)) 56 | buf.WriteString(": ") 57 | buf.WriteString(s) 58 | if len(s) == 0 || s[len(s)-1] != '\n' { 59 | buf.WriteByte('\n') 60 | } 61 | l.output.Write(buf.Bytes()) 62 | return nil 63 | } 64 | 65 | func (l *Logger) SetOutput(w io.Writer) { 66 | l.mu.Lock() 67 | defer l.mu.Unlock() 68 | l.output = w 69 | } 70 | 71 | func (l *Logger) GetOutput() io.Writer { 72 | return l.output 73 | } 74 | 75 | func (l *Logger) Debug(s string, v ...interface{}) { 76 | l.Output(LEVEL_DEBUG, fmt.Sprintf(s, v...)) 77 | } 78 | 79 | func (l *Logger) Info(s string, v ...interface{}) { 80 | l.Output(LEVEL_INFO, fmt.Sprintf(s, v...)) 81 | } 82 | 83 | func (l *Logger) Notice(s string, v ...interface{}) { 84 | l.Output(LEVEL_NOTICE, fmt.Sprintf(s, v...)) 85 | } 86 | 87 | func (l *Logger) Warning(s string, v ...interface{}) { 88 | l.Output(LEVEL_WARNING, fmt.Sprintf(s, v...)) 89 | } 90 | 91 | func (l *Logger) Error(s string, v ...interface{}) { 92 | l.Output(LEVEL_ERROR, fmt.Sprintf(s, v...)) 93 | } 94 | 95 | func (l *Logger) Panic(s string, v ...interface{}) { 96 | l.Output(LEVEL_PANIC, fmt.Sprintf(s, v...)) 97 | panic(s) 98 | } 99 | 100 | func SetOutput(w io.Writer) { 101 | stdLog.SetOutput(w) 102 | } 103 | 104 | func Debug(s string, v ...interface{}) { 105 | stdLog.Debug(s, v...) 106 | } 107 | 108 | func Info(s string, v ...interface{}) { 109 | stdLog.Info(s, v...) 110 | } 111 | 112 | func Notice(s string, v ...interface{}) { 113 | stdLog.Notice(s, v...) 114 | } 115 | 116 | func Warning(s string, v ...interface{}) { 117 | stdLog.Warning(s, v...) 118 | } 119 | 120 | func Error(s string, v ...interface{}) { 121 | stdLog.Error(s, v...) 122 | } 123 | 124 | func Panic(s string, v ...interface{}) { 125 | stdLog.Panic(s, v...) 126 | } -------------------------------------------------------------------------------- /util/gopath/path.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package gopath 6 | 7 | import ( 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | func CurrentPath() (string, error) { 14 | dir, err := filepath.Abs(filepath.Dir(os.Args[0])) 15 | if err != nil { 16 | return "", err 17 | } 18 | return dir, nil 19 | } 20 | 21 | func CurrentParentPath() (string, error) { 22 | path := strings.Join([]string{filepath.Dir(os.Args[0]), "/../"}, "") 23 | realPath, err := filepath.Abs(path) 24 | if err != nil { 25 | return "", err 26 | } 27 | return realPath, nil 28 | } 29 | 30 | func CreatePath(path string) error { 31 | exists := Exists(path) 32 | if !exists { 33 | if err := os.Mkdir(path, os.ModePerm); err != nil { 34 | return err 35 | } 36 | } 37 | return nil 38 | } 39 | 40 | func Exists(path string) bool { 41 | _, err := os.Stat(path) 42 | if err == nil { 43 | return true 44 | } 45 | if os.IsExist(err) { 46 | return true 47 | } 48 | return false 49 | } 50 | 51 | func IsDir(path string) bool { 52 | s, err := os.Stat(path) 53 | if err != nil { 54 | return false 55 | } 56 | return s.IsDir() 57 | } 58 | 59 | func IsFile(path string) bool { 60 | return !IsDir(path) 61 | } 62 | 63 | func RemovePath(path string) error { 64 | return os.RemoveAll(path) 65 | } 66 | -------------------------------------------------------------------------------- /util/goslice/slice.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package goslice 6 | 7 | func FilterSliceInt(sl []int) []int { 8 | var newSlice []int 9 | for _, s := range sl { 10 | if s != 0 { 11 | newSlice = append(newSlice, s) 12 | } 13 | } 14 | return newSlice 15 | } 16 | 17 | func InSlice(v interface{}, sl []interface{}) bool { 18 | for _, vv := range sl { 19 | if vv == v { 20 | return true 21 | } 22 | } 23 | return false 24 | } 25 | 26 | func InSliceInt(v int, sl []int) bool { 27 | for _, vv := range sl { 28 | if vv == v { 29 | return true 30 | } 31 | } 32 | return false 33 | } 34 | 35 | func InSliceString(v string, sl []string) bool { 36 | for _, vv := range sl { 37 | if vv == v { 38 | return true 39 | } 40 | } 41 | return false 42 | } 43 | 44 | func SliceIntersect(slice1, slice2 []interface{}) (diffslice []interface{}) { 45 | for _, v := range slice1 { 46 | if InSlice(v, slice2) { 47 | diffslice = append(diffslice, v) 48 | } 49 | } 50 | return 51 | } 52 | 53 | func SliceIntersectInt(slice1, slice2 []int) (diffslice []int) { 54 | for _, v := range slice1 { 55 | if InSliceInt(v, slice2) { 56 | diffslice = append(diffslice, v) 57 | } 58 | } 59 | return 60 | } 61 | 62 | func SliceComplementaryInt(slice1, slice2 []int) (diffslice []int) { 63 | for _, v := range slice1 { 64 | if !InSliceInt(v, slice2) { 65 | diffslice = append(diffslice, v) 66 | } 67 | } 68 | return 69 | } -------------------------------------------------------------------------------- /util/gostring/string.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 syncd Author. All Rights Reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package gostring 6 | 7 | import ( 8 | "strings" 9 | "strconv" 10 | "math/rand" 11 | "time" 12 | "crypto/md5" 13 | "encoding/hex" 14 | "encoding/base64" 15 | "encoding/json" 16 | ) 17 | 18 | func JoinStrings(multiString ...string) string { 19 | return strings.Join(multiString, "") 20 | } 21 | 22 | func JoinIntSlice2String(intSlice []int, sep string) string { 23 | return strings.Join(IntSlice2StrSlice(intSlice), sep) 24 | } 25 | 26 | func StrSplit2IntSlice(str, sep string) []int { 27 | return StrSlice2IntSlice(StrFilterSliceEmpty(strings.Split(str, sep))) 28 | } 29 | 30 | func Str2StrSlice(str, sep string) []string { 31 | return StrFilterSliceEmpty(strings.Split(str, sep)) 32 | } 33 | 34 | func StrSlice2IntSlice(strSlice []string) []int { 35 | var intSlice []int 36 | for _, s := range strSlice { 37 | i, _ := strconv.Atoi(s) 38 | intSlice = append(intSlice, i) 39 | } 40 | return intSlice 41 | } 42 | 43 | func StrFilterSliceEmpty(strSlice []string) []string { 44 | var filterSlice []string 45 | for _, s := range strSlice { 46 | ss := strings.TrimSpace(s) 47 | if ss != "" { 48 | filterSlice = append(filterSlice, ss) 49 | } 50 | } 51 | return filterSlice 52 | } 53 | 54 | func IntSlice2StrSlice(intSlice []int) []string { 55 | var strSlice []string 56 | for _, i := range intSlice { 57 | s := strconv.Itoa(i) 58 | strSlice = append(strSlice, s) 59 | } 60 | return strSlice 61 | } 62 | 63 | func Str2Int(s string) int { 64 | i, _ := strconv.Atoi(s) 65 | return i 66 | } 67 | 68 | func Int2Str(i int) string { 69 | return strconv.Itoa(i) 70 | } 71 | 72 | func StrRandom(l int) string { 73 | str := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 74 | bytes := []byte(str) 75 | result := []byte{} 76 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 77 | for i := 0; i < l; i++ { 78 | result = append(result, bytes[r.Intn(len(bytes))]) 79 | } 80 | return string(result) 81 | } 82 | 83 | func StrMd5(s string) string { 84 | md5Ctx := md5.New() 85 | md5Ctx.Write([]byte(s)) 86 | return hex.EncodeToString(md5Ctx.Sum(nil)) 87 | } 88 | 89 | func Base64Encode(b []byte) string { 90 | return base64.StdEncoding.EncodeToString(b) 91 | } 92 | 93 | func Base64Decode(s string) ([]byte, error) { 94 | ds, err := base64.StdEncoding.DecodeString(s) 95 | return ds, err 96 | } 97 | 98 | func Base64UrlEncode(b []byte) string { 99 | return base64.URLEncoding.EncodeToString(b) 100 | } 101 | 102 | func Base64UrlDecode(s string) ([]byte, error) { 103 | ds, err := base64.URLEncoding.DecodeString(s) 104 | return ds, err 105 | } 106 | 107 | func JsonEncode(obj interface{}) []byte { 108 | b, _ := json.Marshal(obj) 109 | return b 110 | } 111 | 112 | func JsonDecode(data []byte, obj interface{}) { 113 | json.Unmarshal(data, obj) 114 | } 115 | -------------------------------------------------------------------------------- /web/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # web 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn run build 16 | ``` 17 | 18 | ### Run your tests 19 | ``` 20 | yarn run test 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | yarn run lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /web/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "axios": "^0.19.1", 11 | "blueimp-md5": "^2.10.0", 12 | "codemirror": "^5.43.0", 13 | "element-ui": "^2.4.11", 14 | "js-cookie": "^2.2.0", 15 | "moment": "^2.24.0", 16 | "qs": "^6.6.0", 17 | "vue": "^2.5.21", 18 | "vue-i18n": "^8.7.0", 19 | "vue-router": "^3.0.1", 20 | "vuex": "^3.0.1" 21 | }, 22 | "devDependencies": { 23 | "@fortawesome/fontawesome-free": "^5.6.3", 24 | "@vue/cli-plugin-babel": "^3.3.0", 25 | "@vue/cli-service": "^3.3.0", 26 | "node-sass": "^4.9.0", 27 | "sass-loader": "^7.0.1", 28 | "vue-template-compiler": "^2.5.21" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamans/syncd/71876dc6d3e7caa40e44a33ad47f8aa392881d40/web/public/favicon.ico -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Syncd - 自动化发布系统 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /web/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /web/src/api/deploy.js: -------------------------------------------------------------------------------- 1 | import {get, post} from '@/lib/fetch.js' 2 | 3 | export function applyProjectDetailApi(params) { 4 | return get('/deploy/apply/project/detail', params) 5 | } 6 | 7 | export function applySubmitApi(data) { 8 | return post('/deploy/apply/submit', data) 9 | } 10 | 11 | export function applyProjectAllApi() { 12 | return get('/deploy/apply/project/all') 13 | } 14 | 15 | export function applyListApi(params) { 16 | return get('/deploy/apply/list', params) 17 | } 18 | 19 | export function applyDetailApi(params) { 20 | return get('/deploy/apply/detail', params) 21 | } 22 | 23 | export function applyAuditApi(data) { 24 | return post('/deploy/apply/audit', data) 25 | } 26 | 27 | export function applyUpdateApi(data) { 28 | return post('/deploy/apply/update', data) 29 | } 30 | 31 | export function applyDropApi(data) { 32 | return post('/deploy/apply/drop', data) 33 | } 34 | 35 | export function applyRollbackListApi(params) { 36 | return get('/deploy/apply/rollbacklist', params) 37 | } 38 | 39 | export function buildStartApi(data) { 40 | return post('/deploy/build/start', data) 41 | } 42 | 43 | export function buildStatusApi(params) { 44 | return get('/deploy/build/status', params) 45 | } 46 | 47 | export function buildStopApi(data) { 48 | return post('/deploy/build/stop', data) 49 | } 50 | 51 | export function deployStart(data) { 52 | return post('/deploy/deploy/start', data) 53 | } 54 | 55 | export function deployStatusApi(params) { 56 | return get('/deploy/deploy/status', params) 57 | } 58 | 59 | export function deployStopApi(data) { 60 | return post('/deploy/deploy/stop', data) 61 | } 62 | 63 | export function deployRollbackApi(data) { 64 | return post('/deploy/deploy/rollback', data) 65 | } -------------------------------------------------------------------------------- /web/src/api/login.js: -------------------------------------------------------------------------------- 1 | import {get, post} from '@/lib/fetch.js' 2 | 3 | export function loginApi(data) { 4 | return post('/login', data) 5 | } 6 | 7 | export function loginStatusApi() { 8 | return get("/login/status") 9 | } 10 | 11 | export function logoutApi() { 12 | return post('/logout') 13 | } -------------------------------------------------------------------------------- /web/src/api/project.js: -------------------------------------------------------------------------------- 1 | import {get, post} from '@/lib/fetch.js' 2 | 3 | export function newSpaceApi(data) { 4 | return post('/project/space/add', data) 5 | } 6 | 7 | export function updateSpaceApi(data) { 8 | return post('/project/space/update', data) 9 | } 10 | 11 | export function listSpaceApi(params) { 12 | return get('/project/space/list', params) 13 | } 14 | 15 | export function detailSpaceApi(params) { 16 | return get('/project/space/detail', params) 17 | } 18 | 19 | export function deleteSpaceApi(data) { 20 | return post('/project/space/delete', data) 21 | } 22 | 23 | export function searchMemberApi(params) { 24 | return get('/project/member/search', params) 25 | } 26 | 27 | export function addMemberApi(data) { 28 | return post('/project/member/add', data) 29 | } 30 | 31 | export function listMemberApi(params) { 32 | return get('/project/member/list', params) 33 | } 34 | 35 | export function removeMemberApi(data) { 36 | return post('/project/member/remove', data) 37 | } 38 | 39 | export function newProjectApi(data) { 40 | return post('/project/add', data) 41 | } 42 | 43 | export function updateProjectApi(data) { 44 | return post('/project/update', data) 45 | } 46 | 47 | export function listProjectApi(params) { 48 | return get('/project/list', params) 49 | } 50 | 51 | export function switchStatusProjectApi(data) { 52 | return post('/project/switchstatus', data) 53 | } 54 | 55 | export function detailProjectApi(params) { 56 | return get('/project/detail', params) 57 | } 58 | 59 | export function deleteProjectApi(data) { 60 | return post('/project/delete', data) 61 | } 62 | 63 | export function updateBuildScriptApi(data) { 64 | return post('/project/buildscript', data) 65 | } 66 | 67 | export function updateHookScriptApi(data) { 68 | return post('/project/hookscript', data) 69 | } 70 | -------------------------------------------------------------------------------- /web/src/api/server.js: -------------------------------------------------------------------------------- 1 | import {get, post} from '@/lib/fetch.js' 2 | 3 | export function newGroupApi(data) { 4 | return post('/server/group/add', data) 5 | } 6 | 7 | export function listGroupApi(params) { 8 | return get('/server/group/list', params) 9 | } 10 | 11 | export function deleteGroupApi(data) { 12 | return post('/server/group/delete', data) 13 | } 14 | 15 | export function detailGroupApi(params) { 16 | return get('/server/group/detail', params) 17 | } 18 | 19 | export function updateGroupApi(data) { 20 | return post('/server/group/update', data) 21 | } 22 | 23 | export function newServerApi(data) { 24 | return post('/server/add', data) 25 | } 26 | 27 | export function updateServerApi(data) { 28 | return post('/server/update', data) 29 | } 30 | 31 | export function listServerApi(params) { 32 | return get('/server/list', params) 33 | } 34 | 35 | export function deleteServerApi(data) { 36 | return post('/server/delete', data) 37 | } 38 | 39 | export function detailServerApi(params) { 40 | return get('/server/detail', params) 41 | } -------------------------------------------------------------------------------- /web/src/api/user.js: -------------------------------------------------------------------------------- 1 | import {get, post} from '@/lib/fetch.js' 2 | 3 | export function privListApi() { 4 | return get('/user/role/privlist') 5 | } 6 | 7 | export function newRoleApi(data) { 8 | return post('/user/role/add', data) 9 | } 10 | 11 | export function listRoleApi(params) { 12 | return get('/user/role/list', params) 13 | } 14 | 15 | export function detailRoleApi(params) { 16 | return get('/user/role/detail', params) 17 | } 18 | 19 | export function updateRoleApi(data) { 20 | return post('/user/role/update', data) 21 | } 22 | 23 | export function deleteRoleApi(data) { 24 | return post('/user/role/delete', data) 25 | } 26 | 27 | export function newUserApi(data) { 28 | return post('/user/add', data) 29 | } 30 | 31 | export function updateUserApi(data) { 32 | return post('/user/update', data) 33 | } 34 | 35 | export function listUserApi(params) { 36 | return get('/user/list', params) 37 | } 38 | 39 | export function existsUserApi(params) { 40 | return get('/user/exists', params) 41 | } 42 | 43 | export function detailUserApi(params) { 44 | return get('/user/detail', params) 45 | } 46 | 47 | export function deleteUserApi(data) { 48 | return post('/user/delete', data) 49 | } 50 | 51 | export function userSettingApi(data) { 52 | return post('/user/my/setting', data) 53 | } 54 | 55 | export function userPasswordApi(data) { 56 | return post('/user/my/password', data) 57 | } 58 | -------------------------------------------------------------------------------- /web/src/asset/login_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamans/syncd/71876dc6d3e7caa40e44a33ad47f8aa392881d40/web/src/asset/login_bg.jpg -------------------------------------------------------------------------------- /web/src/asset/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreamans/syncd/71876dc6d3e7caa40e44a33ad47f8aa392881d40/web/src/asset/logo.png -------------------------------------------------------------------------------- /web/src/component/ScrollBar/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 42 | 43 | 55 | -------------------------------------------------------------------------------- /web/src/lang/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | } -------------------------------------------------------------------------------- /web/src/lang/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | 4 | Vue.use(VueI18n) 5 | 6 | import zh from './zh_cn.js' 7 | import en from './en.js' 8 | const messages = { 9 | 'zh-cn': zh, 10 | 'en': en, 11 | } 12 | 13 | let localeLang 14 | if (global.navigator.language) { 15 | localeLang = global.navigator.language 16 | localeLang = localeLang.toLowerCase() 17 | } 18 | if (localeLang.indexOf('en') != 0) { 19 | localeLang = 'zh-cn' 20 | } 21 | 22 | const i18n = new VueI18n({ 23 | locale: localeLang, 24 | messages, 25 | }) 26 | 27 | export default i18n -------------------------------------------------------------------------------- /web/src/lib/code.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'CODE_ERR_NETWORK': -1, 3 | 'CODE_OK': 0, 4 | 'CODE_ERR_SYSTEM': 1000, 5 | 'CODE_ERR_APP': 1001, 6 | 'CODE_ERR_PARAM': 1002, 7 | 'CODE_ERR_DATA_REPEAT': 1003, 8 | 'CODE_ERR_LOGIN_FAILED': 1004, 9 | 'CODE_ERR_NO_LOGIN': 1005, 10 | 'CODE_ERR_NO_PRIV': 1006, 11 | 'CODE_ERR_TASK_ERROR': 1007, 12 | 'CODE_ERR_USER_OR_PASS_WRONG': 1008, 13 | 'CODE_ERR_NO_DATA': 1009, 14 | } -------------------------------------------------------------------------------- /web/src/lib/data.js: -------------------------------------------------------------------------------- 1 | import priv from '@/lib/priv' 2 | 3 | export default { 4 | PageSize: 0, 5 | Page: 0, 6 | Total: 0, 7 | DialogSmallWidth: '500px', 8 | DialogNormalWidth: '750px', 9 | DialogLargeWidth: '900px', 10 | DialogNormalTop: '5vh', 11 | Priv: priv, 12 | 13 | BuildStatusNone: 0, 14 | BuildStatusStart: 1, 15 | BuildStatusSuccess: 2, 16 | BuildStatusFailed: 3, 17 | 18 | ApplyStatusNone: 1, 19 | ApplyStatusIng: 2, 20 | ApplyStatusSuccess: 3, 21 | ApplyStatusFailed: 4, 22 | ApplyStatusDrop: 5, 23 | ApplyStatusRollback: 6, 24 | 25 | DeployModeBranch: 1, 26 | DeployModelTag: 2, 27 | 28 | BuildStatusNone: 0, 29 | BuildStatusStart: 1, 30 | BuildStatusSuccess: 2, 31 | BuildStatusFailed: 3, 32 | 33 | DeployGroupStatusNone: 0, 34 | DeployGroupStatusStart: 1, 35 | DeployGroupStatusSuccess: 2, 36 | DeployGroupStatusFailed: 3, 37 | 38 | AuditStatusPending: 1, 39 | AuditStatusOk: 2, 40 | AuditStatusRefuse: 3, 41 | } -------------------------------------------------------------------------------- /web/src/lib/fetch.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import qs from 'qs' 3 | import Vue from 'vue' 4 | import i18n from '@/lang' 5 | import Code from './code.js' 6 | import Router from '@/router' 7 | 8 | let API_URL = '/api' 9 | let CancelToken = axios.CancelToken 10 | 11 | Vue.prototype.$CancelAjaxRequet = function() {} 12 | Vue.prototype.$IsCancel = axios.isCancel 13 | 14 | const service = axios.create({ 15 | baseURL: API_URL + '/', 16 | timeout: 60000, 17 | withCredentials: true, 18 | }) 19 | 20 | service.interceptors.request.use(config => { 21 | return config 22 | }, error => { 23 | Promise.reject(error) 24 | }) 25 | 26 | service.interceptors.response.use(response => { 27 | let res = response.data 28 | if (!res) { 29 | res = { 30 | code: -1, 31 | message: i18n.t("network_error"), 32 | } 33 | } 34 | if (res.code != 0) { 35 | switch (res.code) { 36 | case Code.CODE_ERR_NETWORK: 37 | case Code.CODE_ERR_SYSTEM: 38 | case Code.CODE_ERR_APP: 39 | case Code.CODE_ERR_PARAM: 40 | Vue.prototype.$message.error(res.message) 41 | break 42 | case Code.CODE_ERR_NO_PRIV: 43 | Vue.prototype.$message.error('无操作权限') 44 | //Router.push({name: 'dashboard'}) 45 | break 46 | case Code.CODE_ERR_NO_LOGIN: 47 | Vue.prototype.$message({ 48 | message: '用户未登录', 49 | type: 'error', 50 | duration: 1000, 51 | onClose: () => { 52 | Router.push({name: 'login'}) 53 | }, 54 | }) 55 | break 56 | } 57 | return Promise.reject(res) 58 | } 59 | return res.data 60 | }, error => { 61 | if (!axios.isCancel(error)) { 62 | let res = { 63 | code: -1, 64 | message: error.message ? error.message : i18n.t("unknown_error"), 65 | } 66 | Vue.prototype.$message.error(res.message) 67 | return Promise.reject(res) 68 | } 69 | return Promise.reject(error) 70 | }) 71 | 72 | export function post(url, data, params, headers) { 73 | if (!params) { 74 | params = {} 75 | } 76 | params._t = new Date().getTime() 77 | let config = { 78 | method: 'post', 79 | url: url, 80 | params, 81 | } 82 | if (data) { 83 | if (headers && headers['Content-Type'] == 'multipart/form-data') { 84 | config.data = data 85 | } else { 86 | config.data = qs.stringify(data, { indices: false }) 87 | } 88 | } 89 | if (headers) { 90 | config.headers = headers 91 | } 92 | 93 | config.cancelToken = new CancelToken(function(cancel) { 94 | Vue.prototype.$CancelAjaxRequet = function() { 95 | cancel() 96 | } 97 | }) 98 | 99 | return service(config) 100 | } 101 | 102 | export function get(url, params, headers) { 103 | if (!params) { 104 | params = {} 105 | } 106 | params._t = new Date().getTime() 107 | let config = { 108 | method: 'get', 109 | url: url, 110 | params, 111 | } 112 | if (headers) { 113 | config.headers = headers 114 | } 115 | 116 | config.cancelToken = new CancelToken(function(cancel) { 117 | Vue.prototype.$CancelAjaxRequet = function() { 118 | cancel() 119 | } 120 | }) 121 | 122 | return service(config) 123 | } 124 | 125 | export default service; -------------------------------------------------------------------------------- /web/src/lib/priv.js: -------------------------------------------------------------------------------- 1 | export default { 2 | DEPLOY_APPLY: 1001, 3 | DEPLOY_VIEW: 1002, 4 | DEPLOY_AUDIT: 1003, 5 | DEPLOY_DEPLOY: 1004, 6 | DEPLOY_DROP: 1005, 7 | DEPLOY_EDIT: 1006, 8 | 9 | PROJECT_SPACE_VIEW: 2001, 10 | PROJECT_SPACE_NEW: 2002, 11 | PROJECT_SPACE_EDIT: 2003, 12 | PROJECT_SPACE_DEL: 2004, 13 | PROJECT_USER_VIEW: 2100, 14 | PROJECT_USER_NEW: 2101, 15 | PROJECT_USER_DEL: 2102, 16 | PROJECT_VIEW: 2201, 17 | PROJECT_NEW: 2202, 18 | PROJECT_EDIT: 2203, 19 | PROJECT_DEL: 2204, 20 | PROJECT_AUDIT: 2205, 21 | PROJECT_BUILD: 2206, 22 | PROJECT_HOOK: 2207, 23 | 24 | USER_ROLE_VIEW: 3001, 25 | USER_ROLE_NEW: 3002, 26 | USER_ROLE_EDIT: 3003, 27 | USER_ROLE_DEL: 3004, 28 | USER_VIEW: 3101, 29 | USER_NEW: 3102, 30 | USER_EDIT: 3103, 31 | USER_DEL: 3104, 32 | 33 | SERVER_GROUP_VIEW: 4001, 34 | SERVER_GROUP_NEW: 4002, 35 | SERVER_GROUP_EDIT: 4003, 36 | SERVER_GROUP_DEL: 4004, 37 | SERVER_VIEW: 4101, 38 | SERVER_NEW: 4102, 39 | SERVER_EDIT: 4103, 40 | SERVER_DEL: 4104, 41 | } -------------------------------------------------------------------------------- /web/src/lib/util.js: -------------------------------------------------------------------------------- 1 | import md5 from 'blueimp-md5' 2 | import Cookies from 'js-cookie' 3 | import moment from 'moment' 4 | 5 | let loginTokenKey = '_syd_identity' 6 | 7 | export default { 8 | 9 | MessageSuccess(cb){ 10 | this.$message({ 11 | message: this.$t('operate_success'), 12 | type: 'success', 13 | duration: 1200, 14 | onClose: cb, 15 | }); 16 | }, 17 | 18 | PageInit() { 19 | this.$root.PageSize = 7 20 | this.$root.Page = 1 21 | this.$root.Total = 0 22 | }, 23 | 24 | PageReset() { 25 | this.$root.Total-- 26 | let maxPage = Math.ceil(this.$root.Total / this.$root.PageSize) 27 | if (this.$root.Page > maxPage) { 28 | this.$root.Page = maxPage 29 | } 30 | if (this.$root.Page < 1) { 31 | this.$root.Page = 1 32 | } 33 | }, 34 | 35 | PageOffset() { 36 | return this.$root.PageSize * (this.$root.Page - 1) 37 | }, 38 | 39 | ConfirmDelete(cb, title) { 40 | if (!title) { 41 | title = '此操作将永久删除该数据, 是否继续?' 42 | } 43 | this.$confirm(title, '提示', { 44 | confirmButtonText: '确定', 45 | cancelButtonText: '取消', 46 | type: 'warning' 47 | }).then(() => { 48 | cb() 49 | }).catch((err) => { 50 | console.log(err) 51 | }) 52 | }, 53 | 54 | Md5Sum(str) { 55 | return md5(str); 56 | }, 57 | 58 | FormatDateTime(unixtime, format) { 59 | if (!unixtime) { 60 | return '--' 61 | } 62 | if (!format) { 63 | format = 'YYYY-MM-DD HH:mm:ss' 64 | } 65 | return moment.unix(unixtime).format(format) 66 | }, 67 | 68 | FormatDateDuration(num) { 69 | return moment.duration(num).humanize(false) 70 | }, 71 | 72 | FormatDateFromNow(unixtime) { 73 | if (!unixtime) { 74 | return '--' 75 | } 76 | return moment.unix(unixtime).fromNow() 77 | }, 78 | 79 | Substr(str, len) { 80 | if (Object.prototype.toString.call(str) != '[object String]') { 81 | return '' 82 | } 83 | let postfix = '' 84 | if (str.length > len) { 85 | postfix = "..." 86 | } 87 | return str.substr(0, len) + postfix 88 | }, 89 | 90 | SetLoginToken(token) { 91 | return Cookies.set(loginTokenKey, token) 92 | }, 93 | 94 | GetLoginToken() { 95 | return Cookies.get(loginTokenKey) 96 | }, 97 | 98 | DeleteLoginToken() { 99 | return Cookies.remove(loginTokenKey) 100 | }, 101 | 102 | CheckPriv(code) { 103 | return this.$store.getters['account/getPriv'].indexOf(code) > -1 104 | }, 105 | 106 | CheckPrivs(codeArr) { 107 | if (!codeArr || !codeArr.length) { 108 | return false 109 | } 110 | let checked = false 111 | codeArr.forEach(code => { 112 | if (this.CheckPriv(code)) { 113 | checked = true 114 | } 115 | }) 116 | return checked 117 | }, 118 | } -------------------------------------------------------------------------------- /web/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import ElementUI from 'element-ui' 3 | import 'element-ui/lib/theme-chalk/index.css' 4 | import moment from 'moment' 5 | import App from './App.vue' 6 | import router from './router' 7 | import store from './store' 8 | import i18n from './lang' 9 | import util from './lib/util.js' 10 | import data from './lib/data.js' 11 | import './scss/app.scss' 12 | 13 | 14 | let localeLang 15 | if (global.navigator.language) { 16 | localeLang = global.navigator.language 17 | localeLang = localeLang.toLowerCase() 18 | } 19 | if (localeLang.indexOf('en') != 0) { 20 | localeLang = 'zh-cn' 21 | } 22 | moment.locale(localeLang) 23 | 24 | Vue.config.productionTip = false 25 | Vue.use(ElementUI); 26 | 27 | new Vue({ 28 | i18n, 29 | router, 30 | store, 31 | methods: util, 32 | data: data, 33 | render: h => h(App) 34 | }).$mount('#app') -------------------------------------------------------------------------------- /web/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import account from './module/account.js' 5 | 6 | Vue.use(Vuex) 7 | 8 | export default new Vuex.Store({ 9 | modules: { 10 | account, 11 | } 12 | }) -------------------------------------------------------------------------------- /web/src/store/module/account.js: -------------------------------------------------------------------------------- 1 | import md5 from 'blueimp-md5' 2 | 3 | const state = { 4 | user_id: 0, 5 | username: '', 6 | email: '', 7 | mobile: '', 8 | privilege: [], 9 | role_name: '', 10 | truename: '', 11 | } 12 | 13 | const getters = { 14 | isLogin(state) { 15 | return state.user_id > 0 16 | }, 17 | getEmail(state) { 18 | return state.email 19 | }, 20 | getUserName(state) { 21 | return state.username 22 | }, 23 | getUserId(state) { 24 | return state.user_id 25 | }, 26 | getAvatar(state) { 27 | return 'https://www.gravatar.com/avatar/' + md5(state.email.toLowerCase()) + '? s=512' 28 | }, 29 | getPriv(state) { 30 | return state.privilege ? state.privilege : [] 31 | }, 32 | getRoleName(state) { 33 | return state.role_name 34 | }, 35 | getMobile(state) { 36 | return state.mobile 37 | }, 38 | getTrueName(state) { 39 | return state.truename 40 | }, 41 | } 42 | 43 | const actions = { 44 | status({ commit }, userInfo) { 45 | commit('setUserInfo', { 46 | user_id: userInfo.user_id, 47 | username: userInfo.username, 48 | email: userInfo.email, 49 | mobile: userInfo.mobile, 50 | privilege: userInfo.privilege, 51 | role_name: userInfo.role_name, 52 | truename: userInfo.truename, 53 | }) 54 | }, 55 | userSetting({ commit }, userInfo) { 56 | commit('userSetting', { 57 | mobile: userInfo.mobile, 58 | truename: userInfo.truename, 59 | }) 60 | }, 61 | } 62 | 63 | const mutations = { 64 | setUserInfo(state, userInfo) { 65 | state.user_id = userInfo.user_id 66 | state.username = userInfo.username 67 | state.email = userInfo.email 68 | state.privilege = userInfo.privilege 69 | state.role_name = userInfo.role_name 70 | state.mobile = userInfo.mobile 71 | state.truename = userInfo.truename 72 | }, 73 | userSetting(state, userInfo) { 74 | state.mobile = userInfo.mobile 75 | state.truename = userInfo.truename 76 | }, 77 | } 78 | 79 | export default { 80 | namespaced: true, 81 | state, 82 | getters, 83 | actions, 84 | mutations 85 | } 86 | -------------------------------------------------------------------------------- /web/src/view/Dashboard.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/view/Login.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 76 | 77 | -------------------------------------------------------------------------------- /web/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devServer: { 3 | proxy: { 4 | '/api': { 5 | target: 'http://localhost:8878/', 6 | changeOrigin: true, 7 | } 8 | } 9 | }, 10 | publicPath: '/static/' 11 | } 12 | --------------------------------------------------------------------------------