mirror of
https://github.com/ouqiang/gocron.git
synced 2024-04-21 12:31:58 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32caa5a4b7 | ||
|
|
738b168689 | ||
|
|
e1fa3cf645 | ||
|
|
3ed64f9f22 | ||
|
|
94c9963e81 | ||
|
|
4c940c2088 | ||
|
|
50638e6dfa | ||
|
|
f081d89fb4 | ||
|
|
fd39434446 | ||
|
|
72349248ae | ||
|
|
02e525ab83 | ||
|
|
0950fc69f9 | ||
|
|
750fb49a37 | ||
|
|
68d569a6de | ||
|
|
3fd5b2ee55 | ||
|
|
e9b475a230 | ||
|
|
8720c18654 | ||
|
|
4daf0a15ba | ||
|
|
3474636e23 | ||
|
|
f0ff9a88a7 | ||
|
|
21028ec6f0 | ||
|
|
c4af1653ff | ||
|
|
250cbdde7c | ||
|
|
a966f3aeda | ||
|
|
3c02d55ebf | ||
|
|
4009e845cb | ||
|
|
37f2405ab3 | ||
|
|
8ec80a02b6 | ||
|
|
601f250882 | ||
|
|
019fee2cce | ||
|
|
d4e0898674 | ||
|
|
d642c9641d | ||
|
|
2ba8cb67c8 | ||
|
|
350dc0881e | ||
|
|
db1ef3b317 | ||
|
|
1c2696798c | ||
|
|
2b1c7f16cf | ||
|
|
337ee35357 | ||
|
|
b509fcec55 | ||
|
|
6e8622e4f3 | ||
|
|
ff2228ed50 | ||
|
|
5d97ffad3b | ||
|
|
d13f1997dd | ||
|
|
5b8a8ae890 |
@@ -0,0 +1 @@
|
||||
.git
|
||||
+8
-7
@@ -23,18 +23,19 @@ _testmain.go
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
.DS_Store
|
||||
.idea
|
||||
log/*
|
||||
data/*
|
||||
conf/install.lock
|
||||
conf/app.ini
|
||||
conf/ansible_hosts.ini
|
||||
log
|
||||
data
|
||||
conf
|
||||
profile/*
|
||||
public/resource/javascript/vue.js
|
||||
gocron
|
||||
/gocron
|
||||
gocron.exe
|
||||
gocron-node
|
||||
/gocron-node
|
||||
gocron-node.exe
|
||||
/bin
|
||||
/packages
|
||||
|
||||
node_modules
|
||||
package.json
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.7.x
|
||||
- 1.9.x
|
||||
script: go test `go list ./... | grep -v vendor`
|
||||
|
||||
notifications:
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM alpine:latest
|
||||
|
||||
COPY gocron /usr/local/gocron
|
||||
|
||||
WORKDIR /usr/local/gocron
|
||||
|
||||
EXPOSE 5920
|
||||
|
||||
ENTRYPOINT ["/usr/local/gocron/gocron", "web"]
|
||||
@@ -1,27 +1,33 @@
|
||||
[](https://travis-ci.org/ouqiang/gocron)
|
||||
# gocron - 定时任务管理系统
|
||||
[](https://travis-ci.org/ouqiang/gocron)
|
||||
[](https://github.com/ouqiang/gocron/releases)
|
||||
[](https://github.com/ouqiang/gocron/blob/master/LICENSE)
|
||||
[](https://github.com/ouqiang/gocron/releases)
|
||||
|
||||
# 项目简介
|
||||
使用Go语言开发的定时任务集中调度和管理系统, 用于替代Linux-crontab [查看文档](https://github.com/ouqiang/gocron/wiki)
|
||||
使用Go语言开发的定时任务集中调度和管理系统, 用于替代Linux-crontab [查看文档](https://github.com/ouqiang/gocron/wiki)
|
||||
|
||||
原有的延时任务拆分为独立项目[延迟队列](https://github.com/ouqiang/delay-queue)
|
||||
|
||||
## 功能特性
|
||||
* Web界面管理定时任务, 支持动态添加、删除、编辑任务
|
||||
* crontab时间表达式,精确到秒
|
||||
* 任务执行失败重试设置
|
||||
* 任务超时设置
|
||||
* 任务依赖配置
|
||||
* Web界面管理定时任务
|
||||
* crontab时间表达式, 精确到秒
|
||||
* 任务执行失败可重试
|
||||
* 任务执行超时, 强制结束
|
||||
* 任务依赖配置, A任务完成后再执行B任务
|
||||
* 账户权限控制
|
||||
* 任务类型
|
||||
* shell任务
|
||||
> 在远程服务器上执行shell命令
|
||||
> 在任务节点上执行shell命令, 支持任务同时在多个节点上运行
|
||||
* HTTP任务
|
||||
> 访问指定的URL地址
|
||||
* 查看任务执行日志
|
||||
> 访问指定的URL地址, 由调度器直接执行, 不依赖任务节点
|
||||
* 查看任务执行结果日志
|
||||
* 任务执行结果通知, 支持邮件、Slack
|
||||
|
||||
### 截图
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### 支持平台
|
||||
> Windows、Linux、Mac OS
|
||||
@@ -31,57 +37,99 @@
|
||||
|
||||
|
||||
## 下载
|
||||
* 调度器(管理后台)
|
||||
* [Linux-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-linux-amd64.tar.gz)
|
||||
* [Mac OS-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-darwin-amd64.tar.gz)
|
||||
* [Windows-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-windows-amd64.zip)
|
||||
* 任务执行器(安装在远程主机上, 执行shell命令需安装)
|
||||
* [Linux-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-node-linux-amd64.tar.gz)
|
||||
* [Mac OS-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-node-darwin-amd64.tar.gz)
|
||||
* [Windows-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-node-windows-amd64.zip)
|
||||
[releases](https://github.com/ouqiang/gocron/releases)
|
||||
|
||||
[版本升级](https://github.com/ouqiang/gocron/wiki/版本升级)
|
||||
|
||||
## 安装
|
||||
|
||||
### 二进制安装
|
||||
> Windows平台默认后台运行)
|
||||
1. 解压压缩包
|
||||
2. `cd 解压目录`
|
||||
3. 启动
|
||||
* 调度器启动
|
||||
* Windows: `gocron.exe web`
|
||||
* Linux、Mac OS: `./gocron web`
|
||||
* 任务执行器启动
|
||||
* Windows: `gocron-node.exe ip:port (默认0.0.0.0:5921)`
|
||||
* Linux、Mac OS: `./gocron-node ip:port (默认0.0.0.0:5921)`
|
||||
* 任务节点启动, 默认监听0.0.0.0:5921
|
||||
* Windows: `gocron-node.exe`
|
||||
* Linux、Mac OS: `./gocron-node`
|
||||
4. 浏览器访问 http://localhost:5920
|
||||
|
||||
### 源码安装
|
||||
1. `go`语言版本1.7+
|
||||
1. `go`语言版本1.9+
|
||||
2. `go get -d github.com/ouqiang/gocron`
|
||||
3. 编译
|
||||
* 调度器 `go build -tags gocron -o gocron`
|
||||
* 任务执行器 `go build -tags node -o gocron-node`
|
||||
4. 启动、访问方式同上
|
||||
3. 编译 `make`
|
||||
4. 启动
|
||||
* gocron `./bin/gocron web`
|
||||
* gocron-node `./bin/gocron-node`
|
||||
|
||||
### 命令
|
||||
|
||||
* gocron
|
||||
* -v 查看版本
|
||||
|
||||
* gocron web
|
||||
* --host 默认0.0.0.0
|
||||
* -p 端口, 指定端口, 默认5920
|
||||
* -e 指定运行环境, dev|test|prod, dev模式下可查看更多日志信息, 默认prod
|
||||
* -d 后台运行
|
||||
* -h 查看帮助
|
||||
* gocron serv
|
||||
* -s stop|status stop:停止gocron status:查看运行状态
|
||||
* gocron-node ip:port, 默认0.0.0.0:5921
|
||||
* gocron-node
|
||||
* -allow-root *nix平台允许以root用户运行
|
||||
* -s ip:port 监听地址
|
||||
* -enable-tls 开启TLS
|
||||
* -ca-file CA证书文件
|
||||
* -cert-file 证书文件
|
||||
* -key-file 私钥文件
|
||||
* -h 查看帮助
|
||||
* -v 查看版本
|
||||
|
||||
## To Do List
|
||||
- [x] 版本升级
|
||||
- [x] 批量开启、关闭、删除任务
|
||||
- [x] 调度器与任务节点通信支持https
|
||||
- [x] 任务分组
|
||||
- [x] 多用户
|
||||
- [x] 权限控制
|
||||
|
||||
## 程序使用的组件
|
||||
* web框架 [Macaron](http://go-macaron.com/)
|
||||
* Web框架 [Macaron](http://go-macaron.com/)
|
||||
* 定时任务调度 [Cron](https://github.com/robfig/cron)
|
||||
* ORM [Xorm](https://github.com/go-xorm/xorm)
|
||||
* UI框架 [Semantic UI](https://semantic-ui.com/)
|
||||
* 依赖管理(所有依赖包放入vendor目录) [Govendor](https://github.com/kardianos/govendor)
|
||||
* 依赖管理 [Govendor](https://github.com/kardianos/govendor)
|
||||
* RPC框架 [gRPC](https://github.com/grpc/grpc)
|
||||
|
||||
## 反馈
|
||||
提交[issue](https://github.com/ouqiang/gocron/issues/new)
|
||||
|
||||
## ChangeLog
|
||||
|
||||
v1.4
|
||||
--------
|
||||
* HTTP任务支持POST请求
|
||||
* 后台手动停止运行中的shell任务
|
||||
* 任务执行失败重试间隔时间支持用户自定义
|
||||
* 修复API接口调用报403错误
|
||||
|
||||
v1.3
|
||||
--------
|
||||
* 支持多用户登录
|
||||
* 增加用户权限控制
|
||||
|
||||
|
||||
v1.2.2
|
||||
--------
|
||||
* 用户登录页增加图形验证码
|
||||
* 支持从旧版本升级
|
||||
* 任务批量开启、关闭、删除
|
||||
* 调度器与任务节点支持HTTPS双向认证
|
||||
* 修复任务列表页总记录数显示错误
|
||||
|
||||
v1.1
|
||||
--------
|
||||
|
||||
* 任务可同时在多个节点上运行
|
||||
* *nix平台默认禁止以root用户运行任务节点
|
||||
* 子任务命令中增加预定义占位符, 子任务可根据主任务运行结果执行相应操作
|
||||
* 删除守护进程模块
|
||||
* Web访问日志输出到终端
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# set -x -u
|
||||
# 构建应用, 生成压缩包 gocron.zip或gocron.tar.gz
|
||||
# ./build.sh -p windows -a amd64
|
||||
# 参数含义
|
||||
# -p 指定平台(windows|linux|darwin)
|
||||
# -a 指定体系架构(amd64|386), 默认amd64
|
||||
|
||||
|
||||
TEMP_DIR=`date +%s`-temp-`echo $RANDOM`
|
||||
|
||||
# 目标平台 windows,linux,darwin
|
||||
OS=''
|
||||
# 目标平台架构
|
||||
ARCH=''
|
||||
# 应用名称
|
||||
APP_NAME='gocron'
|
||||
# 可执行文件名
|
||||
EXEC_NAME=''
|
||||
# 压缩包名称
|
||||
COMPRESS_FILE=''
|
||||
|
||||
|
||||
# -p 平台 -a 架构
|
||||
while getopts "p:a:" OPT;
|
||||
do
|
||||
case $OPT in
|
||||
p) OS=$OPTARG
|
||||
;;
|
||||
a) ARCH=$OPTARG
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z $OS ]];then
|
||||
echo "平台不能为空"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $OS != 'windows' && $OS != 'linux' && $OS != 'darwin' ]];then
|
||||
echo '平台错误,支持的平台 windows linux darmin(osx)'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z $ARCH ]];then
|
||||
ARCH='amd64'
|
||||
fi
|
||||
|
||||
if [[ $ARCH != '386' && $ARCH != 'amd64' ]];then
|
||||
echo 'arch错误,仅支持 386 amd64'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo '开始编译调度器'
|
||||
if [[ $OS = 'windows' ]];then
|
||||
GOOS=$OS GOARCH=$ARCH go build -tags gocron -ldflags '-w -H windowsgui'
|
||||
else
|
||||
GOOS=$OS GOARCH=$ARCH go build -tags gocron -ldflags '-w'
|
||||
fi
|
||||
|
||||
if [[ $? != 0 ]];then
|
||||
exit 1
|
||||
fi
|
||||
echo '编译完成'
|
||||
|
||||
if [[ $OS = 'windows' ]];then
|
||||
EXEC_NAME=${APP_NAME}.exe
|
||||
COMPRESS_FILE=${APP_NAME}-${OS}-${ARCH}.zip
|
||||
else
|
||||
EXEC_NAME=${APP_NAME}
|
||||
COMPRESS_FILE=${APP_NAME}-${OS}-${ARCH}.tar.gz
|
||||
fi
|
||||
|
||||
mkdir -p $TEMP_DIR/$APP_NAME
|
||||
if [[ $? != 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 需要打包的文件
|
||||
PACKAGE_FILENAME=(conf log public data templates ${EXEC_NAME})
|
||||
|
||||
echo '复制文件到临时目录'
|
||||
# 复制文件到临时目录
|
||||
for i in ${PACKAGE_FILENAME[*]}
|
||||
do
|
||||
cp -r $i $TEMP_DIR/$APP_NAME
|
||||
done
|
||||
|
||||
# 删除运行时产生的文件
|
||||
rm -rf $TEMP_DIR/$APP_NAME/conf/*
|
||||
rm -rf $TEMP_DIR/$APP_NAME/log/*
|
||||
rm -rf $TEMP_DIR/$APP_NAME/data/sessions/*
|
||||
rm -rf $TEMP_DIR/$APP_NAME/data/ssh/password/*
|
||||
rm -rf $TEMP_DIR/$APP_NAME/data/ssh/private_key/*
|
||||
|
||||
echo '压缩文件'
|
||||
# 压缩文件
|
||||
cd $TEMP_DIR
|
||||
if [[ $OS = 'windows' ]];then
|
||||
zip -rq $COMPRESS_FILE *
|
||||
else
|
||||
tar czf $COMPRESS_FILE *
|
||||
fi
|
||||
mv $COMPRESS_FILE ../
|
||||
cd ../
|
||||
|
||||
rm $EXEC_NAME
|
||||
rm -rf $TEMP_DIR
|
||||
|
||||
echo '打包完成'
|
||||
echo '生成压缩文件--' $COMPRESS_FILE
|
||||
@@ -1,83 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# set -x -u
|
||||
# 任务节点打包, 生成压缩包 gocron-node.zip或gocron-node.tar.gz
|
||||
# ./build-node.sh -p windows -a amd64
|
||||
# 参数含义
|
||||
# -p 指定平台(windows|linux|darwin)
|
||||
# -a 指定体系架构(amd64|386), 默认amd64
|
||||
|
||||
|
||||
# 目标平台 windows,linux,darwin
|
||||
OS=''
|
||||
# 目标平台架构
|
||||
ARCH=''
|
||||
# 应用名称
|
||||
APP_NAME='gocron-node'
|
||||
# 可执行文件名
|
||||
EXEC_NAME=''
|
||||
# 压缩包名称
|
||||
COMPRESS_FILE=''
|
||||
|
||||
|
||||
# -p 平台 -a 架构
|
||||
while getopts "p:a:" OPT;
|
||||
do
|
||||
case $OPT in
|
||||
p) OS=$OPTARG
|
||||
;;
|
||||
a) ARCH=$OPTARG
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z $OS ]];then
|
||||
echo "平台不能为空"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $OS != 'windows' && $OS != 'linux' && $OS != 'darwin' ]];then
|
||||
echo '平台错误,支持的平台 windows linux darmin(osx)'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z $ARCH ]];then
|
||||
ARCH='amd64'
|
||||
fi
|
||||
|
||||
if [[ $ARCH != '386' && $ARCH != 'amd64' ]];then
|
||||
echo 'arch错误,仅支持 386 amd64'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $OS = 'windows' ]];then
|
||||
EXEC_NAME=${APP_NAME}.exe
|
||||
COMPRESS_FILE=${APP_NAME}-${OS}-${ARCH}.zip
|
||||
else
|
||||
EXEC_NAME=${APP_NAME}
|
||||
COMPRESS_FILE=${APP_NAME}-${OS}-${ARCH}.tar.gz
|
||||
fi
|
||||
|
||||
echo '开始编译任务节点'
|
||||
if [[ $OS = 'windows' ]];then
|
||||
GOOS=$OS GOARCH=$ARCH go build -tags node -ldflags '-w -H windowsgui' -o $EXEC_NAME
|
||||
else
|
||||
GOOS=$OS GOARCH=$ARCH go build -tags node -ldflags '-w' -o $EXEC_NAME
|
||||
fi
|
||||
|
||||
if [[ $? != 0 ]];then
|
||||
exit 1
|
||||
fi
|
||||
echo '编译完成'
|
||||
|
||||
if [[ $OS = 'windows' ]];then
|
||||
zip -rq $COMPRESS_FILE $EXEC_NAME
|
||||
else
|
||||
tar czf $COMPRESS_FILE $EXEC_NAME
|
||||
fi
|
||||
|
||||
|
||||
rm $EXEC_NAME
|
||||
|
||||
echo '打包完成'
|
||||
echo '生成压缩文件--' $COMPRESS_FILE
|
||||
@@ -0,0 +1,192 @@
|
||||
// main gocron
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/app"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/setting"
|
||||
"github.com/ouqiang/gocron/routers"
|
||||
"github.com/ouqiang/gocron/service"
|
||||
"github.com/urfave/cli"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
var AppVersion = "1.4"
|
||||
|
||||
// web服务器默认端口
|
||||
const DefaultPort = 5920
|
||||
|
||||
func main() {
|
||||
cliApp := cli.NewApp()
|
||||
cliApp.Name = "gocron"
|
||||
cliApp.Usage = "gocron service"
|
||||
cliApp.Version = AppVersion
|
||||
cliApp.Commands = getCommands()
|
||||
cliApp.Flags = append(cliApp.Flags, []cli.Flag{}...)
|
||||
cliApp.Run(os.Args)
|
||||
}
|
||||
|
||||
// getCommands
|
||||
func getCommands() []cli.Command {
|
||||
command := cli.Command{
|
||||
Name: "web",
|
||||
Usage: "run web server",
|
||||
Action: runWeb,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "host",
|
||||
Value: "0.0.0.0",
|
||||
Usage: "bind host",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "port,p",
|
||||
Value: DefaultPort,
|
||||
Usage: "bind port",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "env,e",
|
||||
Value: "prod",
|
||||
Usage: "runtime environment, dev|test|prod",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return []cli.Command{command}
|
||||
}
|
||||
|
||||
func runWeb(ctx *cli.Context) {
|
||||
// 设置运行环境
|
||||
setEnvironment(ctx)
|
||||
// 初始化应用
|
||||
app.InitEnv(ctx.App.Version)
|
||||
// 初始化模块 DB、定时任务等
|
||||
initModule()
|
||||
// 捕捉信号,配置热更新等
|
||||
go catchSignal()
|
||||
m := macaron.Classic()
|
||||
|
||||
// 注册路由
|
||||
routers.Register(m)
|
||||
// 注册中间件.
|
||||
routers.RegisterMiddleware(m)
|
||||
host := parseHost(ctx)
|
||||
port := parsePort(ctx)
|
||||
m.Run(host, port)
|
||||
}
|
||||
|
||||
func initModule() {
|
||||
if !app.Installed {
|
||||
return
|
||||
}
|
||||
|
||||
config, err := setting.Read(app.AppConfig)
|
||||
if err != nil {
|
||||
logger.Fatal("读取应用配置失败", err)
|
||||
}
|
||||
app.Setting = config
|
||||
|
||||
// 初始化DB
|
||||
models.Db = models.CreateDb()
|
||||
|
||||
// 版本升级
|
||||
upgradeIfNeed()
|
||||
|
||||
// 初始化定时任务
|
||||
service.ServiceTask.Initialize()
|
||||
}
|
||||
|
||||
// 解析端口
|
||||
func parsePort(ctx *cli.Context) int {
|
||||
port := DefaultPort
|
||||
if ctx.IsSet("port") {
|
||||
port = ctx.Int("port")
|
||||
}
|
||||
if port <= 0 || port >= 65535 {
|
||||
port = DefaultPort
|
||||
}
|
||||
|
||||
return port
|
||||
}
|
||||
|
||||
func parseHost(ctx *cli.Context) string {
|
||||
if ctx.IsSet("host") {
|
||||
return ctx.String("host")
|
||||
}
|
||||
|
||||
return "0.0.0.0"
|
||||
}
|
||||
|
||||
func setEnvironment(ctx *cli.Context) {
|
||||
env := "prod"
|
||||
if ctx.IsSet("env") {
|
||||
env = ctx.String("env")
|
||||
}
|
||||
|
||||
switch env {
|
||||
case "test":
|
||||
macaron.Env = macaron.TEST
|
||||
case "dev":
|
||||
macaron.Env = macaron.DEV
|
||||
default:
|
||||
macaron.Env = macaron.PROD
|
||||
}
|
||||
}
|
||||
|
||||
// 捕捉信号
|
||||
func catchSignal() {
|
||||
c := make(chan os.Signal)
|
||||
// todo 配置热更新, windows 不支持 syscall.SIGUSR1, syscall.SIGUSR2
|
||||
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
|
||||
for {
|
||||
s := <-c
|
||||
logger.Info("收到信号 -- ", s)
|
||||
switch s {
|
||||
case syscall.SIGHUP:
|
||||
logger.Info("收到终端断开信号, 忽略")
|
||||
case syscall.SIGINT, syscall.SIGTERM:
|
||||
shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 应用退出
|
||||
func shutdown() {
|
||||
defer func() {
|
||||
logger.Info("已退出")
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
if !app.Installed {
|
||||
return
|
||||
}
|
||||
logger.Info("应用准备退出")
|
||||
// 停止所有任务调度
|
||||
logger.Info("停止定时任务调度")
|
||||
service.ServiceTask.WaitAndExit()
|
||||
}
|
||||
|
||||
// 判断应用是否需要升级, 当存在版本号文件且版本小于app.VersionId时升级
|
||||
func upgradeIfNeed() {
|
||||
currentVersionId := app.GetCurrentVersionId()
|
||||
// 没有版本号文件
|
||||
if currentVersionId == 0 {
|
||||
return
|
||||
}
|
||||
if currentVersionId >= app.VersionId {
|
||||
return
|
||||
}
|
||||
|
||||
migration := new(models.Migration)
|
||||
logger.Infof("版本升级开始, 当前版本号%d", currentVersionId)
|
||||
|
||||
migration.Upgrade(currentVersionId)
|
||||
app.UpdateVersionFile()
|
||||
|
||||
logger.Infof("已升级到最新版本%d", app.VersionId)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// main gocron-node
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/ouqiang/gocron/modules/rpc/auth"
|
||||
"github.com/ouqiang/gocron/modules/rpc/server"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
)
|
||||
|
||||
var AppVersion = "1.4"
|
||||
|
||||
func main() {
|
||||
var serverAddr string
|
||||
var allowRoot bool
|
||||
var version bool
|
||||
var CAFile string
|
||||
var certFile string
|
||||
var keyFile string
|
||||
var enableTLS bool
|
||||
flag.BoolVar(&allowRoot, "allow-root", false, "./gocron-node -allow-root")
|
||||
flag.StringVar(&serverAddr, "s", "0.0.0.0:5921", "./gocron-node -s ip:port")
|
||||
flag.BoolVar(&version, "v", false, "./gocron-node -v")
|
||||
flag.BoolVar(&enableTLS, "enable-tls", false, "./gocron-node -enable-tls")
|
||||
flag.StringVar(&CAFile, "ca-file", "", "./gocron-node -ca-file path")
|
||||
flag.StringVar(&certFile, "cert-file", "", "./gocron-node -cert-file path")
|
||||
flag.StringVar(&keyFile, "key-file", "", "./gocron-node -key-file path")
|
||||
flag.Parse()
|
||||
|
||||
if version {
|
||||
fmt.Println(AppVersion)
|
||||
return
|
||||
}
|
||||
|
||||
if enableTLS {
|
||||
if !utils.FileExist(CAFile) {
|
||||
fmt.Printf("failed to read ca cert file: %s", CAFile)
|
||||
return
|
||||
}
|
||||
if !utils.FileExist(certFile) {
|
||||
fmt.Printf("failed to read server cert file: %s", certFile)
|
||||
return
|
||||
}
|
||||
if !utils.FileExist(keyFile) {
|
||||
fmt.Printf("failed to read server key file: %s", keyFile)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
certificate := auth.Certificate{
|
||||
CAFile: strings.TrimSpace(CAFile),
|
||||
CertFile: strings.TrimSpace(certFile),
|
||||
KeyFile: strings.TrimSpace(keyFile),
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" && os.Getuid() == 0 && !allowRoot {
|
||||
fmt.Println("Do not run gocron-node as root user")
|
||||
return
|
||||
}
|
||||
|
||||
server.Start(serverAddr, enableTLS, certificate)
|
||||
}
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli"
|
||||
"fmt"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"github.com/ouqiang/gocron/modules/app"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var CmdServ = cli.Command{
|
||||
Name: "serv",
|
||||
Usage: "manage gocron, ./gocron serv -s stop|status",
|
||||
Action: runServ,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "s",
|
||||
Value:"",
|
||||
Usage: "stop|status",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runServ(ctx *cli.Context) {
|
||||
if utils.IsWindows() {
|
||||
fmt.Println("not support on windows")
|
||||
return
|
||||
}
|
||||
option := ctx.String("s")
|
||||
if !utils.InStringSlice([]string{"stop", "status"}, option) {
|
||||
fmt.Println("invalid option")
|
||||
return
|
||||
}
|
||||
app.InitEnv()
|
||||
pid := app.GetPid()
|
||||
if pid <= 0 {
|
||||
fmt.Println("not running")
|
||||
return
|
||||
}
|
||||
process ,err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
fmt.Println("not running", err)
|
||||
return
|
||||
}
|
||||
switch option {
|
||||
case "stop":
|
||||
stop(process)
|
||||
case "status":
|
||||
status(process)
|
||||
}
|
||||
}
|
||||
|
||||
func stop(process *os.Process) {
|
||||
fmt.Println("stopping gocron......")
|
||||
err := process.Signal(syscall.SIGTERM)
|
||||
if err != nil {
|
||||
fmt.Println("failed to kill process", err)
|
||||
} else {
|
||||
fmt.Println("stopped")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func status(process *os.Process) {
|
||||
fmt.Printf("running, pid-[%d]\n", process.Pid)
|
||||
}
|
||||
-226
@@ -1,226 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/ouqiang/gocron/modules/app"
|
||||
"github.com/ouqiang/gocron/routers"
|
||||
"github.com/urfave/cli"
|
||||
"gopkg.in/macaron.v1"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/service"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/setting"
|
||||
"time"
|
||||
"io"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"os/exec"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"github.com/ouqiang/gocron/modules/rpc/grpcpool"
|
||||
)
|
||||
|
||||
// web服务器默认端口
|
||||
const DefaultPort = 5920
|
||||
|
||||
const InitProcess = 1
|
||||
|
||||
var CmdWeb = cli.Command{
|
||||
Name: "web",
|
||||
Usage: "run web server",
|
||||
Action: runWeb,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "host",
|
||||
Value: "0.0.0.0",
|
||||
Usage: "bind host",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "port,p",
|
||||
Value: DefaultPort,
|
||||
Usage: "bind port",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "env,e",
|
||||
Value: "prod",
|
||||
Usage: "runtime environment, dev|test|prod",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "d",
|
||||
Usage: "-d=true, run as daemon process",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runWeb(ctx *cli.Context) {
|
||||
// 设置守护进程
|
||||
becomeDaemon(ctx)
|
||||
// 设置运行环境
|
||||
setEnvironment(ctx)
|
||||
// 初始化应用
|
||||
app.InitEnv()
|
||||
app.WritePid()
|
||||
// 初始化模块 DB、定时任务等
|
||||
initModule()
|
||||
// 捕捉信号,配置热更新等
|
||||
go catchSignal()
|
||||
m := macaron.NewWithLogger(getWebLogWriter())
|
||||
|
||||
// 注册路由
|
||||
routers.Register(m)
|
||||
// 注册中间件.
|
||||
routers.RegisterMiddleware(m)
|
||||
host := parseHost(ctx)
|
||||
port := parsePort(ctx)
|
||||
fmt.Println("server start")
|
||||
m.Run(host, port)
|
||||
}
|
||||
|
||||
func becomeDaemon(ctx *cli.Context) {
|
||||
// 不支持windows
|
||||
if utils.IsWindows() {
|
||||
return
|
||||
}
|
||||
if !ctx.IsSet("d") {
|
||||
return
|
||||
}
|
||||
|
||||
if os.Getppid() == InitProcess {
|
||||
// 子进程不再处理
|
||||
return
|
||||
}
|
||||
|
||||
filePath, _:= filepath.Abs(os.Args[0])
|
||||
cmd := exec.Command(filePath, os.Args[1:]...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
logger.Fatal("创建守护进程失败", err)
|
||||
}
|
||||
|
||||
// 父进程退出, 子进程由init-1号进程收养
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func initModule() {
|
||||
if !app.Installed {
|
||||
return
|
||||
}
|
||||
|
||||
config, err := setting.Read(app.AppConfig)
|
||||
if err != nil {
|
||||
logger.Fatal("读取应用配置失败", err)
|
||||
}
|
||||
app.Setting = config
|
||||
|
||||
models.Db = models.CreateDb()
|
||||
|
||||
// 初始化定时任务
|
||||
serviceTask := new(service.Task)
|
||||
serviceTask.Initialize()
|
||||
}
|
||||
|
||||
// 解析端口
|
||||
func parsePort(ctx *cli.Context) int {
|
||||
var port int = DefaultPort
|
||||
if ctx.IsSet("port") {
|
||||
port = ctx.Int("port")
|
||||
}
|
||||
if port <= 0 || port >= 65535 {
|
||||
port = DefaultPort
|
||||
}
|
||||
|
||||
return port
|
||||
}
|
||||
|
||||
func parseHost(ctx *cli.Context) string {
|
||||
if ctx.IsSet("host") {
|
||||
return ctx.String("host")
|
||||
}
|
||||
|
||||
return "0.0.0.0"
|
||||
}
|
||||
|
||||
func setEnvironment(ctx *cli.Context) {
|
||||
var env string = "prod"
|
||||
if ctx.IsSet("env") {
|
||||
env = ctx.String("env")
|
||||
}
|
||||
|
||||
switch env {
|
||||
case "test":
|
||||
macaron.Env = macaron.TEST
|
||||
case "dev":
|
||||
macaron.Env = macaron.DEV
|
||||
default:
|
||||
macaron.Env = macaron.PROD
|
||||
}
|
||||
}
|
||||
|
||||
// 捕捉信号
|
||||
func catchSignal() {
|
||||
c := make(chan os.Signal)
|
||||
// todo 配置热更新, windows 不支持 syscall.SIGUSR1, syscall.SIGUSR2
|
||||
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
|
||||
for {
|
||||
s := <- c
|
||||
logger.Info("收到信号 -- ", s)
|
||||
switch s {
|
||||
case syscall.SIGHUP:
|
||||
logger.Info("收到终端断开信号, 忽略")
|
||||
case syscall.SIGINT, syscall.SIGTERM:
|
||||
shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getWebLogWriter() io.Writer {
|
||||
if macaron.Env == macaron.DEV {
|
||||
return os.Stdout
|
||||
}
|
||||
logFile := app.LogDir + "/access.log"
|
||||
var err error
|
||||
w, err := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND ,0666)
|
||||
if err != nil {
|
||||
fmt.Printf("日志文件[%s]打开失败", logFile)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// 应用退出
|
||||
func shutdown() {
|
||||
defer func() {
|
||||
app.RemovePid()
|
||||
logger.Info("已退出")
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
if !app.Installed {
|
||||
return
|
||||
}
|
||||
logger.Info("应用准备退出")
|
||||
serviceTask := new(service.Task)
|
||||
// 停止所有任务调度
|
||||
logger.Info("停止定时任务调度")
|
||||
serviceTask.StopAll()
|
||||
// 释放gRPC连接池
|
||||
grpcpool.Pool.ReleaseAll()
|
||||
|
||||
taskNumInRunning := service.TaskNum.Num()
|
||||
logger.Infof("正在运行的任务有%d个", taskNumInRunning)
|
||||
if taskNumInRunning > 0 {
|
||||
logger.Info("等待所有任务执行完成后退出")
|
||||
}
|
||||
for {
|
||||
if taskNumInRunning <= 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(3 * time.Second)
|
||||
taskNumInRunning = service.TaskNum.Num()
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// +build node
|
||||
// 任务节点
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ouqiang/gocron/modules/rpc/server"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var addr string
|
||||
if (len(os.Args) < 2) {
|
||||
addr = "0.0.0.0:5921"
|
||||
} else {
|
||||
addr = os.Args[1]
|
||||
}
|
||||
for {
|
||||
server.Start(addr)
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// +build gocron
|
||||
// 调度中心
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
|
||||
"github.com/ouqiang/gocron/cmd"
|
||||
)
|
||||
|
||||
const AppVersion = "1.0"
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "gocron"
|
||||
app.Usage = "gocron service"
|
||||
app.Version = AppVersion
|
||||
app.Commands = []cli.Command{
|
||||
cmd.CmdWeb,
|
||||
cmd.CmdServ,
|
||||
}
|
||||
app.Flags = append(app.Flags, []cli.Flag{}...)
|
||||
app.Run(os.Args)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
all: build
|
||||
|
||||
build: gocron node
|
||||
|
||||
gocron:
|
||||
|
||||
go build -o bin/gocron ./cmd/gocron
|
||||
|
||||
|
||||
node:
|
||||
|
||||
go build -o bin/gocron-node ./cmd/node
|
||||
|
||||
clean:
|
||||
|
||||
rm bin/gocron
|
||||
rm bin/gocron-node
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
LDFLAGS = -w -s -X main.AppVersion=${VERSION}
|
||||
|
||||
|
||||
all: build
|
||||
|
||||
|
||||
build: gocron node
|
||||
|
||||
|
||||
gocron:
|
||||
|
||||
env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./gocron_darwin_amd64/gocron ./cmd/gocron
|
||||
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./gocron_linux_amd64/gocron ./cmd/gocron
|
||||
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./gocron_windows_amd64/gocron.exe ./cmd/gocron
|
||||
|
||||
node:
|
||||
|
||||
env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./gocron-node_darwin_amd64/gocron-node ./cmd/node
|
||||
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./gocron-node_linux_amd64/gocron-node ./cmd/node
|
||||
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./gocron-node_windows_amd64/gocron-node.exe ./cmd/node
|
||||
|
||||
|
||||
clean:
|
||||
|
||||
rm -rf gocron_darwin_amd64
|
||||
rm -rf gocron_linux_amd64
|
||||
rm -rf gocron_windows_amd64
|
||||
|
||||
rm -rf gocron-node_darwin_amd64
|
||||
rm -rf gocron-node_linux_amd64
|
||||
rm -rf gocron-node_windows_amd64
|
||||
|
||||
+51
-51
@@ -1,94 +1,94 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/go-xorm/xorm"
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
// 主机
|
||||
type Host struct {
|
||||
Id int16 `xorm:"smallint pk autoincr"`
|
||||
Name string `xorm:"varchar(64) notnull"` // 主机名称
|
||||
Alias string `xorm:"varchar(32) notnull default '' "` // 主机别名
|
||||
Port int `xorm:"notnull default 22"` // 主机端口
|
||||
Remark string `xorm:"varchar(100) notnull default '' "` // 备注
|
||||
BaseModel `xorm:"-"`
|
||||
Id int16 `xorm:"smallint pk autoincr"`
|
||||
Name string `xorm:"varchar(64) notnull"` // 主机名称
|
||||
Alias string `xorm:"varchar(32) notnull default '' "` // 主机别名
|
||||
Port int `xorm:"notnull default 22"` // 主机端口
|
||||
Remark string `xorm:"varchar(100) notnull default '' "` // 备注
|
||||
BaseModel `xorm:"-"`
|
||||
Selected bool `xorm:"-"`
|
||||
}
|
||||
|
||||
// 新增
|
||||
func (host *Host) Create() (insertId int16, err error) {
|
||||
_, err = Db.Insert(host)
|
||||
if err == nil {
|
||||
insertId = host.Id
|
||||
}
|
||||
_, err = Db.Insert(host)
|
||||
if err == nil {
|
||||
insertId = host.Id
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
func (host *Host) UpdateBean(id int16) (int64, error) {
|
||||
return Db.ID(id).Cols("name,alias,port,remark").Update(host)
|
||||
func (host *Host) UpdateBean(id int16) (int64, error) {
|
||||
return Db.ID(id).Cols("name,alias,port,remark").Update(host)
|
||||
}
|
||||
|
||||
|
||||
// 更新
|
||||
func (host *Host) Update(id int, data CommonMap) (int64, error) {
|
||||
return Db.Table(host).ID(id).Update(data)
|
||||
return Db.Table(host).ID(id).Update(data)
|
||||
}
|
||||
|
||||
// 删除
|
||||
func (host *Host) Delete(id int) (int64, error) {
|
||||
return Db.Id(id).Delete(new(Host))
|
||||
return Db.Id(id).Delete(new(Host))
|
||||
}
|
||||
|
||||
func (host *Host) Find(id int) error {
|
||||
_, err := Db.Id(id).Get(host)
|
||||
_, err := Db.Id(id).Get(host)
|
||||
|
||||
return err
|
||||
return err
|
||||
}
|
||||
|
||||
func (host *Host) NameExists(name string, id int16) (bool, error) {
|
||||
if id == 0 {
|
||||
count, err := Db.Where("name = ?", name).Count(host);
|
||||
return count > 0, err
|
||||
}
|
||||
func (host *Host) NameExists(name string, id int16) (bool, error) {
|
||||
if id == 0 {
|
||||
count, err := Db.Where("name = ?", name).Count(host)
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
count, err := Db.Where("name = ? AND id != ?", name, id).Count(host);
|
||||
return count > 0, err
|
||||
count, err := Db.Where("name = ? AND id != ?", name, id).Count(host)
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
func (host *Host) List(params CommonMap) ([]Host, error) {
|
||||
host.parsePageAndPageSize(params)
|
||||
list := make([]Host, 0)
|
||||
session := Db.Desc("id")
|
||||
host.parseWhere(session, params)
|
||||
err := session.Limit(host.PageSize, host.pageLimitOffset()).Find(&list)
|
||||
host.parsePageAndPageSize(params)
|
||||
list := make([]Host, 0)
|
||||
session := Db.Desc("id")
|
||||
host.parseWhere(session, params)
|
||||
err := session.Limit(host.PageSize, host.pageLimitOffset()).Find(&list)
|
||||
|
||||
return list, err
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (host *Host) AllList() ([]Host, error) {
|
||||
list := make([]Host, 0)
|
||||
err := Db.Cols("name,port").Desc("id").Find(&list)
|
||||
list := make([]Host, 0)
|
||||
err := Db.Cols("name,port").Desc("id").Find(&list)
|
||||
|
||||
return list, err
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (host *Host) Total(params CommonMap) (int64, error) {
|
||||
session := Db.NewSession()
|
||||
host.parseWhere(session, params)
|
||||
return session.Count(host)
|
||||
session := Db.NewSession()
|
||||
host.parseWhere(session, params)
|
||||
return session.Count(host)
|
||||
}
|
||||
|
||||
// 解析where
|
||||
func (host *Host) parseWhere(session *xorm.Session, params CommonMap) {
|
||||
if len(params) == 0 {
|
||||
return
|
||||
}
|
||||
id, ok := params["Id"]
|
||||
if ok && id.(int) > 0 {
|
||||
session.And("id = ?", id)
|
||||
}
|
||||
name, ok := params["Name"]
|
||||
if ok && name.(string) != "" {
|
||||
session.And("name = ?", name)
|
||||
}
|
||||
}
|
||||
func (host *Host) parseWhere(session *xorm.Session, params CommonMap) {
|
||||
if len(params) == 0 {
|
||||
return
|
||||
}
|
||||
id, ok := params["Id"]
|
||||
if ok && id.(int) > 0 {
|
||||
session.And("id = ?", id)
|
||||
}
|
||||
name, ok := params["Name"]
|
||||
if ok && name.(string) != "" {
|
||||
session.And("name = ?", name)
|
||||
}
|
||||
}
|
||||
|
||||
+17
-17
@@ -1,36 +1,36 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 用户登录日志
|
||||
|
||||
type LoginLog struct {
|
||||
Id int `xorm:"pk autoincr notnull "`
|
||||
Username string `xorm:"varchar(32) notnull"`
|
||||
Ip string `xorm:"varchar(15) not null"`
|
||||
Created time.Time `xorm:"datetime notnull created"`
|
||||
BaseModel `xorm:"-"`
|
||||
Id int `xorm:"pk autoincr notnull "`
|
||||
Username string `xorm:"varchar(32) notnull"`
|
||||
Ip string `xorm:"varchar(15) not null"`
|
||||
Created time.Time `xorm:"datetime notnull created"`
|
||||
BaseModel `xorm:"-"`
|
||||
}
|
||||
|
||||
func (log *LoginLog) Create() (insertId int, err error) {
|
||||
_, err = Db.Insert(log)
|
||||
if err == nil {
|
||||
insertId = log.Id
|
||||
}
|
||||
_, err = Db.Insert(log)
|
||||
if err == nil {
|
||||
insertId = log.Id
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
func (log *LoginLog) List(params CommonMap) ([]LoginLog, error) {
|
||||
log.parsePageAndPageSize(params)
|
||||
list := make([]LoginLog, 0)
|
||||
err := Db.Desc("id").Limit(log.PageSize, log.pageLimitOffset()).Find(&list)
|
||||
log.parsePageAndPageSize(params)
|
||||
list := make([]LoginLog, 0)
|
||||
err := Db.Desc("id").Limit(log.PageSize, log.pageLimitOffset()).Find(&list)
|
||||
|
||||
return list, err
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (log *LoginLog) Total() (int64, error) {
|
||||
return Db.Count(log)
|
||||
}
|
||||
return Db.Count(log)
|
||||
}
|
||||
|
||||
+181
-31
@@ -1,44 +1,194 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
// 创建数据库表
|
||||
"github.com/go-xorm/xorm"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
)
|
||||
|
||||
type Migration struct{}
|
||||
|
||||
func (migration *Migration) Exec(dbName string) error {
|
||||
if !isDatabaseExist(dbName) {
|
||||
return errors.New("数据库不存在")
|
||||
}
|
||||
setting := new(Setting)
|
||||
task := new(Task)
|
||||
tables := []interface{}{
|
||||
&User{}, task, &TaskLog{}, &Host{}, setting,&LoginLog{},
|
||||
}
|
||||
for _, table := range tables {
|
||||
exist, err:= Db.IsTableExist(table)
|
||||
if exist {
|
||||
return errors.New("数据表已存在")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = Db.Sync2(table)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
setting.InitBasicField()
|
||||
task.CreateTestTask()
|
||||
// 首次安装, 创建数据库表
|
||||
func (migration *Migration) Install(dbName string) error {
|
||||
if !isDatabaseExist(dbName) {
|
||||
return errors.New("数据库不存在")
|
||||
}
|
||||
setting := new(Setting)
|
||||
task := new(Task)
|
||||
tables := []interface{}{
|
||||
&User{}, task, &TaskLog{}, &Host{}, setting, &LoginLog{}, &TaskHost{},
|
||||
}
|
||||
for _, table := range tables {
|
||||
exist, err := Db.IsTableExist(table)
|
||||
if exist {
|
||||
return errors.New("数据表已存在")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = Db.Sync2(table)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
setting.InitBasicField()
|
||||
task.CreateTestTask()
|
||||
|
||||
return nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建数据库
|
||||
// 判断数据库是否存在
|
||||
func isDatabaseExist(name string) bool {
|
||||
_, err := Db.Exec("use ?", name)
|
||||
_, err := Db.Exec("use ?", name)
|
||||
|
||||
return err != nil
|
||||
return err != nil
|
||||
}
|
||||
|
||||
// 迭代升级数据库, 新建表、新增字段等
|
||||
func (migration *Migration) Upgrade(oldVersionId int) {
|
||||
// v1.2版本不支持升级
|
||||
if oldVersionId == 120 {
|
||||
return
|
||||
}
|
||||
|
||||
versionIds := []int{110, 122, 130, 140}
|
||||
upgradeFuncs := []func(*xorm.Session) error{
|
||||
migration.upgradeFor110,
|
||||
migration.upgradeFor122,
|
||||
migration.upgradeFor130,
|
||||
migration.upgradeFor140,
|
||||
}
|
||||
|
||||
startIndex := -1
|
||||
// 从当前版本的下一版本开始升级
|
||||
for i, value := range versionIds {
|
||||
if value > oldVersionId {
|
||||
startIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if startIndex == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
length := len(versionIds)
|
||||
if startIndex >= length {
|
||||
return
|
||||
}
|
||||
|
||||
session := Db.NewSession()
|
||||
err := session.Begin()
|
||||
if err != nil {
|
||||
logger.Fatalf("开启事务失败-%s", err.Error())
|
||||
}
|
||||
for startIndex < length {
|
||||
err = upgradeFuncs[startIndex](session)
|
||||
if err == nil {
|
||||
startIndex++
|
||||
continue
|
||||
}
|
||||
dbErr := session.Rollback()
|
||||
if dbErr != nil {
|
||||
logger.Fatalf("事务回滚失败-%s", dbErr.Error())
|
||||
}
|
||||
logger.Fatal(err)
|
||||
}
|
||||
err = session.Commit()
|
||||
if err != nil {
|
||||
logger.Fatalf("提交事务失败-%s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 升级到v1.1版本
|
||||
func (migration *Migration) upgradeFor110(session *xorm.Session) error {
|
||||
logger.Info("开始升级到v1.1")
|
||||
// 创建表task_host
|
||||
err := session.Sync2(new(TaskHost))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tableName := TablePrefix + "task"
|
||||
// 把task对应的host_id写入task_host表
|
||||
sql := fmt.Sprintf("SELECT id, host_id FROM %s WHERE host_id > 0", tableName)
|
||||
results, err := session.Query(sql)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, value := range results {
|
||||
taskHostModel := &TaskHost{}
|
||||
taskId, err := strconv.Atoi(string(value["id"]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hostId, err := strconv.Atoi(string(value["host_id"]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
taskHostModel.TaskId = taskId
|
||||
taskHostModel.HostId = int16(hostId)
|
||||
_, err = session.Insert(taskHostModel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 删除task表host_id字段
|
||||
_, err = session.Exec(fmt.Sprintf("ALTER TABLE %s DROP COLUMN host_id", tableName))
|
||||
|
||||
logger.Info("已升级到v1.1\n")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// 升级到1.2.2版本
|
||||
func (migration *Migration) upgradeFor122(session *xorm.Session) error {
|
||||
logger.Info("开始升级到v1.2.2")
|
||||
|
||||
tableName := TablePrefix + "task"
|
||||
// task表增加tag字段
|
||||
_, err := session.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN tag VARCHAR(32) NOT NULL DEFAULT '' ", tableName))
|
||||
|
||||
logger.Info("已升级到v1.2.2\n")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// 升级到v1.3版本
|
||||
func (migration *Migration) upgradeFor130(session *xorm.Session) error {
|
||||
logger.Info("开始升级到v1.3")
|
||||
|
||||
tableName := TablePrefix + "user"
|
||||
// 删除user表deleted字段
|
||||
_, err := session.Exec(fmt.Sprintf("ALTER TABLE %s DROP COLUMN deleted", tableName))
|
||||
|
||||
logger.Info("已升级到v1.3\n")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// 升级到v1.4版本
|
||||
func (migration *Migration) upgradeFor140(session *xorm.Session) error {
|
||||
logger.Info("开始升级到v1.4")
|
||||
|
||||
tableName := TablePrefix + "task"
|
||||
// task表增加字段
|
||||
// retry_interval 重试间隔时间(秒)
|
||||
// http_method http请求方法
|
||||
sql := fmt.Sprintf(
|
||||
"ALTER TABLE %s ADD COLUMN retry_interval SMALLINT NOT NULL DEFAULT 0,ADD COLUMN http_method TINYINT NOT NULL DEFAULT 1", tableName)
|
||||
_, err := session.Exec(sql)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("已升级到v1.4\n")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
+82
-109
@@ -1,151 +1,124 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/go-xorm/core"
|
||||
"github.com/go-xorm/xorm"
|
||||
"gopkg.in/macaron.v1"
|
||||
"strings"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/app"
|
||||
"strconv"
|
||||
"time"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/go-xorm/core"
|
||||
"github.com/go-xorm/xorm"
|
||||
"github.com/ouqiang/gocron/modules/app"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/setting"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
type Status int8
|
||||
type CommonMap map[string]interface{}
|
||||
|
||||
var TablePrefix string = ""
|
||||
var TablePrefix = ""
|
||||
var Db *xorm.Engine
|
||||
|
||||
const (
|
||||
Disabled Status = 0 // 禁用
|
||||
Failure Status = 0 // 失败
|
||||
Enabled Status = 1 // 启用
|
||||
Running Status = 1 // 运行中
|
||||
Finish Status = 2 // 完成
|
||||
Cancel Status = 3 // 取消
|
||||
Waiting Status = 5 // 等待中
|
||||
Disabled Status = 0 // 禁用
|
||||
Failure Status = 0 // 失败
|
||||
Enabled Status = 1 // 启用
|
||||
Running Status = 1 // 运行中
|
||||
Finish Status = 2 // 完成
|
||||
Cancel Status = 3 // 取消
|
||||
)
|
||||
|
||||
const (
|
||||
Page = 1 // 当前页数
|
||||
PageSize = 20 // 每页多少条数据
|
||||
MaxPageSize = 100000 // 每次最多取多少条
|
||||
Page = 1 // 当前页数
|
||||
PageSize = 20 // 每页多少条数据
|
||||
MaxPageSize = 100000 // 每次最多取多少条
|
||||
)
|
||||
|
||||
const DefaultTimeFormat = "2006-01-02 15:04:05"
|
||||
|
||||
type BaseModel struct {
|
||||
Page int `xorm:"-"`
|
||||
PageSize int `xorm:"-"`
|
||||
type BaseModel struct {
|
||||
Page int `xorm:"-"`
|
||||
PageSize int `xorm:"-"`
|
||||
}
|
||||
|
||||
func (model *BaseModel) parsePageAndPageSize(params CommonMap) {
|
||||
page, ok := params["Page"]
|
||||
if ok {
|
||||
model.Page = page.(int)
|
||||
}
|
||||
pageSize, ok := params["PageSize"]
|
||||
if ok {
|
||||
model.PageSize = pageSize.(int)
|
||||
}
|
||||
if model.Page <= 0 {
|
||||
model.Page = Page
|
||||
}
|
||||
if model.PageSize <= 0 || model.PageSize > MaxPageSize {
|
||||
model.PageSize = PageSize
|
||||
}
|
||||
page, ok := params["Page"]
|
||||
if ok {
|
||||
model.Page = page.(int)
|
||||
}
|
||||
pageSize, ok := params["PageSize"]
|
||||
if ok {
|
||||
model.PageSize = pageSize.(int)
|
||||
}
|
||||
if model.Page <= 0 {
|
||||
model.Page = Page
|
||||
}
|
||||
if model.PageSize <= 0 {
|
||||
model.PageSize = MaxPageSize
|
||||
}
|
||||
}
|
||||
|
||||
func (model *BaseModel) pageLimitOffset() int {
|
||||
return (model.Page - 1) * model.PageSize
|
||||
return (model.Page - 1) * model.PageSize
|
||||
}
|
||||
|
||||
// 创建Db
|
||||
func CreateDb() *xorm.Engine {
|
||||
config := getDbConfig()
|
||||
dsn := getDbEngineDSN(config["engine"], config)
|
||||
engine, err := xorm.NewEngine(config["engine"], dsn)
|
||||
if err != nil {
|
||||
logger.Fatal("创建xorm引擎失败", err)
|
||||
}
|
||||
maxIdleConns, err := strconv.Atoi(config["max_idle_conns"])
|
||||
maxOpenConns, err := strconv.Atoi(config["max_open_conns"])
|
||||
if maxIdleConns <= 0 {
|
||||
maxIdleConns = 30
|
||||
}
|
||||
if maxOpenConns <= 0 {
|
||||
maxOpenConns = 100
|
||||
}
|
||||
engine.SetMaxIdleConns(maxIdleConns)
|
||||
engine.SetMaxOpenConns(maxOpenConns)
|
||||
dsn := getDbEngineDSN(app.Setting)
|
||||
engine, err := xorm.NewEngine(app.Setting.Db.Engine, dsn)
|
||||
if err != nil {
|
||||
logger.Fatal("创建xorm引擎失败", err)
|
||||
}
|
||||
engine.SetMaxIdleConns(app.Setting.Db.MaxIdleConns)
|
||||
engine.SetMaxOpenConns(app.Setting.Db.MaxOpenConns)
|
||||
|
||||
if config["prefix"] != "" {
|
||||
// 设置表前缀
|
||||
TablePrefix = config["prefix"]
|
||||
mapper := core.NewPrefixMapper(core.SnakeMapper{}, config["prefix"])
|
||||
engine.SetTableMapper(mapper)
|
||||
}
|
||||
// 本地环境开启日志
|
||||
if macaron.Env == macaron.DEV {
|
||||
engine.ShowSQL(true)
|
||||
engine.Logger().SetLevel(core.LOG_DEBUG)
|
||||
}
|
||||
if app.Setting.Db.Prefix != "" {
|
||||
// 设置表前缀
|
||||
TablePrefix = app.Setting.Db.Prefix
|
||||
mapper := core.NewPrefixMapper(core.SnakeMapper{}, app.Setting.Db.Prefix)
|
||||
engine.SetTableMapper(mapper)
|
||||
}
|
||||
// 本地环境开启日志
|
||||
if macaron.Env == macaron.DEV {
|
||||
engine.ShowSQL(true)
|
||||
engine.Logger().SetLevel(core.LOG_DEBUG)
|
||||
}
|
||||
|
||||
go keepDbAlived(engine)
|
||||
go keepDbAlived(engine)
|
||||
|
||||
return engine
|
||||
return engine
|
||||
}
|
||||
|
||||
// 创建临时数据库连接
|
||||
func CreateTmpDb(config map[string]string) (*xorm.Engine, error) {
|
||||
dsn := getDbEngineDSN(config["engine"], config)
|
||||
func CreateTmpDb(setting *setting.Setting) (*xorm.Engine, error) {
|
||||
dsn := getDbEngineDSN(setting)
|
||||
|
||||
return xorm.NewEngine(config["engine"], dsn)
|
||||
return xorm.NewEngine(setting.Db.Engine, dsn)
|
||||
}
|
||||
|
||||
// 获取数据库引擎DSN mysql,sqlite
|
||||
func getDbEngineDSN(engine string, config map[string]string) string {
|
||||
engine = strings.ToLower(engine)
|
||||
var dsn string = ""
|
||||
switch engine {
|
||||
case "mysql":
|
||||
dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s",
|
||||
config["user"],
|
||||
config["password"],
|
||||
config["host"],
|
||||
config["port"],
|
||||
config["database"],
|
||||
config["charset"])
|
||||
}
|
||||
func getDbEngineDSN(setting *setting.Setting) string {
|
||||
engine := strings.ToLower(setting.Db.Engine)
|
||||
dsn := ""
|
||||
switch engine {
|
||||
case "mysql":
|
||||
dsn = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s",
|
||||
setting.Db.User,
|
||||
setting.Db.Password,
|
||||
setting.Db.Host,
|
||||
setting.Db.Port,
|
||||
setting.Db.Database,
|
||||
setting.Db.Charset)
|
||||
}
|
||||
|
||||
return dsn
|
||||
return dsn
|
||||
}
|
||||
|
||||
|
||||
// 获取数据库配置
|
||||
func getDbConfig() map[string]string {
|
||||
var db map[string]string = make(map[string]string)
|
||||
db["user"] = app.Setting.Key("db.user").String()
|
||||
db["password"] = app.Setting.Key("db.password").String()
|
||||
db["host"] = app.Setting.Key("db.host").String()
|
||||
db["port"] = app.Setting.Key("db.port").String()
|
||||
db["database"] = app.Setting.Key("db.database").String()
|
||||
db["charset"] = app.Setting.Key("db.charset").String()
|
||||
db["prefix"] = app.Setting.Key("db.prefix").String()
|
||||
db["engine"] = app.Setting.Key("db.engine").String()
|
||||
db["max_idle_conns"] = app.Setting.Key("db.max.idle.conns").String()
|
||||
db["max_open_conns"] = app.Setting.Key("db.max.open.conns").String()
|
||||
|
||||
return db
|
||||
func keepDbAlived(engine *xorm.Engine) {
|
||||
t := time.Tick(180 * time.Second)
|
||||
for {
|
||||
<-t
|
||||
engine.Ping()
|
||||
}
|
||||
}
|
||||
|
||||
func keepDbAlived(engine *xorm.Engine) {
|
||||
t := time.Tick(180 * time.Second)
|
||||
for {
|
||||
<- t
|
||||
engine.Ping()
|
||||
}
|
||||
}
|
||||
+100
-100
@@ -1,14 +1,14 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
Id int `xorm:"int pk autoincr"`
|
||||
Code string `xorm:"varchar(32) notnull"`
|
||||
Key string `xorm:"varchar(64) notnull"`
|
||||
Value string `xorm:"varchar(4096) notnull default '' "`
|
||||
type Setting struct {
|
||||
Id int `xorm:"int pk autoincr"`
|
||||
Code string `xorm:"varchar(32) notnull"`
|
||||
Key string `xorm:"varchar(64) notnull"`
|
||||
Value string `xorm:"varchar(4096) notnull default '' "`
|
||||
}
|
||||
|
||||
const SlackCode = "slack"
|
||||
@@ -21,154 +21,154 @@ const MailUserKey = "user"
|
||||
|
||||
// 初始化基本字段 邮件、slack等
|
||||
func (setting *Setting) InitBasicField() {
|
||||
setting.Code = SlackCode;
|
||||
setting.Key = SlackUrlKey
|
||||
Db.Insert(setting)
|
||||
setting.Code = SlackCode
|
||||
setting.Key = SlackUrlKey
|
||||
Db.Insert(setting)
|
||||
|
||||
setting.Id = 0
|
||||
setting.Code = MailCode
|
||||
setting.Key = MailServerKey
|
||||
Db.Insert(setting)
|
||||
setting.Id = 0
|
||||
setting.Code = MailCode
|
||||
setting.Key = MailServerKey
|
||||
Db.Insert(setting)
|
||||
}
|
||||
|
||||
// region slack配置
|
||||
|
||||
type Slack struct {
|
||||
Url string
|
||||
Channels []Channel
|
||||
Url string
|
||||
Channels []Channel
|
||||
}
|
||||
|
||||
type Channel struct {
|
||||
Id int
|
||||
Name string
|
||||
Id int
|
||||
Name string
|
||||
}
|
||||
|
||||
func (setting *Setting) Slack() (Slack, error) {
|
||||
list := make([]Setting, 0)
|
||||
err := Db.Where("code = ?", SlackCode).Find(&list)
|
||||
slack := Slack{Url:"", Channels:make([]Channel, 0)}
|
||||
if err != nil {
|
||||
return slack, err
|
||||
}
|
||||
func (setting *Setting) Slack() (Slack, error) {
|
||||
list := make([]Setting, 0)
|
||||
err := Db.Where("code = ?", SlackCode).Find(&list)
|
||||
slack := Slack{Url: "", Channels: make([]Channel, 0)}
|
||||
if err != nil {
|
||||
return slack, err
|
||||
}
|
||||
|
||||
setting.formatSlack(list, &slack)
|
||||
setting.formatSlack(list, &slack)
|
||||
|
||||
return slack, err
|
||||
return slack, err
|
||||
}
|
||||
|
||||
func (setting *Setting) formatSlack(list []Setting, slack *Slack) {
|
||||
for _, v := range list {
|
||||
if v.Key == SlackUrlKey {
|
||||
slack.Url = v.Value
|
||||
continue
|
||||
}
|
||||
func (setting *Setting) formatSlack(list []Setting, slack *Slack) {
|
||||
for _, v := range list {
|
||||
if v.Key == SlackUrlKey {
|
||||
slack.Url = v.Value
|
||||
continue
|
||||
}
|
||||
|
||||
slack.Channels = append(slack.Channels, Channel{
|
||||
v.Id, v.Value,
|
||||
})
|
||||
}
|
||||
slack.Channels = append(slack.Channels, Channel{
|
||||
v.Id, v.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 更新slack webhook url
|
||||
func (setting *Setting) UpdateSlackUrl(url string) (int64, error) {
|
||||
setting.Value = url
|
||||
setting.Value = url
|
||||
|
||||
return Db.Cols("value").Update(setting, Setting{Code:SlackCode, Key:SlackUrlKey})
|
||||
return Db.Cols("value").Update(setting, Setting{Code: SlackCode, Key: SlackUrlKey})
|
||||
}
|
||||
|
||||
|
||||
// 创建slack渠道
|
||||
func (setting *Setting) CreateChannel(channel string) (int64, error) {
|
||||
setting.Code = SlackCode
|
||||
setting.Key = SlackChannelKey
|
||||
setting.Value = channel
|
||||
setting.Code = SlackCode
|
||||
setting.Key = SlackChannelKey
|
||||
setting.Value = channel
|
||||
|
||||
return Db.Insert(setting)
|
||||
return Db.Insert(setting)
|
||||
}
|
||||
|
||||
func (setting *Setting) IsChannelExist(channel string) (bool) {
|
||||
setting.Code = SlackCode
|
||||
setting.Key = SlackChannelKey
|
||||
setting.Value = channel
|
||||
func (setting *Setting) IsChannelExist(channel string) bool {
|
||||
setting.Code = SlackCode
|
||||
setting.Key = SlackChannelKey
|
||||
setting.Value = channel
|
||||
|
||||
count, _ := Db.Count(setting)
|
||||
count, _ := Db.Count(setting)
|
||||
|
||||
return count > 0
|
||||
return count > 0
|
||||
}
|
||||
|
||||
// 删除slack渠道
|
||||
func (setting *Setting) RemoveChannel(id int) (int64, error) {
|
||||
setting.Code = SlackCode
|
||||
setting.Key = SlackChannelKey
|
||||
setting.Id = id
|
||||
return Db.Delete(setting)
|
||||
func (setting *Setting) RemoveChannel(id int) (int64, error) {
|
||||
setting.Code = SlackCode
|
||||
setting.Key = SlackChannelKey
|
||||
setting.Id = id
|
||||
return Db.Delete(setting)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
type Mail struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
MailUsers []MailUser
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
MailUsers []MailUser
|
||||
}
|
||||
|
||||
type MailUser struct {
|
||||
Id int
|
||||
Username string
|
||||
Email string
|
||||
Id int
|
||||
Username string
|
||||
Email string
|
||||
}
|
||||
|
||||
// region 邮件配置
|
||||
func (setting *Setting) Mail() (Mail, error) {
|
||||
list := make([]Setting, 0)
|
||||
err := Db.Where("code = ?", MailCode).Find(&list)
|
||||
mail := Mail{MailUsers:make([]MailUser, 0)}
|
||||
if err != nil {
|
||||
return mail, err
|
||||
}
|
||||
func (setting *Setting) Mail() (Mail, error) {
|
||||
list := make([]Setting, 0)
|
||||
err := Db.Where("code = ?", MailCode).Find(&list)
|
||||
mail := Mail{MailUsers: make([]MailUser, 0)}
|
||||
if err != nil {
|
||||
return mail, err
|
||||
}
|
||||
|
||||
setting.formatMail(list, &mail)
|
||||
setting.formatMail(list, &mail)
|
||||
|
||||
return mail, err
|
||||
return mail, err
|
||||
}
|
||||
|
||||
func (setting *Setting) formatMail(list []Setting, mail *Mail) {
|
||||
mailUser := MailUser{}
|
||||
for _, v := range list {
|
||||
if v.Key == MailServerKey {
|
||||
json.Unmarshal([]byte(v.Value), mail)
|
||||
continue
|
||||
}
|
||||
json.Unmarshal([]byte(v.Value), &mailUser)
|
||||
mailUser.Id = v.Id
|
||||
mail.MailUsers = append(mail.MailUsers, mailUser)
|
||||
}
|
||||
func (setting *Setting) formatMail(list []Setting, mail *Mail) {
|
||||
mailUser := MailUser{}
|
||||
for _, v := range list {
|
||||
if v.Key == MailServerKey {
|
||||
json.Unmarshal([]byte(v.Value), mail)
|
||||
continue
|
||||
}
|
||||
json.Unmarshal([]byte(v.Value), &mailUser)
|
||||
mailUser.Id = v.Id
|
||||
mail.MailUsers = append(mail.MailUsers, mailUser)
|
||||
}
|
||||
}
|
||||
|
||||
func (setting *Setting) UpdateMailServer(config string) (int64, error) {
|
||||
setting.Value = config
|
||||
return Db.Cols("value").Update(setting, Setting{Code:MailCode, Key:MailServerKey})
|
||||
func (setting *Setting) UpdateMailServer(config string) (int64, error) {
|
||||
setting.Value = config
|
||||
return Db.Cols("value").Update(setting, Setting{Code: MailCode, Key: MailServerKey})
|
||||
}
|
||||
|
||||
func (setting *Setting) CreateMailUser(username, email string) (int64, error) {
|
||||
setting.Code = MailCode
|
||||
setting.Key = MailUserKey
|
||||
mailUser := MailUser{0, username, email}
|
||||
jsonByte, err := json.Marshal(mailUser)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
setting.Value = string(jsonByte)
|
||||
setting.Code = MailCode
|
||||
setting.Key = MailUserKey
|
||||
mailUser := MailUser{0, username, email}
|
||||
jsonByte, err := json.Marshal(mailUser)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
setting.Value = string(jsonByte)
|
||||
|
||||
return Db.Insert(setting)
|
||||
return Db.Insert(setting)
|
||||
}
|
||||
|
||||
func (setting *Setting) RemoveMailUser(id int) (int64, error) {
|
||||
setting.Code = MailCode
|
||||
setting.Key = MailUserKey
|
||||
setting.Id = id
|
||||
return Db.Delete(setting)
|
||||
func (setting *Setting) RemoveMailUser(id int) (int64, error) {
|
||||
setting.Code = MailCode
|
||||
setting.Key = MailUserKey
|
||||
setting.Id = id
|
||||
return Db.Delete(setting)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// endregion
|
||||
|
||||
+198
-160
@@ -1,247 +1,285 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/go-xorm/xorm"
|
||||
"errors"
|
||||
"strings"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
type TaskProtocol int8
|
||||
|
||||
const (
|
||||
TaskHTTP TaskProtocol = iota + 1 // HTTP协议
|
||||
TaskRPC // RPC方式执行命令
|
||||
TaskHTTP TaskProtocol = iota + 1 // HTTP协议
|
||||
TaskRPC // RPC方式执行命令
|
||||
)
|
||||
|
||||
type TaskLevel int8
|
||||
|
||||
const (
|
||||
TaskLevelParent TaskLevel = 1 // 父任务
|
||||
TaskLevelChild TaskLevel = 2 // 子任务(依赖任务)
|
||||
TaskLevelParent TaskLevel = 1 // 父任务
|
||||
TaskLevelChild TaskLevel = 2 // 子任务(依赖任务)
|
||||
)
|
||||
|
||||
type TaskDependencyStatus int8
|
||||
|
||||
const (
|
||||
TaskDependencyStatusStrong TaskDependencyStatus = 1 // 强依赖
|
||||
TaskDependencyStatusWeak TaskDependencyStatus = 2 // 弱依赖
|
||||
TaskDependencyStatusStrong TaskDependencyStatus = 1 // 强依赖
|
||||
TaskDependencyStatusWeak TaskDependencyStatus = 2 // 弱依赖
|
||||
)
|
||||
|
||||
type TaskHTTPMethod int8
|
||||
|
||||
const (
|
||||
TaskHTTPMethodGet TaskHTTPMethod = 1
|
||||
TaskHttpMethodPost TaskHTTPMethod = 2
|
||||
)
|
||||
|
||||
// 任务
|
||||
type Task struct {
|
||||
Id int `xorm:"int pk autoincr"`
|
||||
Name string `xorm:"varchar(32) notnull"` // 任务名称
|
||||
Level TaskLevel `xorm:"smallint notnull index default 1"` // 任务等级 1: 主任务 2: 依赖任务
|
||||
DependencyTaskId string `xorm:"varchar(64) notnull default ''"` // 依赖任务ID,多个ID逗号分隔
|
||||
DependencyStatus TaskDependencyStatus `xorm:"smallint notnull default 1"` // 依赖关系 1:强依赖 主任务执行成功, 依赖任务才会被执行 2:弱依赖
|
||||
Spec string `xorm:"varchar(64) notnull"` // crontab
|
||||
Protocol TaskProtocol `xorm:"tinyint notnull index"` // 协议 1:http 2:系统命令
|
||||
Command string `xorm:"varchar(256) notnull"` // URL地址或shell命令
|
||||
Timeout int `xorm:"mediumint notnull default 0"` // 任务执行超时时间(单位秒),0不限制
|
||||
Multi int8 `xorm:"tinyint notnull default 1"` // 是否允许多实例运行
|
||||
RetryTimes int8 `xorm:"tinyint notnull default 0"` // 重试次数
|
||||
HostId int16 `xorm:"smallint notnull index default 0"` // RPC host id,
|
||||
NotifyStatus int8 `xorm:"smallint notnull default 1"` // 任务执行结束是否通知 0: 不通知 1: 失败通知 2: 执行结束通知
|
||||
NotifyType int8 `xorm:"smallint notnull default 0"` // 通知类型 1: 邮件 2: slack
|
||||
NotifyReceiverId string `xorm:"varchar(256) notnull default '' "` // 通知接受者ID, setting表主键ID,多个ID逗号分隔
|
||||
Remark string `xorm:"varchar(100) notnull default ''"` // 备注
|
||||
Status Status `xorm:"tinyint notnull index default 0"` // 状态 1:正常 0:停止
|
||||
Created time.Time `xorm:"datetime notnull created"` // 创建时间
|
||||
Deleted time.Time `xorm:"datetime deleted"` // 删除时间
|
||||
BaseModel `xorm:"-"`
|
||||
Id int `xorm:"int pk autoincr"`
|
||||
Name string `xorm:"varchar(32) notnull"` // 任务名称
|
||||
Level TaskLevel `xorm:"tinyint notnull index default 1"` // 任务等级 1: 主任务 2: 依赖任务
|
||||
DependencyTaskId string `xorm:"varchar(64) notnull default ''"` // 依赖任务ID,多个ID逗号分隔
|
||||
DependencyStatus TaskDependencyStatus `xorm:"tinyint notnull default 1"` // 依赖关系 1:强依赖 主任务执行成功, 依赖任务才会被执行 2:弱依赖
|
||||
Spec string `xorm:"varchar(64) notnull"` // crontab
|
||||
Protocol TaskProtocol `xorm:"tinyint notnull index"` // 协议 1:http 2:系统命令
|
||||
Command string `xorm:"varchar(256) notnull"` // URL地址或shell命令
|
||||
HttpMethod TaskHTTPMethod `xorm:"tinyint notnull default 1"` // http请求方法
|
||||
Timeout int `xorm:"mediumint notnull default 0"` // 任务执行超时时间(单位秒),0不限制
|
||||
Multi int8 `xorm:"tinyint notnull default 1"` // 是否允许多实例运行
|
||||
RetryTimes int8 `xorm:"tinyint notnull default 0"` // 重试次数
|
||||
RetryInterval int16 `xorm:"smallint notnull default 0"` // 重试间隔时间
|
||||
NotifyStatus int8 `xorm:"tinyint notnull default 1"` // 任务执行结束是否通知 0: 不通知 1: 失败通知 2: 执行结束通知
|
||||
NotifyType int8 `xorm:"tinyint notnull default 0"` // 通知类型 1: 邮件 2: slack
|
||||
NotifyReceiverId string `xorm:"varchar(256) notnull default '' "` // 通知接受者ID, setting表主键ID,多个ID逗号分隔
|
||||
Tag string `xorm:"varchar(32) notnull default ''"`
|
||||
Remark string `xorm:"varchar(100) notnull default ''"` // 备注
|
||||
Status Status `xorm:"tinyint notnull index default 0"` // 状态 1:正常 0:停止
|
||||
Created time.Time `xorm:"datetime notnull created"` // 创建时间
|
||||
Deleted time.Time `xorm:"datetime deleted"` // 删除时间
|
||||
BaseModel `xorm:"-"`
|
||||
Hosts []TaskHostDetail `xorm:"-"`
|
||||
}
|
||||
|
||||
type TaskHost struct {
|
||||
Task `xorm:"extends"`
|
||||
Name string
|
||||
Port int
|
||||
Alias string
|
||||
}
|
||||
|
||||
func (TaskHost) TableName() string {
|
||||
return TablePrefix + "task"
|
||||
func taskHostTableName() []string {
|
||||
return []string{TablePrefix + "task_host", "th"}
|
||||
}
|
||||
|
||||
// 新增
|
||||
func (task *Task) Create() (insertId int, err error) {
|
||||
_, err = Db.Insert(task)
|
||||
if err == nil {
|
||||
insertId = task.Id
|
||||
}
|
||||
_, err = Db.Insert(task)
|
||||
if err == nil {
|
||||
insertId = task.Id
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
// 新增测试任务
|
||||
func (task *Task) CreateTestTask() {
|
||||
// HTTP任务
|
||||
task.Name = "测试HTTP任务"
|
||||
task.Level = TaskLevelParent
|
||||
task.Protocol = TaskHTTP
|
||||
task.Spec = "*/30 * * * * *"
|
||||
// 查询IP地址区域信息
|
||||
task.Command = "http://ip.taobao.com/service/getIpInfo.php?ip=117.27.140.253"
|
||||
task.Status = Enabled
|
||||
task.Create()
|
||||
// HTTP任务
|
||||
task.Name = "测试HTTP任务"
|
||||
task.Level = TaskLevelParent
|
||||
task.Protocol = TaskHTTP
|
||||
task.Spec = "*/30 * * * * *"
|
||||
task.Tag = "test-task"
|
||||
// 查询IP地址区域信息
|
||||
task.Command = "http://ip.taobao.com/service/getIpInfo.php?ip=117.27.140.253"
|
||||
task.Status = Enabled
|
||||
task.Create()
|
||||
}
|
||||
|
||||
func (task *Task) UpdateBean(id int) (int64, error) {
|
||||
return Db.ID(id).
|
||||
Cols("name,spec,protocol,command,timeout,multi,retry_times,host_id,remark,notify_status,notify_type,notify_receiver_id, dependency_task_id, dependency_status").
|
||||
Update(task)
|
||||
func (task *Task) UpdateBean(id int) (int64, error) {
|
||||
return Db.ID(id).
|
||||
Cols(`name,spec,protocol,command,timeout,multi,
|
||||
retry_times,retry_interval,remark,notify_status,
|
||||
notify_type,notify_receiver_id, dependency_task_id, dependency_status, tag,http_method`).
|
||||
Update(task)
|
||||
}
|
||||
|
||||
// 更新
|
||||
func (task *Task) Update(id int, data CommonMap) (int64, error) {
|
||||
return Db.Table(task).ID(id).Update(data)
|
||||
return Db.Table(task).ID(id).Update(data)
|
||||
}
|
||||
|
||||
// 删除
|
||||
func (task *Task) Delete(id int) (int64, error) {
|
||||
return Db.Id(id).Delete(task)
|
||||
return Db.Id(id).Delete(task)
|
||||
}
|
||||
|
||||
// 禁用
|
||||
func (task *Task) Disable(id int) (int64, error) {
|
||||
return task.Update(id, CommonMap{"status": Disabled})
|
||||
return task.Update(id, CommonMap{"status": Disabled})
|
||||
}
|
||||
|
||||
// 激活
|
||||
func (task *Task) Enable(id int) (int64, error) {
|
||||
return task.Update(id, CommonMap{"status": Enabled})
|
||||
return task.Update(id, CommonMap{"status": Enabled})
|
||||
}
|
||||
|
||||
// 获取所有激活任务
|
||||
func (task *Task) ActiveList() ([]TaskHost, error) {
|
||||
list := make([]TaskHost, 0)
|
||||
fields := "t.*, host.alias,host.name,host.port"
|
||||
err := Db.Alias("t").
|
||||
Join("LEFT", hostTableName(), "t.host_id=host.id").
|
||||
Where("t.status = ? AND t.level = ?", Enabled, TaskLevelParent).
|
||||
Cols(fields).
|
||||
Find(&list)
|
||||
func (task *Task) ActiveList(page, pageSize int) ([]Task, error) {
|
||||
params := CommonMap{"Page": page, "PageSize": pageSize}
|
||||
task.parsePageAndPageSize(params)
|
||||
list := make([]Task, 0)
|
||||
err := Db.Where("status = ? AND level = ?", Enabled, TaskLevelParent).Limit(task.PageSize, task.pageLimitOffset()).
|
||||
Find(&list)
|
||||
|
||||
return list, err
|
||||
if err != nil {
|
||||
return list, err
|
||||
}
|
||||
|
||||
return task.setHostsForTasks(list)
|
||||
}
|
||||
|
||||
// 获取某个主机下的所有激活任务
|
||||
func (task *Task) ActiveListByHostId(hostId int16) ([]TaskHost, error) {
|
||||
list := make([]TaskHost, 0)
|
||||
fields := "t.*, host.alias,host.name,host.port"
|
||||
err := Db.Alias("t").
|
||||
Join("LEFT", hostTableName(), "t.host_id=host.id").
|
||||
Where("t.status = ? AND t.host_id = ? AND t.level = ?", Enabled, hostId, TaskLevelParent).
|
||||
Cols(fields).
|
||||
Find(&list)
|
||||
func (task *Task) ActiveListByHostId(hostId int16) ([]Task, error) {
|
||||
taskHostModel := new(TaskHost)
|
||||
taskIds, err := taskHostModel.GetTaskIdsByHostId(hostId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list := make([]Task, 0)
|
||||
err = Db.Where("status = ? AND level = ?", Enabled, TaskLevelParent).
|
||||
In("id", taskIds...).
|
||||
Find(&list)
|
||||
if err != nil {
|
||||
return list, err
|
||||
}
|
||||
|
||||
return list, err
|
||||
return task.setHostsForTasks(list)
|
||||
}
|
||||
|
||||
// 判断主机id是否有引用
|
||||
func (task *Task) HostIdExist(hostId int16) (bool, error) {
|
||||
count, err := Db.Where("host_id = ?", hostId).Count(task);
|
||||
func (task *Task) setHostsForTasks(tasks []Task) ([]Task, error) {
|
||||
taskHostModel := new(TaskHost)
|
||||
var err error
|
||||
for i, value := range tasks {
|
||||
taskHostDetails, err := taskHostModel.GetHostIdsByTaskId(value.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tasks[i].Hosts = taskHostDetails
|
||||
}
|
||||
|
||||
return count > 0, err
|
||||
return tasks, err
|
||||
}
|
||||
|
||||
// 判断任务名称是否存在
|
||||
func (task *Task) NameExist(name string, id int) (bool, error) {
|
||||
if id > 0 {
|
||||
count, err := Db.Where("name = ? AND status = ? AND id != ?", name, Enabled, id).Count(task);
|
||||
return count > 0, err
|
||||
}
|
||||
count, err := Db.Where("name = ? AND status = ?", name, Enabled).Count(task);
|
||||
func (task *Task) NameExist(name string, id int) (bool, error) {
|
||||
if id > 0 {
|
||||
count, err := Db.Where("name = ? AND status = ? AND id != ?", name, Enabled, id).Count(task)
|
||||
return count > 0, err
|
||||
}
|
||||
count, err := Db.Where("name = ? AND status = ?", name, Enabled).Count(task)
|
||||
|
||||
return count > 0, err
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
func (task *Task) GetStatus(id int) (Status, error) {
|
||||
exist, err := Db.Id(id).Get(task)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !exist {
|
||||
return 0, errors.New("not exist")
|
||||
}
|
||||
exist, err := Db.Id(id).Get(task)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !exist {
|
||||
return 0, errors.New("not exist")
|
||||
}
|
||||
|
||||
return task.Status, nil
|
||||
return task.Status, nil
|
||||
}
|
||||
|
||||
func(task *Task) Detail(id int) (TaskHost, error) {
|
||||
taskHost := TaskHost{}
|
||||
fields := "t.*, host.alias,host.name,host.port"
|
||||
_, err := Db.Alias("t").Join("LEFT", hostTableName(), "t.host_id=host.id").Where("t.id=?", id).Cols(fields).Get(&taskHost)
|
||||
func (task *Task) Detail(id int) (Task, error) {
|
||||
t := Task{}
|
||||
_, err := Db.Where("id=?", id).Get(&t)
|
||||
|
||||
return taskHost, err
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
|
||||
taskHostModel := new(TaskHost)
|
||||
t.Hosts, err = taskHostModel.GetHostIdsByTaskId(id)
|
||||
|
||||
return t, err
|
||||
}
|
||||
|
||||
func (task *Task) List(params CommonMap) ([]TaskHost, error) {
|
||||
task.parsePageAndPageSize(params)
|
||||
list := make([]TaskHost, 0)
|
||||
fields := "t.*, host.alias,host.name"
|
||||
session := Db.Alias("t").Join("LEFT", hostTableName(), "t.host_id=host.id")
|
||||
task.parseWhere(session, params)
|
||||
err := session.Cols(fields).Desc("t.id").Limit(task.PageSize, task.pageLimitOffset()).Find(&list)
|
||||
func (task *Task) List(params CommonMap) ([]Task, error) {
|
||||
task.parsePageAndPageSize(params)
|
||||
list := make([]Task, 0)
|
||||
session := Db.Alias("t").Join("LEFT", taskHostTableName(), "t.id = th.task_id")
|
||||
task.parseWhere(session, params)
|
||||
err := session.GroupBy("t.id").Desc("t.id").Cols("t.*").Limit(task.PageSize, task.pageLimitOffset()).Find(&list)
|
||||
|
||||
return list, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return task.setHostsForTasks(list)
|
||||
}
|
||||
|
||||
// 获取依赖任务列表
|
||||
func (task *Task) GetDependencyTaskList(ids string) ([]TaskHost, error) {
|
||||
list := make([]TaskHost, 0)
|
||||
if ids == "" {
|
||||
return list, nil
|
||||
}
|
||||
idList := strings.Split(ids, ",")
|
||||
taskIds := make([]interface{}, len(idList))
|
||||
for i, v := range idList {
|
||||
taskIds[i] = v
|
||||
}
|
||||
fields := "t.*, host.alias,host.name,host.port"
|
||||
err := Db.Alias("t").
|
||||
Join("LEFT", hostTableName(), "t.host_id=host.id").
|
||||
Where("t.level = ?", TaskLevelChild).
|
||||
In("t.id", taskIds).
|
||||
Cols(fields).
|
||||
Find(&list)
|
||||
func (task *Task) GetDependencyTaskList(ids string) ([]Task, error) {
|
||||
list := make([]Task, 0)
|
||||
if ids == "" {
|
||||
return list, nil
|
||||
}
|
||||
idList := strings.Split(ids, ",")
|
||||
taskIds := make([]interface{}, len(idList))
|
||||
for i, v := range idList {
|
||||
taskIds[i] = v
|
||||
}
|
||||
fields := "t.*"
|
||||
err := Db.Alias("t").
|
||||
Where("t.level = ?", TaskLevelChild).
|
||||
In("t.id", taskIds).
|
||||
Cols(fields).
|
||||
Find(&list)
|
||||
|
||||
return list, err
|
||||
if err != nil {
|
||||
return list, err
|
||||
}
|
||||
|
||||
return task.setHostsForTasks(list)
|
||||
}
|
||||
|
||||
func (task *Task) Total(params CommonMap) (int64, error) {
|
||||
session := Db.Alias("t").Join("LEFT", hostTableName(), "t.host_id=host.id")
|
||||
task.parseWhere(session, params)
|
||||
return session.Count(task)
|
||||
session := Db.Alias("t").Join("LEFT", taskHostTableName(), "t.id = th.task_id")
|
||||
task.parseWhere(session, params)
|
||||
list := make([]Task, 0)
|
||||
|
||||
err := session.GroupBy("t.id").Find(&list)
|
||||
|
||||
return int64(len(list)), err
|
||||
}
|
||||
|
||||
// 解析where
|
||||
func (task *Task) parseWhere(session *xorm.Session, params CommonMap) {
|
||||
if len(params) == 0 {
|
||||
return
|
||||
}
|
||||
id, ok := params["Id"]
|
||||
if ok && id.(int) > 0 {
|
||||
session.And("t.id = ?", id)
|
||||
}
|
||||
hostId, ok := params["HostId"]
|
||||
if ok && hostId.(int) > 0 {
|
||||
session.And("host_id = ?", hostId)
|
||||
}
|
||||
name, ok := params["Name"]
|
||||
if ok && name.(string) != "" {
|
||||
session.And("t.name LIKE ?", "%" + name.(string) + "%")
|
||||
}
|
||||
protocol, ok := params["Protocol"]
|
||||
if ok && protocol.(int) > 0 {
|
||||
session.And("protocol = ?", protocol)
|
||||
}
|
||||
status, ok := params["Status"]
|
||||
if ok && status.(int) > -1 {
|
||||
session.And("status = ?", status)
|
||||
}
|
||||
}
|
||||
func (task *Task) parseWhere(session *xorm.Session, params CommonMap) {
|
||||
if len(params) == 0 {
|
||||
return
|
||||
}
|
||||
id, ok := params["Id"]
|
||||
if ok && id.(int) > 0 {
|
||||
session.And("t.id = ?", id)
|
||||
}
|
||||
hostId, ok := params["HostId"]
|
||||
if ok && hostId.(int) > 0 {
|
||||
session.And("th.host_id = ?", hostId)
|
||||
}
|
||||
name, ok := params["Name"]
|
||||
if ok && name.(string) != "" {
|
||||
session.And("t.name LIKE ?", "%"+name.(string)+"%")
|
||||
}
|
||||
protocol, ok := params["Protocol"]
|
||||
if ok && protocol.(int) > 0 {
|
||||
session.And("protocol = ?", protocol)
|
||||
}
|
||||
status, ok := params["Status"]
|
||||
if ok && status.(int) > -1 {
|
||||
session.And("status = ?", status)
|
||||
}
|
||||
|
||||
func hostTableName() []string {
|
||||
return []string{TablePrefix + "host", "host"}
|
||||
}
|
||||
tag, ok := params["Tag"]
|
||||
if ok && tag.(string) != "" {
|
||||
session.And("tag = ? ", tag)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package models
|
||||
|
||||
type TaskHost struct {
|
||||
Id int `xorm:"int pk autoincr"`
|
||||
TaskId int `xorm:"int not null index"`
|
||||
HostId int16 `xorm:"smallint not null index"`
|
||||
}
|
||||
|
||||
type TaskHostDetail struct {
|
||||
TaskHost `xorm:"extends"`
|
||||
Name string
|
||||
Port int
|
||||
Alias string
|
||||
}
|
||||
|
||||
func (TaskHostDetail) TableName() string {
|
||||
return TablePrefix + "task_host"
|
||||
}
|
||||
|
||||
func hostTableName() []string {
|
||||
return []string{TablePrefix + "host", "h"}
|
||||
}
|
||||
|
||||
func (th *TaskHost) Remove(taskId int) error {
|
||||
_, err := Db.Where("task_id = ?", taskId).Delete(new(TaskHost))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (th *TaskHost) Add(taskId int, hostIds []int) error {
|
||||
|
||||
err := th.Remove(taskId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
taskHosts := make([]TaskHost, len(hostIds))
|
||||
for i, value := range hostIds {
|
||||
taskHosts[i].TaskId = taskId
|
||||
taskHosts[i].HostId = int16(value)
|
||||
}
|
||||
|
||||
_, err = Db.Insert(&taskHosts)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (th *TaskHost) GetHostIdsByTaskId(taskId int) ([]TaskHostDetail, error) {
|
||||
list := make([]TaskHostDetail, 0)
|
||||
fields := "th.id,th.host_id,h.alias,h.name,h.port"
|
||||
err := Db.Alias("th").
|
||||
Join("LEFT", hostTableName(), "th.host_id=h.id").
|
||||
Where("th.task_id = ?", taskId).
|
||||
Cols(fields).
|
||||
Find(&list)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (th *TaskHost) GetTaskIdsByHostId(hostId int16) ([]interface{}, error) {
|
||||
list := make([]TaskHost, 0)
|
||||
err := Db.Where("host_id = ?", hostId).Cols("task_id").Find(&list)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
taskIds := make([]interface{}, len(list))
|
||||
for i, value := range list {
|
||||
taskIds[i] = value.TaskId
|
||||
}
|
||||
|
||||
return taskIds, err
|
||||
}
|
||||
|
||||
// 判断主机id是否有引用
|
||||
func (th *TaskHost) HostIdExist(hostId int16) (bool, error) {
|
||||
count, err := Db.Where("host_id = ?", hostId).Count(th)
|
||||
|
||||
return count > 0, err
|
||||
}
|
||||
+65
-65
@@ -1,99 +1,99 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/go-xorm/xorm"
|
||||
"time"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
type TaskType int8
|
||||
|
||||
|
||||
// 任务执行日志
|
||||
type TaskLog struct {
|
||||
Id int64 `xorm:"bigint pk autoincr"`
|
||||
TaskId int `xorm:"int notnull index default 0"` // 任务id
|
||||
Name string `xorm:"varchar(32) notnull"` // 任务名称
|
||||
Spec string `xorm:"varchar(64) notnull"` // crontab
|
||||
Protocol TaskProtocol `xorm:"tinyint notnull index"` // 协议 1:http 2:RPC
|
||||
Command string `xorm:"varchar(256) notnull"` // URL地址或shell命令
|
||||
Timeout int `xorm:"mediumint notnull default 0"` // 任务执行超时时间(单位秒),0不限制
|
||||
RetryTimes int8 `xorm:"tinyint notnull default 0"` // 任务重试次数
|
||||
Hostname string `xorm:"varchar(128) notnull defalut '' "` // RPC主机名,逗号分隔
|
||||
StartTime time.Time `xorm:"datetime created"` // 开始执行时间
|
||||
EndTime time.Time `xorm:"datetime updated"` // 执行完成(失败)时间
|
||||
Status Status `xorm:"tinyint notnull index default 1"` // 状态 0:执行失败 1:执行中 2:执行完毕 3:任务取消(上次任务未执行完成) 4:异步执行
|
||||
Result string `xorm:"mediumtext notnull defalut '' "` // 执行结果
|
||||
TotalTime int `xorm:"-"` // 执行总时长
|
||||
BaseModel `xorm:"-"`
|
||||
Id int64 `xorm:"bigint pk autoincr"`
|
||||
TaskId int `xorm:"int notnull index default 0"` // 任务id
|
||||
Name string `xorm:"varchar(32) notnull"` // 任务名称
|
||||
Spec string `xorm:"varchar(64) notnull"` // crontab
|
||||
Protocol TaskProtocol `xorm:"tinyint notnull index"` // 协议 1:http 2:RPC
|
||||
Command string `xorm:"varchar(256) notnull"` // URL地址或shell命令
|
||||
Timeout int `xorm:"mediumint notnull default 0"` // 任务执行超时时间(单位秒),0不限制
|
||||
RetryTimes int8 `xorm:"tinyint notnull default 0"` // 任务重试次数
|
||||
Hostname string `xorm:"varchar(128) notnull defalut '' "` // RPC主机名,逗号分隔
|
||||
StartTime time.Time `xorm:"datetime created"` // 开始执行时间
|
||||
EndTime time.Time `xorm:"datetime updated"` // 执行完成(失败)时间
|
||||
Status Status `xorm:"tinyint notnull index default 1"` // 状态 0:执行失败 1:执行中 2:执行完毕 3:任务取消(上次任务未执行完成) 4:异步执行
|
||||
Result string `xorm:"mediumtext notnull defalut '' "` // 执行结果
|
||||
TotalTime int `xorm:"-"` // 执行总时长
|
||||
BaseModel `xorm:"-"`
|
||||
}
|
||||
|
||||
func (taskLog *TaskLog) Create() (insertId int64, err error) {
|
||||
_, err = Db.Insert(taskLog)
|
||||
if err == nil {
|
||||
insertId = taskLog.Id
|
||||
}
|
||||
_, err = Db.Insert(taskLog)
|
||||
if err == nil {
|
||||
insertId = taskLog.Id
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
// 更新
|
||||
func (taskLog *TaskLog) Update(id int64, data CommonMap) (int64, error) {
|
||||
return Db.Table(taskLog).ID(id).Update(data)
|
||||
return Db.Table(taskLog).ID(id).Update(data)
|
||||
}
|
||||
|
||||
func (taskLog *TaskLog) List(params CommonMap) ([]TaskLog, error) {
|
||||
taskLog.parsePageAndPageSize(params)
|
||||
list := make([]TaskLog, 0)
|
||||
session := Db.Desc("id")
|
||||
taskLog.parseWhere(session, params)
|
||||
err := session.Limit(taskLog.PageSize, taskLog.pageLimitOffset()).Find(&list)
|
||||
if len(list) > 0 {
|
||||
for i, item := range list {
|
||||
endTime := item.EndTime
|
||||
if item.Status == Running {
|
||||
endTime = time.Now()
|
||||
}
|
||||
execSeconds := endTime.Sub(item.StartTime).Seconds()
|
||||
list[i].TotalTime = int(execSeconds)
|
||||
}
|
||||
}
|
||||
taskLog.parsePageAndPageSize(params)
|
||||
list := make([]TaskLog, 0)
|
||||
session := Db.Desc("id")
|
||||
taskLog.parseWhere(session, params)
|
||||
err := session.Limit(taskLog.PageSize, taskLog.pageLimitOffset()).Find(&list)
|
||||
if len(list) > 0 {
|
||||
for i, item := range list {
|
||||
endTime := item.EndTime
|
||||
if item.Status == Running {
|
||||
endTime = time.Now()
|
||||
}
|
||||
execSeconds := endTime.Sub(item.StartTime).Seconds()
|
||||
list[i].TotalTime = int(execSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
return list, err
|
||||
return list, err
|
||||
}
|
||||
|
||||
// 清空表
|
||||
func (taskLog *TaskLog) Clear() (int64, error) {
|
||||
return Db.Where("1=1").Delete(taskLog);
|
||||
func (taskLog *TaskLog) Clear() (int64, error) {
|
||||
return Db.Where("1=1").Delete(taskLog)
|
||||
}
|
||||
|
||||
// 删除N个月前的日志
|
||||
func (taskLog *TaskLog) Remove(id int) (int64, error) {
|
||||
t := time.Now().AddDate(0, -id, 0)
|
||||
return Db.Where("start_time <= ?", t.Format(DefaultTimeFormat)).Delete(taskLog)
|
||||
t := time.Now().AddDate(0, -id, 0)
|
||||
return Db.Where("start_time <= ?", t.Format(DefaultTimeFormat)).Delete(taskLog)
|
||||
}
|
||||
|
||||
func (taskLog *TaskLog) Total(params CommonMap) (int64, error) {
|
||||
session := Db.NewSession()
|
||||
defer session.Close()
|
||||
taskLog.parseWhere(session, params)
|
||||
return session.Count(taskLog)
|
||||
session := Db.NewSession()
|
||||
defer session.Close()
|
||||
taskLog.parseWhere(session, params)
|
||||
return session.Count(taskLog)
|
||||
}
|
||||
|
||||
// 解析where
|
||||
func (taskLog *TaskLog) parseWhere(session *xorm.Session, params CommonMap) {
|
||||
if len(params) == 0 {
|
||||
return
|
||||
}
|
||||
taskId, ok := params["TaskId"]
|
||||
if ok && taskId.(int) > 0 {
|
||||
session.And("task_id = ?", taskId)
|
||||
}
|
||||
protocol, ok := params["Protocol"]
|
||||
if ok && protocol.(int) > 0 {
|
||||
session.And("protocol = ?", protocol)
|
||||
}
|
||||
status, ok := params["Status"]
|
||||
if ok && status.(int) > -1 {
|
||||
session.And("status = ?", status)
|
||||
}
|
||||
}
|
||||
func (taskLog *TaskLog) parseWhere(session *xorm.Session, params CommonMap) {
|
||||
if len(params) == 0 {
|
||||
return
|
||||
}
|
||||
taskId, ok := params["TaskId"]
|
||||
if ok && taskId.(int) > 0 {
|
||||
session.And("task_id = ?", taskId)
|
||||
}
|
||||
protocol, ok := params["Protocol"]
|
||||
if ok && protocol.(int) > 0 {
|
||||
session.And("protocol = ?", protocol)
|
||||
}
|
||||
status, ok := params["Status"]
|
||||
if ok && status.(int) > -1 {
|
||||
session.And("status = ?", status)
|
||||
}
|
||||
}
|
||||
|
||||
+66
-50
@@ -1,110 +1,126 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"time"
|
||||
"time"
|
||||
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
)
|
||||
|
||||
const PasswordSaltLength = 6
|
||||
|
||||
// 用户model
|
||||
type User struct {
|
||||
Id int `xorm:"pk autoincr notnull "`
|
||||
Name string `xorm:"varchar(32) notnull unique"` // 用户名
|
||||
Password string `xorm:"char(32) notnull "` // 密码
|
||||
Salt string `xorm:"char(6) notnull "` // 密码盐值
|
||||
Email string `xorm:"varchar(50) notnull unique default '' "` // 邮箱
|
||||
Created time.Time `xorm:"datetime notnull created"`
|
||||
Updated time.Time `xorm:"datetime updated"`
|
||||
Deleted time.Time `xorm:"datetime deleted"`
|
||||
IsAdmin int8 `xorm:"tinyint notnull default 0"` // 是否是管理员 1:管理员 0:普通用户
|
||||
Status Status `xorm:"tinyint notnull default 1"` // 1: 正常 0:禁用
|
||||
BaseModel `xorm:"-"`
|
||||
Id int `xorm:"pk autoincr notnull "`
|
||||
Name string `xorm:"varchar(32) notnull unique"` // 用户名
|
||||
Password string `xorm:"char(32) notnull "` // 密码
|
||||
Salt string `xorm:"char(6) notnull "` // 密码盐值
|
||||
Email string `xorm:"varchar(50) notnull unique default '' "` // 邮箱
|
||||
Created time.Time `xorm:"datetime notnull created"`
|
||||
Updated time.Time `xorm:"datetime updated"`
|
||||
IsAdmin int8 `xorm:"tinyint notnull default 0"` // 是否是管理员 1:管理员 0:普通用户
|
||||
Status Status `xorm:"tinyint notnull default 1"` // 1: 正常 0:禁用
|
||||
BaseModel `xorm:"-"`
|
||||
}
|
||||
|
||||
// 新增
|
||||
func (user *User) Create() (insertId int, err error) {
|
||||
user.Status = Enabled
|
||||
user.Salt = user.generateSalt()
|
||||
user.Password = user.encryptPassword(user.Password, user.Salt)
|
||||
user.Status = Enabled
|
||||
user.Salt = user.generateSalt()
|
||||
user.Password = user.encryptPassword(user.Password, user.Salt)
|
||||
|
||||
_, err = Db.Insert(user)
|
||||
if err == nil {
|
||||
insertId = user.Id
|
||||
}
|
||||
_, err = Db.Insert(user)
|
||||
if err == nil {
|
||||
insertId = user.Id
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
// 更新
|
||||
func (user *User) Update(id int, data CommonMap) (int64, error) {
|
||||
return Db.Table(user).ID(id).Update(data)
|
||||
return Db.Table(user).ID(id).Update(data)
|
||||
}
|
||||
|
||||
func (user *User) UpdatePassword(id int, password string) (int64, error) {
|
||||
salt := user.generateSalt()
|
||||
safePassword := user.encryptPassword(password, salt)
|
||||
func (user *User) UpdatePassword(id int, password string) (int64, error) {
|
||||
salt := user.generateSalt()
|
||||
safePassword := user.encryptPassword(password, salt)
|
||||
|
||||
return user.Update(id, CommonMap{"password": safePassword, "salt": salt})
|
||||
return user.Update(id, CommonMap{"password": safePassword, "salt": salt})
|
||||
}
|
||||
|
||||
// 删除
|
||||
func (user *User) Delete(id int) (int64, error) {
|
||||
return Db.Id(id).Delete(user)
|
||||
return Db.Id(id).Delete(user)
|
||||
}
|
||||
|
||||
// 禁用
|
||||
func (user *User) Disable(id int) (int64, error) {
|
||||
return user.Update(id, CommonMap{"status": Disabled})
|
||||
return user.Update(id, CommonMap{"status": Disabled})
|
||||
}
|
||||
|
||||
// 激活
|
||||
func (user *User) Enable(id int) (int64, error) {
|
||||
return user.Update(id, CommonMap{"status": Enabled})
|
||||
return user.Update(id, CommonMap{"status": Enabled})
|
||||
}
|
||||
|
||||
// 验证用户名和密码
|
||||
func (user *User) Match(username, password string) bool {
|
||||
where := "(name = ? OR email = ?)"
|
||||
_, err := Db.Where(where, username, username).Get(user)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
hashPassword := user.encryptPassword(password, user.Salt)
|
||||
if hashPassword != user.Password {
|
||||
return false
|
||||
}
|
||||
where := "(name = ? OR email = ?) AND status =? "
|
||||
_, err := Db.Where(where, username, username, Enabled).Get(user)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
hashPassword := user.encryptPassword(password, user.Salt)
|
||||
if hashPassword != user.Password {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
return true
|
||||
}
|
||||
|
||||
// 获取用户详情
|
||||
func (user *User) Find(id int) error {
|
||||
_, err := Db.Id(id).Get(user)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// 用户名是否存在
|
||||
func (user *User) UsernameExists(username string) (int64, error) {
|
||||
return Db.Where("name = ?", username).Count(user)
|
||||
func (user *User) UsernameExists(username string, uid int) (int64, error) {
|
||||
if uid > 0 {
|
||||
return Db.Where("name = ? AND id != ?", username, uid).Count(user)
|
||||
}
|
||||
|
||||
return Db.Where("name = ?", username).Count(user)
|
||||
}
|
||||
|
||||
// 邮箱地址是否存在
|
||||
func (user *User) EmailExists(email string) (int64, error) {
|
||||
return Db.Where("email = ?", email).Count(user)
|
||||
func (user *User) EmailExists(email string, uid int) (int64, error) {
|
||||
if uid > 0 {
|
||||
return Db.Where("email = ? AND id != ?", email, uid).Count(user)
|
||||
}
|
||||
|
||||
return Db.Where("email = ?", email).Count(user)
|
||||
}
|
||||
|
||||
func (user *User) List() ([]User, error) {
|
||||
list := make([]User, 0)
|
||||
err := Db.Desc("id").Find(&list)
|
||||
func (user *User) List(params CommonMap) ([]User, error) {
|
||||
user.parsePageAndPageSize(params)
|
||||
list := make([]User, 0)
|
||||
err := Db.Desc("id").Limit(user.PageSize, user.pageLimitOffset()).Find(&list)
|
||||
|
||||
return list, err
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (user *User) Total() (int64, error) {
|
||||
return Db.Count(user)
|
||||
return Db.Count(user)
|
||||
}
|
||||
|
||||
// 密码加密
|
||||
func (user *User) encryptPassword(password, salt string) string {
|
||||
return utils.Md5(password + salt)
|
||||
return utils.Md5(password + salt)
|
||||
}
|
||||
|
||||
// 生成密码盐值
|
||||
func (user *User) generateSalt() string {
|
||||
return utils.RandString(PasswordSaltLength)
|
||||
return utils.RandString(PasswordSaltLength)
|
||||
}
|
||||
|
||||
+101
-76
@@ -1,97 +1,122 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os"
|
||||
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"runtime"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"gopkg.in/ini.v1"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/setting"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
AppDir string // 应用根目录
|
||||
ConfDir string // 配置目录
|
||||
LogDir string // 日志目录
|
||||
DataDir string // 存放session等
|
||||
AppConfig string // 应用配置文件
|
||||
Installed bool // 应用是否安装过
|
||||
Setting *ini.Section // 应用配置
|
||||
PidFile string
|
||||
AppDir string // 应用根目录
|
||||
ConfDir string // 配置目录
|
||||
LogDir string // 日志目录
|
||||
DataDir string // 存放session等
|
||||
AppConfig string // 应用配置文件
|
||||
Installed bool // 应用是否安装过
|
||||
Setting *setting.Setting // 应用配置
|
||||
VersionId int // 版本号
|
||||
VersionFile string // 版本号文件
|
||||
)
|
||||
|
||||
|
||||
func InitEnv() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
logger.InitLogger()
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
AppDir = wd
|
||||
ConfDir = AppDir + "/conf"
|
||||
LogDir = AppDir + "/log"
|
||||
DataDir = AppDir + "/data"
|
||||
AppConfig = ConfDir + "/app.ini"
|
||||
PidFile = LogDir + "/gocron.pid"
|
||||
checkDirExists(ConfDir, LogDir, DataDir)
|
||||
Installed = IsInstalled()
|
||||
func InitEnv(versionString string) {
|
||||
logger.InitLogger()
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
AppDir = wd
|
||||
ConfDir = AppDir + "/conf"
|
||||
LogDir = AppDir + "/log"
|
||||
DataDir = AppDir + "/data"
|
||||
AppConfig = ConfDir + "/app.ini"
|
||||
VersionFile = ConfDir + "/.version"
|
||||
createDirIfNotExists(ConfDir, LogDir, DataDir)
|
||||
Installed = IsInstalled()
|
||||
VersionId = ToNumberVersion(versionString)
|
||||
}
|
||||
|
||||
// 判断应用是否安装过
|
||||
// 判断应用是否已安装
|
||||
func IsInstalled() bool {
|
||||
_, err := os.Stat(ConfDir + "/install.lock")
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
_, err := os.Stat(ConfDir + "/install.lock")
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func WritePid() {
|
||||
pid := os.Getpid()
|
||||
pidStr := strconv.Itoa(pid)
|
||||
err := ioutil.WriteFile(PidFile, []byte(pidStr), 0644)
|
||||
if err != nil {
|
||||
logger.Fatal("写入pid文件失败", err)
|
||||
}
|
||||
}
|
||||
|
||||
func GetPid() int {
|
||||
bytes, err := ioutil.ReadFile(PidFile)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
pidStr := string(bytes)
|
||||
pid, err := strconv.Atoi(pidStr)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return pid
|
||||
}
|
||||
|
||||
func RemovePid() {
|
||||
os.Remove(PidFile)
|
||||
return true
|
||||
}
|
||||
|
||||
// 创建安装锁文件
|
||||
func CreateInstallLock() error {
|
||||
_, err := os.Create(ConfDir + "/install.lock")
|
||||
if err != nil {
|
||||
logger.Error("创建安装锁文件conf/install.lock失败")
|
||||
}
|
||||
_, err := os.Create(ConfDir + "/install.lock")
|
||||
if err != nil {
|
||||
logger.Error("创建安装锁文件conf/install.lock失败")
|
||||
}
|
||||
|
||||
return err
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新应用版本号文件
|
||||
func UpdateVersionFile() {
|
||||
err := ioutil.WriteFile(VersionFile,
|
||||
[]byte(strconv.Itoa(VersionId)),
|
||||
0644,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取应用当前版本号, 从版本号文件中读取
|
||||
func GetCurrentVersionId() int {
|
||||
if !utils.FileExist(VersionFile) {
|
||||
return 0
|
||||
}
|
||||
|
||||
bytes, err := ioutil.ReadFile(VersionFile)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
||||
versionId, err := strconv.Atoi(strings.TrimSpace(string(bytes)))
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
||||
return versionId
|
||||
}
|
||||
|
||||
// 把字符串版本号a.b.c转换为整数版本号abc
|
||||
func ToNumberVersion(versionString string) int {
|
||||
v := strings.Replace(versionString, ".", "", -1)
|
||||
if len(v) < 3 {
|
||||
v += "0"
|
||||
}
|
||||
|
||||
versionId, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
||||
return versionId
|
||||
}
|
||||
|
||||
// 检测目录是否存在
|
||||
func checkDirExists(path ...string) {
|
||||
for _, value := range path {
|
||||
if !utils.FileExist(value) {
|
||||
logger.Fatal(value + "目录不存在或无权限访问")
|
||||
}
|
||||
}
|
||||
}
|
||||
func createDirIfNotExists(path ...string) {
|
||||
for _, value := range path {
|
||||
if utils.FileExist(value) {
|
||||
continue
|
||||
}
|
||||
err := os.Mkdir(value, 0755)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("创建目录失败:%s", err.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,81 +3,80 @@ package httpclient
|
||||
// http-client
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
"fmt"
|
||||
"bytes"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ResponseWrapper struct {
|
||||
StatusCode int
|
||||
Body string
|
||||
Header http.Header
|
||||
type ResponseWrapper struct {
|
||||
StatusCode int
|
||||
Body string
|
||||
Header http.Header
|
||||
}
|
||||
|
||||
func Get(url string, timeout int) ResponseWrapper {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return createRequestError(err)
|
||||
}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return createRequestError(err)
|
||||
}
|
||||
|
||||
return request(req, timeout)
|
||||
return request(req, timeout)
|
||||
}
|
||||
|
||||
func PostParams(url string,params string, timeout int) ResponseWrapper {
|
||||
buf := bytes.NewBufferString(params)
|
||||
req, err := http.NewRequest("POST", url, buf)
|
||||
if err != nil {
|
||||
return createRequestError(err)
|
||||
}
|
||||
req.Header.Set("Content-type", "application/x-www-form-urlencoded")
|
||||
func PostParams(url string, params string, timeout int) ResponseWrapper {
|
||||
buf := bytes.NewBufferString(params)
|
||||
req, err := http.NewRequest("POST", url, buf)
|
||||
if err != nil {
|
||||
return createRequestError(err)
|
||||
}
|
||||
req.Header.Set("Content-type", "application/x-www-form-urlencoded")
|
||||
|
||||
return request(req, timeout)
|
||||
return request(req, timeout)
|
||||
}
|
||||
|
||||
func PostJson(url string, body string, timeout int) ResponseWrapper {
|
||||
buf := bytes.NewBufferString(body)
|
||||
req, err := http.NewRequest("POST", url, buf)
|
||||
if err != nil {
|
||||
return createRequestError(err)
|
||||
}
|
||||
req.Header.Set("Content-type", "application/json")
|
||||
buf := bytes.NewBufferString(body)
|
||||
req, err := http.NewRequest("POST", url, buf)
|
||||
if err != nil {
|
||||
return createRequestError(err)
|
||||
}
|
||||
req.Header.Set("Content-type", "application/json")
|
||||
|
||||
return request(req, timeout)
|
||||
return request(req, timeout)
|
||||
}
|
||||
|
||||
func request(req *http.Request, timeout int) ResponseWrapper {
|
||||
wrapper := ResponseWrapper{StatusCode: 0, Body: "", Header: make(http.Header)}
|
||||
client := &http.Client{}
|
||||
if timeout > 0 {
|
||||
client.Timeout = time.Duration(timeout) * time.Second
|
||||
}
|
||||
setRequestHeader(req)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
wrapper.Body = fmt.Sprintf("执行HTTP请求错误-%s", err.Error())
|
||||
return wrapper
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
wrapper.Body = fmt.Sprintf("读取HTTP请求返回值失败-%s", err.Error())
|
||||
return wrapper
|
||||
}
|
||||
wrapper.StatusCode = resp.StatusCode
|
||||
wrapper.Body = string(body)
|
||||
wrapper.Header = resp.Header
|
||||
wrapper := ResponseWrapper{StatusCode: 0, Body: "", Header: make(http.Header)}
|
||||
client := &http.Client{}
|
||||
if timeout > 0 {
|
||||
client.Timeout = time.Duration(timeout) * time.Second
|
||||
}
|
||||
setRequestHeader(req)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
wrapper.Body = fmt.Sprintf("执行HTTP请求错误-%s", err.Error())
|
||||
return wrapper
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
wrapper.Body = fmt.Sprintf("读取HTTP请求返回值失败-%s", err.Error())
|
||||
return wrapper
|
||||
}
|
||||
wrapper.StatusCode = resp.StatusCode
|
||||
wrapper.Body = string(body)
|
||||
wrapper.Header = resp.Header
|
||||
|
||||
return wrapper
|
||||
return wrapper
|
||||
}
|
||||
|
||||
func setRequestHeader(req *http.Request) {
|
||||
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8,en;q=0.6")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 golang/gocron")
|
||||
func setRequestHeader(req *http.Request) {
|
||||
req.Header.Set("User-Agent", "golang/gocron")
|
||||
}
|
||||
|
||||
func createRequestError(err error) ResponseWrapper {
|
||||
errorMessage := fmt.Sprintf("创建HTTP请求错误-%s", err.Error())
|
||||
return ResponseWrapper{0, errorMessage, make(http.Header)}
|
||||
}
|
||||
errorMessage := fmt.Sprintf("创建HTTP请求错误-%s", err.Error())
|
||||
return ResponseWrapper{0, errorMessage, make(http.Header)}
|
||||
}
|
||||
|
||||
+79
-78
@@ -1,11 +1,12 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"github.com/cihub/seelog"
|
||||
"gopkg.in/macaron.v1"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/cihub/seelog"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
// 日志库
|
||||
@@ -22,113 +23,113 @@ const (
|
||||
FATAL
|
||||
)
|
||||
|
||||
func InitLogger() {
|
||||
config := getLogConfig()
|
||||
l, err := seelog.LoggerFromConfigAsString(config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
logger = l
|
||||
func InitLogger() {
|
||||
config := getLogConfig()
|
||||
l, err := seelog.LoggerFromConfigAsString(config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
logger = l
|
||||
}
|
||||
|
||||
func Debug(v ...interface{}) {
|
||||
if macaron.Env != macaron.DEV {
|
||||
return
|
||||
}
|
||||
if macaron.Env != macaron.DEV {
|
||||
return
|
||||
}
|
||||
write(DEBUG, v)
|
||||
}
|
||||
|
||||
func Debugf(format string, v ...interface{}) {
|
||||
if macaron.Env != macaron.DEV {
|
||||
return
|
||||
}
|
||||
writef(DEBUG, format, v...)
|
||||
func Debugf(format string, v ...interface{}) {
|
||||
if macaron.Env != macaron.DEV {
|
||||
return
|
||||
}
|
||||
writef(DEBUG, format, v...)
|
||||
}
|
||||
|
||||
func Info(v ...interface{}) {
|
||||
write(INFO, v)
|
||||
}
|
||||
|
||||
func Infof(format string, v ...interface{}) {
|
||||
writef(INFO, format, v...)
|
||||
func Infof(format string, v ...interface{}) {
|
||||
writef(INFO, format, v...)
|
||||
}
|
||||
|
||||
func Warn(v ...interface{}) {
|
||||
write(WARN, v)
|
||||
}
|
||||
|
||||
func Warnf(format string, v ...interface{}) {
|
||||
writef(WARN, format, v...)
|
||||
func Warnf(format string, v ...interface{}) {
|
||||
writef(WARN, format, v...)
|
||||
}
|
||||
|
||||
func Error(v ...interface{}) {
|
||||
write(ERROR, v)
|
||||
}
|
||||
|
||||
func Errorf(format string, v ...interface{}) {
|
||||
writef(ERROR, format, v...)
|
||||
func Errorf(format string, v ...interface{}) {
|
||||
writef(ERROR, format, v...)
|
||||
}
|
||||
|
||||
func Fatal(v ...interface{}) {
|
||||
write(FATAL, v)
|
||||
}
|
||||
|
||||
func Fatalf(format string, v ...interface{}) {
|
||||
writef(FATAL, format, v...)
|
||||
func Fatalf(format string, v ...interface{}) {
|
||||
writef(FATAL, format, v...)
|
||||
}
|
||||
|
||||
func write(level Level, v ...interface{}) {
|
||||
defer logger.Flush()
|
||||
defer logger.Flush()
|
||||
|
||||
content := ""
|
||||
if macaron.Env == macaron.DEV {
|
||||
pc, file, line, ok := runtime.Caller(2)
|
||||
if ok {
|
||||
content = fmt.Sprintf("#%s#%s#%d行#", file, runtime.FuncForPC(pc).Name(), line)
|
||||
}
|
||||
}
|
||||
content := ""
|
||||
if macaron.Env == macaron.DEV {
|
||||
pc, file, line, ok := runtime.Caller(2)
|
||||
if ok {
|
||||
content = fmt.Sprintf("#%s#%s#%d行#", file, runtime.FuncForPC(pc).Name(), line)
|
||||
}
|
||||
}
|
||||
|
||||
switch level {
|
||||
case DEBUG:
|
||||
logger.Debug(content, v)
|
||||
case INFO:
|
||||
logger.Info(content, v)
|
||||
case WARN:
|
||||
logger.Warn(content, v)
|
||||
case FATAL:
|
||||
logger.Critical(content, v)
|
||||
os.Exit(1)
|
||||
case ERROR:
|
||||
logger.Error(content, v)
|
||||
switch level {
|
||||
case DEBUG:
|
||||
logger.Debug(content, v)
|
||||
case INFO:
|
||||
logger.Info(content, v)
|
||||
case WARN:
|
||||
logger.Warn(content, v)
|
||||
case FATAL:
|
||||
logger.Critical(content, v)
|
||||
os.Exit(1)
|
||||
case ERROR:
|
||||
logger.Error(content, v)
|
||||
}
|
||||
}
|
||||
|
||||
func writef(level Level, format string, v ...interface{}) {
|
||||
defer logger.Flush()
|
||||
func writef(level Level, format string, v ...interface{}) {
|
||||
defer logger.Flush()
|
||||
|
||||
content := ""
|
||||
if macaron.Env == macaron.DEV {
|
||||
pc, file, line, ok := runtime.Caller(2)
|
||||
if ok {
|
||||
content = fmt.Sprintf("#%s#%s#%d行#", file, runtime.FuncForPC(pc).Name(), line)
|
||||
}
|
||||
}
|
||||
content := ""
|
||||
if macaron.Env == macaron.DEV {
|
||||
pc, file, line, ok := runtime.Caller(2)
|
||||
if ok {
|
||||
content = fmt.Sprintf("#%s#%s#%d行#", file, runtime.FuncForPC(pc).Name(), line)
|
||||
}
|
||||
}
|
||||
|
||||
format = content + format
|
||||
format = content + format
|
||||
|
||||
switch level {
|
||||
case DEBUG:
|
||||
logger.Debugf(format, v...)
|
||||
case INFO:
|
||||
logger.Infof(format, v...)
|
||||
case WARN:
|
||||
logger.Warnf(format, v...)
|
||||
case FATAL:
|
||||
logger.Criticalf(format, v...)
|
||||
os.Exit(1)
|
||||
case ERROR:
|
||||
logger.Errorf(format, v...)
|
||||
}
|
||||
switch level {
|
||||
case DEBUG:
|
||||
logger.Debugf(format, v...)
|
||||
case INFO:
|
||||
logger.Infof(format, v...)
|
||||
case WARN:
|
||||
logger.Warnf(format, v...)
|
||||
case FATAL:
|
||||
logger.Criticalf(format, v...)
|
||||
os.Exit(1)
|
||||
case ERROR:
|
||||
logger.Errorf(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func getLogConfig() string {
|
||||
@@ -145,16 +146,16 @@ func getLogConfig() string {
|
||||
</formats>
|
||||
</seelog>`
|
||||
|
||||
consoleConfig := ""
|
||||
if macaron.Env == macaron.DEV {
|
||||
consoleConfig =
|
||||
`
|
||||
consoleConfig := ""
|
||||
if macaron.Env == macaron.DEV {
|
||||
consoleConfig =
|
||||
`
|
||||
<filter levels="info,debug,critical,warn,error">
|
||||
<console />
|
||||
</filter>
|
||||
`
|
||||
}
|
||||
config = fmt.Sprintf(config, consoleConfig)
|
||||
}
|
||||
config = fmt.Sprintf(config, consoleConfig)
|
||||
|
||||
return config
|
||||
return config
|
||||
}
|
||||
|
||||
+67
-68
@@ -1,84 +1,83 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"strconv"
|
||||
"strings"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"time"
|
||||
"github.com/go-gomail/gomail"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-gomail/gomail"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
)
|
||||
|
||||
// @author qiang.ou<qingqianludao@gmail.com>
|
||||
// @date 2017/5/1-00:19
|
||||
|
||||
|
||||
type Mail struct {
|
||||
|
||||
}
|
||||
|
||||
func (mail *Mail) Send(msg Message) {
|
||||
model := new(models.Setting)
|
||||
mailSetting, err := model.Mail()
|
||||
logger.Debugf("%+v", mailSetting)
|
||||
if err != nil {
|
||||
logger.Error("#mail#从数据库获取mail配置失败", err)
|
||||
return
|
||||
}
|
||||
if mailSetting.Host == "" {
|
||||
logger.Error("#mail#Host为空")
|
||||
return
|
||||
}
|
||||
if mailSetting.Port == 0 {
|
||||
logger.Error("#mail#Port为空")
|
||||
return
|
||||
}
|
||||
if mailSetting.User == "" {
|
||||
logger.Error("#mail#User为空")
|
||||
return
|
||||
}
|
||||
if mailSetting.Password == "" {
|
||||
logger.Error("#mail#Password为空")
|
||||
return
|
||||
}
|
||||
toUsers := mail.getActiveMailUsers(mailSetting, msg)
|
||||
mail.send(mailSetting, toUsers, msg)
|
||||
func (mail *Mail) Send(msg Message) {
|
||||
model := new(models.Setting)
|
||||
mailSetting, err := model.Mail()
|
||||
logger.Debugf("%+v", mailSetting)
|
||||
if err != nil {
|
||||
logger.Error("#mail#从数据库获取mail配置失败", err)
|
||||
return
|
||||
}
|
||||
if mailSetting.Host == "" {
|
||||
logger.Error("#mail#Host为空")
|
||||
return
|
||||
}
|
||||
if mailSetting.Port == 0 {
|
||||
logger.Error("#mail#Port为空")
|
||||
return
|
||||
}
|
||||
if mailSetting.User == "" {
|
||||
logger.Error("#mail#User为空")
|
||||
return
|
||||
}
|
||||
if mailSetting.Password == "" {
|
||||
logger.Error("#mail#Password为空")
|
||||
return
|
||||
}
|
||||
toUsers := mail.getActiveMailUsers(mailSetting, msg)
|
||||
mail.send(mailSetting, toUsers, msg)
|
||||
}
|
||||
|
||||
func (mail *Mail) send(mailSetting models.Mail, toUsers []string, msg Message) {
|
||||
body := msg["content"].(string)
|
||||
body = strings.Replace(body, "\n", "<br>", -1)
|
||||
gomailMessage := gomail.NewMessage()
|
||||
gomailMessage.SetHeader("From", mailSetting.User)
|
||||
gomailMessage.SetHeader("To", toUsers...)
|
||||
gomailMessage.SetHeader("Subject", "gocron-定时任务监控通知")
|
||||
gomailMessage.SetBody("text/html", body)
|
||||
mailer := gomail.NewPlainDialer(mailSetting.Host, mailSetting.Port,
|
||||
mailSetting.User, mailSetting.Password)
|
||||
maxTimes := 3
|
||||
i := 0
|
||||
for i < maxTimes {
|
||||
err := mailer.DialAndSend(gomailMessage)
|
||||
if err == nil {
|
||||
break;
|
||||
}
|
||||
i += 1
|
||||
time.Sleep(2 * time.Second)
|
||||
if i < maxTimes {
|
||||
logger.Errorf("mail#发送消息失败#%s#消息内容-%s", err.Error(), msg["content"])
|
||||
}
|
||||
}
|
||||
func (mail *Mail) send(mailSetting models.Mail, toUsers []string, msg Message) {
|
||||
body := msg["content"].(string)
|
||||
body = strings.Replace(body, "\n", "<br>", -1)
|
||||
gomailMessage := gomail.NewMessage()
|
||||
gomailMessage.SetHeader("From", mailSetting.User)
|
||||
gomailMessage.SetHeader("To", toUsers...)
|
||||
gomailMessage.SetHeader("Subject", "gocron-定时任务监控通知")
|
||||
gomailMessage.SetBody("text/html", body)
|
||||
mailer := gomail.NewPlainDialer(mailSetting.Host, mailSetting.Port,
|
||||
mailSetting.User, mailSetting.Password)
|
||||
maxTimes := 3
|
||||
i := 0
|
||||
for i < maxTimes {
|
||||
err := mailer.DialAndSend(gomailMessage)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
i += 1
|
||||
time.Sleep(2 * time.Second)
|
||||
if i < maxTimes {
|
||||
logger.Errorf("mail#发送消息失败#%s#消息内容-%s", err.Error(), msg["content"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (mail *Mail) getActiveMailUsers(mailSetting models.Mail, msg Message) []string {
|
||||
taskReceiverIds := strings.Split(msg["task_receiver_id"].(string), ",")
|
||||
users := []string{}
|
||||
for _, v := range(mailSetting.MailUsers) {
|
||||
if utils.InStringSlice(taskReceiverIds, strconv.Itoa(v.Id)) {
|
||||
users = append(users, v.Email)
|
||||
}
|
||||
}
|
||||
func (mail *Mail) getActiveMailUsers(mailSetting models.Mail, msg Message) []string {
|
||||
taskReceiverIds := strings.Split(msg["task_receiver_id"].(string), ",")
|
||||
users := []string{}
|
||||
for _, v := range mailSetting.MailUsers {
|
||||
if utils.InStringSlice(taskReceiverIds, strconv.Itoa(v.Id)) {
|
||||
users = append(users, v.Email)
|
||||
}
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
+35
-34
@@ -1,52 +1,53 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"fmt"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
)
|
||||
|
||||
type Message map[string]interface{}
|
||||
|
||||
type Notifiable interface {
|
||||
Send(msg Message)
|
||||
Send(msg Message)
|
||||
}
|
||||
|
||||
var queue chan Message = make(chan Message, 100)
|
||||
var queue = make(chan Message, 100)
|
||||
|
||||
func init() {
|
||||
go run()
|
||||
func init() {
|
||||
go run()
|
||||
}
|
||||
|
||||
// 把消息推入队列
|
||||
func Push(msg Message) {
|
||||
queue <- msg
|
||||
queue <- msg
|
||||
}
|
||||
|
||||
func run() {
|
||||
for msg := range queue {
|
||||
// 根据任务配置发送通知
|
||||
taskType, taskTypeOk := msg["task_type"]
|
||||
_, taskReceiverIdOk := msg["task_receiver_id"]
|
||||
_, nameOk := msg["name"]
|
||||
_, outputOk := msg["output"]
|
||||
_, statusOk := msg["status"]
|
||||
if !taskTypeOk || !taskReceiverIdOk || !nameOk || !outputOk || !statusOk {
|
||||
logger.Errorf("#notify#参数不完整#%+v", msg)
|
||||
continue
|
||||
}
|
||||
msg["content"] = fmt.Sprintf("============\n============\n============\n任务名称: %s\n状态: %s\n输出:\n %s\n", msg["name"], msg["status"], msg["output"])
|
||||
logger.Debugf("%+v", msg)
|
||||
switch(taskType.(int8)) {
|
||||
case 1:
|
||||
// 邮件
|
||||
mail := Mail{}
|
||||
go mail.Send(msg)
|
||||
case 2:
|
||||
// Slack
|
||||
slack := Slack{}
|
||||
go slack.Send(msg)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
for msg := range queue {
|
||||
// 根据任务配置发送通知
|
||||
taskType, taskTypeOk := msg["task_type"]
|
||||
_, taskReceiverIdOk := msg["task_receiver_id"]
|
||||
_, nameOk := msg["name"]
|
||||
_, outputOk := msg["output"]
|
||||
_, statusOk := msg["status"]
|
||||
if !taskTypeOk || !taskReceiverIdOk || !nameOk || !outputOk || !statusOk {
|
||||
logger.Errorf("#notify#参数不完整#%+v", msg)
|
||||
continue
|
||||
}
|
||||
msg["content"] = fmt.Sprintf("============\n============\n============\n任务名称: %s\n状态: %s\n输出:\n %s\n", msg["name"], msg["status"], msg["output"])
|
||||
logger.Debugf("%+v", msg)
|
||||
switch taskType.(int8) {
|
||||
case 1:
|
||||
// 邮件
|
||||
mail := Mail{}
|
||||
go mail.Send(msg)
|
||||
case 2:
|
||||
// Slack
|
||||
slack := Slack{}
|
||||
go slack.Send(msg)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
+64
-62
@@ -1,78 +1,80 @@
|
||||
package notify
|
||||
|
||||
// 发送消息到slack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ouqiang/gocron/modules/httpclient"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"strings"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"strconv"
|
||||
"time"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/httpclient"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
)
|
||||
|
||||
type Slack struct {}
|
||||
type Slack struct{}
|
||||
|
||||
func (slack *Slack) Send(msg Message) {
|
||||
model := new(models.Setting)
|
||||
slackSetting, err := model.Slack()
|
||||
if err != nil {
|
||||
logger.Error("#slack#从数据库获取slack配置失败", err)
|
||||
return
|
||||
}
|
||||
if slackSetting.Url == "" {
|
||||
logger.Error("#slack#webhook-url为空")
|
||||
return
|
||||
}
|
||||
if len(slackSetting.Channels) == 0 {
|
||||
logger.Error("#slack#channels配置为空")
|
||||
return
|
||||
}
|
||||
logger.Debugf("%+v", slackSetting)
|
||||
channels := slack.getActiveSlackChannels(slackSetting, msg)
|
||||
logger.Debugf("%+v", channels)
|
||||
for _, channel := range(channels) {
|
||||
slack.send(msg, slackSetting.Url, channel)
|
||||
}
|
||||
func (slack *Slack) Send(msg Message) {
|
||||
model := new(models.Setting)
|
||||
slackSetting, err := model.Slack()
|
||||
if err != nil {
|
||||
logger.Error("#slack#从数据库获取slack配置失败", err)
|
||||
return
|
||||
}
|
||||
if slackSetting.Url == "" {
|
||||
logger.Error("#slack#webhook-url为空")
|
||||
return
|
||||
}
|
||||
if len(slackSetting.Channels) == 0 {
|
||||
logger.Error("#slack#channels配置为空")
|
||||
return
|
||||
}
|
||||
logger.Debugf("%+v", slackSetting)
|
||||
channels := slack.getActiveSlackChannels(slackSetting, msg)
|
||||
logger.Debugf("%+v", channels)
|
||||
for _, channel := range channels {
|
||||
slack.send(msg, slackSetting.Url, channel)
|
||||
}
|
||||
}
|
||||
|
||||
func (slack *Slack) send(msg Message, slackUrl string, channel string) {
|
||||
formatBody := slack.format(msg["content"].(string), channel)
|
||||
timeout := 30
|
||||
maxTimes := 3
|
||||
i := 0
|
||||
for i < maxTimes {
|
||||
resp := httpclient.PostJson(slackUrl, formatBody, timeout)
|
||||
if resp.StatusCode == 200 {
|
||||
break;
|
||||
}
|
||||
i += 1
|
||||
time.Sleep(2 * time.Second)
|
||||
if i < maxTimes {
|
||||
logger.Errorf("slack#发送消息失败#%s#消息内容-%s", resp.Body, msg["content"])
|
||||
}
|
||||
}
|
||||
func (slack *Slack) send(msg Message, slackUrl string, channel string) {
|
||||
formatBody := slack.format(msg["content"].(string), channel)
|
||||
timeout := 30
|
||||
maxTimes := 3
|
||||
i := 0
|
||||
for i < maxTimes {
|
||||
resp := httpclient.PostJson(slackUrl, formatBody, timeout)
|
||||
if resp.StatusCode == 200 {
|
||||
break
|
||||
}
|
||||
i += 1
|
||||
time.Sleep(2 * time.Second)
|
||||
if i < maxTimes {
|
||||
logger.Errorf("slack#发送消息失败#%s#消息内容-%s", resp.Body, msg["content"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (slack *Slack) getActiveSlackChannels(slackSetting models.Slack, msg Message) []string {
|
||||
taskReceiverIds := strings.Split(msg["task_receiver_id"].(string), ",")
|
||||
channels := []string{}
|
||||
for _, v := range(slackSetting.Channels) {
|
||||
if utils.InStringSlice(taskReceiverIds, strconv.Itoa(v.Id)) {
|
||||
channels = append(channels, v.Name)
|
||||
}
|
||||
}
|
||||
func (slack *Slack) getActiveSlackChannels(slackSetting models.Slack, msg Message) []string {
|
||||
taskReceiverIds := strings.Split(msg["task_receiver_id"].(string), ",")
|
||||
channels := []string{}
|
||||
for _, v := range slackSetting.Channels {
|
||||
if utils.InStringSlice(taskReceiverIds, strconv.Itoa(v.Id)) {
|
||||
channels = append(channels, v.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return channels
|
||||
return channels
|
||||
}
|
||||
|
||||
// 格式化消息内容
|
||||
func (slack *Slack) format(content string, channel string) string {
|
||||
content = utils.EscapeJson(content)
|
||||
specialChars := []string{"&", "<", ">"}
|
||||
replaceChars := []string{"&", "<", ">"}
|
||||
content = utils.ReplaceStrings(content, specialChars, replaceChars)
|
||||
func (slack *Slack) format(content string, channel string) string {
|
||||
content = utils.EscapeJson(content)
|
||||
specialChars := []string{"&", "<", ">"}
|
||||
replaceChars := []string{"&", "<", ">"}
|
||||
content = utils.ReplaceStrings(content, specialChars, replaceChars)
|
||||
|
||||
return fmt.Sprintf(`{"text":"%s","username":"监控", "channel":"%s"}`, content, channel)
|
||||
}
|
||||
return fmt.Sprintf(`{"text":"%s","username":"监控", "channel":"%s"}`, content, channel)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
type Certificate struct {
|
||||
CAFile string
|
||||
CertFile string
|
||||
KeyFile string
|
||||
ServerName string
|
||||
}
|
||||
|
||||
func (c Certificate) GetTLSConfigForServer() (*tls.Config, error) {
|
||||
certificate, err := tls.LoadX509KeyPair(
|
||||
c.CertFile,
|
||||
c.KeyFile,
|
||||
)
|
||||
|
||||
certPool := x509.NewCertPool()
|
||||
bs, err := ioutil.ReadFile(c.CAFile)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("failed to read client ca cert: %s", err))
|
||||
}
|
||||
|
||||
ok := certPool.AppendCertsFromPEM(bs)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to append client certs")
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
Certificates: []tls.Certificate{certificate},
|
||||
ClientCAs: certPool,
|
||||
}
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
func (c Certificate) GetTransportCredsForClient() (credentials.TransportCredentials, error) {
|
||||
certificate, err := tls.LoadX509KeyPair(
|
||||
c.CertFile,
|
||||
c.KeyFile,
|
||||
)
|
||||
|
||||
certPool := x509.NewCertPool()
|
||||
bs, err := ioutil.ReadFile(c.CAFile)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("failed to read ca cert: %s", err))
|
||||
}
|
||||
|
||||
ok := certPool.AppendCertsFromPEM(bs)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to append certs")
|
||||
}
|
||||
|
||||
transportCreds := credentials.NewTLS(&tls.Config{
|
||||
ServerName: c.ServerName,
|
||||
Certificates: []tls.Certificate{certificate},
|
||||
RootCAs: certPool,
|
||||
})
|
||||
|
||||
return transportCreds, nil
|
||||
}
|
||||
@@ -1,80 +1,88 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
pb "github.com/ouqiang/gocron/modules/rpc/proto"
|
||||
"golang.org/x/net/context"
|
||||
"fmt"
|
||||
"time"
|
||||
"errors"
|
||||
"github.com/ouqiang/gocron/modules/rpc/grpcpool"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/rpc/grpcpool"
|
||||
pb "github.com/ouqiang/gocron/modules/rpc/proto"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
var (
|
||||
errUnavailable = errors.New("无法连接远程服务器")
|
||||
taskMap sync.Map
|
||||
)
|
||||
|
||||
func ExecWithRetry(ip string, port int, taskReq *pb.TaskRequest) (string, error) {
|
||||
tryTimes := 60
|
||||
i := 0
|
||||
for i < tryTimes {
|
||||
output, err := Exec(ip, port, taskReq)
|
||||
if err != errUnavailable {
|
||||
return output, err
|
||||
}
|
||||
i++
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
var (
|
||||
errUnavailable = errors.New("无法连接远程服务器")
|
||||
)
|
||||
|
||||
return "", errUnavailable
|
||||
func generateTaskUniqueKey(ip string, port int, id int64) string {
|
||||
return fmt.Sprintf("%s:%d:%d", ip, port, id)
|
||||
}
|
||||
|
||||
func Exec(ip string, port int, taskReq *pb.TaskRequest) (string, error) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Error("panic#rpc/client.go:Exec#", err)
|
||||
}
|
||||
} ()
|
||||
addr := fmt.Sprintf("%s:%d", ip, port)
|
||||
conn, err := grpcpool.Pool.Get(addr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
isConnClosed := false
|
||||
defer func() {
|
||||
if !isConnClosed {
|
||||
grpcpool.Pool.Put(addr, conn)
|
||||
}
|
||||
}()
|
||||
c := pb.NewTaskClient(conn)
|
||||
if taskReq.Timeout <= 0 || taskReq.Timeout > 86400 {
|
||||
taskReq.Timeout = 86400
|
||||
}
|
||||
timeout := time.Duration(taskReq.Timeout) * time.Second
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
resp, err := c.Run(ctx, taskReq)
|
||||
if err != nil {
|
||||
return parseGRPCError(err, conn, &isConnClosed)
|
||||
}
|
||||
func Stop(ip string, port int, id int64) {
|
||||
key := generateTaskUniqueKey(ip, port, id)
|
||||
cancel, ok := taskMap.Load(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
cancel.(context.CancelFunc)()
|
||||
}
|
||||
|
||||
if resp.Error == "" {
|
||||
return resp.Output, nil
|
||||
}
|
||||
func Exec(ip string, port int, taskReq *pb.TaskRequest) (string, error) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Error("panic#rpc/client.go:Exec#", err)
|
||||
}
|
||||
}()
|
||||
addr := fmt.Sprintf("%s:%d", ip, port)
|
||||
conn, err := grpcpool.Pool.Get(addr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
isConnClosed := false
|
||||
defer func() {
|
||||
if !isConnClosed {
|
||||
grpcpool.Pool.Put(addr, conn)
|
||||
}
|
||||
}()
|
||||
c := pb.NewTaskClient(conn)
|
||||
if taskReq.Timeout <= 0 || taskReq.Timeout > 86400 {
|
||||
taskReq.Timeout = 86400
|
||||
}
|
||||
timeout := time.Duration(taskReq.Timeout) * time.Second
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
taskUniqueKey := generateTaskUniqueKey(ip, port, taskReq.Id)
|
||||
taskMap.Store(taskUniqueKey, cancel)
|
||||
defer taskMap.Delete(taskUniqueKey)
|
||||
resp, err := c.Run(ctx, taskReq)
|
||||
if err != nil {
|
||||
return parseGRPCError(err, conn, &isConnClosed)
|
||||
}
|
||||
|
||||
return resp.Output, errors.New(resp.Error)
|
||||
if resp.Error == "" {
|
||||
return resp.Output, nil
|
||||
}
|
||||
|
||||
return resp.Output, errors.New(resp.Error)
|
||||
}
|
||||
|
||||
func parseGRPCError(err error, conn *grpc.ClientConn, connClosed *bool) (string, error) {
|
||||
switch grpc.Code(err) {
|
||||
case codes.Unavailable, codes.Internal:
|
||||
conn.Close()
|
||||
*connClosed = true
|
||||
return "", errUnavailable
|
||||
case codes.DeadlineExceeded:
|
||||
return "", errors.New("执行超时, 强制结束")
|
||||
}
|
||||
return "", err
|
||||
switch grpc.Code(err) {
|
||||
case codes.Unavailable, codes.Internal:
|
||||
conn.Close()
|
||||
*connClosed = true
|
||||
return "", errUnavailable
|
||||
case codes.DeadlineExceeded:
|
||||
return "", errors.New("执行超时, 强制结束")
|
||||
case codes.Canceled:
|
||||
return "", errors.New("手动停止")
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -1,120 +1,140 @@
|
||||
package grpcpool
|
||||
|
||||
import (
|
||||
"github.com/silenceper/pool"
|
||||
"sync"
|
||||
"time"
|
||||
"google.golang.org/grpc"
|
||||
"errors"
|
||||
)
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
||||
var (
|
||||
Pool GRPCPool
|
||||
"github.com/ouqiang/gocron/modules/app"
|
||||
"github.com/ouqiang/gocron/modules/rpc/auth"
|
||||
"github.com/silenceper/pool"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidConn = errors.New("invalid connection")
|
||||
Pool GRPCPool
|
||||
)
|
||||
|
||||
func init() {
|
||||
Pool = GRPCPool{
|
||||
make(map[string]pool.Pool),
|
||||
sync.RWMutex{},
|
||||
}
|
||||
var (
|
||||
ErrInvalidConn = errors.New("invalid connection")
|
||||
)
|
||||
|
||||
func init() {
|
||||
Pool = GRPCPool{
|
||||
make(map[string]pool.Pool),
|
||||
sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
type GRPCPool struct {
|
||||
// map key格式 ip:port
|
||||
conns map[string]pool.Pool
|
||||
sync.RWMutex
|
||||
// map key格式 ip:port
|
||||
conns map[string]pool.Pool
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (p *GRPCPool) Get(addr string) (*grpc.ClientConn, error) {
|
||||
p.RLock()
|
||||
pool, ok := p.conns[addr]
|
||||
p.RUnlock()
|
||||
if !ok {
|
||||
err := p.newCommonPool(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
func (p *GRPCPool) Get(addr string) (*grpc.ClientConn, error) {
|
||||
p.RLock()
|
||||
pool, ok := p.conns[addr]
|
||||
p.RUnlock()
|
||||
if !ok {
|
||||
err := p.newCommonPool(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
p.RLock()
|
||||
pool = p.conns[addr]
|
||||
p.RUnlock()
|
||||
conn, err := pool.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.RLock()
|
||||
pool = p.conns[addr]
|
||||
p.RUnlock()
|
||||
conn, err := pool.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn.(*grpc.ClientConn), nil
|
||||
return conn.(*grpc.ClientConn), nil
|
||||
}
|
||||
|
||||
func (p *GRPCPool) Put(addr string, conn *grpc.ClientConn) error {
|
||||
p.RLock()
|
||||
defer p.RUnlock()
|
||||
pool, ok := p.conns[addr]
|
||||
if ok {
|
||||
return pool.Put(conn)
|
||||
}
|
||||
p.RLock()
|
||||
defer p.RUnlock()
|
||||
pool, ok := p.conns[addr]
|
||||
if ok {
|
||||
return pool.Put(conn)
|
||||
}
|
||||
|
||||
return ErrInvalidConn
|
||||
return ErrInvalidConn
|
||||
}
|
||||
|
||||
|
||||
// 释放连接池
|
||||
func (p *GRPCPool) Release(addr string) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
pool, ok := p.conns[addr]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
pool.Release()
|
||||
delete(p.conns, addr)
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
pool, ok := p.conns[addr]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
pool.Release()
|
||||
delete(p.conns, addr)
|
||||
}
|
||||
|
||||
// 释放所有连接池
|
||||
func (p *GRPCPool) ReleaseAll() {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
for _, pool := range(p.conns) {
|
||||
pool.Release()
|
||||
}
|
||||
func (p *GRPCPool) ReleaseAll() {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
for _, pool := range p.conns {
|
||||
pool.Release()
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化底层连接池
|
||||
func (p *GRPCPool) newCommonPool(addr string) (error) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
commonPool, ok := p.conns[addr]
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
poolConfig := &pool.PoolConfig{
|
||||
InitialCap: 1,
|
||||
MaxCap: 30,
|
||||
Factory: func() (interface{}, error) {
|
||||
return grpc.Dial(addr, grpc.WithInsecure())
|
||||
},
|
||||
Close: func(v interface{}) error {
|
||||
conn, ok := v.(*grpc.ClientConn)
|
||||
if ok && conn != nil {
|
||||
return conn.Close()
|
||||
}
|
||||
return ErrInvalidConn
|
||||
},
|
||||
IdleTimeout: 3 * time.Minute,
|
||||
}
|
||||
func (p *GRPCPool) newCommonPool(addr string) error {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
commonPool, ok := p.conns[addr]
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
poolConfig := &pool.PoolConfig{
|
||||
InitialCap: 1,
|
||||
MaxCap: 30,
|
||||
Factory: func() (interface{}, error) {
|
||||
if !app.Setting.EnableTLS {
|
||||
return grpc.Dial(addr, grpc.WithInsecure())
|
||||
}
|
||||
|
||||
commonPool, err := pool.NewChannelPool(poolConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
server := strings.Split(addr, ":")
|
||||
|
||||
p.conns[addr] = commonPool
|
||||
certificate := auth.Certificate{
|
||||
CAFile: app.Setting.CAFile,
|
||||
CertFile: app.Setting.CertFile,
|
||||
KeyFile: app.Setting.KeyFile,
|
||||
ServerName: server[0],
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
transportCreds, err := certificate.GetTransportCredsForClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return grpc.Dial(addr, grpc.WithTransportCredentials(transportCreds))
|
||||
},
|
||||
Close: func(v interface{}) error {
|
||||
conn, ok := v.(*grpc.ClientConn)
|
||||
if ok && conn != nil {
|
||||
return conn.Close()
|
||||
}
|
||||
return ErrInvalidConn
|
||||
},
|
||||
IdleTimeout: 3 * time.Minute,
|
||||
}
|
||||
|
||||
commonPool, err := pool.NewChannelPool(poolConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.conns[addr] = commonPool
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
|
||||
type TaskRequest struct {
|
||||
Command string `protobuf:"bytes,2,opt,name=command" json:"command,omitempty"`
|
||||
Timeout int32 `protobuf:"varint,3,opt,name=timeout" json:"timeout,omitempty"`
|
||||
Id int64 `protobuf:"varint,4,opt,name=id" json:"id,omitempty"`
|
||||
}
|
||||
|
||||
func (m *TaskRequest) Reset() { *m = TaskRequest{} }
|
||||
@@ -57,6 +58,13 @@ func (m *TaskRequest) GetTimeout() int32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *TaskRequest) GetId() int64 {
|
||||
if m != nil {
|
||||
return m.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type TaskResponse struct {
|
||||
Output string `protobuf:"bytes,1,opt,name=output" json:"output,omitempty"`
|
||||
Error string `protobuf:"bytes,2,opt,name=error" json:"error,omitempty"`
|
||||
@@ -161,16 +169,17 @@ var _Task_serviceDesc = grpc.ServiceDesc{
|
||||
func init() { proto.RegisterFile("task.proto", fileDescriptor0) }
|
||||
|
||||
var fileDescriptor0 = []byte{
|
||||
// 170 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x8f, 0xb1, 0x0e, 0x82, 0x40,
|
||||
0x0c, 0x86, 0x45, 0x04, 0x63, 0x75, 0xd0, 0xc6, 0x98, 0x8b, 0x13, 0x61, 0x62, 0x30, 0x0c, 0xea,
|
||||
0xe8, 0xe2, 0x2b, 0x5c, 0x7c, 0x01, 0xc4, 0x1b, 0x0c, 0x81, 0x9e, 0xbd, 0xde, 0xfb, 0x1b, 0x38,
|
||||
0x48, 0x18, 0xbf, 0x36, 0xfd, 0xfa, 0xff, 0x00, 0x52, 0xb9, 0xa6, 0xb4, 0x4c, 0x42, 0x18, 0xb3,
|
||||
0xad, 0xf3, 0x27, 0x6c, 0x5f, 0x95, 0x6b, 0xb4, 0xf9, 0x79, 0xe3, 0x04, 0x15, 0xac, 0x6b, 0x6a,
|
||||
0xdb, 0xaa, 0xfb, 0xa8, 0x65, 0x16, 0x15, 0x1b, 0x3d, 0x61, 0xbf, 0x91, 0x6f, 0x6b, 0xc8, 0x8b,
|
||||
0x8a, 0xb3, 0xa8, 0x48, 0xf4, 0x84, 0xf9, 0x03, 0x76, 0x41, 0xe1, 0x2c, 0x75, 0xce, 0xe0, 0x09,
|
||||
0x52, 0xf2, 0x62, 0xbd, 0xa8, 0x68, 0x50, 0x8c, 0x84, 0x47, 0x48, 0x0c, 0x33, 0xf1, 0x68, 0x0e,
|
||||
0x70, 0xbd, 0xc3, 0xaa, 0xbf, 0xc6, 0x0b, 0xc4, 0xda, 0x77, 0xb8, 0x2f, 0xd9, 0xd6, 0xe5, 0x2c,
|
||||
0xd2, 0xf9, 0x30, 0x9b, 0x84, 0x0f, 0xf9, 0xe2, 0x9d, 0x0e, 0x15, 0x6e, 0xff, 0x00, 0x00, 0x00,
|
||||
0xff, 0xff, 0xef, 0x3c, 0x71, 0x6b, 0xd0, 0x00, 0x00, 0x00,
|
||||
// 184 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x8f, 0xcf, 0x8a, 0x83, 0x30,
|
||||
0x10, 0xc6, 0x37, 0x46, 0x5d, 0x76, 0x76, 0x59, 0xda, 0xa1, 0x94, 0xd0, 0x93, 0x78, 0xf2, 0x50,
|
||||
0x3c, 0xb4, 0x3d, 0xf6, 0x25, 0x1a, 0xfa, 0x02, 0x56, 0x73, 0x10, 0xd1, 0x49, 0x93, 0xc9, 0xfb,
|
||||
0x17, 0xff, 0x81, 0xc7, 0xdf, 0x0c, 0xf3, 0xfd, 0xe6, 0x03, 0xe0, 0xca, 0x77, 0xa5, 0x75, 0xc4,
|
||||
0x84, 0xd2, 0xd9, 0x3a, 0x7f, 0xc0, 0xef, 0xb3, 0xf2, 0x9d, 0x36, 0xef, 0x60, 0x3c, 0xa3, 0x82,
|
||||
0xef, 0x9a, 0xfa, 0xbe, 0x1a, 0x1a, 0x15, 0x65, 0xa2, 0xf8, 0xd1, 0x2b, 0x8e, 0x1b, 0x6e, 0x7b,
|
||||
0x43, 0x81, 0x95, 0xcc, 0x44, 0x91, 0xe8, 0x15, 0xf1, 0x1f, 0xa2, 0xb6, 0x51, 0x71, 0x26, 0x0a,
|
||||
0xa9, 0xa3, 0xb6, 0xc9, 0xef, 0xf0, 0x37, 0x47, 0x7a, 0x4b, 0x83, 0x37, 0x78, 0x84, 0x94, 0x02,
|
||||
0xdb, 0xc0, 0x4a, 0x4c, 0x91, 0x0b, 0xe1, 0x01, 0x12, 0xe3, 0x1c, 0xb9, 0xc5, 0x34, 0xc3, 0xe5,
|
||||
0x06, 0xf1, 0x78, 0x8d, 0x67, 0x90, 0x3a, 0x0c, 0xb8, 0x2b, 0x9d, 0xad, 0xcb, 0xcd, 0x8b, 0xa7,
|
||||
0xfd, 0x66, 0x32, 0x1b, 0xf2, 0xaf, 0x57, 0x3a, 0x55, 0xba, 0x7e, 0x02, 0x00, 0x00, 0xff, 0xff,
|
||||
0xd7, 0x7f, 0x8a, 0x9d, 0xe0, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ service Task {
|
||||
message TaskRequest {
|
||||
string command = 2; // 命令
|
||||
int32 timeout = 3; // 任务执行超时时间
|
||||
int64 id = 4; // 执行任务唯一ID
|
||||
}
|
||||
|
||||
message TaskResponse {
|
||||
|
||||
@@ -1,50 +1,59 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"golang.org/x/net/context"
|
||||
"net"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
"google.golang.org/grpc"
|
||||
pb "github.com/ouqiang/gocron/modules/rpc/proto"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"net"
|
||||
|
||||
"github.com/ouqiang/gocron/modules/rpc/auth"
|
||||
pb "github.com/ouqiang/gocron/modules/rpc/proto"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
)
|
||||
|
||||
type Server struct {}
|
||||
type Server struct{}
|
||||
|
||||
func (s Server) Run(ctx context.Context, req *pb.TaskRequest) (*pb.TaskResponse, error) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
grpclog.Println(err)
|
||||
}
|
||||
} ()
|
||||
output, err := utils.ExecShell(ctx, req.Command)
|
||||
resp := new(pb.TaskResponse)
|
||||
resp.Output = output
|
||||
if err != nil {
|
||||
resp.Error = err.Error()
|
||||
} else {
|
||||
resp.Error = ""
|
||||
}
|
||||
func (s Server) Run(ctx context.Context, req *pb.TaskRequest) (*pb.TaskResponse, error) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
grpclog.Println(err)
|
||||
}
|
||||
}()
|
||||
output, err := utils.ExecShell(ctx, req.Command)
|
||||
resp := new(pb.TaskResponse)
|
||||
resp.Output = output
|
||||
if err != nil {
|
||||
resp.Error = err.Error()
|
||||
} else {
|
||||
resp.Error = ""
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func Start(addr string) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
grpclog.Println("panic", err)
|
||||
}
|
||||
} ()
|
||||
l, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
grpclog.Fatal(err)
|
||||
}
|
||||
s := grpc.NewServer()
|
||||
pb.RegisterTaskServer(s, Server{})
|
||||
grpclog.Println("listen address ", addr)
|
||||
err = s.Serve(l)
|
||||
if err != nil {
|
||||
grpclog.Fatal(err)
|
||||
}
|
||||
}
|
||||
func Start(addr string, enableTLS bool, certificate auth.Certificate) {
|
||||
l, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
grpclog.Fatal(err)
|
||||
}
|
||||
|
||||
var s *grpc.Server
|
||||
if enableTLS {
|
||||
tlsConfig, err := certificate.GetTLSConfigForServer()
|
||||
if err != nil {
|
||||
grpclog.Fatal(err)
|
||||
}
|
||||
opt := grpc.Creds(credentials.NewTLS(tlsConfig))
|
||||
s = grpc.NewServer(opt)
|
||||
pb.RegisterTaskServer(s, Server{})
|
||||
grpclog.Printf("listen %s with TLS", addr)
|
||||
} else {
|
||||
s = grpc.NewServer()
|
||||
pb.RegisterTaskServer(s, Server{})
|
||||
grpclog.Printf("listen %s", addr)
|
||||
}
|
||||
|
||||
err = s.Serve(l)
|
||||
grpclog.Fatal(err)
|
||||
}
|
||||
|
||||
+99
-30
@@ -1,46 +1,115 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"gopkg.in/ini.v1"
|
||||
"errors"
|
||||
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
const DefaultSection = "default"
|
||||
|
||||
// 读取配置
|
||||
func Read(filename string) (*ini.Section,error) {
|
||||
config, err := ini.Load(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
section := config.Section(DefaultSection)
|
||||
type Setting struct {
|
||||
Db struct {
|
||||
Engine string
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
Database string
|
||||
Prefix string
|
||||
Charset string
|
||||
MaxIdleConns int
|
||||
MaxOpenConns int
|
||||
}
|
||||
AllowIps string
|
||||
AppName string
|
||||
ApiKey string
|
||||
ApiSecret string
|
||||
ApiSignEnable bool
|
||||
|
||||
return section, nil
|
||||
EnableTLS bool
|
||||
CAFile string
|
||||
CertFile string
|
||||
KeyFile string
|
||||
|
||||
ConcurrencyQueue int
|
||||
}
|
||||
|
||||
// 读取配置
|
||||
func Read(filename string) (*Setting, error) {
|
||||
config, err := ini.Load(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
section := config.Section(DefaultSection)
|
||||
|
||||
var s Setting
|
||||
|
||||
s.Db.Engine = section.Key("db.engine").MustString("mysql")
|
||||
s.Db.Host = section.Key("db.host").MustString("127.0.0.1")
|
||||
s.Db.Port = section.Key("db.port").MustInt(3306)
|
||||
s.Db.User = section.Key("db.user").MustString("")
|
||||
s.Db.Password = section.Key("db.password").MustString("")
|
||||
s.Db.Database = section.Key("db.database").MustString("gocron")
|
||||
s.Db.Prefix = section.Key("db.prefix").MustString("")
|
||||
s.Db.Charset = section.Key("db.charset").MustString("utf8")
|
||||
s.Db.MaxIdleConns = section.Key("db.max.idle.conns").MustInt(30)
|
||||
s.Db.MaxOpenConns = section.Key("db.max.open.conns").MustInt(100)
|
||||
|
||||
s.AllowIps = section.Key("allow_ips").MustString("")
|
||||
s.AppName = section.Key("app.name").MustString("定时任务管理系统")
|
||||
s.ApiKey = section.Key("api.key").MustString("")
|
||||
s.ApiSecret = section.Key("api.secret").MustString("")
|
||||
s.ApiSignEnable = section.Key("api.sign.enable").MustBool(true)
|
||||
s.ConcurrencyQueue = section.Key("concurrency.queue").MustInt(500)
|
||||
|
||||
s.EnableTLS = section.Key("enable_tls").MustBool(false)
|
||||
s.CAFile = section.Key("ca_file").MustString("")
|
||||
s.CertFile = section.Key("cert_file").MustString("")
|
||||
s.KeyFile = section.Key("key_file").MustString("")
|
||||
|
||||
if s.EnableTLS {
|
||||
if !utils.FileExist(s.CAFile) {
|
||||
logger.Fatalf("failed to read ca cert file: %s", s.CAFile)
|
||||
}
|
||||
|
||||
if !utils.FileExist(s.CertFile) {
|
||||
logger.Fatalf("failed to read client cert file: %s", s.CertFile)
|
||||
}
|
||||
|
||||
if !utils.FileExist(s.KeyFile) {
|
||||
logger.Fatalf("failed to read client key file: %s", s.KeyFile)
|
||||
}
|
||||
}
|
||||
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// 写入配置
|
||||
func Write(config []string, filename string) error {
|
||||
if len(config) == 0 {
|
||||
return errors.New("参数不能为空")
|
||||
}
|
||||
if len(config) % 2 != 0 {
|
||||
return errors.New("参数不匹配")
|
||||
}
|
||||
if len(config) == 0 {
|
||||
return errors.New("参数不能为空")
|
||||
}
|
||||
if len(config)%2 != 0 {
|
||||
return errors.New("参数不匹配")
|
||||
}
|
||||
|
||||
file := ini.Empty()
|
||||
file := ini.Empty()
|
||||
|
||||
section, err := file.NewSection(DefaultSection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 0 ;i < len(config); {
|
||||
_, err = section.NewKey(config[i], config[i+1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i += 2
|
||||
}
|
||||
err = file.SaveTo(filename)
|
||||
section, err := file.NewSection(DefaultSection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 0; i < len(config); {
|
||||
_, err = section.NewKey(config[i], config[i+1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i += 2
|
||||
}
|
||||
err = file.SaveTo(filename)
|
||||
|
||||
return err
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/ssh"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type HostAuthType int8 // 认证方式
|
||||
|
||||
const (
|
||||
HostPassword = 1 // 密码认证
|
||||
HostPublicKey = 2 // 公钥认证
|
||||
)
|
||||
|
||||
const SSHConnectTimeout = 10
|
||||
|
||||
|
||||
type SSHConfig struct {
|
||||
AuthType HostAuthType
|
||||
User string
|
||||
Password string
|
||||
PrivateKey string
|
||||
Host string
|
||||
Port int
|
||||
ExecTimeout int// 执行超时时间
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Output string
|
||||
Err error
|
||||
}
|
||||
|
||||
func parseSSHConfig(sshConfig SSHConfig) (config *ssh.ClientConfig, err error) {
|
||||
timeout := time.Duration(SSHConnectTimeout) * time.Second
|
||||
// 密码认证
|
||||
if sshConfig.AuthType == HostPassword {
|
||||
config = &ssh.ClientConfig{
|
||||
User: sshConfig.User,
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.Password(sshConfig.Password),
|
||||
},
|
||||
Timeout: timeout,
|
||||
HostKeyCallback:func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
signer, err := ssh.ParsePrivateKey([]byte(sshConfig.PrivateKey))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 公钥认证
|
||||
config = &ssh.ClientConfig{
|
||||
User: sshConfig.User,
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.PublicKeys(signer),
|
||||
},
|
||||
Timeout: timeout,
|
||||
HostKeyCallback:func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 执行shell命令
|
||||
func Exec(sshConfig SSHConfig, cmd string) (output string, err error) {
|
||||
client, err := getClient(sshConfig)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
session, err := client.NewSession()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
// 后台运行
|
||||
if sshConfig.ExecTimeout < 0 {
|
||||
go session.CombinedOutput(cmd)
|
||||
time.Sleep(10 * time.Second)
|
||||
return "", nil
|
||||
}
|
||||
// 不限制超时
|
||||
if sshConfig.ExecTimeout == 0 {
|
||||
outputByte, execErr := session.CombinedOutput(cmd)
|
||||
output = string(outputByte)
|
||||
err = execErr
|
||||
return
|
||||
}
|
||||
|
||||
var resultChan chan Result = make(chan Result)
|
||||
var timeoutChan chan bool = make(chan bool)
|
||||
go func() {
|
||||
output, err := session.CombinedOutput(cmd)
|
||||
resultChan <- Result{string(output), err}
|
||||
}()
|
||||
// todo 等待超时后,如何停止远程正在执行的任务, 使用timeout命令,但不具有通用性
|
||||
go triggerTimeout(timeoutChan, sshConfig.ExecTimeout)
|
||||
select {
|
||||
case result := <- resultChan:
|
||||
output = result.Output
|
||||
err = result.Err
|
||||
case <- timeoutChan:
|
||||
output = ""
|
||||
err = errors.New("timeout")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getClient(sshConfig SSHConfig) (*ssh.Client, error) {
|
||||
config, err := parseSSHConfig(sshConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addr := fmt.Sprintf("%s:%d", sshConfig.Host, sshConfig.Port)
|
||||
|
||||
return ssh.Dial("tcp", addr, config)
|
||||
}
|
||||
|
||||
|
||||
func triggerTimeout(ch chan bool, timeout int){
|
||||
// 最长执行时间不能超过24小时
|
||||
if timeout <= 0 || timeout > 86400 {
|
||||
timeout = 86400
|
||||
}
|
||||
time.Sleep(time.Duration(timeout) * time.Second)
|
||||
close(ch)
|
||||
}
|
||||
|
||||
+29
-27
@@ -1,16 +1,17 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
)
|
||||
|
||||
// json 格式输出
|
||||
|
||||
type response struct {
|
||||
Code int `json:"code"` // 状态码 0:成功 非0:失败
|
||||
Message string `json:"message"` // 信息
|
||||
Data interface{} `json:"data"` // 数据
|
||||
Code int `json:"code"` // 状态码 0:成功 非0:失败
|
||||
Message string `json:"message"` // 信息
|
||||
Data interface{} `json:"data"` // 数据
|
||||
}
|
||||
|
||||
type JsonResponse struct{}
|
||||
@@ -20,45 +21,46 @@ const ResponseFailure = 1
|
||||
const NotFound = 2
|
||||
const AuthError = 3
|
||||
const ServerError = 4
|
||||
const CaptchaError = 5
|
||||
|
||||
const SuccessContent = "操作成功"
|
||||
const FailureContent = "操作失败"
|
||||
|
||||
func JsonResponseByErr(err error) string {
|
||||
json := JsonResponse{}
|
||||
if err != nil {
|
||||
return json.CommonFailure(FailureContent, err)
|
||||
}
|
||||
json := JsonResponse{}
|
||||
if err != nil {
|
||||
return json.CommonFailure(FailureContent, err)
|
||||
}
|
||||
|
||||
return json.Success(SuccessContent, nil)
|
||||
return json.Success(SuccessContent, nil)
|
||||
}
|
||||
|
||||
func (j *JsonResponse) Success(message string, data interface{}) string {
|
||||
return j.response(ResponseSuccess, message, data)
|
||||
return j.response(ResponseSuccess, message, data)
|
||||
}
|
||||
|
||||
func (j *JsonResponse) Failure(code int, message string) string {
|
||||
return j.response(code, message, nil)
|
||||
return j.response(code, message, nil)
|
||||
}
|
||||
|
||||
func (j *JsonResponse) CommonFailure(message string, err... error) string {
|
||||
if len(err) > 0 {
|
||||
logger.Warn(err)
|
||||
}
|
||||
return j.Failure(ResponseFailure, message)
|
||||
func (j *JsonResponse) CommonFailure(message string, err ...error) string {
|
||||
if len(err) > 0 {
|
||||
logger.Warn(err)
|
||||
}
|
||||
return j.Failure(ResponseFailure, message)
|
||||
}
|
||||
|
||||
func (j *JsonResponse) response(code int, message string, data interface{}) string {
|
||||
resp := response{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
}
|
||||
resp := response{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
result, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
result, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
return string(result)
|
||||
return string(result)
|
||||
}
|
||||
|
||||
+54
-65
@@ -1,107 +1,96 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"math/rand"
|
||||
"time"
|
||||
"runtime"
|
||||
"github.com/Tang-RoseChild/mahonia"
|
||||
"strings"
|
||||
"os"
|
||||
"fmt"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Tang-RoseChild/mahonia"
|
||||
)
|
||||
|
||||
// 生成长度为length的随机字符串
|
||||
func RandString(length int64) string {
|
||||
sources := []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
result := []byte{}
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
sourceLength := len(sources)
|
||||
var i int64 = 0
|
||||
for ; i < length; i++ {
|
||||
result = append(result, sources[r.Intn(sourceLength)])
|
||||
}
|
||||
sources := []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
result := []byte{}
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
sourceLength := len(sources)
|
||||
var i int64 = 0
|
||||
for ; i < length; i++ {
|
||||
result = append(result, sources[r.Intn(sourceLength)])
|
||||
}
|
||||
|
||||
return string(result)
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// 生成32位MD5摘要
|
||||
func Md5(str string) string {
|
||||
m := md5.New()
|
||||
m.Write([]byte(str))
|
||||
m := md5.New()
|
||||
m.Write([]byte(str))
|
||||
|
||||
return hex.EncodeToString(m.Sum(nil))
|
||||
return hex.EncodeToString(m.Sum(nil))
|
||||
}
|
||||
|
||||
// 生成0-max之间随机数
|
||||
func RandNumber(max int) int {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
return r.Intn(max)
|
||||
}
|
||||
|
||||
// 判断当前系统是否是windows
|
||||
func IsWindows() bool {
|
||||
return runtime.GOOS == "windows"
|
||||
return r.Intn(max)
|
||||
}
|
||||
|
||||
// GBK编码转换为UTF8
|
||||
func GBK2UTF8(s string) (string, bool) {
|
||||
dec := mahonia.NewDecoder("gbk")
|
||||
dec := mahonia.NewDecoder("gbk")
|
||||
|
||||
return dec.ConvertStringOK(s)
|
||||
return dec.ConvertStringOK(s)
|
||||
}
|
||||
|
||||
// 批量替换字符串
|
||||
func ReplaceStrings(s string, old []string, replace []string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
if len(old) != len(replace) {
|
||||
return s
|
||||
}
|
||||
func ReplaceStrings(s string, old []string, replace []string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
if len(old) != len(replace) {
|
||||
return s
|
||||
}
|
||||
|
||||
for i, v := range old {
|
||||
s = strings.Replace(s, v, replace[i], 1000)
|
||||
}
|
||||
for i, v := range old {
|
||||
s = strings.Replace(s, v, replace[i], 1000)
|
||||
}
|
||||
|
||||
return s
|
||||
return s
|
||||
}
|
||||
|
||||
func InStringSlice(slice []string, element string) bool {
|
||||
element = strings.TrimSpace(element)
|
||||
for _, v := range slice {
|
||||
if strings.TrimSpace(v) == element{
|
||||
return true
|
||||
}
|
||||
}
|
||||
element = strings.TrimSpace(element)
|
||||
for _, v := range slice {
|
||||
if strings.TrimSpace(v) == element {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return false
|
||||
}
|
||||
|
||||
// 转义json特殊字符
|
||||
func EscapeJson(s string) string {
|
||||
specialChars := []string{"\\", "\b","\f", "\n", "\r", "\t", "\"",}
|
||||
replaceChars := []string{ "\\\\", "\\b", "\\f", "\\n", "\\r", "\\t", "\\\"",}
|
||||
func EscapeJson(s string) string {
|
||||
specialChars := []string{"\\", "\b", "\f", "\n", "\r", "\t", "\""}
|
||||
replaceChars := []string{"\\\\", "\\b", "\\f", "\\n", "\\r", "\\t", "\\\""}
|
||||
|
||||
return ReplaceStrings(s, specialChars, replaceChars)
|
||||
return ReplaceStrings(s, specialChars, replaceChars)
|
||||
}
|
||||
|
||||
// 判断文件是否存在及是否有权限访问
|
||||
func FileExist(file string) bool {
|
||||
_, err := os.Stat(file)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
if os.IsPermission(err) {
|
||||
return false
|
||||
}
|
||||
_, err := os.Stat(file)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
if os.IsPermission(err) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
return true
|
||||
}
|
||||
|
||||
// 格式化环境变量
|
||||
func FormatUnixEnv(key, value string) string {
|
||||
return fmt.Sprintf("export %s=%s; ", key, value)
|
||||
}
|
||||
+12
-12
@@ -3,22 +3,22 @@ package utils
|
||||
import "testing"
|
||||
|
||||
func TestRandString(t *testing.T) {
|
||||
str := RandString(32)
|
||||
if len(str) != 32 {
|
||||
t.Fatalf("长度不匹配,目标长度32, 实际%d-%s", len(str), str)
|
||||
}
|
||||
str := RandString(32)
|
||||
if len(str) != 32 {
|
||||
t.Fatalf("长度不匹配,目标长度32, 实际%d-%s", len(str), str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMd5(t *testing.T) {
|
||||
str := Md5("123456")
|
||||
if len(str) != 32 {
|
||||
t.Fatalf("长度不匹配,目标长度32, 实际%d-%s", len(str), str)
|
||||
}
|
||||
str := Md5("123456")
|
||||
if len(str) != 32 {
|
||||
t.Fatalf("长度不匹配,目标长度32, 实际%d-%s", len(str), str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandNumber(t *testing.T) {
|
||||
num := RandNumber(10000)
|
||||
if num <= 0 && num >= 10000 {
|
||||
t.Fatalf("随机数不在有效范围内-%d", num)
|
||||
}
|
||||
num := RandNumber(10000)
|
||||
if num <= 0 && num >= 10000 {
|
||||
t.Fatalf("随机数不在有效范围内-%d", num)
|
||||
}
|
||||
}
|
||||
|
||||
+27
-26
@@ -3,35 +3,36 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"golang.org/x/net/context"
|
||||
"errors"
|
||||
"errors"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
output string
|
||||
err error
|
||||
output string
|
||||
err error
|
||||
}
|
||||
|
||||
// 执行shell命令,可设置执行超时时间
|
||||
func ExecShell(ctx context.Context, command string) (string, error) {
|
||||
cmd := exec.Command("/bin/bash", "-c", command)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
var resultChan chan Result = make(chan Result)
|
||||
go func() {
|
||||
output ,err := cmd.CombinedOutput()
|
||||
resultChan <- Result{string(output), err}
|
||||
}()
|
||||
select {
|
||||
case <- ctx.Done():
|
||||
if cmd.Process.Pid > 0 {
|
||||
syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
||||
}
|
||||
return "", errors.New("timeout killed")
|
||||
case result := <- resultChan:
|
||||
return result.output, result.err
|
||||
}
|
||||
}
|
||||
func ExecShell(ctx context.Context, command string) (string, error) {
|
||||
cmd := exec.Command("/bin/bash", "-c", command)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
resultChan := make(chan Result)
|
||||
go func() {
|
||||
output, err := cmd.CombinedOutput()
|
||||
resultChan <- Result{string(output), err}
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if cmd.Process.Pid > 0 {
|
||||
syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
||||
}
|
||||
return "", errors.New("timeout killed")
|
||||
case result := <-resultChan:
|
||||
return result.output, result.err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,51 +3,49 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"golang.org/x/net/context"
|
||||
"errors"
|
||||
"errors"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
output string
|
||||
err error
|
||||
output string
|
||||
err error
|
||||
}
|
||||
|
||||
// 执行shell命令,可设置执行超时时间
|
||||
func ExecShell(ctx context.Context, command string) (string, error) {
|
||||
cmd := exec.Command("cmd", "/C", command)
|
||||
// 隐藏cmd窗口
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
HideWindow: true,
|
||||
}
|
||||
var resultChan chan Result = make(chan Result)
|
||||
go func() {
|
||||
output ,err := cmd.CombinedOutput()
|
||||
resultChan <- Result{string(output), err}
|
||||
}()
|
||||
select {
|
||||
case <- ctx.Done():
|
||||
if cmd.Process.Pid > 0 {
|
||||
exec.Command("taskkill", "/F", "/T", "/PID", strconv.Itoa(cmd.Process.Pid)).Run()
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
return "", errors.New("timeout killed")
|
||||
case result := <- resultChan:
|
||||
return ConvertEncoding(result.output), result.err
|
||||
}
|
||||
|
||||
|
||||
return "", nil
|
||||
func ExecShell(ctx context.Context, command string) (string, error) {
|
||||
cmd := exec.Command("cmd", "/C", command)
|
||||
// 隐藏cmd窗口
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
HideWindow: true,
|
||||
}
|
||||
var resultChan chan Result = make(chan Result)
|
||||
go func() {
|
||||
output, err := cmd.CombinedOutput()
|
||||
resultChan <- Result{string(output), err}
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if cmd.Process.Pid > 0 {
|
||||
exec.Command("taskkill", "/F", "/T", "/PID", strconv.Itoa(cmd.Process.Pid)).Run()
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
return "", errors.New("timeout killed")
|
||||
case result := <-resultChan:
|
||||
return ConvertEncoding(result.output), result.err
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertEncoding(outputGBK string) (string) {
|
||||
// windows平台编码为gbk,需转换为utf8才能入库
|
||||
outputUTF8, ok := GBK2UTF8(outputGBK)
|
||||
if ok {
|
||||
return outputUTF8
|
||||
}
|
||||
func ConvertEncoding(outputGBK string) string {
|
||||
// windows平台编码为gbk,需转换为utf8才能入库
|
||||
outputUTF8, ok := GBK2UTF8(outputGBK)
|
||||
if ok {
|
||||
return outputUTF8
|
||||
}
|
||||
|
||||
return "命令输出转换编码失败(gbk to utf8)"
|
||||
}
|
||||
return "命令输出转换编码失败(gbk to utf8)"
|
||||
}
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#set -x -u
|
||||
# 构建应用, 生成压缩包 gocron.zip或gocron.tar.gz
|
||||
# ./package.sh -v 1.4
|
||||
|
||||
VERSION=''
|
||||
GOCRON_APP_NAME='gocron'
|
||||
GOCRON_NODE_APP_NAME='gocron-node'
|
||||
PACKAGE_DIR='./packages'
|
||||
|
||||
|
||||
# 用法
|
||||
usage() {
|
||||
echo 'usage: ./package.sh -v version'
|
||||
}
|
||||
|
||||
# 初始化
|
||||
init() {
|
||||
rm -rf ${PACKAGE_DIR}
|
||||
mkdir -p ${PACKAGE_DIR}
|
||||
}
|
||||
|
||||
# 构建应用
|
||||
build() {
|
||||
make -f makefile.cross-compiles VERSION=${VERSION}
|
||||
|
||||
if [[ $? -ne 0 ]];then
|
||||
echo 'make error'
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 清理
|
||||
clean() {
|
||||
make -f makefile.cross-compiles clean
|
||||
}
|
||||
|
||||
# 打包gocron
|
||||
package_gocron() {
|
||||
local OS=$1
|
||||
local GOCRON_COMPRESS_FILE=''
|
||||
local PLATFORM_NAME=${GOCRON_APP_NAME}_${OS}_amd64
|
||||
|
||||
if [[ ! -d ${PACKAGE_DIR}/${PLATFORM_NAME} ]];then
|
||||
mkdir ${PACKAGE_DIR}/${PLATFORM_NAME}
|
||||
fi
|
||||
|
||||
for file in public templates LICENSE README.md Dockerfile-release; do
|
||||
cp -r ${file} ${PLATFORM_NAME}
|
||||
done
|
||||
|
||||
if [[ ${OS} = 'windows' ]];then
|
||||
GOCRON_COMPRESS_FILE=${GOCRON_APP_NAME}-v${VERSION}-${OS}-amd64.zip
|
||||
zip -rq ${PACKAGE_DIR}/${PLATFORM_NAME}/${GOCRON_COMPRESS_FILE} ${PLATFORM_NAME}
|
||||
else
|
||||
GOCRON_COMPRESS_FILE=${GOCRON_APP_NAME}-v${VERSION}-${OS}-amd64.tar.gz
|
||||
tar czf ${PACKAGE_DIR}/${PLATFORM_NAME}/${GOCRON_COMPRESS_FILE} ${PLATFORM_NAME}
|
||||
fi
|
||||
}
|
||||
|
||||
# 打包gocron-node
|
||||
package_gocron_node() {
|
||||
local OS=$1
|
||||
local GOCRON_NODE_COMPRESS_FILE=''
|
||||
local PLATFORM_NAME=${GOCRON_NODE_APP_NAME}_${OS}_amd64
|
||||
|
||||
if [[ ! -d ${PACKAGE_DIR}/${PLATFORM_NAME} ]];then
|
||||
mkdir ${PACKAGE_DIR}/${PLATFORM_NAME}
|
||||
fi
|
||||
|
||||
if [[ ${OS} = 'windows' ]];then
|
||||
GOCRON_NODE_COMPRESS_FILE=${GOCRON_NODE_APP_NAME}-v${VERSION}-${OS}-amd64.zip
|
||||
zip -rq ${PACKAGE_DIR}/${PLATFORM_NAME}/${GOCRON_NODE_COMPRESS_FILE} ${PLATFORM_NAME}
|
||||
else
|
||||
GOCRON_NODE_COMPRESS_FILE=${GOCRON_NODE_APP_NAME}-v${VERSION}-${OS}-amd64.tar.gz
|
||||
tar czf ${PACKAGE_DIR}/${PLATFORM_NAME}/${GOCRON_NODE_COMPRESS_FILE} ${PLATFORM_NAME}
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
package_multi() {
|
||||
for os in darwin linux windows; do
|
||||
package_gocron ${os}
|
||||
package_gocron_node ${os}
|
||||
done
|
||||
}
|
||||
|
||||
while getopts "v:" OPT; do
|
||||
case ${OPT} in
|
||||
v) VERSION=${OPTARG}
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z ${VERSION} ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
run() {
|
||||
init
|
||||
build
|
||||
package_multi
|
||||
clean
|
||||
}
|
||||
|
||||
|
||||
run
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,13 +10,16 @@ function Util() {
|
||||
swal("操作成功", '保存成功', 'success');
|
||||
};
|
||||
// ajax成功处理
|
||||
util.ajaxSuccess = function(response, callback) {
|
||||
util.ajaxSuccess = function(response, callback, failureCallback) {
|
||||
if (response.code === undefined) {
|
||||
swal(FAILURE_MESSAGE, '服务端返回值无法解析', 'error');
|
||||
return;
|
||||
}
|
||||
if (response.code != SUCCESS) {
|
||||
swal(FAILURE_MESSAGE, response.message ,'error');
|
||||
if (failureCallback !== undefined) {
|
||||
failureCallback(response.code, response.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (callback !== undefined) {
|
||||
@@ -39,12 +42,12 @@ function Util() {
|
||||
).error(util.ajaxFailure);
|
||||
};
|
||||
// post请求
|
||||
util.post = function(url, params, callback) {
|
||||
util.post = function(url, params, callback, failureCallback) {
|
||||
$.post(
|
||||
url,
|
||||
util.objectTrim(params),
|
||||
function(response) {
|
||||
util.ajaxSuccess(response, callback);
|
||||
util.ajaxSuccess(response, callback, failureCallback);
|
||||
},
|
||||
'json'
|
||||
).error(util.ajaxFailure);
|
||||
@@ -57,7 +60,7 @@ function Util() {
|
||||
type: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
confirmButtonText: '删除',
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonColor: '#d33',
|
||||
cancelButtonText: "取消",
|
||||
closeOnConfirm: false,
|
||||
|
||||
+15
-14
@@ -1,20 +1,21 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"gopkg.in/macaron.v1"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
func ParsePageAndPageSize(ctx *macaron.Context, params models.CommonMap) {
|
||||
page := ctx.QueryInt("page")
|
||||
pageSize := ctx.QueryInt("page_size")
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = models.PageSize
|
||||
}
|
||||
// ParsePageAndPageSize 解析查询参数中的页数和每页数量
|
||||
func ParsePageAndPageSize(ctx *macaron.Context, params models.CommonMap) {
|
||||
page := ctx.QueryInt("page")
|
||||
pageSize := ctx.QueryInt("page_size")
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = models.PageSize
|
||||
}
|
||||
|
||||
params["Page"] = page
|
||||
params["PageSize"] = pageSize
|
||||
}
|
||||
params["Page"] = page
|
||||
params["PageSize"] = pageSize
|
||||
}
|
||||
|
||||
+2
-2
@@ -3,6 +3,6 @@ package routers
|
||||
import "gopkg.in/macaron.v1"
|
||||
|
||||
// 首页
|
||||
func Home(ctx *macaron.Context) {
|
||||
ctx.Redirect("/task")
|
||||
func Home(ctx *macaron.Context) {
|
||||
ctx.Redirect("/task")
|
||||
}
|
||||
|
||||
+162
-157
@@ -1,194 +1,199 @@
|
||||
package host
|
||||
|
||||
import (
|
||||
"gopkg.in/macaron.v1"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"strconv"
|
||||
"github.com/ouqiang/gocron/service"
|
||||
"github.com/Unknwon/paginater"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"github.com/ouqiang/gocron/routers/base"
|
||||
"github.com/go-macaron/binding"
|
||||
"github.com/ouqiang/gocron/modules/rpc/grpcpool"
|
||||
"strings"
|
||||
"github.com/ouqiang/gocron/modules/rpc/client"
|
||||
"github.com/ouqiang/gocron/modules/rpc/proto"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Unknwon/paginater"
|
||||
"github.com/go-macaron/binding"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/rpc/client"
|
||||
"github.com/ouqiang/gocron/modules/rpc/grpcpool"
|
||||
"github.com/ouqiang/gocron/modules/rpc/proto"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"github.com/ouqiang/gocron/routers/base"
|
||||
"github.com/ouqiang/gocron/service"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
func Index(ctx *macaron.Context) {
|
||||
hostModel := new(models.Host)
|
||||
queryParams := parseQueryParams(ctx)
|
||||
total, err := hostModel.Total(queryParams)
|
||||
hosts, err := hostModel.List(queryParams)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
name, ok := queryParams["name"].(string)
|
||||
var safeNameHTML = ""
|
||||
if ok {
|
||||
safeNameHTML = template.HTMLEscapeString(name)
|
||||
}
|
||||
PageParams := fmt.Sprintf("id=%d&name=%s&page_size=%d",
|
||||
queryParams["Id"], safeNameHTML, queryParams["PageSize"]);
|
||||
queryParams["PageParams"] = template.URL(PageParams)
|
||||
p := paginater.New(int(total), queryParams["PageSize"].(int), queryParams["Page"].(int), 5)
|
||||
ctx.Data["Pagination"] = p
|
||||
ctx.Data["Title"] = "主机列表"
|
||||
ctx.Data["Hosts"] = hosts
|
||||
ctx.Data["Params"] = queryParams
|
||||
ctx.HTML(200, "host/index")
|
||||
// Index 主机列表
|
||||
func Index(ctx *macaron.Context) {
|
||||
hostModel := new(models.Host)
|
||||
queryParams := parseQueryParams(ctx)
|
||||
total, err := hostModel.Total(queryParams)
|
||||
hosts, err := hostModel.List(queryParams)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
name, ok := queryParams["name"].(string)
|
||||
var safeNameHTML = ""
|
||||
if ok {
|
||||
safeNameHTML = template.HTMLEscapeString(name)
|
||||
}
|
||||
PageParams := fmt.Sprintf("id=%d&name=%s&page_size=%d",
|
||||
queryParams["Id"], safeNameHTML, queryParams["PageSize"])
|
||||
queryParams["PageParams"] = template.URL(PageParams)
|
||||
p := paginater.New(int(total), queryParams["PageSize"].(int), queryParams["Page"].(int), 5)
|
||||
ctx.Data["Pagination"] = p
|
||||
ctx.Data["Title"] = "主机列表"
|
||||
ctx.Data["Hosts"] = hosts
|
||||
ctx.Data["Params"] = queryParams
|
||||
ctx.HTML(200, "host/index")
|
||||
}
|
||||
|
||||
func Create(ctx *macaron.Context) {
|
||||
ctx.Data["Title"] = "添加主机"
|
||||
ctx.HTML(200, "host/host_form")
|
||||
// Create 创建主机页面
|
||||
func Create(ctx *macaron.Context) {
|
||||
ctx.Data["Title"] = "添加主机"
|
||||
ctx.HTML(200, "host/host_form")
|
||||
}
|
||||
|
||||
func Edit(ctx *macaron.Context) {
|
||||
ctx.Data["Title"] = "编辑主机"
|
||||
hostModel := new(models.Host)
|
||||
id := ctx.ParamsInt(":id")
|
||||
err := hostModel.Find(id)
|
||||
if err != nil {
|
||||
logger.Errorf("获取主机详情失败#主机id-%d", id)
|
||||
}
|
||||
ctx.Data["Host"] = hostModel
|
||||
ctx.HTML(200, "host/host_form")
|
||||
// Edit 修改主机页面
|
||||
func Edit(ctx *macaron.Context) {
|
||||
ctx.Data["Title"] = "编辑主机"
|
||||
hostModel := new(models.Host)
|
||||
id := ctx.ParamsInt(":id")
|
||||
err := hostModel.Find(id)
|
||||
if err != nil {
|
||||
logger.Errorf("获取主机详情失败#主机id-%d", id)
|
||||
}
|
||||
ctx.Data["Host"] = hostModel
|
||||
ctx.HTML(200, "host/host_form")
|
||||
}
|
||||
|
||||
type HostForm struct {
|
||||
Id int16
|
||||
Name string `binding:"Required;MaxSize(64)"`
|
||||
Alias string `binding:"Required;MaxSize(32)"`
|
||||
Port int `binding:"Required;Range(1-65535)"`
|
||||
Remark string
|
||||
Id int16
|
||||
Name string `binding:"Required;MaxSize(64)"`
|
||||
Alias string `binding:"Required;MaxSize(32)"`
|
||||
Port int `binding:"Required;Range(1-65535)"`
|
||||
Remark string
|
||||
}
|
||||
|
||||
// Error 表单验证错误处理
|
||||
func (f HostForm) Error(ctx *macaron.Context, errs binding.Errors) {
|
||||
if len(errs) == 0 {
|
||||
return
|
||||
}
|
||||
json := utils.JsonResponse{}
|
||||
content := json.CommonFailure("表单验证失败, 请检测输入")
|
||||
if len(errs) == 0 {
|
||||
return
|
||||
}
|
||||
json := utils.JsonResponse{}
|
||||
content := json.CommonFailure("表单验证失败, 请检测输入")
|
||||
|
||||
ctx.Resp.Write([]byte(content))
|
||||
ctx.Resp.Write([]byte(content))
|
||||
}
|
||||
|
||||
func Store(ctx *macaron.Context, form HostForm) string {
|
||||
json := utils.JsonResponse{}
|
||||
hostModel := new(models.Host)
|
||||
id := form.Id
|
||||
nameExist, err := hostModel.NameExists(form.Name, form.Id)
|
||||
if err != nil {
|
||||
return json.CommonFailure("操作失败", err)
|
||||
}
|
||||
if nameExist {
|
||||
return json.CommonFailure("主机名已存在")
|
||||
}
|
||||
// Store 保存、修改主机信息
|
||||
func Store(ctx *macaron.Context, form HostForm) string {
|
||||
json := utils.JsonResponse{}
|
||||
hostModel := new(models.Host)
|
||||
id := form.Id
|
||||
nameExist, err := hostModel.NameExists(form.Name, form.Id)
|
||||
if err != nil {
|
||||
return json.CommonFailure("操作失败", err)
|
||||
}
|
||||
if nameExist {
|
||||
return json.CommonFailure("主机名已存在")
|
||||
}
|
||||
|
||||
hostModel.Name = strings.TrimSpace(form.Name)
|
||||
hostModel.Alias = strings.TrimSpace(form.Alias)
|
||||
hostModel.Port = form.Port
|
||||
hostModel.Remark = strings.TrimSpace(form.Remark)
|
||||
isCreate := false
|
||||
oldHostModel := new(models.Host)
|
||||
err = oldHostModel.Find(int(id))
|
||||
if err != nil {
|
||||
return json.CommonFailure("主机不存在")
|
||||
}
|
||||
hostModel.Name = strings.TrimSpace(form.Name)
|
||||
hostModel.Alias = strings.TrimSpace(form.Alias)
|
||||
hostModel.Port = form.Port
|
||||
hostModel.Remark = strings.TrimSpace(form.Remark)
|
||||
isCreate := false
|
||||
oldHostModel := new(models.Host)
|
||||
err = oldHostModel.Find(int(id))
|
||||
if err != nil {
|
||||
return json.CommonFailure("主机不存在")
|
||||
}
|
||||
|
||||
if id > 0 {
|
||||
_, err = hostModel.UpdateBean(id)
|
||||
} else {
|
||||
isCreate = true
|
||||
id, err = hostModel.Create()
|
||||
}
|
||||
if err != nil {
|
||||
return json.CommonFailure("保存失败", err)
|
||||
}
|
||||
if id > 0 {
|
||||
_, err = hostModel.UpdateBean(id)
|
||||
} else {
|
||||
isCreate = true
|
||||
id, err = hostModel.Create()
|
||||
}
|
||||
if err != nil {
|
||||
return json.CommonFailure("保存失败", err)
|
||||
}
|
||||
|
||||
if !isCreate {
|
||||
oldAddr := fmt.Sprintf("%s:%d", oldHostModel.Name, oldHostModel.Port)
|
||||
newAddr := fmt.Sprintf("%s:%d", hostModel.Name, hostModel.Port)
|
||||
if oldAddr != newAddr {
|
||||
grpcpool.Pool.Release(oldAddr)
|
||||
}
|
||||
if !isCreate {
|
||||
oldAddr := fmt.Sprintf("%s:%d", oldHostModel.Name, oldHostModel.Port)
|
||||
newAddr := fmt.Sprintf("%s:%d", hostModel.Name, hostModel.Port)
|
||||
if oldAddr != newAddr {
|
||||
grpcpool.Pool.Release(oldAddr)
|
||||
}
|
||||
|
||||
taskModel := new(models.TaskHost)
|
||||
tasks, err := taskModel.ActiveListByHostId(id)
|
||||
if err != nil {
|
||||
return json.CommonFailure("刷新任务主机信息失败", err)
|
||||
}
|
||||
serviceTask := new(service.Task)
|
||||
serviceTask.BatchAdd(tasks)
|
||||
}
|
||||
taskModel := new(models.Task)
|
||||
tasks, err := taskModel.ActiveListByHostId(id)
|
||||
if err != nil {
|
||||
return json.CommonFailure("刷新任务主机信息失败", err)
|
||||
}
|
||||
service.ServiceTask.BatchAdd(tasks)
|
||||
}
|
||||
|
||||
return json.Success("保存成功", nil)
|
||||
return json.Success("保存成功", nil)
|
||||
}
|
||||
|
||||
func Remove(ctx *macaron.Context) string {
|
||||
id, err := strconv.Atoi(ctx.Params(":id"))
|
||||
json := utils.JsonResponse{}
|
||||
if err != nil {
|
||||
return json.CommonFailure("参数错误", err)
|
||||
}
|
||||
taskModel := new(models.Task)
|
||||
exist,err := taskModel.HostIdExist(int16(id))
|
||||
if err != nil {
|
||||
return json.CommonFailure("操作失败", err)
|
||||
}
|
||||
if exist {
|
||||
return json.CommonFailure("有任务引用此主机,不能删除")
|
||||
}
|
||||
// Remove 删除主机
|
||||
func Remove(ctx *macaron.Context) string {
|
||||
id, err := strconv.Atoi(ctx.Params(":id"))
|
||||
json := utils.JsonResponse{}
|
||||
if err != nil {
|
||||
return json.CommonFailure("参数错误", err)
|
||||
}
|
||||
taskHostModel := new(models.TaskHost)
|
||||
exist, err := taskHostModel.HostIdExist(int16(id))
|
||||
if err != nil {
|
||||
return json.CommonFailure("操作失败", err)
|
||||
}
|
||||
if exist {
|
||||
return json.CommonFailure("有任务引用此主机,不能删除")
|
||||
}
|
||||
|
||||
hostModel := new(models.Host)
|
||||
err = hostModel.Find(int(id))
|
||||
if err != nil {
|
||||
return json.CommonFailure("主机不存在")
|
||||
}
|
||||
hostModel := new(models.Host)
|
||||
err = hostModel.Find(int(id))
|
||||
if err != nil {
|
||||
return json.CommonFailure("主机不存在")
|
||||
}
|
||||
|
||||
_, err =hostModel.Delete(id)
|
||||
if err != nil {
|
||||
return json.CommonFailure("操作失败", err)
|
||||
}
|
||||
_, err = hostModel.Delete(id)
|
||||
if err != nil {
|
||||
return json.CommonFailure("操作失败", err)
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", hostModel.Name, hostModel.Port)
|
||||
grpcpool.Pool.Release(addr)
|
||||
addr := fmt.Sprintf("%s:%d", hostModel.Name, hostModel.Port)
|
||||
grpcpool.Pool.Release(addr)
|
||||
|
||||
|
||||
return json.Success("操作成功", nil)
|
||||
return json.Success("操作成功", nil)
|
||||
}
|
||||
|
||||
func Ping(ctx *macaron.Context) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
hostModel := new(models.Host)
|
||||
err := hostModel.Find(id)
|
||||
json := utils.JsonResponse{}
|
||||
if err != nil || hostModel.Id <= 0{
|
||||
return json.CommonFailure("主机不存在", err)
|
||||
}
|
||||
// Ping 测试主机是否可连接
|
||||
func Ping(ctx *macaron.Context) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
hostModel := new(models.Host)
|
||||
err := hostModel.Find(id)
|
||||
json := utils.JsonResponse{}
|
||||
if err != nil || hostModel.Id <= 0 {
|
||||
return json.CommonFailure("主机不存在", err)
|
||||
}
|
||||
|
||||
taskReq := &rpc.TaskRequest{}
|
||||
taskReq.Command = "echo hello"
|
||||
taskReq.Timeout = 10
|
||||
output, err := client.Exec(hostModel.Name, hostModel.Port, taskReq)
|
||||
if err != nil {
|
||||
return json.CommonFailure("连接失败-"+err.Error()+" "+output, err)
|
||||
}
|
||||
|
||||
taskReq := &rpc.TaskRequest{}
|
||||
taskReq.Command = "echo hello"
|
||||
taskReq.Timeout = 10
|
||||
output, err := client.Exec(hostModel.Name, hostModel.Port, taskReq)
|
||||
if err != nil {
|
||||
return json.CommonFailure("连接失败-" + err.Error() + " " + output, err)
|
||||
}
|
||||
|
||||
return json.Success("连接成功", nil)
|
||||
return json.Success("连接成功", nil)
|
||||
}
|
||||
|
||||
// 解析查询参数
|
||||
func parseQueryParams(ctx *macaron.Context) (models.CommonMap) {
|
||||
var params models.CommonMap = models.CommonMap{}
|
||||
params["Id"] = ctx.QueryInt("id")
|
||||
params["Name"] = ctx.QueryTrim("name")
|
||||
base.ParsePageAndPageSize(ctx, params)
|
||||
func parseQueryParams(ctx *macaron.Context) models.CommonMap {
|
||||
var params = models.CommonMap{}
|
||||
params["Id"] = ctx.QueryInt("id")
|
||||
params["Name"] = ctx.QueryTrim("name")
|
||||
base.ParsePageAndPageSize(ctx, params)
|
||||
|
||||
return params
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
+122
-114
@@ -1,156 +1,164 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/app"
|
||||
"github.com/ouqiang/gocron/modules/setting"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"gopkg.in/macaron.v1"
|
||||
"strconv"
|
||||
"fmt"
|
||||
"github.com/ouqiang/gocron/service"
|
||||
"github.com/go-macaron/binding"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-macaron/binding"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/app"
|
||||
"github.com/ouqiang/gocron/modules/setting"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"github.com/ouqiang/gocron/service"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
// 系统安装
|
||||
|
||||
type InstallForm struct {
|
||||
DbType string `binding:"In(mysql)"`
|
||||
DbHost string `binding:"Required;MaxSize(50)"`
|
||||
DbPort int `binding:"Required;Range(1,65535)"`
|
||||
DbUsername string `binding:"Required;MaxSize(50)"`
|
||||
DbPassword string `binding:"Required;MaxSize(30)"`
|
||||
DbName string `binding:"Required;MaxSize(50)"`
|
||||
DbTablePrefix string `binding:"MaxSize(20)"`
|
||||
AdminUsername string `binding:"Required;MinSize(3)"`
|
||||
AdminPassword string `binding:"Required;MinSize(6)"`
|
||||
ConfirmAdminPassword string `binding:"Required;MinSize(6)"`
|
||||
AdminEmail string `binding:"Required;Email;MaxSize(50)"`
|
||||
DbType string `binding:"In(mysql)"`
|
||||
DbHost string `binding:"Required;MaxSize(50)"`
|
||||
DbPort int `binding:"Required;Range(1,65535)"`
|
||||
DbUsername string `binding:"Required;MaxSize(50)"`
|
||||
DbPassword string `binding:"Required;MaxSize(30)"`
|
||||
DbName string `binding:"Required;MaxSize(50)"`
|
||||
DbTablePrefix string `binding:"MaxSize(20)"`
|
||||
AdminUsername string `binding:"Required;MinSize(3)"`
|
||||
AdminPassword string `binding:"Required;MinSize(6)"`
|
||||
ConfirmAdminPassword string `binding:"Required;MinSize(6)"`
|
||||
AdminEmail string `binding:"Required;Email;MaxSize(50)"`
|
||||
}
|
||||
|
||||
func (f InstallForm) Error(ctx *macaron.Context, errs binding.Errors) {
|
||||
if len(errs) == 0 {
|
||||
return
|
||||
}
|
||||
json := utils.JsonResponse{}
|
||||
content := json.CommonFailure("表单验证失败, 请检测输入")
|
||||
if len(errs) == 0 {
|
||||
return
|
||||
}
|
||||
json := utils.JsonResponse{}
|
||||
content := json.CommonFailure("表单验证失败, 请检测输入")
|
||||
|
||||
ctx.Resp.Write([]byte(content))
|
||||
ctx.Resp.Write([]byte(content))
|
||||
}
|
||||
|
||||
func Create(ctx *macaron.Context) {
|
||||
if app.Installed {
|
||||
ctx.Redirect("/")
|
||||
}
|
||||
ctx.Data["Title"] = "安装"
|
||||
ctx.Data["DisableNav"] = true
|
||||
ctx.HTML(200, "install/create")
|
||||
if app.Installed {
|
||||
ctx.Redirect("/")
|
||||
}
|
||||
ctx.Data["Title"] = "安装"
|
||||
ctx.Data["DisableNav"] = true
|
||||
ctx.HTML(200, "install/create")
|
||||
}
|
||||
|
||||
// 安装
|
||||
func Store(ctx *macaron.Context, form InstallForm) string {
|
||||
json := utils.JsonResponse{}
|
||||
if app.Installed {
|
||||
return json.CommonFailure("系统已安装!")
|
||||
}
|
||||
if form.AdminPassword != form.ConfirmAdminPassword {
|
||||
return json.CommonFailure("两次输入密码不匹配")
|
||||
}
|
||||
err := testDbConnection(form)
|
||||
if err != nil {
|
||||
return json.CommonFailure("数据库连接失败", err)
|
||||
}
|
||||
// 写入数据库配置
|
||||
err = writeConfig(form)
|
||||
if err != nil {
|
||||
return json.CommonFailure("数据库配置写入文件失败", err)
|
||||
}
|
||||
json := utils.JsonResponse{}
|
||||
if app.Installed {
|
||||
return json.CommonFailure("系统已安装!")
|
||||
}
|
||||
if form.AdminPassword != form.ConfirmAdminPassword {
|
||||
return json.CommonFailure("两次输入密码不匹配")
|
||||
}
|
||||
err := testDbConnection(form)
|
||||
if err != nil {
|
||||
return json.CommonFailure("数据库连接失败", err)
|
||||
}
|
||||
// 写入数据库配置
|
||||
err = writeConfig(form)
|
||||
if err != nil {
|
||||
return json.CommonFailure("数据库配置写入文件失败", err)
|
||||
}
|
||||
|
||||
appConfig, err := setting.Read(app.AppConfig)
|
||||
if err != nil {
|
||||
return json.CommonFailure("读取应用配置失败", err)
|
||||
}
|
||||
app.Setting = appConfig
|
||||
appConfig, err := setting.Read(app.AppConfig)
|
||||
if err != nil {
|
||||
return json.CommonFailure("读取应用配置失败", err)
|
||||
}
|
||||
app.Setting = appConfig
|
||||
|
||||
models.Db = models.CreateDb()
|
||||
// 创建数据库表
|
||||
migration := new(models.Migration)
|
||||
err = migration.Exec(form.DbName)
|
||||
if err != nil {
|
||||
return json.CommonFailure(fmt.Sprintf("创建数据库表失败-%s", err.Error()), err)
|
||||
}
|
||||
models.Db = models.CreateDb()
|
||||
// 创建数据库表
|
||||
migration := new(models.Migration)
|
||||
err = migration.Install(form.DbName)
|
||||
if err != nil {
|
||||
return json.CommonFailure(fmt.Sprintf("创建数据库表失败-%s", err.Error()), err)
|
||||
}
|
||||
|
||||
// 创建管理员账号
|
||||
err = createAdminUser(form)
|
||||
if err != nil {
|
||||
return json.CommonFailure("创建管理员账号失败", err)
|
||||
}
|
||||
// 创建管理员账号
|
||||
err = createAdminUser(form)
|
||||
if err != nil {
|
||||
return json.CommonFailure("创建管理员账号失败", err)
|
||||
}
|
||||
|
||||
// 创建安装锁
|
||||
err = app.CreateInstallLock()
|
||||
if err != nil {
|
||||
return json.CommonFailure("创建文件安装锁失败", err)
|
||||
}
|
||||
// 创建安装锁
|
||||
err = app.CreateInstallLock()
|
||||
if err != nil {
|
||||
return json.CommonFailure("创建文件安装锁失败", err)
|
||||
}
|
||||
|
||||
app.Installed = true
|
||||
// 初始化定时任务
|
||||
serviceTask := new(service.Task)
|
||||
serviceTask.Initialize()
|
||||
// 更新版本号文件
|
||||
app.UpdateVersionFile()
|
||||
|
||||
return json.Success("安装成功", nil)
|
||||
app.Installed = true
|
||||
// 初始化定时任务
|
||||
service.ServiceTask.Initialize()
|
||||
|
||||
return json.Success("安装成功", nil)
|
||||
}
|
||||
|
||||
// 配置写入文件
|
||||
func writeConfig(form InstallForm) error {
|
||||
dbConfig := []string{
|
||||
"db.engine", form.DbType,
|
||||
"db.host", form.DbHost,
|
||||
"db.port", strconv.Itoa(form.DbPort),
|
||||
"db.user", form.DbUsername,
|
||||
"db.password",form.DbPassword,
|
||||
"db.database", form.DbName,
|
||||
"db.prefix", form.DbTablePrefix,
|
||||
"db.charset", "utf8",
|
||||
"db.max.idle.conns", "30",
|
||||
"db.max.open.conns", "100",
|
||||
"allow_ips", "",
|
||||
"app.name", "定时任务管理系统", // 应用名称
|
||||
"api.key", "",
|
||||
"api.secret", "",
|
||||
}
|
||||
dbConfig := []string{
|
||||
"db.engine", form.DbType,
|
||||
"db.host", form.DbHost,
|
||||
"db.port", strconv.Itoa(form.DbPort),
|
||||
"db.user", form.DbUsername,
|
||||
"db.password", form.DbPassword,
|
||||
"db.database", form.DbName,
|
||||
"db.prefix", form.DbTablePrefix,
|
||||
"db.charset", "utf8",
|
||||
"db.max.idle.conns", "30",
|
||||
"db.max.open.conns", "100",
|
||||
"allow_ips", "",
|
||||
"app.name", "定时任务管理系统", // 应用名称
|
||||
"api.key", "",
|
||||
"api.secret", "",
|
||||
"enable_tls", "false",
|
||||
"concurrency.queue", "500",
|
||||
"ca_file", "",
|
||||
"cert_file", "",
|
||||
"key_file", "",
|
||||
}
|
||||
|
||||
return setting.Write(dbConfig, app.AppConfig)
|
||||
return setting.Write(dbConfig, app.AppConfig)
|
||||
}
|
||||
|
||||
// 创建管理员账号
|
||||
func createAdminUser(form InstallForm) error {
|
||||
user := new(models.User)
|
||||
user.Name = form.AdminUsername
|
||||
user.Password = form.AdminPassword
|
||||
user.Email = form.AdminEmail
|
||||
user.IsAdmin = 1
|
||||
_, err := user.Create()
|
||||
user := new(models.User)
|
||||
user.Name = form.AdminUsername
|
||||
user.Password = form.AdminPassword
|
||||
user.Email = form.AdminEmail
|
||||
user.IsAdmin = 1
|
||||
_, err := user.Create()
|
||||
|
||||
return err
|
||||
return err
|
||||
}
|
||||
|
||||
// 测试数据库连接
|
||||
func testDbConnection(form InstallForm) error {
|
||||
var dbConfig map[string]string = make(map[string]string)
|
||||
dbConfig["engine"] = form.DbType
|
||||
dbConfig["host"] = form.DbHost
|
||||
dbConfig["port"] = strconv.Itoa(form.DbPort)
|
||||
dbConfig["user"] = form.DbUsername
|
||||
dbConfig["password"] = form.DbPassword
|
||||
dbConfig["charset"] = "utf8"
|
||||
db, err := models.CreateTmpDb(dbConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var s setting.Setting
|
||||
s.Db.Engine = form.DbType
|
||||
s.Db.Host = form.DbHost
|
||||
s.Db.Port = form.DbPort
|
||||
s.Db.User = form.DbUsername
|
||||
s.Db.Password = form.DbPassword
|
||||
s.Db.Charset = "utf8"
|
||||
db, err := models.CreateTmpDb(&s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer db.Close()
|
||||
err = db.Ping()
|
||||
defer db.Close()
|
||||
err = db.Ping()
|
||||
|
||||
return err
|
||||
return err
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
package loginlog
|
||||
|
||||
import (
|
||||
"gopkg.in/macaron.v1"
|
||||
"github.com/Unknwon/paginater"
|
||||
"fmt"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/routers/base"
|
||||
"html/template"
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
||||
"github.com/Unknwon/paginater"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/routers/base"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
func Index(ctx *macaron.Context) {
|
||||
loginLogModel := new(models.LoginLog)
|
||||
params := models.CommonMap{}
|
||||
base.ParsePageAndPageSize(ctx, params)
|
||||
total, err := loginLogModel.Total()
|
||||
loginLogs, err := loginLogModel.List(params)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
PageParams := fmt.Sprintf("page_size=%d", params["PageSize"]);
|
||||
params["PageParams"] = template.URL(PageParams)
|
||||
p := paginater.New(int(total), params["PageSize"].(int), params["Page"].(int), 5)
|
||||
ctx.Data["Pagination"] = p
|
||||
ctx.Data["Title"] = "登录日志"
|
||||
ctx.Data["LoginLogs"] = loginLogs
|
||||
ctx.Data["Params"] = params
|
||||
ctx.HTML(200, "manage/login_log")
|
||||
}
|
||||
func Index(ctx *macaron.Context) {
|
||||
loginLogModel := new(models.LoginLog)
|
||||
params := models.CommonMap{}
|
||||
base.ParsePageAndPageSize(ctx, params)
|
||||
total, err := loginLogModel.Total()
|
||||
loginLogs, err := loginLogModel.List(params)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
PageParams := fmt.Sprintf("page_size=%d", params["PageSize"])
|
||||
params["PageParams"] = template.URL(PageParams)
|
||||
p := paginater.New(int(total), params["PageSize"].(int), params["Page"].(int), 5)
|
||||
ctx.Data["Pagination"] = p
|
||||
ctx.Data["Title"] = "登录日志"
|
||||
ctx.Data["LoginLogs"] = loginLogs
|
||||
ctx.Data["Params"] = params
|
||||
ctx.HTML(200, "manage/login_log")
|
||||
}
|
||||
|
||||
+84
-85
@@ -1,138 +1,137 @@
|
||||
package manage
|
||||
|
||||
import (
|
||||
"gopkg.in/macaron.v1"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"encoding/json"
|
||||
)
|
||||
"encoding/json"
|
||||
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
// region slack
|
||||
|
||||
func EditSlack(ctx *macaron.Context) {
|
||||
ctx.Data["Title"] = "Slack配置"
|
||||
settingModel := new(models.Setting)
|
||||
slack, err := settingModel.Slack()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
ctx.Data["Slack"] = slack
|
||||
ctx.HTML(200, "manage/slack")
|
||||
func EditSlack(ctx *macaron.Context) {
|
||||
ctx.Data["Title"] = "Slack配置"
|
||||
settingModel := new(models.Setting)
|
||||
slack, err := settingModel.Slack()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
ctx.Data["Slack"] = slack
|
||||
ctx.HTML(200, "manage/slack")
|
||||
}
|
||||
|
||||
func Slack(ctx *macaron.Context) string {
|
||||
settingModel := new(models.Setting)
|
||||
slack, err := settingModel.Slack()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
json := utils.JsonResponse{}
|
||||
settingModel := new(models.Setting)
|
||||
slack, err := settingModel.Slack()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
json := utils.JsonResponse{}
|
||||
|
||||
return json.Success("", slack)
|
||||
return json.Success("", slack)
|
||||
}
|
||||
|
||||
func UpdateSlackUrl(ctx *macaron.Context) string {
|
||||
url := ctx.QueryTrim("url")
|
||||
settingModel := new(models.Setting)
|
||||
_, err := settingModel.UpdateSlackUrl(url)
|
||||
url := ctx.QueryTrim("url")
|
||||
settingModel := new(models.Setting)
|
||||
_, err := settingModel.UpdateSlackUrl(url)
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
func CreateSlackChannel(ctx *macaron.Context) string {
|
||||
channel := ctx.QueryTrim("channel")
|
||||
settingModel := new(models.Setting)
|
||||
if settingModel.IsChannelExist(channel) {
|
||||
json := utils.JsonResponse{}
|
||||
func CreateSlackChannel(ctx *macaron.Context) string {
|
||||
channel := ctx.QueryTrim("channel")
|
||||
settingModel := new(models.Setting)
|
||||
if settingModel.IsChannelExist(channel) {
|
||||
json := utils.JsonResponse{}
|
||||
|
||||
return json.CommonFailure("Channel已存在")
|
||||
}
|
||||
_, err := settingModel.CreateChannel(channel)
|
||||
return json.CommonFailure("Channel已存在")
|
||||
}
|
||||
_, err := settingModel.CreateChannel(channel)
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
func RemoveSlackChannel(ctx *macaron.Context) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
settingModel := new(models.Setting)
|
||||
_, err := settingModel.RemoveChannel(id)
|
||||
func RemoveSlackChannel(ctx *macaron.Context) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
settingModel := new(models.Setting)
|
||||
_, err := settingModel.RemoveChannel(id)
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region 邮件
|
||||
|
||||
func EditMail(ctx *macaron.Context) {
|
||||
ctx.Data["Title"] = "邮件配置"
|
||||
settingModel := new(models.Setting)
|
||||
mail, err := settingModel.Mail()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
ctx.Data["Mail"] = mail
|
||||
ctx.HTML(200, "manage/mail")
|
||||
func EditMail(ctx *macaron.Context) {
|
||||
ctx.Data["Title"] = "邮件配置"
|
||||
settingModel := new(models.Setting)
|
||||
mail, err := settingModel.Mail()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
ctx.Data["Mail"] = mail
|
||||
ctx.HTML(200, "manage/mail")
|
||||
}
|
||||
|
||||
func Mail(ctx *macaron.Context) string {
|
||||
settingModel := new(models.Setting)
|
||||
mail, err := settingModel.Mail()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
settingModel := new(models.Setting)
|
||||
mail, err := settingModel.Mail()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
json := utils.JsonResponse{}
|
||||
json := utils.JsonResponse{}
|
||||
|
||||
return json.Success("", mail)
|
||||
return json.Success("", mail)
|
||||
}
|
||||
|
||||
|
||||
type MailServerForm struct {
|
||||
Host string `binding:"Required;MaxSize(100)"`
|
||||
Port int `binding:"Required;Range(1-65535)"`
|
||||
User string `binding:"Required;MaxSize(64);Email"`
|
||||
Password string `binding:"Required;MaxSize(64)"`
|
||||
Host string `binding:"Required;MaxSize(100)"`
|
||||
Port int `binding:"Required;Range(1-65535)"`
|
||||
User string `binding:"Required;MaxSize(64);Email"`
|
||||
Password string `binding:"Required;MaxSize(64)"`
|
||||
}
|
||||
|
||||
func UpdateMailServer(ctx *macaron.Context, form MailServerForm) string {
|
||||
jsonByte, _ := json.Marshal(form)
|
||||
settingModel := new(models.Setting)
|
||||
_, err := settingModel.UpdateMailServer(string(jsonByte))
|
||||
jsonByte, _ := json.Marshal(form)
|
||||
settingModel := new(models.Setting)
|
||||
_, err := settingModel.UpdateMailServer(string(jsonByte))
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
func ClearMailServer(ctx *macaron.Context) string {
|
||||
jsonByte, _ := json.Marshal(MailServerForm{})
|
||||
settingModel := new(models.Setting)
|
||||
_, err := settingModel.UpdateMailServer(string(jsonByte))
|
||||
jsonByte, _ := json.Marshal(MailServerForm{})
|
||||
settingModel := new(models.Setting)
|
||||
_, err := settingModel.UpdateMailServer(string(jsonByte))
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
func CreateMailUser(ctx *macaron.Context) string {
|
||||
username := ctx.QueryTrim("username")
|
||||
email := ctx.QueryTrim("email")
|
||||
settingModel := new(models.Setting)
|
||||
if username == "" || email == "" {
|
||||
json := utils.JsonResponse{}
|
||||
func CreateMailUser(ctx *macaron.Context) string {
|
||||
username := ctx.QueryTrim("username")
|
||||
email := ctx.QueryTrim("email")
|
||||
settingModel := new(models.Setting)
|
||||
if username == "" || email == "" {
|
||||
json := utils.JsonResponse{}
|
||||
|
||||
return json.CommonFailure("用户名、邮箱均不能为空")
|
||||
}
|
||||
_, err := settingModel.CreateMailUser(username, email)
|
||||
return json.CommonFailure("用户名、邮箱均不能为空")
|
||||
}
|
||||
_, err := settingModel.CreateMailUser(username, email)
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
func RemoveMailUser(ctx *macaron.Context) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
settingModel := new(models.Setting)
|
||||
_, err := settingModel.RemoveMailUser(id)
|
||||
func RemoveMailUser(ctx *macaron.Context) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
settingModel := new(models.Setting)
|
||||
_, err := settingModel.RemoveMailUser(id)
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
// endregion
|
||||
// endregion
|
||||
|
||||
+279
-234
@@ -1,24 +1,29 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"github.com/go-macaron/binding"
|
||||
"github.com/ouqiang/gocron/routers/install"
|
||||
"gopkg.in/macaron.v1"
|
||||
"github.com/ouqiang/gocron/routers/task"
|
||||
"github.com/ouqiang/gocron/routers/host"
|
||||
"github.com/ouqiang/gocron/routers/tasklog"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"github.com/go-macaron/session"
|
||||
"github.com/go-macaron/toolbox"
|
||||
"strings"
|
||||
"github.com/ouqiang/gocron/modules/app"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/routers/user"
|
||||
"github.com/go-macaron/gzip"
|
||||
"github.com/ouqiang/gocron/routers/manage"
|
||||
"github.com/ouqiang/gocron/routers/loginlog"
|
||||
"time"
|
||||
"strconv"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-macaron/binding"
|
||||
"github.com/go-macaron/cache"
|
||||
"github.com/go-macaron/captcha"
|
||||
"github.com/go-macaron/gzip"
|
||||
"github.com/go-macaron/session"
|
||||
"github.com/go-macaron/toolbox"
|
||||
"github.com/ouqiang/gocron/modules/app"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"github.com/ouqiang/gocron/routers/host"
|
||||
"github.com/ouqiang/gocron/routers/install"
|
||||
"github.com/ouqiang/gocron/routers/loginlog"
|
||||
"github.com/ouqiang/gocron/routers/manage"
|
||||
"github.com/ouqiang/gocron/routers/task"
|
||||
"github.com/ouqiang/gocron/routers/tasklog"
|
||||
"github.com/ouqiang/gocron/routers/user"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
// 静态文件目录
|
||||
@@ -26,255 +31,295 @@ const StaticDir = "public"
|
||||
|
||||
// 路由注册
|
||||
func Register(m *macaron.Macaron) {
|
||||
// 所有GET方法,自动注册HEAD方法
|
||||
m.SetAutoHead(true)
|
||||
// 首页
|
||||
m.Get("/", Home)
|
||||
// 系统安装
|
||||
m.Group("/install", func() {
|
||||
m.Get("", install.Create)
|
||||
m.Post("/store", binding.Bind(install.InstallForm{}), install.Store)
|
||||
})
|
||||
// 所有GET方法,自动注册HEAD方法
|
||||
m.SetAutoHead(true)
|
||||
// 首页
|
||||
m.Get("/", Home)
|
||||
// 系统安装
|
||||
m.Group("/install", func() {
|
||||
m.Get("", install.Create)
|
||||
m.Post("/store", binding.Bind(install.InstallForm{}), install.Store)
|
||||
})
|
||||
|
||||
// 用户
|
||||
m.Group("/user", func() {
|
||||
m.Get("/login", user.Login)
|
||||
m.Post("/login", user.ValidateLogin)
|
||||
m.Get("/logout", user.Logout)
|
||||
m.Get("/editPassword", user.EditPassword)
|
||||
m.Post("/editPassword", user.UpdatePassword)
|
||||
})
|
||||
// 用户
|
||||
m.Group("/user", func() {
|
||||
m.Get("", user.Index)
|
||||
m.Get("/create", user.Create)
|
||||
m.Get("/edit/:id", user.Edit)
|
||||
m.Post("/store", binding.Bind(user.UserForm{}), user.Store)
|
||||
m.Post("/remove/:id", user.Remove)
|
||||
m.Get("/login", user.Login)
|
||||
m.Post("/login", user.ValidateLogin)
|
||||
m.Get("/logout", user.Logout)
|
||||
m.Post("/enable/:id", user.Enable)
|
||||
m.Post("/disable/:id", user.Disable)
|
||||
m.Get("/editMyPassword", user.EditMyPassword)
|
||||
m.Post("/editMyPassword", user.UpdateMyPassword)
|
||||
m.Get("/editPassword/:id", user.EditPassword)
|
||||
m.Post("/editPassword/:id", user.UpdatePassword)
|
||||
})
|
||||
|
||||
// 定时任务
|
||||
m.Group("/task", func() {
|
||||
m.Get("/create", task.Create)
|
||||
m.Post("/store", binding.Bind(task.TaskForm{}), task.Store)
|
||||
m.Get("/edit/:id", task.Edit)
|
||||
m.Get("", task.Index)
|
||||
m.Get("/log", tasklog.Index)
|
||||
m.Post("/log/clear", tasklog.Clear)
|
||||
m.Post("/remove/:id", task.Remove)
|
||||
m.Post("/enable/:id", task.Enable)
|
||||
m.Post("/disable/:id", task.Disable)
|
||||
m.Get("/run/:id", task.Run)
|
||||
})
|
||||
// 定时任务
|
||||
m.Group("/task", func() {
|
||||
m.Get("/create", task.Create)
|
||||
m.Post("/store", binding.Bind(task.TaskForm{}), task.Store)
|
||||
m.Get("/edit/:id", task.Edit)
|
||||
m.Get("", task.Index)
|
||||
m.Get("/log", tasklog.Index)
|
||||
m.Post("/log/clear", tasklog.Clear)
|
||||
m.Post("/log/stop", tasklog.Stop)
|
||||
m.Post("/remove/:id", task.Remove)
|
||||
m.Post("/enable/:id", task.Enable)
|
||||
m.Post("/disable/:id", task.Disable)
|
||||
m.Get("/run/:id", task.Run)
|
||||
})
|
||||
|
||||
// 主机
|
||||
m.Group("/host", func() {
|
||||
m.Get("/create", host.Create)
|
||||
m.Get("/edit/:id", host.Edit)
|
||||
m.Post("/store", binding.Bind(host.HostForm{}), host.Store)
|
||||
m.Get("", host.Index)
|
||||
m.Get("/ping/:id", host.Ping)
|
||||
m.Post("/remove/:id", host.Remove)
|
||||
})
|
||||
// 主机
|
||||
m.Group("/host", func() {
|
||||
m.Get("/create", host.Create)
|
||||
m.Get("/edit/:id", host.Edit)
|
||||
m.Post("/store", binding.Bind(host.HostForm{}), host.Store)
|
||||
m.Get("", host.Index)
|
||||
m.Get("/ping/:id", host.Ping)
|
||||
m.Post("/remove/:id", host.Remove)
|
||||
})
|
||||
|
||||
// 管理
|
||||
m.Group("/manage", func() {
|
||||
m.Group("/slack", func() {
|
||||
m.Get("/", manage.Slack)
|
||||
m.Get("/edit", manage.EditSlack)
|
||||
m.Post("/url", manage.UpdateSlackUrl)
|
||||
m.Post("/channel", manage.CreateSlackChannel)
|
||||
m.Post("/channel/remove/:id", manage.RemoveSlackChannel)
|
||||
})
|
||||
m.Group("/mail", func() {
|
||||
m.Get("/", manage.Mail)
|
||||
m.Get("/edit", manage.EditMail)
|
||||
m.Post("/server", binding.Bind(manage.MailServerForm{}), manage.UpdateMailServer)
|
||||
m.Post("/server/clear", manage.ClearMailServer)
|
||||
m.Post("/user", manage.CreateMailUser)
|
||||
m.Post("/user/remove/:id", manage.RemoveMailUser)
|
||||
})
|
||||
m.Get("/login-log", loginlog.Index)
|
||||
})
|
||||
// 管理
|
||||
m.Group("/manage", func() {
|
||||
m.Group("/slack", func() {
|
||||
m.Get("/", manage.Slack)
|
||||
m.Get("/edit", manage.EditSlack)
|
||||
m.Post("/url", manage.UpdateSlackUrl)
|
||||
m.Post("/channel", manage.CreateSlackChannel)
|
||||
m.Post("/channel/remove/:id", manage.RemoveSlackChannel)
|
||||
})
|
||||
m.Group("/mail", func() {
|
||||
m.Get("/", manage.Mail)
|
||||
m.Get("/edit", manage.EditMail)
|
||||
m.Post("/server", binding.Bind(manage.MailServerForm{}), manage.UpdateMailServer)
|
||||
m.Post("/server/clear", manage.ClearMailServer)
|
||||
m.Post("/user", manage.CreateMailUser)
|
||||
m.Post("/user/remove/:id", manage.RemoveMailUser)
|
||||
})
|
||||
m.Get("/login-log", loginlog.Index)
|
||||
})
|
||||
|
||||
// API
|
||||
m.Group("/api/v1", func() {
|
||||
m.Post("/tasklog/remove/:id", tasklog.Remove)
|
||||
m.Post("/task/enable/:id", task.Enable)
|
||||
m.Post("/task/disable/:id", task.Disable)
|
||||
}, apiAuth);
|
||||
// API
|
||||
m.Group("/api/v1", func() {
|
||||
m.Post("/tasklog/remove/:id", tasklog.Remove)
|
||||
m.Post("/task/enable/:id", task.Enable)
|
||||
m.Post("/task/disable/:id", task.Disable)
|
||||
}, apiAuth)
|
||||
|
||||
// 404错误
|
||||
m.NotFound(func(ctx *macaron.Context) {
|
||||
if isGetRequest(ctx) && !isAjaxRequest(ctx) {
|
||||
ctx.Data["Title"] = "404 - NOT FOUND"
|
||||
ctx.HTML(404, "error/404")
|
||||
} else {
|
||||
json := utils.JsonResponse{}
|
||||
ctx.Resp.Write([]byte(json.Failure(utils.NotFound, "您访问的地址不存在")))
|
||||
}
|
||||
})
|
||||
// 50x错误
|
||||
m.InternalServerError(func(ctx *macaron.Context) {
|
||||
logger.Debug("500错误")
|
||||
if isGetRequest(ctx) && !isAjaxRequest(ctx) {
|
||||
ctx.Data["Title"] = "500 - INTERNAL SERVER ERROR"
|
||||
ctx.HTML(500, "error/500")
|
||||
} else {
|
||||
json := utils.JsonResponse{}
|
||||
ctx.Resp.Write([]byte(json.Failure(utils.ServerError, "网站暂时无法访问,请稍后再试")))
|
||||
}
|
||||
})
|
||||
// 404错误
|
||||
m.NotFound(func(ctx *macaron.Context) {
|
||||
if isGetRequest(ctx) && !isAjaxRequest(ctx) {
|
||||
ctx.Data["Title"] = "404 - NOT FOUND"
|
||||
ctx.HTML(http.StatusNotFound, "error/404")
|
||||
} else {
|
||||
json := utils.JsonResponse{}
|
||||
ctx.Resp.Write([]byte(json.Failure(utils.NotFound, "您访问的地址不存在")))
|
||||
}
|
||||
})
|
||||
// 50x错误
|
||||
m.InternalServerError(func(ctx *macaron.Context) {
|
||||
logger.Debug("500错误")
|
||||
if isGetRequest(ctx) && !isAjaxRequest(ctx) {
|
||||
ctx.Data["Title"] = "500 - INTERNAL SERVER ERROR"
|
||||
ctx.HTML(http.StatusInternalServerError, "error/500")
|
||||
} else {
|
||||
json := utils.JsonResponse{}
|
||||
ctx.Resp.Write([]byte(json.Failure(utils.ServerError, "网站暂时无法访问,请稍后再试")))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 中间件注册
|
||||
func RegisterMiddleware(m *macaron.Macaron) {
|
||||
m.Use(macaron.Logger())
|
||||
m.Use(macaron.Recovery())
|
||||
if macaron.Env != macaron.DEV {
|
||||
m.Use(gzip.Gziper())
|
||||
}
|
||||
m.Use(macaron.Static(StaticDir))
|
||||
m.Use(macaron.Renderer(macaron.RenderOptions{
|
||||
Directory: "templates",
|
||||
Extensions: []string{".html"},
|
||||
// 模板语法分隔符,默认为 ["{{", "}}"]
|
||||
Delims: macaron.Delims{"{{{", "}}}"},
|
||||
// 追加的 Content-Type 头信息,默认为 "UTF-8"
|
||||
Charset: "UTF-8",
|
||||
// 渲染具有缩进格式的 JSON,默认为不缩进
|
||||
IndentJSON: true,
|
||||
// 渲染具有缩进格式的 XML,默认为不缩进
|
||||
IndentXML: true,
|
||||
}))
|
||||
m.Use(session.Sessioner(session.Options{
|
||||
Provider: "file",
|
||||
ProviderConfig: app.DataDir + "/sessions",
|
||||
}))
|
||||
m.Use(toolbox.Toolboxer(m))
|
||||
checkAppInstall(m)
|
||||
m.Use(func(ctx *macaron.Context, sess session.Store){
|
||||
if app.Installed {
|
||||
ipAuth(ctx)
|
||||
userAuth(ctx, sess)
|
||||
setShareData(ctx, sess)
|
||||
}
|
||||
})
|
||||
m.Use(macaron.Logger())
|
||||
m.Use(macaron.Recovery())
|
||||
if macaron.Env != macaron.DEV {
|
||||
m.Use(gzip.Gziper())
|
||||
}
|
||||
m.Use(macaron.Static(StaticDir))
|
||||
m.Use(macaron.Renderer(macaron.RenderOptions{
|
||||
// 模板语法分隔符,默认为 ["{{", "}}"]
|
||||
Delims: macaron.Delims{"{%", "%}"},
|
||||
Funcs: []template.FuncMap{map[string]interface{}{
|
||||
"HostFormat": func(index int) bool {
|
||||
return (index+1)%3 == 0
|
||||
},
|
||||
"unescape": func(str string) template.HTML {
|
||||
return template.HTML(str)
|
||||
},
|
||||
}},
|
||||
}))
|
||||
m.Use(cache.Cacher())
|
||||
m.Use(captcha.Captchaer())
|
||||
m.Use(session.Sessioner(session.Options{
|
||||
Provider: "file",
|
||||
ProviderConfig: app.DataDir + "/sessions",
|
||||
}))
|
||||
m.Use(toolbox.Toolboxer(m))
|
||||
checkAppInstall(m)
|
||||
m.Use(func(ctx *macaron.Context, sess session.Store) {
|
||||
if app.Installed {
|
||||
ipAuth(ctx)
|
||||
userAuth(ctx, sess)
|
||||
urlAuth(ctx, sess)
|
||||
setShareData(ctx, sess)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// region 自定义中间件
|
||||
|
||||
/** 系统未安装,重定向到安装页面 **/
|
||||
func checkAppInstall(m *macaron.Macaron) {
|
||||
m.Use(func(ctx *macaron.Context) {
|
||||
installUrl := "/install"
|
||||
if strings.HasPrefix(ctx.Req.URL.Path, installUrl) {
|
||||
return
|
||||
}
|
||||
if !app.Installed {
|
||||
ctx.Redirect(installUrl)
|
||||
}
|
||||
})
|
||||
func checkAppInstall(m *macaron.Macaron) {
|
||||
m.Use(func(ctx *macaron.Context) {
|
||||
installUrl := "/install"
|
||||
if strings.HasPrefix(ctx.Req.URL.Path, installUrl) {
|
||||
return
|
||||
}
|
||||
if !app.Installed {
|
||||
ctx.Redirect(installUrl)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// IP验证, 通过反向代理访问gocron,需设置Header X-Real-IP才能获取到客户端真实IP
|
||||
func ipAuth(ctx *macaron.Context) {
|
||||
allowIpsStr := app.Setting.Key("allow_ips").String()
|
||||
if allowIpsStr == "" {
|
||||
return
|
||||
}
|
||||
clientIp := ctx.RemoteAddr()
|
||||
allowIps := strings.Split(allowIpsStr, ",")
|
||||
if !utils.InStringSlice(allowIps, clientIp) {
|
||||
logger.Warnf("非法IP访问-%s", clientIp)
|
||||
ctx.Status(403)
|
||||
}
|
||||
func ipAuth(ctx *macaron.Context) {
|
||||
allowIpsStr := app.Setting.AllowIps
|
||||
if allowIpsStr == "" {
|
||||
return
|
||||
}
|
||||
clientIp := ctx.RemoteAddr()
|
||||
allowIps := strings.Split(allowIpsStr, ",")
|
||||
if !utils.InStringSlice(allowIps, clientIp) {
|
||||
logger.Warnf("非法IP访问-%s", clientIp)
|
||||
ctx.Status(http.StatusForbidden)
|
||||
}
|
||||
}
|
||||
|
||||
// 用户认证
|
||||
func userAuth(ctx *macaron.Context, sess session.Store) {
|
||||
if user.IsLogin(sess) {
|
||||
return
|
||||
}
|
||||
uri := ctx.Req.URL.Path
|
||||
found := false
|
||||
excludePaths := []string{"/install", "/user/login", "/api"}
|
||||
for _, path := range excludePaths {
|
||||
if strings.HasPrefix(uri, path) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
ctx.Redirect("/user/login")
|
||||
}
|
||||
func userAuth(ctx *macaron.Context, sess session.Store) {
|
||||
if user.IsLogin(sess) {
|
||||
return
|
||||
}
|
||||
uri := ctx.Req.URL.Path
|
||||
found := false
|
||||
excludePaths := []string{"/install", "/user/login", "/api"}
|
||||
for _, path := range excludePaths {
|
||||
if strings.HasPrefix(uri, path) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
ctx.Redirect("/user/login")
|
||||
}
|
||||
}
|
||||
|
||||
// URL权限验证
|
||||
func urlAuth(ctx *macaron.Context, sess session.Store) {
|
||||
if user.IsAdmin(sess) {
|
||||
return
|
||||
}
|
||||
uri := strings.TrimSpace(ctx.Req.URL.Path)
|
||||
uri = strings.TrimRight(uri, "/")
|
||||
if strings.HasPrefix(uri, "/api") {
|
||||
return
|
||||
}
|
||||
// 普通用户允许访问的URL地址
|
||||
allowPaths := []string{
|
||||
"",
|
||||
"/task",
|
||||
"/task/log",
|
||||
"/host",
|
||||
"/user/login",
|
||||
"/user/logout",
|
||||
"/user/editMyPassword",
|
||||
}
|
||||
for _, path := range allowPaths {
|
||||
if path == uri {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusUnauthorized)
|
||||
|
||||
}
|
||||
|
||||
/** 设置共享数据 **/
|
||||
func setShareData(ctx *macaron.Context, sess session.Store) {
|
||||
ctx.Data["URI"] = ctx.Req.URL.Path
|
||||
urlPath := strings.TrimPrefix(ctx.Req.URL.Path, "/")
|
||||
paths := strings.Split(urlPath, "/")
|
||||
ctx.Data["Controller"] = ""
|
||||
ctx.Data["Action"] = ""
|
||||
if len(paths) > 0 {
|
||||
ctx.Data["Controller"] = paths[0]
|
||||
}
|
||||
if len(paths) > 1 {
|
||||
ctx.Data["Action"] = paths[1]
|
||||
}
|
||||
ctx.Data["LoginUsername"] = user.Username(sess)
|
||||
ctx.Data["LoginUid"] = user.Uid(sess)
|
||||
ctx.Data["AppName"] = app.Setting.Key("app.name").String()
|
||||
func setShareData(ctx *macaron.Context, sess session.Store) {
|
||||
ctx.Data["URI"] = ctx.Req.URL.Path
|
||||
urlPath := strings.TrimPrefix(ctx.Req.URL.Path, "/")
|
||||
paths := strings.Split(urlPath, "/")
|
||||
ctx.Data["Controller"] = ""
|
||||
ctx.Data["Action"] = ""
|
||||
if len(paths) > 0 {
|
||||
ctx.Data["Controller"] = paths[0]
|
||||
}
|
||||
if len(paths) > 1 {
|
||||
ctx.Data["Action"] = paths[1]
|
||||
}
|
||||
ctx.Data["LoginUsername"] = user.Username(sess)
|
||||
ctx.Data["LoginUid"] = user.Uid(sess)
|
||||
ctx.Data["IsAdmin"] = user.IsAdmin(sess)
|
||||
ctx.Data["AppName"] = app.Setting.AppName
|
||||
}
|
||||
|
||||
/** API接口签名验证 **/
|
||||
func apiAuth(ctx *macaron.Context) {
|
||||
apiSignEnable := app.Setting.Key("api.sign.enable").String()
|
||||
apiSignEnable = strings.TrimSpace(apiSignEnable)
|
||||
if apiSignEnable == "false" {
|
||||
return
|
||||
}
|
||||
apiKey := app.Setting.Key("api.key").String()
|
||||
apiSecret := app.Setting.Key("api.secret").String()
|
||||
apiKey = strings.TrimSpace(apiKey)
|
||||
apiSecret = strings.TrimSpace(apiSecret)
|
||||
json := utils.JsonResponse{}
|
||||
if apiKey == "" || apiSecret == "" {
|
||||
msg := json.CommonFailure("使用API前, 请先配置密钥")
|
||||
ctx.Write([]byte(msg))
|
||||
return
|
||||
}
|
||||
currentTimestamp := time.Now().Unix()
|
||||
time := ctx.QueryInt64("time")
|
||||
if time <= 0 {
|
||||
msg := json.CommonFailure("参数time不能为空")
|
||||
ctx.Write([]byte(msg))
|
||||
return
|
||||
}
|
||||
if time < (currentTimestamp - 1800) {
|
||||
msg := json.CommonFailure("time无效")
|
||||
ctx.Write([]byte(msg))
|
||||
return
|
||||
}
|
||||
sign := ctx.QueryTrim("sign")
|
||||
if sign == "" {
|
||||
msg := json.CommonFailure("参数sign不能为空")
|
||||
ctx.Write([]byte(msg))
|
||||
return
|
||||
}
|
||||
raw := apiKey + strconv.FormatInt(time, 10) + strings.TrimSpace(ctx.Req.URL.Path) + apiSecret
|
||||
realSign := utils.Md5(raw)
|
||||
if sign != realSign {
|
||||
msg := json.CommonFailure("签名验证失败")
|
||||
ctx.Write([]byte(msg))
|
||||
return
|
||||
}
|
||||
func apiAuth(ctx *macaron.Context) {
|
||||
if !app.Setting.ApiSignEnable {
|
||||
return
|
||||
}
|
||||
apiKey := strings.TrimSpace(app.Setting.ApiKey)
|
||||
apiSecret := strings.TrimSpace(app.Setting.ApiSecret)
|
||||
json := utils.JsonResponse{}
|
||||
if apiKey == "" || apiSecret == "" {
|
||||
msg := json.CommonFailure("使用API前, 请先配置密钥")
|
||||
ctx.Write([]byte(msg))
|
||||
return
|
||||
}
|
||||
currentTimestamp := time.Now().Unix()
|
||||
time := ctx.QueryInt64("time")
|
||||
if time <= 0 {
|
||||
msg := json.CommonFailure("参数time不能为空")
|
||||
ctx.Write([]byte(msg))
|
||||
return
|
||||
}
|
||||
if time < (currentTimestamp - 1800) {
|
||||
msg := json.CommonFailure("time无效")
|
||||
ctx.Write([]byte(msg))
|
||||
return
|
||||
}
|
||||
sign := ctx.QueryTrim("sign")
|
||||
if sign == "" {
|
||||
msg := json.CommonFailure("参数sign不能为空")
|
||||
ctx.Write([]byte(msg))
|
||||
return
|
||||
}
|
||||
raw := apiKey + strconv.FormatInt(time, 10) + strings.TrimSpace(ctx.Req.URL.Path) + apiSecret
|
||||
realSign := utils.Md5(raw)
|
||||
if sign != realSign {
|
||||
msg := json.CommonFailure("签名验证失败")
|
||||
ctx.Write([]byte(msg))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
func isAjaxRequest(ctx *macaron.Context) bool {
|
||||
req := ctx.Req.Header.Get("X-Requested-With")
|
||||
if req == "XMLHttpRequest" {
|
||||
return true
|
||||
}
|
||||
req := ctx.Req.Header.Get("X-Requested-With")
|
||||
if req == "XMLHttpRequest" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
return false
|
||||
}
|
||||
|
||||
func isGetRequest(ctx *macaron.Context) bool {
|
||||
return ctx.Req.Method == "GET"
|
||||
}
|
||||
return ctx.Req.Method == "GET"
|
||||
}
|
||||
|
||||
+274
-231
@@ -1,298 +1,341 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"gopkg.in/macaron.v1"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"github.com/ouqiang/gocron/service"
|
||||
"strconv"
|
||||
"github.com/jakecoffman/cron"
|
||||
"github.com/Unknwon/paginater"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"github.com/ouqiang/gocron/routers/base"
|
||||
"github.com/go-macaron/binding"
|
||||
"strings"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Unknwon/paginater"
|
||||
"github.com/go-macaron/binding"
|
||||
"github.com/jakecoffman/cron"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"github.com/ouqiang/gocron/routers/base"
|
||||
"github.com/ouqiang/gocron/service"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
type TaskForm struct {
|
||||
Id int
|
||||
Level models.TaskLevel `binding:"Required;In(1,2)"`
|
||||
DependencyStatus models.TaskDependencyStatus
|
||||
DependencyTaskId string
|
||||
Name string `binding:"Required;MaxSize(32)"`
|
||||
Spec string
|
||||
Protocol models.TaskProtocol `binding:"In(1,2)"`
|
||||
Command string `binding:"Required;MaxSize(256)"`
|
||||
Timeout int `binding:"Range(0,86400)"`
|
||||
Multi int8 `binding:"In(1,2)"`
|
||||
RetryTimes int8
|
||||
HostId int16
|
||||
Remark string
|
||||
NotifyStatus int8 `binding:"In(1,2,3)"`
|
||||
NotifyType int8 `binding:"In(1,2,3)"`
|
||||
NotifyReceiverId string
|
||||
Id int
|
||||
Level models.TaskLevel `binding:"Required;In(1,2)"`
|
||||
DependencyStatus models.TaskDependencyStatus
|
||||
DependencyTaskId string
|
||||
Name string `binding:"Required;MaxSize(32)"`
|
||||
Spec string
|
||||
Protocol models.TaskProtocol `binding:"In(1,2)"`
|
||||
Command string `binding:"Required;MaxSize(256)"`
|
||||
HttpMethod models.TaskHTTPMethod `binding:"In(1,2)"`
|
||||
Timeout int `binding:"Range(0,86400)"`
|
||||
Multi int8 `binding:"In(1,2)"`
|
||||
RetryTimes int8
|
||||
RetryInterval int16
|
||||
HostId string
|
||||
Tag string
|
||||
Remark string
|
||||
NotifyStatus int8 `binding:"In(1,2,3)"`
|
||||
NotifyType int8 `binding:"In(1,2,3)"`
|
||||
NotifyReceiverId string
|
||||
}
|
||||
|
||||
|
||||
func (f TaskForm) Error(ctx *macaron.Context, errs binding.Errors) {
|
||||
if len(errs) == 0 {
|
||||
return
|
||||
}
|
||||
json := utils.JsonResponse{}
|
||||
content := json.CommonFailure("表单验证失败, 请检测输入")
|
||||
if len(errs) == 0 {
|
||||
return
|
||||
}
|
||||
json := utils.JsonResponse{}
|
||||
content := json.CommonFailure("表单验证失败, 请检测输入")
|
||||
|
||||
ctx.Resp.Write([]byte(content))
|
||||
ctx.Resp.Write([]byte(content))
|
||||
}
|
||||
|
||||
// 首页
|
||||
func Index(ctx *macaron.Context) {
|
||||
taskModel := new(models.Task)
|
||||
queryParams := parseQueryParams(ctx)
|
||||
total, err := taskModel.Total(queryParams)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
tasks, err := taskModel.List(queryParams)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
name, ok := queryParams["name"].(string)
|
||||
var safeNameHTML = ""
|
||||
if ok {
|
||||
safeNameHTML = template.HTMLEscapeString(name)
|
||||
}
|
||||
PageParams := fmt.Sprintf("id=%d&host_id=%d&name=%s&protocol=%d&status=%d&page_size=%d",
|
||||
queryParams["Id"], queryParams["HostId"], safeNameHTML, queryParams["Protocol"], queryParams["Status"], queryParams["PageSize"]);
|
||||
queryParams["PageParams"] = template.URL(PageParams)
|
||||
p := paginater.New(int(total), queryParams["PageSize"].(int), queryParams["Page"].(int), 5)
|
||||
ctx.Data["Pagination"] = p
|
||||
setHostsToTemplate(ctx)
|
||||
ctx.Data["Params"] = queryParams
|
||||
ctx.Data["Title"] = "任务列表"
|
||||
ctx.Data["Tasks"] = tasks
|
||||
ctx.HTML(200, "task/index")
|
||||
func Index(ctx *macaron.Context) {
|
||||
taskModel := new(models.Task)
|
||||
queryParams := parseQueryParams(ctx)
|
||||
total, err := taskModel.Total(queryParams)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
tasks, err := taskModel.List(queryParams)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
name, ok := queryParams["name"].(string)
|
||||
var safeNameHTML = ""
|
||||
if ok {
|
||||
safeNameHTML = template.HTMLEscapeString(name)
|
||||
}
|
||||
PageParams := fmt.Sprintf("id=%d&host_id=%d&name=%s&protocol=%d&tag=%s&status=%d&page_size=%d",
|
||||
queryParams["Id"], queryParams["HostId"], safeNameHTML, queryParams["Protocol"], queryParams["Tag"], queryParams["Status"], queryParams["PageSize"])
|
||||
queryParams["PageParams"] = template.URL(PageParams)
|
||||
p := paginater.New(int(total), queryParams["PageSize"].(int), queryParams["Page"].(int), 5)
|
||||
ctx.Data["Pagination"] = p
|
||||
setHostsToTemplate(ctx)
|
||||
ctx.Data["Params"] = queryParams
|
||||
ctx.Data["Title"] = "任务列表"
|
||||
ctx.Data["Tasks"] = tasks
|
||||
ctx.HTML(200, "task/index")
|
||||
}
|
||||
|
||||
// 新增页面
|
||||
func Create(ctx *macaron.Context) {
|
||||
setHostsToTemplate(ctx)
|
||||
ctx.Data["Title"] = "添加任务"
|
||||
ctx.HTML(200, "task/task_form")
|
||||
func Create(ctx *macaron.Context) {
|
||||
setHostsToTemplate(ctx)
|
||||
ctx.Data["Title"] = "添加任务"
|
||||
ctx.HTML(200, "task/task_form")
|
||||
}
|
||||
|
||||
// 编辑页面
|
||||
func Edit(ctx *macaron.Context) {
|
||||
id := ctx.ParamsInt(":id")
|
||||
taskModel := new(models.Task)
|
||||
task, err := taskModel.Detail(id)
|
||||
if err != nil || task.Id != id {
|
||||
logger.Errorf("编辑任务#获取任务详情失败#任务ID-%d#%s", id, err.Error())
|
||||
ctx.Redirect("/task")
|
||||
}
|
||||
ctx.Data["Task"] = task
|
||||
ctx.Data["Title"] = "编辑"
|
||||
setHostsToTemplate(ctx)
|
||||
ctx.HTML(200, "task/task_form")
|
||||
func Edit(ctx *macaron.Context) {
|
||||
id := ctx.ParamsInt(":id")
|
||||
taskModel := new(models.Task)
|
||||
task, err := taskModel.Detail(id)
|
||||
if err != nil || task.Id != id {
|
||||
logger.Errorf("编辑任务#获取任务详情失败#任务ID-%d#%s", id, err.Error())
|
||||
ctx.Redirect("/task")
|
||||
}
|
||||
hostModel := new(models.Host)
|
||||
hostModel.PageSize = -1
|
||||
hosts, err := hostModel.List(models.CommonMap{})
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
} else {
|
||||
for i, host := range hosts {
|
||||
if inHosts(task.Hosts, host.Id) {
|
||||
hosts[i].Selected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["Task"] = task
|
||||
ctx.Data["Hosts"] = hosts
|
||||
ctx.Data["Title"] = "编辑"
|
||||
ctx.HTML(200, "task/task_form")
|
||||
}
|
||||
|
||||
// 保存任务
|
||||
func Store(ctx *macaron.Context, form TaskForm) string {
|
||||
json := utils.JsonResponse{}
|
||||
taskModel := models.Task{}
|
||||
var id int = form.Id
|
||||
nameExists, err := taskModel.NameExist(form.Name, form.Id)
|
||||
if err != nil {
|
||||
return json.CommonFailure(utils.FailureContent, err)
|
||||
}
|
||||
if nameExists {
|
||||
return json.CommonFailure("任务名称已存在")
|
||||
}
|
||||
// 保存任务 todo 拆分为多个方法 快变成意大利面条式代码了
|
||||
func Store(ctx *macaron.Context, form TaskForm) string {
|
||||
json := utils.JsonResponse{}
|
||||
taskModel := models.Task{}
|
||||
var id = form.Id
|
||||
nameExists, err := taskModel.NameExist(form.Name, form.Id)
|
||||
if err != nil {
|
||||
return json.CommonFailure(utils.FailureContent, err)
|
||||
}
|
||||
if nameExists {
|
||||
return json.CommonFailure("任务名称已存在")
|
||||
}
|
||||
|
||||
if form.Protocol == models.TaskRPC && form.HostId <= 0 {
|
||||
return json.CommonFailure("请选择主机名")
|
||||
}
|
||||
if form.Protocol == models.TaskRPC && form.HostId == "" {
|
||||
return json.CommonFailure("请选择主机名")
|
||||
}
|
||||
|
||||
if form.Protocol == models.TaskRPC {
|
||||
taskModel.HostId = form.HostId
|
||||
} else {
|
||||
taskModel.HostId = 0
|
||||
}
|
||||
taskModel.Name = form.Name
|
||||
taskModel.Protocol = form.Protocol
|
||||
taskModel.Command = form.Command
|
||||
taskModel.Timeout = form.Timeout
|
||||
taskModel.Remark = form.Remark
|
||||
taskModel.Multi = form.Multi
|
||||
taskModel.RetryTimes = form.RetryTimes
|
||||
if taskModel.Multi != 1 {
|
||||
taskModel.Multi = 0
|
||||
}
|
||||
taskModel.NotifyStatus = form.NotifyStatus - 1
|
||||
taskModel.NotifyType = form.NotifyType - 1
|
||||
taskModel.NotifyReceiverId = form.NotifyReceiverId
|
||||
taskModel.Spec = form.Spec
|
||||
taskModel.Level = form.Level
|
||||
taskModel.DependencyStatus = form.DependencyStatus
|
||||
taskModel.DependencyTaskId = strings.TrimSpace(form.DependencyTaskId)
|
||||
if taskModel.NotifyStatus > 0 && taskModel.NotifyReceiverId == "" {
|
||||
return json.CommonFailure("至少选择一个通知接收者")
|
||||
}
|
||||
if taskModel.Protocol == models.TaskHTTP {
|
||||
command := strings.ToLower(taskModel.Command)
|
||||
if !strings.HasPrefix(command, "http://") && !strings.HasPrefix(command, "https://") {
|
||||
return json.CommonFailure("请输入正确的URL地址")
|
||||
}
|
||||
if taskModel.Timeout > 300 {
|
||||
return json.CommonFailure("HTTP任务超时时间不能超过300秒")
|
||||
}
|
||||
}
|
||||
taskModel.Name = form.Name
|
||||
taskModel.Protocol = form.Protocol
|
||||
taskModel.Command = strings.TrimSpace(form.Command)
|
||||
taskModel.Timeout = form.Timeout
|
||||
taskModel.Tag = form.Tag
|
||||
taskModel.Remark = form.Remark
|
||||
taskModel.Multi = form.Multi
|
||||
taskModel.RetryTimes = form.RetryTimes
|
||||
taskModel.RetryInterval = form.RetryInterval
|
||||
if taskModel.Multi != 1 {
|
||||
taskModel.Multi = 0
|
||||
}
|
||||
taskModel.NotifyStatus = form.NotifyStatus - 1
|
||||
taskModel.NotifyType = form.NotifyType - 1
|
||||
taskModel.NotifyReceiverId = form.NotifyReceiverId
|
||||
taskModel.Spec = form.Spec
|
||||
taskModel.Level = form.Level
|
||||
taskModel.DependencyStatus = form.DependencyStatus
|
||||
taskModel.DependencyTaskId = strings.TrimSpace(form.DependencyTaskId)
|
||||
if taskModel.NotifyStatus > 0 && taskModel.NotifyReceiverId == "" {
|
||||
return json.CommonFailure("至少选择一个通知接收者")
|
||||
}
|
||||
taskModel.HttpMethod = form.HttpMethod
|
||||
if taskModel.Protocol == models.TaskHTTP {
|
||||
command := strings.ToLower(taskModel.Command)
|
||||
if !strings.HasPrefix(command, "http://") && !strings.HasPrefix(command, "https://") {
|
||||
return json.CommonFailure("请输入正确的URL地址")
|
||||
}
|
||||
if taskModel.Timeout > 300 {
|
||||
return json.CommonFailure("HTTP任务超时时间不能超过300秒")
|
||||
}
|
||||
}
|
||||
|
||||
if taskModel.RetryTimes > 10 || taskModel.RetryTimes < 0 {
|
||||
return json.CommonFailure("任务重试次数取值0-10")
|
||||
}
|
||||
if taskModel.RetryTimes > 10 || taskModel.RetryTimes < 0 {
|
||||
return json.CommonFailure("任务重试次数取值0-10")
|
||||
}
|
||||
|
||||
if (taskModel.DependencyStatus != models.TaskDependencyStatusStrong &&
|
||||
taskModel.DependencyStatus != models.TaskDependencyStatusWeak) {
|
||||
return json.CommonFailure("请选择依赖关系")
|
||||
}
|
||||
if taskModel.RetryInterval > 3600 || taskModel.RetryInterval < 0 {
|
||||
return json.CommonFailure("任务重试间隔时间取值0-3600")
|
||||
}
|
||||
|
||||
if taskModel.Level == models.TaskLevelParent {
|
||||
_, err = cron.Parse(form.Spec)
|
||||
if err != nil {
|
||||
return json.CommonFailure("crontab表达式解析失败", err)
|
||||
}
|
||||
} else {
|
||||
taskModel.DependencyTaskId = ""
|
||||
taskModel.Spec = ""
|
||||
}
|
||||
if taskModel.DependencyStatus != models.TaskDependencyStatusStrong &&
|
||||
taskModel.DependencyStatus != models.TaskDependencyStatusWeak {
|
||||
return json.CommonFailure("请选择依赖关系")
|
||||
}
|
||||
|
||||
if id > 0 && taskModel.DependencyTaskId != "" {
|
||||
dependencyTaskIds := strings.Split(taskModel.DependencyTaskId, ",")
|
||||
if utils.InStringSlice(dependencyTaskIds, strconv.Itoa(id)) {
|
||||
return json.CommonFailure("不允许设置当前任务为子任务")
|
||||
}
|
||||
}
|
||||
if taskModel.Level == models.TaskLevelParent {
|
||||
_, err = cron.Parse(form.Spec)
|
||||
if err != nil {
|
||||
return json.CommonFailure("crontab表达式解析失败", err)
|
||||
}
|
||||
} else {
|
||||
taskModel.DependencyTaskId = ""
|
||||
taskModel.Spec = ""
|
||||
}
|
||||
|
||||
if id == 0 {
|
||||
// 任务添加后开始调度执行
|
||||
taskModel.Status = models.Running
|
||||
id, err = taskModel.Create()
|
||||
} else {
|
||||
_, err = taskModel.UpdateBean(id)
|
||||
}
|
||||
if id > 0 && taskModel.DependencyTaskId != "" {
|
||||
dependencyTaskIds := strings.Split(taskModel.DependencyTaskId, ",")
|
||||
if utils.InStringSlice(dependencyTaskIds, strconv.Itoa(id)) {
|
||||
return json.CommonFailure("不允许设置当前任务为子任务")
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return json.CommonFailure("保存失败", err)
|
||||
}
|
||||
if id == 0 {
|
||||
// 任务添加后开始调度执行
|
||||
taskModel.Status = models.Running
|
||||
id, err = taskModel.Create()
|
||||
} else {
|
||||
_, err = taskModel.UpdateBean(id)
|
||||
}
|
||||
|
||||
status, err := taskModel.GetStatus(id)
|
||||
if status == models.Enabled && taskModel.Level == models.TaskLevelParent {
|
||||
addTaskToTimer(id)
|
||||
}
|
||||
if err != nil {
|
||||
return json.CommonFailure("保存失败", err)
|
||||
}
|
||||
|
||||
return json.Success("保存成功", nil)
|
||||
taskHostModel := new(models.TaskHost)
|
||||
if form.Protocol == models.TaskRPC {
|
||||
hostIdStrList := strings.Split(form.HostId, ",")
|
||||
hostIds := make([]int, len(hostIdStrList))
|
||||
for i, hostIdStr := range hostIdStrList {
|
||||
hostIds[i], _ = strconv.Atoi(hostIdStr)
|
||||
}
|
||||
taskHostModel.Add(id, hostIds)
|
||||
} else {
|
||||
taskHostModel.Remove(id)
|
||||
}
|
||||
|
||||
status, _ := taskModel.GetStatus(id)
|
||||
if status == models.Enabled && taskModel.Level == models.TaskLevelParent {
|
||||
addTaskToTimer(id)
|
||||
}
|
||||
|
||||
return json.Success("保存成功", nil)
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
func Remove(ctx *macaron.Context) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
json := utils.JsonResponse{}
|
||||
taskModel := new(models.Task)
|
||||
_, err := taskModel.Delete(id)
|
||||
if err != nil {
|
||||
return json.CommonFailure(utils.FailureContent, err)
|
||||
}
|
||||
id := ctx.ParamsInt(":id")
|
||||
json := utils.JsonResponse{}
|
||||
taskModel := new(models.Task)
|
||||
_, err := taskModel.Delete(id)
|
||||
if err != nil {
|
||||
return json.CommonFailure(utils.FailureContent, err)
|
||||
}
|
||||
|
||||
service.Cron.RemoveJob(strconv.Itoa(id))
|
||||
taskHostModel := new(models.TaskHost)
|
||||
taskHostModel.Remove(id)
|
||||
|
||||
return json.Success(utils.SuccessContent, nil)
|
||||
service.ServiceTask.Remove(id)
|
||||
|
||||
return json.Success(utils.SuccessContent, nil)
|
||||
}
|
||||
|
||||
// 激活任务
|
||||
func Enable(ctx *macaron.Context) string {
|
||||
return changeStatus(ctx, models.Enabled)
|
||||
return changeStatus(ctx, models.Enabled)
|
||||
}
|
||||
|
||||
// 暂停任务
|
||||
func Disable(ctx *macaron.Context) string {
|
||||
return changeStatus(ctx, models.Disabled)
|
||||
return changeStatus(ctx, models.Disabled)
|
||||
}
|
||||
|
||||
// 手动运行任务
|
||||
func Run(ctx *macaron.Context) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
json := utils.JsonResponse{}
|
||||
taskModel := new(models.Task)
|
||||
task , err := taskModel.Detail(id)
|
||||
if err != nil || task.Id <= 0 {
|
||||
return json.CommonFailure("获取任务详情失败", err)
|
||||
}
|
||||
id := ctx.ParamsInt(":id")
|
||||
json := utils.JsonResponse{}
|
||||
taskModel := new(models.Task)
|
||||
task, err := taskModel.Detail(id)
|
||||
if err != nil || task.Id <= 0 {
|
||||
return json.CommonFailure("获取任务详情失败", err)
|
||||
}
|
||||
|
||||
task.Spec = "手动运行"
|
||||
serviceTask := new(service.Task)
|
||||
serviceTask.Run(task)
|
||||
task.Spec = "手动运行"
|
||||
service.ServiceTask.Run(task)
|
||||
|
||||
return json.Success("任务已开始运行, 请到任务日志中查看结果", nil);
|
||||
return json.Success("任务已开始运行, 请到任务日志中查看结果", nil)
|
||||
}
|
||||
|
||||
// 改变任务状态
|
||||
func changeStatus(ctx *macaron.Context, status models.Status) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
json := utils.JsonResponse{}
|
||||
taskModel := new(models.Task)
|
||||
_, err := taskModel.Update(id, models.CommonMap{
|
||||
"Status": status,
|
||||
})
|
||||
if err != nil {
|
||||
return json.CommonFailure(utils.FailureContent, err)
|
||||
}
|
||||
id := ctx.ParamsInt(":id")
|
||||
json := utils.JsonResponse{}
|
||||
taskModel := new(models.Task)
|
||||
_, err := taskModel.Update(id, models.CommonMap{
|
||||
"Status": status,
|
||||
})
|
||||
if err != nil {
|
||||
return json.CommonFailure(utils.FailureContent, err)
|
||||
}
|
||||
|
||||
if status == models.Enabled {
|
||||
addTaskToTimer(id)
|
||||
} else {
|
||||
service.Cron.RemoveJob(strconv.Itoa(id))
|
||||
}
|
||||
if status == models.Enabled {
|
||||
addTaskToTimer(id)
|
||||
} else {
|
||||
service.ServiceTask.Remove(id)
|
||||
}
|
||||
|
||||
return json.Success(utils.SuccessContent, nil)
|
||||
return json.Success(utils.SuccessContent, nil)
|
||||
}
|
||||
|
||||
// 添加任务到定时器
|
||||
func addTaskToTimer(id int) {
|
||||
taskModel := new(models.Task)
|
||||
task, err := taskModel.Detail(id)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
func addTaskToTimer(id int) {
|
||||
taskModel := new(models.Task)
|
||||
task, err := taskModel.Detail(id)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
taskService := service.Task{}
|
||||
taskService.Add(task)
|
||||
service.ServiceTask.RemoveAndAdd(task)
|
||||
}
|
||||
|
||||
// 解析查询参数
|
||||
func parseQueryParams(ctx *macaron.Context) (models.CommonMap) {
|
||||
var params models.CommonMap = models.CommonMap{}
|
||||
params["Id"] = ctx.QueryInt("id")
|
||||
params["HostId"] = ctx.QueryInt("host_id")
|
||||
params["Name"] = ctx.QueryTrim("name")
|
||||
params["Protocol"] = ctx.QueryInt("protocol")
|
||||
status := ctx.QueryInt("status")
|
||||
if status >=0 {
|
||||
status -= 1
|
||||
}
|
||||
params["Status"] = status
|
||||
base.ParsePageAndPageSize(ctx, params)
|
||||
func parseQueryParams(ctx *macaron.Context) models.CommonMap {
|
||||
var params models.CommonMap = models.CommonMap{}
|
||||
params["Id"] = ctx.QueryInt("id")
|
||||
params["HostId"] = ctx.QueryInt("host_id")
|
||||
params["Name"] = ctx.QueryTrim("name")
|
||||
params["Protocol"] = ctx.QueryInt("protocol")
|
||||
params["Tag"] = ctx.QueryTrim("tag")
|
||||
status := ctx.QueryInt("status")
|
||||
if status >= 0 {
|
||||
status -= 1
|
||||
}
|
||||
params["Status"] = status
|
||||
base.ParsePageAndPageSize(ctx, params)
|
||||
|
||||
return params
|
||||
return params
|
||||
}
|
||||
|
||||
func setHostsToTemplate(ctx *macaron.Context) {
|
||||
hostModel := new(models.Host)
|
||||
hosts, err := hostModel.List(models.CommonMap{})
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
ctx.Data["Hosts"] = hosts
|
||||
}
|
||||
func setHostsToTemplate(ctx *macaron.Context) {
|
||||
hostModel := new(models.Host)
|
||||
hostModel.PageSize = -1
|
||||
hosts, err := hostModel.List(models.CommonMap{})
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
ctx.Data["Hosts"] = hosts
|
||||
}
|
||||
|
||||
func inHosts(slice []models.TaskHostDetail, element int16) bool {
|
||||
for _, v := range slice {
|
||||
if v.HostId == element {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
+86
-60
@@ -3,78 +3,104 @@ package tasklog
|
||||
// 任务日志
|
||||
|
||||
import (
|
||||
"gopkg.in/macaron.v1"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"github.com/Unknwon/paginater"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"github.com/ouqiang/gocron/routers/base"
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
||||
"github.com/Unknwon/paginater"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"github.com/ouqiang/gocron/routers/base"
|
||||
"github.com/ouqiang/gocron/service"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
func Index(ctx *macaron.Context) {
|
||||
logModel := new(models.TaskLog)
|
||||
queryParams := parseQueryParams(ctx)
|
||||
total, err := logModel.Total(queryParams)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
logs, err := logModel.List(queryParams)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
PageParams := fmt.Sprintf("task_id=%d&protocol=%d&status=%d&page_size=%d",
|
||||
queryParams["TaskId"], queryParams["Protocol"], queryParams["Status"],
|
||||
queryParams["PageSize"]);
|
||||
queryParams["PageParams"] = template.URL(PageParams)
|
||||
p := paginater.New(int(total), queryParams["PageSize"].(int), queryParams["Page"].(int), 5)
|
||||
ctx.Data["Pagination"] = p
|
||||
ctx.Data["Title"] = "任务日志"
|
||||
ctx.Data["Logs"] = logs
|
||||
ctx.Data["Params"] = queryParams
|
||||
ctx.HTML(200, "task/log")
|
||||
func Index(ctx *macaron.Context) {
|
||||
logModel := new(models.TaskLog)
|
||||
queryParams := parseQueryParams(ctx)
|
||||
total, err := logModel.Total(queryParams)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
logs, err := logModel.List(queryParams)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
PageParams := fmt.Sprintf("task_id=%d&protocol=%d&status=%d&page_size=%d",
|
||||
queryParams["TaskId"], queryParams["Protocol"], queryParams["Status"],
|
||||
queryParams["PageSize"])
|
||||
queryParams["PageParams"] = template.URL(PageParams)
|
||||
p := paginater.New(int(total), queryParams["PageSize"].(int), queryParams["Page"].(int), 5)
|
||||
ctx.Data["Pagination"] = p
|
||||
ctx.Data["Title"] = "任务日志"
|
||||
ctx.Data["Logs"] = logs
|
||||
ctx.Data["Params"] = queryParams
|
||||
ctx.HTML(200, "task/log")
|
||||
}
|
||||
|
||||
// 清空日志
|
||||
func Clear(ctx *macaron.Context) string {
|
||||
taskLogModel := new(models.TaskLog)
|
||||
_, err := taskLogModel.Clear()
|
||||
json := utils.JsonResponse{}
|
||||
if err != nil {
|
||||
return json.CommonFailure(utils.FailureContent)
|
||||
}
|
||||
func Clear(ctx *macaron.Context) string {
|
||||
taskLogModel := new(models.TaskLog)
|
||||
_, err := taskLogModel.Clear()
|
||||
json := utils.JsonResponse{}
|
||||
if err != nil {
|
||||
return json.CommonFailure(utils.FailureContent)
|
||||
}
|
||||
|
||||
return json.Success(utils.SuccessContent, nil)
|
||||
return json.Success(utils.SuccessContent, nil)
|
||||
}
|
||||
|
||||
// 停止运行中的任务
|
||||
func Stop(ctx *macaron.Context) string {
|
||||
id := ctx.QueryInt64("id")
|
||||
taskId := ctx.QueryInt("task_id")
|
||||
taskModel := new(models.Task)
|
||||
task, err := taskModel.Detail(taskId)
|
||||
json := utils.JsonResponse{}
|
||||
if err != nil {
|
||||
return json.CommonFailure("获取任务信息失败#"+err.Error(), err)
|
||||
}
|
||||
if task.Protocol != models.TaskRPC {
|
||||
return json.CommonFailure("仅支持SHELL任务手动停止")
|
||||
}
|
||||
if len(task.Hosts) == 0 {
|
||||
return json.CommonFailure("任务节点列表为空")
|
||||
}
|
||||
for _, host := range task.Hosts {
|
||||
service.ServiceTask.Stop(host.Name, host.Port, id)
|
||||
|
||||
}
|
||||
|
||||
return json.Success("已执行停止操作, 请等待任务退出", nil)
|
||||
}
|
||||
|
||||
// 删除N个月前的日志
|
||||
func Remove(ctx *macaron.Context) string {
|
||||
month := ctx.ParamsInt(":id")
|
||||
json := utils.JsonResponse{}
|
||||
if month < 1 || month > 12 {
|
||||
return json.CommonFailure("参数取值范围1-12")
|
||||
}
|
||||
taskLogModel := new(models.TaskLog)
|
||||
_, err := taskLogModel.Remove(month)
|
||||
if err != nil {
|
||||
return json.CommonFailure("删除失败", err)
|
||||
}
|
||||
month := ctx.ParamsInt(":id")
|
||||
json := utils.JsonResponse{}
|
||||
if month < 1 || month > 12 {
|
||||
return json.CommonFailure("参数取值范围1-12")
|
||||
}
|
||||
taskLogModel := new(models.TaskLog)
|
||||
_, err := taskLogModel.Remove(month)
|
||||
if err != nil {
|
||||
return json.CommonFailure("删除失败", err)
|
||||
}
|
||||
|
||||
return json.Success("删除成功", nil)
|
||||
return json.Success("删除成功", nil)
|
||||
}
|
||||
|
||||
// 解析查询参数
|
||||
func parseQueryParams(ctx *macaron.Context) (models.CommonMap) {
|
||||
var params models.CommonMap = models.CommonMap{}
|
||||
params["TaskId"] = ctx.QueryInt("task_id")
|
||||
params["Protocol"] = ctx.QueryInt("protocol")
|
||||
status := ctx.QueryInt("status")
|
||||
if status >=0 {
|
||||
status -= 1
|
||||
}
|
||||
params["Status"] = status
|
||||
base.ParsePageAndPageSize(ctx, params)
|
||||
func parseQueryParams(ctx *macaron.Context) models.CommonMap {
|
||||
var params models.CommonMap = models.CommonMap{}
|
||||
params["TaskId"] = ctx.QueryInt("task_id")
|
||||
params["Protocol"] = ctx.QueryInt("protocol")
|
||||
status := ctx.QueryInt("status")
|
||||
if status >= 0 {
|
||||
status -= 1
|
||||
}
|
||||
params["Status"] = status
|
||||
base.ParsePageAndPageSize(ctx, params)
|
||||
|
||||
return params
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
+300
-92
@@ -1,123 +1,331 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"gopkg.in/macaron.v1"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/go-macaron/session"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
"github.com/Unknwon/paginater"
|
||||
"github.com/go-macaron/captcha"
|
||||
"github.com/go-macaron/session"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"github.com/ouqiang/gocron/routers/base"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
// @author qiang.ou<qingqianludao@gmail.com>
|
||||
// @date 2017/4/23-14:16
|
||||
|
||||
func Login(ctx *macaron.Context) {
|
||||
ctx.Data["Title"] = "用户登录"
|
||||
ctx.HTML(200, "user/login")
|
||||
// UserForm 用户表单
|
||||
type UserForm struct {
|
||||
Id int
|
||||
Name string `binding:"Required;MaxSize(32)"` // 用户名
|
||||
Password string // 密码
|
||||
ConfirmPassword string // 确认密码
|
||||
Email string `binding:"Required;MaxSize(50)"` // 邮箱
|
||||
IsAdmin int8 // 是否是管理员 1:管理员 0:普通用户
|
||||
Status models.Status
|
||||
}
|
||||
|
||||
func EditPassword(ctx *macaron.Context) {
|
||||
ctx.Data["Title"] = "修改密码"
|
||||
ctx.HTML(200, "user/editPassword")
|
||||
// Index 用户列表页
|
||||
func Index(ctx *macaron.Context) {
|
||||
queryParams := parseQueryParams(ctx)
|
||||
userModel := new(models.User)
|
||||
users, err := userModel.List(queryParams)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
total, err := userModel.Total()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
PageParams := fmt.Sprintf("page_size=%d", queryParams["PageSize"])
|
||||
queryParams["PageParams"] = template.URL(PageParams)
|
||||
p := paginater.New(int(total), queryParams["PageSize"].(int), queryParams["Page"].(int), 5)
|
||||
ctx.Data["Pagination"] = p
|
||||
ctx.Data["Params"] = queryParams
|
||||
ctx.Data["Title"] = "用户列表"
|
||||
ctx.Data["Users"] = users
|
||||
ctx.HTML(200, "user/index")
|
||||
}
|
||||
|
||||
func UpdatePassword(ctx *macaron.Context, sess session.Store) string {
|
||||
oldPassword := ctx.QueryTrim("old_password")
|
||||
newPassword := ctx.QueryTrim("new_password")
|
||||
confirmNewPassword := ctx.QueryTrim("confirm_new_password")
|
||||
json := utils.JsonResponse{}
|
||||
if oldPassword == "" || newPassword == "" || confirmNewPassword == "" {
|
||||
return json.CommonFailure("原密码和新密码均不能为空")
|
||||
}
|
||||
if newPassword != confirmNewPassword {
|
||||
return json.CommonFailure("两次输入密码不一致")
|
||||
}
|
||||
if oldPassword == newPassword {
|
||||
return json.CommonFailure("原密码与新密码不能相同")
|
||||
}
|
||||
userModel := new(models.User)
|
||||
if !userModel.Match(Username(sess), oldPassword) {
|
||||
return json.CommonFailure("原密码输入错误")
|
||||
}
|
||||
_, err := userModel.UpdatePassword(Uid(sess), newPassword)
|
||||
if err != nil {
|
||||
return json.CommonFailure("修改失败")
|
||||
}
|
||||
// 解析查询参数
|
||||
func parseQueryParams(ctx *macaron.Context) models.CommonMap {
|
||||
var params models.CommonMap = models.CommonMap{}
|
||||
base.ParsePageAndPageSize(ctx, params)
|
||||
|
||||
return json.Success("修改成功", nil)
|
||||
return params
|
||||
}
|
||||
|
||||
func ValidateLogin(ctx *macaron.Context, sess session.Store) string {
|
||||
username := ctx.QueryTrim("username")
|
||||
password := ctx.QueryTrim("password")
|
||||
json := utils.JsonResponse{}
|
||||
if username == "" || password == "" {
|
||||
return json.CommonFailure("用户名、密码不能为空")
|
||||
}
|
||||
userModel := new (models.User)
|
||||
if !userModel.Match(username, password) {
|
||||
return json.CommonFailure("用户名或密码错误")
|
||||
}
|
||||
|
||||
loginLogModel := new(models.LoginLog)
|
||||
loginLogModel.Username = userModel.Name
|
||||
loginLogModel.Ip = ctx.RemoteAddr()
|
||||
_, err := loginLogModel.Create()
|
||||
if err != nil {
|
||||
logger.Error("记录用户登录日志失败", err)
|
||||
}
|
||||
|
||||
|
||||
sess.Set("username", userModel.Name)
|
||||
sess.Set("uid", userModel.Id)
|
||||
sess.Set("isAdmin", userModel.IsAdmin)
|
||||
|
||||
return json.Success("登录成功", nil)
|
||||
// Create 新增用户页
|
||||
func Create(ctx *macaron.Context) {
|
||||
userModel := new(models.User)
|
||||
userModel.Status = models.Enabled
|
||||
userModel.IsAdmin = 0
|
||||
ctx.Data["User"] = userModel
|
||||
ctx.Data["Title"] = "添加用户"
|
||||
ctx.HTML(200, "user/user_form")
|
||||
}
|
||||
|
||||
// 编辑页面
|
||||
func Edit(ctx *macaron.Context) {
|
||||
ctx.Data["Title"] = "编辑用户"
|
||||
userModel := new(models.User)
|
||||
id := ctx.ParamsInt(":id")
|
||||
err := userModel.Find(id)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
ctx.Data["User"] = userModel
|
||||
ctx.HTML(200, "user/user_form")
|
||||
}
|
||||
|
||||
// 保存任务
|
||||
func Store(ctx *macaron.Context, form UserForm) string {
|
||||
form.Name = strings.TrimSpace(form.Name)
|
||||
form.Email = strings.TrimSpace(form.Email)
|
||||
form.Password = strings.TrimSpace(form.Password)
|
||||
form.ConfirmPassword = strings.TrimSpace(form.ConfirmPassword)
|
||||
json := utils.JsonResponse{}
|
||||
userModel := models.User{}
|
||||
nameExists, err := userModel.UsernameExists(form.Name, form.Id)
|
||||
if err != nil {
|
||||
return json.CommonFailure(utils.FailureContent, err)
|
||||
}
|
||||
if nameExists > 0 {
|
||||
return json.CommonFailure("用户名已存在")
|
||||
}
|
||||
|
||||
emailExists, err := userModel.EmailExists(form.Email, form.Id)
|
||||
if err != nil {
|
||||
return json.CommonFailure(utils.FailureContent, err)
|
||||
}
|
||||
if emailExists > 0 {
|
||||
return json.CommonFailure("邮箱已存在")
|
||||
}
|
||||
|
||||
if form.Id == 0 {
|
||||
if form.Password == "" {
|
||||
return json.CommonFailure("请输入密码")
|
||||
}
|
||||
if form.ConfirmPassword == "" {
|
||||
return json.CommonFailure("请再次输入密码")
|
||||
}
|
||||
if form.Password != form.ConfirmPassword {
|
||||
return json.CommonFailure("两次密码输入不一致")
|
||||
}
|
||||
}
|
||||
userModel.Name = form.Name
|
||||
userModel.Email = form.Email
|
||||
userModel.Password = form.Password
|
||||
userModel.IsAdmin = form.IsAdmin
|
||||
userModel.Status = form.Status
|
||||
|
||||
if form.Id == 0 {
|
||||
_, err = userModel.Create()
|
||||
if err != nil {
|
||||
return json.CommonFailure("添加失败", err)
|
||||
}
|
||||
} else {
|
||||
_, err = userModel.Update(form.Id, models.CommonMap{
|
||||
"name": form.Name,
|
||||
"email": form.Email,
|
||||
"status": form.Status,
|
||||
"is_admin": form.IsAdmin,
|
||||
})
|
||||
if err != nil {
|
||||
return json.CommonFailure("修改失败", err)
|
||||
}
|
||||
}
|
||||
|
||||
return json.Success("保存成功", nil)
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
func Remove(ctx *macaron.Context) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
json := utils.JsonResponse{}
|
||||
|
||||
userModel := new(models.User)
|
||||
_, err := userModel.Delete(id)
|
||||
if err != nil {
|
||||
return json.CommonFailure(utils.FailureContent, err)
|
||||
}
|
||||
|
||||
return json.Success(utils.SuccessContent, nil)
|
||||
}
|
||||
|
||||
// 激活用户
|
||||
func Enable(ctx *macaron.Context) string {
|
||||
return changeStatus(ctx, models.Enabled)
|
||||
}
|
||||
|
||||
// 禁用用户
|
||||
func Disable(ctx *macaron.Context) string {
|
||||
return changeStatus(ctx, models.Disabled)
|
||||
}
|
||||
|
||||
// 改变任务状态
|
||||
func changeStatus(ctx *macaron.Context, status models.Status) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
json := utils.JsonResponse{}
|
||||
userModel := new(models.User)
|
||||
_, err := userModel.Update(id, models.CommonMap{
|
||||
"Status": status,
|
||||
})
|
||||
if err != nil {
|
||||
return json.CommonFailure(utils.FailureContent, err)
|
||||
}
|
||||
|
||||
return json.Success(utils.SuccessContent, nil)
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func Login(ctx *macaron.Context) {
|
||||
ctx.Data["Title"] = "用户登录"
|
||||
ctx.HTML(200, "user/login")
|
||||
}
|
||||
|
||||
// EditPassword 修改密码页面
|
||||
func EditPassword(ctx *macaron.Context) {
|
||||
id := ctx.ParamsInt(":id")
|
||||
ctx.Data["Title"] = "修改密码"
|
||||
ctx.Data["Id"] = id
|
||||
ctx.HTML(200, "user/editPassword")
|
||||
}
|
||||
|
||||
// UpdatePassword 更新我的密码
|
||||
func UpdatePassword(ctx *macaron.Context) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
newPassword := ctx.QueryTrim("new_password")
|
||||
confirmNewPassword := ctx.QueryTrim("confirm_new_password")
|
||||
json := utils.JsonResponse{}
|
||||
if newPassword == "" || confirmNewPassword == "" {
|
||||
return json.CommonFailure("请输入密码")
|
||||
}
|
||||
userModel := new(models.User)
|
||||
_, err := userModel.UpdatePassword(id, newPassword)
|
||||
if err != nil {
|
||||
return json.CommonFailure("修改失败")
|
||||
}
|
||||
|
||||
return json.Success("修改成功", nil)
|
||||
}
|
||||
|
||||
// EditMyPassword 修改我的密码页面
|
||||
func EditMyPassword(ctx *macaron.Context) {
|
||||
ctx.Data["Title"] = "修改密码"
|
||||
ctx.HTML(200, "user/editMyPassword")
|
||||
}
|
||||
|
||||
// UpdateMyPassword 更新我的密码
|
||||
func UpdateMyPassword(ctx *macaron.Context, sess session.Store) string {
|
||||
oldPassword := ctx.QueryTrim("old_password")
|
||||
newPassword := ctx.QueryTrim("new_password")
|
||||
confirmNewPassword := ctx.QueryTrim("confirm_new_password")
|
||||
json := utils.JsonResponse{}
|
||||
if oldPassword == "" || newPassword == "" || confirmNewPassword == "" {
|
||||
return json.CommonFailure("原密码和新密码均不能为空")
|
||||
}
|
||||
if newPassword != confirmNewPassword {
|
||||
return json.CommonFailure("两次输入密码不一致")
|
||||
}
|
||||
if oldPassword == newPassword {
|
||||
return json.CommonFailure("原密码与新密码不能相同")
|
||||
}
|
||||
userModel := new(models.User)
|
||||
if !userModel.Match(Username(sess), oldPassword) {
|
||||
return json.CommonFailure("原密码输入错误")
|
||||
}
|
||||
_, err := userModel.UpdatePassword(Uid(sess), newPassword)
|
||||
if err != nil {
|
||||
return json.CommonFailure("修改失败")
|
||||
}
|
||||
|
||||
return json.Success("修改成功", nil)
|
||||
}
|
||||
|
||||
// ValidateLogin 验证用户登录
|
||||
func ValidateLogin(ctx *macaron.Context, sess session.Store, cpt *captcha.Captcha) string {
|
||||
username := ctx.QueryTrim("username")
|
||||
password := ctx.QueryTrim("password")
|
||||
json := utils.JsonResponse{}
|
||||
if username == "" || password == "" {
|
||||
return json.CommonFailure("用户名、密码不能为空")
|
||||
}
|
||||
userModel := new(models.User)
|
||||
if !userModel.Match(username, password) {
|
||||
return json.CommonFailure("用户名或密码错误")
|
||||
}
|
||||
if !cpt.VerifyReq(ctx.Req) {
|
||||
return json.Failure(utils.CaptchaError, "验证码错误")
|
||||
}
|
||||
|
||||
loginLogModel := new(models.LoginLog)
|
||||
loginLogModel.Username = userModel.Name
|
||||
loginLogModel.Ip = ctx.RemoteAddr()
|
||||
_, err := loginLogModel.Create()
|
||||
if err != nil {
|
||||
logger.Error("记录用户登录日志失败", err)
|
||||
}
|
||||
|
||||
sess.Set("username", userModel.Name)
|
||||
sess.Set("uid", userModel.Id)
|
||||
sess.Set("isAdmin", userModel.IsAdmin)
|
||||
|
||||
return json.Success("登录成功", nil)
|
||||
}
|
||||
|
||||
// Logout 用户退出
|
||||
func Logout(ctx *macaron.Context, sess session.Store) {
|
||||
if IsLogin(sess) {
|
||||
err := sess.Destory(ctx)
|
||||
if err != nil {
|
||||
logger.Error("用户退出登录失败", err)
|
||||
}
|
||||
}
|
||||
if IsLogin(sess) {
|
||||
err := sess.Destory(ctx)
|
||||
if err != nil {
|
||||
logger.Error("用户退出登录失败", err)
|
||||
}
|
||||
}
|
||||
|
||||
Login(ctx)
|
||||
Login(ctx)
|
||||
}
|
||||
|
||||
func Username(sess session.Store) string {
|
||||
username,ok := sess.Get("username").(string)
|
||||
if ok {
|
||||
return username
|
||||
}
|
||||
// Username 获取session中的用户名
|
||||
func Username(sess session.Store) string {
|
||||
username, ok := sess.Get("username").(string)
|
||||
if ok {
|
||||
return username
|
||||
}
|
||||
|
||||
return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
func Uid(sess session.Store) int {
|
||||
uid,ok := sess.Get("uid").(int)
|
||||
if ok {
|
||||
return uid
|
||||
}
|
||||
// Uid 获取session中的Uid
|
||||
func Uid(sess session.Store) int {
|
||||
uid, ok := sess.Get("uid").(int)
|
||||
if ok {
|
||||
return uid
|
||||
}
|
||||
|
||||
return 0
|
||||
return 0
|
||||
}
|
||||
|
||||
func IsLogin(sess session.Store) bool {
|
||||
uid, ok := sess.Get("uid").(int)
|
||||
if ok && uid > 0 {
|
||||
return true
|
||||
}
|
||||
// IsLogin 判断用户是否已登录
|
||||
func IsLogin(sess session.Store) bool {
|
||||
uid, ok := sess.Get("uid").(int)
|
||||
if ok && uid > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
return false
|
||||
}
|
||||
|
||||
func IsAdmin(sess session.Store) bool {
|
||||
isAdmin, ok := sess.Get("isAdmin").(int8)
|
||||
if ok && isAdmin > 0 {
|
||||
return true
|
||||
}
|
||||
// IsAdmin 判断当前用户是否是管理员
|
||||
func IsAdmin(sess session.Store) bool {
|
||||
isAdmin, ok := sess.Get("isAdmin").(int8)
|
||||
if ok && isAdmin > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 295 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 108 KiB |
+369
-288
@@ -1,390 +1,471 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"strconv"
|
||||
"time"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/jakecoffman/cron"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ouqiang/gocron/modules/httpclient"
|
||||
"github.com/ouqiang/gocron/modules/notify"
|
||||
"sync"
|
||||
rpcClient "github.com/ouqiang/gocron/modules/rpc/client"
|
||||
pb "github.com/ouqiang/gocron/modules/rpc/proto"
|
||||
"strings"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jakecoffman/cron"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/app"
|
||||
"github.com/ouqiang/gocron/modules/httpclient"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/notify"
|
||||
rpcClient "github.com/ouqiang/gocron/modules/rpc/client"
|
||||
pb "github.com/ouqiang/gocron/modules/rpc/proto"
|
||||
)
|
||||
|
||||
// 定时任务调度管理器
|
||||
var Cron *cron.Cron
|
||||
// 同一任务是否有实例处于运行中
|
||||
var runInstance Instance
|
||||
// 任务计数-正在运行中的任务
|
||||
var TaskNum TaskCount
|
||||
var (
|
||||
ServiceTask Task
|
||||
)
|
||||
|
||||
var (
|
||||
// 定时任务调度管理器
|
||||
serviceCron *cron.Cron
|
||||
|
||||
// 同一任务是否有实例处于运行中
|
||||
runInstance Instance
|
||||
|
||||
// 任务计数-正在运行的任务
|
||||
taskCount TaskCount
|
||||
|
||||
// 并发队列, 限制同时运行的任务数量
|
||||
concurrencyQueue ConcurrencyQueue
|
||||
)
|
||||
|
||||
// 并发队列
|
||||
type ConcurrencyQueue struct {
|
||||
queue chan struct{}
|
||||
}
|
||||
|
||||
func (cq *ConcurrencyQueue) Add() {
|
||||
cq.queue <- struct{}{}
|
||||
}
|
||||
|
||||
func (cq *ConcurrencyQueue) Done() {
|
||||
<-cq.queue
|
||||
}
|
||||
|
||||
// 任务计数
|
||||
type TaskCount struct {
|
||||
num int
|
||||
sync.RWMutex
|
||||
wg sync.WaitGroup
|
||||
exit chan struct{}
|
||||
}
|
||||
|
||||
func (c *TaskCount) Add() {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.num += 1
|
||||
func (tc *TaskCount) Add() {
|
||||
tc.wg.Add(1)
|
||||
}
|
||||
|
||||
func (c *TaskCount) Done() {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.num -= 1
|
||||
func (tc *TaskCount) Done() {
|
||||
tc.wg.Done()
|
||||
}
|
||||
|
||||
func (c *TaskCount) Num() int {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
func (tc *TaskCount) Exit() {
|
||||
tc.wg.Done()
|
||||
<-tc.exit
|
||||
}
|
||||
|
||||
return c.num
|
||||
func (tc *TaskCount) Wait() {
|
||||
tc.Add()
|
||||
tc.wg.Wait()
|
||||
close(tc.exit)
|
||||
}
|
||||
|
||||
// 任务ID作为Key
|
||||
type Instance struct {
|
||||
Status map[int]bool
|
||||
sync.RWMutex
|
||||
m sync.Map
|
||||
}
|
||||
|
||||
// 是否有任务处于运行中
|
||||
func (i *Instance) has(key int) bool {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
running, ok := i.Status[key]
|
||||
if ok && running {
|
||||
return true
|
||||
}
|
||||
_, ok := i.m.Load(key)
|
||||
|
||||
return false
|
||||
return ok
|
||||
}
|
||||
|
||||
func (i *Instance) add(key int) {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
i.Status[key] = true
|
||||
func (i *Instance) add(key int) {
|
||||
i.m.Store(key, struct{}{})
|
||||
}
|
||||
|
||||
func (i *Instance) done(key int) {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
delete(i.Status, key)
|
||||
func (i *Instance) done(key int) {
|
||||
i.m.Delete(key)
|
||||
}
|
||||
|
||||
type Task struct{}
|
||||
|
||||
type TaskResult struct {
|
||||
Result string
|
||||
Err error
|
||||
RetryTimes int8
|
||||
Result string
|
||||
Err error
|
||||
RetryTimes int8
|
||||
}
|
||||
|
||||
// 初始化任务, 从数据库取出所有任务, 添加到定时任务并运行
|
||||
func (task *Task) Initialize() {
|
||||
Cron = cron.New()
|
||||
Cron.Start()
|
||||
runInstance = Instance{make(map[int]bool), sync.RWMutex{}}
|
||||
TaskNum = TaskCount{0, sync.RWMutex{}}
|
||||
func (task Task) Initialize() {
|
||||
serviceCron = cron.New()
|
||||
serviceCron.Start()
|
||||
concurrencyQueue = ConcurrencyQueue{queue: make(chan struct{}, app.Setting.ConcurrencyQueue)}
|
||||
taskCount = TaskCount{sync.WaitGroup{}, make(chan struct{})}
|
||||
go taskCount.Wait()
|
||||
|
||||
taskModel := new(models.Task)
|
||||
taskList, err := taskModel.ActiveList()
|
||||
if err != nil {
|
||||
logger.Error("定时任务初始化#获取任务列表错误-", err.Error())
|
||||
return
|
||||
}
|
||||
if len(taskList) == 0 {
|
||||
logger.Debug("任务列表为空")
|
||||
return
|
||||
}
|
||||
task.BatchAdd(taskList)
|
||||
logger.Info("开始初始化定时任务")
|
||||
taskModel := new(models.Task)
|
||||
taskNum := 0
|
||||
page := 1
|
||||
pageSize := 1000
|
||||
maxPage := 1000
|
||||
for page < maxPage {
|
||||
taskList, err := taskModel.ActiveList(page, pageSize)
|
||||
if err != nil {
|
||||
logger.Fatalf("定时任务初始化#获取任务列表错误-", err.Error())
|
||||
}
|
||||
if len(taskList) == 0 {
|
||||
break
|
||||
}
|
||||
for _, item := range taskList {
|
||||
task.Add(item)
|
||||
taskNum++
|
||||
}
|
||||
page++
|
||||
}
|
||||
logger.Infof("定时任务初始化完成, 共%d个定时任务添加到调度器", taskNum)
|
||||
}
|
||||
|
||||
// 批量添加任务
|
||||
func (task *Task) BatchAdd(tasks []models.TaskHost) {
|
||||
for _, item := range tasks {
|
||||
task.Add(item)
|
||||
}
|
||||
func (task Task) BatchAdd(tasks []models.Task) {
|
||||
for _, item := range tasks {
|
||||
task.RemoveAndAdd(item)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务后添加
|
||||
func (task Task) RemoveAndAdd(taskModel models.Task) {
|
||||
task.Remove(taskModel.Id)
|
||||
task.Add(taskModel)
|
||||
}
|
||||
|
||||
// 添加任务
|
||||
func (task *Task) Add(taskModel models.TaskHost) {
|
||||
if taskModel.Level == models.TaskLevelChild {
|
||||
logger.Errorf("添加任务失败#不允许添加子任务到调度器#任务Id-%d", taskModel.Id);
|
||||
return
|
||||
}
|
||||
taskFunc := createJob(taskModel)
|
||||
if taskFunc == nil {
|
||||
logger.Error("创建任务处理Job失败,不支持的任务协议#", taskModel.Protocol)
|
||||
return
|
||||
}
|
||||
func (task Task) Add(taskModel models.Task) {
|
||||
if taskModel.Level == models.TaskLevelChild {
|
||||
logger.Errorf("添加任务失败#不允许添加子任务到调度器#任务Id-%d", taskModel.Id)
|
||||
return
|
||||
}
|
||||
taskFunc := createJob(taskModel)
|
||||
if taskFunc == nil {
|
||||
logger.Error("创建任务处理Job失败,不支持的任务协议#", taskModel.Protocol)
|
||||
return
|
||||
}
|
||||
|
||||
cronName := strconv.Itoa(taskModel.Id)
|
||||
// Cron任务采用数组存储, 删除任务需遍历数组, 并对数组重新赋值, 任务较多时,有性能问题
|
||||
Cron.RemoveJob(cronName)
|
||||
err := Cron.AddFunc(taskModel.Spec, taskFunc, cronName)
|
||||
if err != nil {
|
||||
logger.Error("添加任务到调度器失败#", err)
|
||||
}
|
||||
cronName := strconv.Itoa(taskModel.Id)
|
||||
err := serviceCron.AddFunc(taskModel.Spec, taskFunc, cronName)
|
||||
if err != nil {
|
||||
logger.Error("添加任务到调度器失败#", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 停止所有任务
|
||||
func (task *Task) StopAll() {
|
||||
Cron.Stop()
|
||||
// 停止运行中的任务
|
||||
func (task Task) Stop(ip string, port int, id int64) {
|
||||
rpcClient.Stop(ip, port, id)
|
||||
}
|
||||
|
||||
func (task Task) Remove(id int) {
|
||||
serviceCron.RemoveJob(strconv.Itoa(id))
|
||||
}
|
||||
|
||||
// 等待所有任务结束后退出
|
||||
func (task Task) WaitAndExit() {
|
||||
serviceCron.Stop()
|
||||
taskCount.Exit()
|
||||
}
|
||||
|
||||
// 直接运行任务
|
||||
func (task *Task) Run(taskModel models.TaskHost) {
|
||||
go createJob(taskModel)()
|
||||
func (task Task) Run(taskModel models.Task) {
|
||||
go createJob(taskModel)()
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
Run(taskModel models.TaskHost) (string, error)
|
||||
Run(taskModel models.Task, taskUniqueId int64) (string, error)
|
||||
}
|
||||
|
||||
|
||||
// HTTP任务
|
||||
type HTTPHandler struct{}
|
||||
|
||||
// http任务执行时间不超过300秒
|
||||
const HttpExecTimeout = 300
|
||||
|
||||
func (h *HTTPHandler) Run(taskModel models.TaskHost) (result string, err error) {
|
||||
if taskModel.Timeout <= 0 || taskModel.Timeout > HttpExecTimeout {
|
||||
taskModel.Timeout = HttpExecTimeout
|
||||
}
|
||||
resp := httpclient.Get(taskModel.Command, taskModel.Timeout)
|
||||
// 返回状态码非200,均为失败
|
||||
if resp.StatusCode != 200 {
|
||||
return resp.Body, errors.New(fmt.Sprintf("HTTP状态码非200-->%d", resp.StatusCode))
|
||||
}
|
||||
func (h *HTTPHandler) Run(taskModel models.Task, taskUniqueId int64) (result string, err error) {
|
||||
if taskModel.Timeout <= 0 || taskModel.Timeout > HttpExecTimeout {
|
||||
taskModel.Timeout = HttpExecTimeout
|
||||
}
|
||||
var resp httpclient.ResponseWrapper
|
||||
if taskModel.HttpMethod == models.TaskHTTPMethodGet {
|
||||
resp = httpclient.Get(taskModel.Command, taskModel.Timeout)
|
||||
} else {
|
||||
urlFields := strings.Split(taskModel.Command, "?")
|
||||
taskModel.Command = urlFields[0]
|
||||
var params string
|
||||
if len(urlFields) >= 2 {
|
||||
params = urlFields[1]
|
||||
}
|
||||
resp = httpclient.PostParams(taskModel.Command, params, taskModel.Timeout)
|
||||
}
|
||||
// 返回状态码非200,均为失败
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return resp.Body, errors.New(fmt.Sprintf("HTTP状态码非200-->%d", resp.StatusCode))
|
||||
}
|
||||
|
||||
return resp.Body, err
|
||||
return resp.Body, err
|
||||
}
|
||||
|
||||
// RPC调用执行任务
|
||||
type RPCHandler struct {}
|
||||
type RPCHandler struct{}
|
||||
|
||||
func (h *RPCHandler) Run(taskModel models.TaskHost) (result string, err error) {
|
||||
taskRequest := new(pb.TaskRequest)
|
||||
taskRequest.Timeout = int32(taskModel.Timeout)
|
||||
taskRequest.Command = taskModel.Command
|
||||
func (h *RPCHandler) Run(taskModel models.Task, taskUniqueId int64) (result string, err error) {
|
||||
taskRequest := new(pb.TaskRequest)
|
||||
taskRequest.Timeout = int32(taskModel.Timeout)
|
||||
taskRequest.Command = taskModel.Command
|
||||
taskRequest.Id = taskUniqueId
|
||||
resultChan := make(chan TaskResult, len(taskModel.Hosts))
|
||||
for _, taskHost := range taskModel.Hosts {
|
||||
go func(th models.TaskHostDetail) {
|
||||
output, err := rpcClient.Exec(th.Name, th.Port, taskRequest)
|
||||
errorMessage := ""
|
||||
if err != nil {
|
||||
errorMessage = err.Error()
|
||||
}
|
||||
outputMessage := fmt.Sprintf("主机: [%s-%s]\n%s\n%s\n\n",
|
||||
th.Alias, th.Name, errorMessage, output,
|
||||
)
|
||||
resultChan <- TaskResult{Err: err, Result: outputMessage}
|
||||
}(taskHost)
|
||||
}
|
||||
|
||||
return rpcClient.ExecWithRetry(taskModel.Name, taskModel.Port, taskRequest)
|
||||
var aggregationErr error = nil
|
||||
aggregationResult := ""
|
||||
for i := 0; i < len(taskModel.Hosts); i++ {
|
||||
taskResult := <-resultChan
|
||||
aggregationResult += taskResult.Result
|
||||
if taskResult.Err != nil {
|
||||
aggregationErr = taskResult.Err
|
||||
}
|
||||
}
|
||||
|
||||
return aggregationResult, aggregationErr
|
||||
}
|
||||
|
||||
|
||||
// 创建任务日志
|
||||
func createTaskLog(taskModel models.TaskHost, status models.Status) (int64, error) {
|
||||
taskLogModel := new(models.TaskLog)
|
||||
taskLogModel.TaskId = taskModel.Id
|
||||
taskLogModel.Name = taskModel.Task.Name
|
||||
taskLogModel.Spec = taskModel.Spec
|
||||
taskLogModel.Protocol = taskModel.Protocol
|
||||
taskLogModel.Command = taskModel.Command
|
||||
taskLogModel.Timeout = taskModel.Timeout
|
||||
if taskModel.Protocol == models.TaskRPC {
|
||||
taskLogModel.Hostname = taskModel.Alias + "-" + taskModel.Name
|
||||
}
|
||||
taskLogModel.StartTime = time.Now()
|
||||
taskLogModel.Status = status
|
||||
insertId, err := taskLogModel.Create()
|
||||
func createTaskLog(taskModel models.Task, status models.Status) (int64, error) {
|
||||
taskLogModel := new(models.TaskLog)
|
||||
taskLogModel.TaskId = taskModel.Id
|
||||
taskLogModel.Name = taskModel.Name
|
||||
taskLogModel.Spec = taskModel.Spec
|
||||
taskLogModel.Protocol = taskModel.Protocol
|
||||
taskLogModel.Command = taskModel.Command
|
||||
taskLogModel.Timeout = taskModel.Timeout
|
||||
if taskModel.Protocol == models.TaskRPC {
|
||||
aggregationHost := ""
|
||||
for _, host := range taskModel.Hosts {
|
||||
aggregationHost += fmt.Sprintf("%s-%s<br>", host.Alias, host.Name)
|
||||
}
|
||||
taskLogModel.Hostname = aggregationHost
|
||||
}
|
||||
taskLogModel.StartTime = time.Now()
|
||||
taskLogModel.Status = status
|
||||
insertId, err := taskLogModel.Create()
|
||||
|
||||
return insertId, err
|
||||
return insertId, err
|
||||
}
|
||||
|
||||
// 更新任务日志
|
||||
func updateTaskLog(taskLogId int64, taskResult TaskResult) (int64, error) {
|
||||
taskLogModel := new(models.TaskLog)
|
||||
var status models.Status
|
||||
var result string = taskResult.Result
|
||||
if taskResult.Err != nil {
|
||||
status = models.Failure
|
||||
} else {
|
||||
status = models.Finish
|
||||
}
|
||||
return taskLogModel.Update(taskLogId, models.CommonMap{
|
||||
"retry_times": taskResult.RetryTimes,
|
||||
"status": status,
|
||||
"result": result,
|
||||
})
|
||||
taskLogModel := new(models.TaskLog)
|
||||
var status models.Status
|
||||
result := taskResult.Result
|
||||
if taskResult.Err != nil {
|
||||
status = models.Failure
|
||||
} else {
|
||||
status = models.Finish
|
||||
}
|
||||
return taskLogModel.Update(taskLogId, models.CommonMap{
|
||||
"retry_times": taskResult.RetryTimes,
|
||||
"status": status,
|
||||
"result": result,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func createJob(taskModel models.TaskHost) cron.FuncJob {
|
||||
var handler Handler = createHandler(taskModel)
|
||||
if handler == nil {
|
||||
return nil
|
||||
}
|
||||
taskFunc := func() {
|
||||
TaskNum.Add()
|
||||
defer TaskNum.Done()
|
||||
taskLogId := beforeExecJob(taskModel)
|
||||
if taskLogId <= 0 {
|
||||
return
|
||||
}
|
||||
logger.Infof("开始执行任务#%s#命令-%s", taskModel.Task.Name, taskModel.Command)
|
||||
taskResult := execJob(handler, taskModel)
|
||||
logger.Infof("任务完成#%s#命令-%s", taskModel.Task.Name, taskModel.Command)
|
||||
afterExecJob(taskModel, taskResult, taskLogId)
|
||||
}
|
||||
func createJob(taskModel models.Task) cron.FuncJob {
|
||||
handler := createHandler(taskModel)
|
||||
if handler == nil {
|
||||
return nil
|
||||
}
|
||||
taskFunc := func() {
|
||||
taskCount.Add()
|
||||
defer taskCount.Done()
|
||||
|
||||
return taskFunc
|
||||
taskLogId := beforeExecJob(taskModel)
|
||||
if taskLogId <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if taskModel.Multi == 0 {
|
||||
runInstance.add(taskModel.Id)
|
||||
defer runInstance.done(taskModel.Id)
|
||||
}
|
||||
|
||||
concurrencyQueue.Add()
|
||||
defer concurrencyQueue.Done()
|
||||
|
||||
logger.Infof("开始执行任务#%s#命令-%s", taskModel.Name, taskModel.Command)
|
||||
taskResult := execJob(handler, taskModel, taskLogId)
|
||||
logger.Infof("任务完成#%s#命令-%s", taskModel.Name, taskModel.Command)
|
||||
afterExecJob(taskModel, taskResult, taskLogId)
|
||||
}
|
||||
|
||||
return taskFunc
|
||||
}
|
||||
|
||||
func createHandler(taskModel models.TaskHost) Handler {
|
||||
var handler Handler = nil
|
||||
switch taskModel.Protocol {
|
||||
case models.TaskHTTP:
|
||||
handler = new(HTTPHandler)
|
||||
case models.TaskRPC:
|
||||
handler = new(RPCHandler)
|
||||
}
|
||||
func createHandler(taskModel models.Task) Handler {
|
||||
var handler Handler = nil
|
||||
switch taskModel.Protocol {
|
||||
case models.TaskHTTP:
|
||||
handler = new(HTTPHandler)
|
||||
case models.TaskRPC:
|
||||
handler = new(RPCHandler)
|
||||
}
|
||||
|
||||
|
||||
return handler;
|
||||
return handler
|
||||
}
|
||||
|
||||
// 任务前置操作
|
||||
func beforeExecJob(taskModel models.TaskHost) (taskLogId int64) {
|
||||
if taskModel.Multi == 0 && runInstance.has(taskModel.Id) {
|
||||
createTaskLog(taskModel, models.Cancel)
|
||||
return
|
||||
}
|
||||
if taskModel.Multi == 0 {
|
||||
runInstance.add(taskModel.Id)
|
||||
}
|
||||
taskLogId, err := createTaskLog(taskModel, models.Running)
|
||||
if err != nil {
|
||||
logger.Error("任务开始执行#写入任务日志失败-", err)
|
||||
return
|
||||
}
|
||||
func beforeExecJob(taskModel models.Task) (taskLogId int64) {
|
||||
if taskModel.Multi == 0 && runInstance.has(taskModel.Id) {
|
||||
createTaskLog(taskModel, models.Cancel)
|
||||
return
|
||||
}
|
||||
taskLogId, err := createTaskLog(taskModel, models.Running)
|
||||
if err != nil {
|
||||
logger.Error("任务开始执行#写入任务日志失败-", err)
|
||||
return
|
||||
}
|
||||
logger.Debugf("任务命令-%s", taskModel.Command)
|
||||
|
||||
logger.Debugf("任务命令-%s", taskModel.Command)
|
||||
|
||||
return taskLogId
|
||||
return taskLogId
|
||||
}
|
||||
|
||||
// 任务执行后置操作
|
||||
func afterExecJob(taskModel models.TaskHost, taskResult TaskResult, taskLogId int64) {
|
||||
if taskResult.Err != nil {
|
||||
taskResult.Result = taskResult.Err.Error() + "\n" + taskResult.Result
|
||||
}
|
||||
_, err := updateTaskLog(taskLogId, taskResult)
|
||||
if err != nil {
|
||||
logger.Error("任务结束#更新任务日志失败-", err)
|
||||
}
|
||||
func afterExecJob(taskModel models.Task, taskResult TaskResult, taskLogId int64) {
|
||||
_, err := updateTaskLog(taskLogId, taskResult)
|
||||
if err != nil {
|
||||
logger.Error("任务结束#更新任务日志失败-", err)
|
||||
}
|
||||
|
||||
// 发送邮件
|
||||
go SendNotification(taskModel, taskResult)
|
||||
// 执行依赖任务
|
||||
go execDependencyTask(taskModel, taskResult)
|
||||
// 发送邮件
|
||||
go SendNotification(taskModel, taskResult)
|
||||
// 执行依赖任务
|
||||
go execDependencyTask(taskModel, taskResult)
|
||||
}
|
||||
|
||||
// 执行依赖任务, 多个任务并发执行
|
||||
func execDependencyTask(taskModel models.TaskHost, taskResult TaskResult) {
|
||||
// 父任务才能执行子任务
|
||||
if taskModel.Level != models.TaskLevelParent {
|
||||
return
|
||||
}
|
||||
func execDependencyTask(taskModel models.Task, taskResult TaskResult) {
|
||||
// 父任务才能执行子任务
|
||||
if taskModel.Level != models.TaskLevelParent {
|
||||
return
|
||||
}
|
||||
|
||||
// 是否存在子任务
|
||||
dependencyTaskId := strings.TrimSpace(taskModel.DependencyTaskId)
|
||||
if dependencyTaskId == "" {
|
||||
return
|
||||
}
|
||||
// 是否存在子任务
|
||||
dependencyTaskId := strings.TrimSpace(taskModel.DependencyTaskId)
|
||||
if dependencyTaskId == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 父子任务关系为强依赖, 父任务执行失败, 不执行依赖任务
|
||||
if taskModel.DependencyStatus == models.TaskDependencyStatusStrong && taskResult.Err != nil {
|
||||
logger.Infof("父子任务为强依赖关系, 父任务执行失败, 不运行依赖任务#主任务ID-%d", taskModel.Id)
|
||||
return
|
||||
}
|
||||
// 父子任务关系为强依赖, 父任务执行失败, 不执行依赖任务
|
||||
if taskModel.DependencyStatus == models.TaskDependencyStatusStrong && taskResult.Err != nil {
|
||||
logger.Infof("父子任务为强依赖关系, 父任务执行失败, 不运行依赖任务#主任务ID-%d", taskModel.Id)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取子任务
|
||||
model := new(models.Task)
|
||||
tasks , err := model.GetDependencyTaskList(dependencyTaskId)
|
||||
if err != nil {
|
||||
logger.Errorf("获取依赖任务失败#主任务ID-%d#%s", taskModel.Id, err.Error())
|
||||
return
|
||||
}
|
||||
if len(tasks) == 0 {
|
||||
logger.Errorf("依赖任务列表为空#主任务ID-%d", taskModel.Id)
|
||||
}
|
||||
|
||||
serviceTask := new(Task)
|
||||
for _, task := range tasks {
|
||||
task.Spec = fmt.Sprintf("依赖任务(主任务ID-%d)", taskModel.Id)
|
||||
serviceTask.Run(task)
|
||||
}
|
||||
// 获取子任务
|
||||
model := new(models.Task)
|
||||
tasks, err := model.GetDependencyTaskList(dependencyTaskId)
|
||||
if err != nil {
|
||||
logger.Errorf("获取依赖任务失败#主任务ID-%d#%s", taskModel.Id, err.Error())
|
||||
return
|
||||
}
|
||||
if len(tasks) == 0 {
|
||||
logger.Errorf("依赖任务列表为空#主任务ID-%d", taskModel.Id)
|
||||
}
|
||||
for _, task := range tasks {
|
||||
task.Spec = fmt.Sprintf("依赖任务(主任务ID-%d)", taskModel.Id)
|
||||
ServiceTask.Run(task)
|
||||
}
|
||||
}
|
||||
|
||||
// 发送任务结果通知
|
||||
func SendNotification(taskModel models.TaskHost, taskResult TaskResult) {
|
||||
var statusName string
|
||||
// 未开启通知
|
||||
if taskModel.NotifyStatus == 0 {
|
||||
return
|
||||
}
|
||||
if taskModel.NotifyStatus == 1 && taskResult.Err == nil {
|
||||
// 执行失败才发送通知
|
||||
return
|
||||
}
|
||||
if taskModel.NotifyReceiverId == "" {
|
||||
return
|
||||
}
|
||||
if taskResult.Err != nil {
|
||||
statusName = "失败"
|
||||
} else {
|
||||
statusName = "成功"
|
||||
}
|
||||
// 发送通知
|
||||
msg := notify.Message{
|
||||
"task_type": taskModel.NotifyType,
|
||||
"task_receiver_id": taskModel.NotifyReceiverId,
|
||||
"name": taskModel.Task.Name,
|
||||
"output": taskResult.Result,
|
||||
"status": statusName,
|
||||
"taskId": taskModel.Id,
|
||||
};
|
||||
notify.Push(msg)
|
||||
func SendNotification(taskModel models.Task, taskResult TaskResult) {
|
||||
var statusName string
|
||||
// 未开启通知
|
||||
if taskModel.NotifyStatus == 0 {
|
||||
return
|
||||
}
|
||||
if taskModel.NotifyStatus == 1 && taskResult.Err == nil {
|
||||
// 执行失败才发送通知
|
||||
return
|
||||
}
|
||||
if taskModel.NotifyReceiverId == "" {
|
||||
return
|
||||
}
|
||||
if taskResult.Err != nil {
|
||||
statusName = "失败"
|
||||
} else {
|
||||
statusName = "成功"
|
||||
}
|
||||
// 发送通知
|
||||
msg := notify.Message{
|
||||
"task_type": taskModel.NotifyType,
|
||||
"task_receiver_id": taskModel.NotifyReceiverId,
|
||||
"name": taskModel.Name,
|
||||
"output": taskResult.Result,
|
||||
"status": statusName,
|
||||
"taskId": taskModel.Id,
|
||||
}
|
||||
notify.Push(msg)
|
||||
}
|
||||
|
||||
// 执行具体任务
|
||||
func execJob(handler Handler, taskModel models.TaskHost) TaskResult {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Error("panic#service/task.go:execJob#", err)
|
||||
}
|
||||
} ()
|
||||
if taskModel.Multi == 0 {
|
||||
defer runInstance.done(taskModel.Id)
|
||||
}
|
||||
// 默认只运行任务一次
|
||||
var execTimes int8 = 1
|
||||
if (taskModel.RetryTimes > 0) {
|
||||
execTimes += taskModel.RetryTimes
|
||||
}
|
||||
var i int8 = 0
|
||||
var output string
|
||||
var err error
|
||||
for i < execTimes {
|
||||
output, err = handler.Run(taskModel)
|
||||
if err == nil {
|
||||
return TaskResult{Result: output, Err: err, RetryTimes: i}
|
||||
}
|
||||
i++
|
||||
if i < execTimes {
|
||||
logger.Warnf("任务执行失败#任务id-%d#重试第%d次#输出-%s#错误-%s", taskModel.Id, i, output, err.Error())
|
||||
// 重试间隔时间,每次递增1分钟
|
||||
time.Sleep( time.Duration(i) * time.Minute)
|
||||
}
|
||||
}
|
||||
func execJob(handler Handler, taskModel models.Task, taskUniqueId int64) TaskResult {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Error("panic#service/task.go:execJob#", err)
|
||||
}
|
||||
}()
|
||||
// 默认只运行任务一次
|
||||
var execTimes int8 = 1
|
||||
if taskModel.RetryTimes > 0 {
|
||||
execTimes += taskModel.RetryTimes
|
||||
}
|
||||
var i int8 = 0
|
||||
var output string
|
||||
var err error
|
||||
for i < execTimes {
|
||||
output, err = handler.Run(taskModel, taskUniqueId)
|
||||
if err == nil {
|
||||
return TaskResult{Result: output, Err: err, RetryTimes: i}
|
||||
}
|
||||
i++
|
||||
if i < execTimes {
|
||||
logger.Warnf("任务执行失败#任务id-%d#重试第%d次#输出-%s#错误-%s", taskModel.Id, i, output, err.Error())
|
||||
if taskModel.RetryInterval > 0 {
|
||||
time.Sleep(time.Duration(taskModel.RetryInterval) * time.Second)
|
||||
} else {
|
||||
// 默认重试间隔时间,每次递增1分钟
|
||||
time.Sleep(time.Duration(i) * time.Minute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TaskResult{Result: output, Err: err, RetryTimes: taskModel.RetryTimes}
|
||||
}
|
||||
return TaskResult{Result: output, Err: err, RetryTimes: taskModel.RetryTimes}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="renderer" content="webkit">
|
||||
<title>{{{.Title}}}</title>
|
||||
<title>{%.Title%}</title>
|
||||
<link type="text/css" rel="stylesheet" href="/resource/css/framework.css" />
|
||||
<link type="text/css" rel="stylesheet" href="/resource/css/main.css" />
|
||||
<link type="text/css" rel="stylesheet" href="/resource/sweetalert/sweetalert.css" />
|
||||
@@ -29,29 +29,29 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
{{{if not .DisableNav}}}
|
||||
{%if not .DisableNav%}
|
||||
<!--header begin-->
|
||||
<header>
|
||||
<div class="bigcontainer">
|
||||
<div id="logo">
|
||||
{{{.AppName}}}
|
||||
{%.AppName%}
|
||||
</div>
|
||||
<div class="user">
|
||||
<div class="ui inline labeled icon top right pointing dropdown">
|
||||
{{{if not .LoginUsername}}}
|
||||
{%if not .LoginUsername%}
|
||||
你好
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="menu">
|
||||
<a class="item" href="/user/login"><i class="sign in icon"></i>登录</a>
|
||||
</div>
|
||||
{{{else}}}
|
||||
你好,{{{.LoginUsername}}}
|
||||
{%else%}
|
||||
你好,{%.LoginUsername%}
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="menu">
|
||||
<a href="/user/editPassword" class="item"><i class="edit icon"></i>修改密码</a>
|
||||
<a href="/user/editMyPassword" class="item"><i class="edit icon"></i>修改密码</a>
|
||||
<a class="item" href="/user/logout"><i class="sign out icon"></i>退出</a>
|
||||
</div>
|
||||
{{{end}}}
|
||||
{%end%}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,14 +60,13 @@
|
||||
<div class="ui teal inverted menu">
|
||||
<div class="bigcontainer">
|
||||
<div class="right menu">
|
||||
<a class="item {{{if or (eq .Controller "task") (eq .Controller "delaytask")}}}active{{{end}}}" href="/task"><i class="tasks icon"></i>任务</a>
|
||||
<a class="item {{{if eq .Controller "host"}}}active{{{end}}}" href="/host"><i class="linux icon"></i>主机</a>
|
||||
<!-- <a class="item {{{if eq .Controller "user"}}}active{{{end}}}" href="/user"><i class="user icon"></i>账户</a> -->
|
||||
{{{if gt .LoginUid 0}}}
|
||||
<a class="item {{{if eq .Controller "manage"}}}active{{{end}}}" href="/manage/slack/edit"><i class="settings icon"></i>管理</a>
|
||||
{{{end}}}
|
||||
<a class="item" href="https://github.com/ouqiang/gocron/wiki" target="_blank"><i class="file text icon"></i>查看文档</a>
|
||||
<a class="item {%if or (eq .Controller "task") (eq .Controller "delaytask")%}active{%end%}" href="/task"><i class="tasks icon"></i>任务</a>
|
||||
<a class="item {%if eq .Controller "host"%}active{%end%}" href="/host"><i class="linux icon"></i>任务节点</a>
|
||||
{%if .IsAdmin%}
|
||||
<a class="item {%if eq .Controller "user"%}active{%end%}" href="/user"><i class="user icon"></i>用户管理</a>
|
||||
<a class="item {%if eq .Controller "manage"%}active{%end%}" href="/manage/slack/edit"><i class="settings icon"></i>系统管理</a>
|
||||
{%end%}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{{end}}}
|
||||
{%end%}
|
||||
@@ -1,41 +1,41 @@
|
||||
{{{if gt .Pagination.Total 0}}}
|
||||
{%if gt .Pagination.Total 0%}
|
||||
|
||||
<div class="ui pagination menu">
|
||||
{{{if not .Pagination.IsFirst}}}
|
||||
<a class="item" href="{{{.URI}}}?{{{ .Params.PageParams}}}&page=1">
|
||||
{%if not .Pagination.IsFirst%}
|
||||
<a class="item" href="{%.URI%}?{% .Params.PageParams%}&page=1">
|
||||
首页
|
||||
</a>
|
||||
{{{end}}}
|
||||
{%end%}
|
||||
|
||||
{{{if .Pagination.HasPrevious}}}
|
||||
<a class="icon item" href="{{{.URI}}}?{{{ .Params.PageParams}}}&page={{{.Pagination.Previous}}}">
|
||||
{%if .Pagination.HasPrevious%}
|
||||
<a class="icon item" href="{%.URI%}?{% .Params.PageParams%}&page={%.Pagination.Previous%}">
|
||||
<i class="icon left arrow"></i>
|
||||
</a>
|
||||
{{{end}}}
|
||||
{%end%}
|
||||
|
||||
{{{range .Pagination.Pages}}}
|
||||
{{{if eq .Num -1}}}
|
||||
{%range .Pagination.Pages%}
|
||||
{%if eq .Num -1%}
|
||||
<div class="disabled item">
|
||||
...
|
||||
</div>
|
||||
{{{else}}}
|
||||
<a class="icon {{{if .IsCurrent}}}active{{{end}}} item" href="{{{$.URI}}}?{{{ $.Params.PageParams}}}&page={{{.Num}}}">
|
||||
{{{.Num}}}
|
||||
{%else%}
|
||||
<a class="icon {%if .IsCurrent%}active{%end%} item" href="{%$.URI%}?{% $.Params.PageParams%}&page={%.Num%}">
|
||||
{%.Num%}
|
||||
</a>
|
||||
{{{end}}}
|
||||
{{{end}}}
|
||||
{%end%}
|
||||
{%end%}
|
||||
|
||||
{{{if .Pagination.HasNext}}}
|
||||
<a class="icon item" href="{{{.URI}}}?{{{ .Params.PageParams}}}&page={{{.Pagination.Next}}}">
|
||||
{%if .Pagination.HasNext%}
|
||||
<a class="icon item" href="{%.URI%}?{% .Params.PageParams%}&page={%.Pagination.Next%}">
|
||||
<i class="icon right arrow"></i>
|
||||
</a>
|
||||
{{{end}}}
|
||||
{{{if not .Pagination.IsLast}}}
|
||||
<a class="icon item" href="{{{.URI}}}?{{{ .Params.PageParams}}}&page={{{.Pagination.TotalPages}}}">
|
||||
{%end%}
|
||||
{%if not .Pagination.IsLast%}
|
||||
<a class="icon item" href="{%.URI%}?{% .Params.PageParams%}&page={%.Pagination.TotalPages%}">
|
||||
尾页
|
||||
</a>
|
||||
{{{end}}}
|
||||
<div class="item">{{{.Pagination.Total}}}条/{{{.Pagination.TotalPages}}}页</div>
|
||||
{%end%}
|
||||
<div class="item">{%.Pagination.Total%}条/{%.Pagination.TotalPages%}页</div>
|
||||
</div>
|
||||
|
||||
{{{end}}}
|
||||
{%end%}
|
||||
@@ -1,4 +1,4 @@
|
||||
{{{ template "common/header" . }}}
|
||||
{% template "common/header" . %}
|
||||
<script>
|
||||
swal({
|
||||
title: "404 - NOT FOUND",
|
||||
@@ -9,4 +9,4 @@
|
||||
location.href = "/"
|
||||
});
|
||||
</script>
|
||||
{{{ template "common/footer" . }}}
|
||||
{% template "common/footer" . %}
|
||||
@@ -1,4 +1,4 @@
|
||||
{{{ template "common/header" . }}}
|
||||
{% template "common/header" . %}
|
||||
<script>
|
||||
swal({
|
||||
title: "500 - INTERNAL SERVER ERROR",
|
||||
@@ -8,4 +8,4 @@
|
||||
confirmButtonText: "确定"
|
||||
});
|
||||
</script>
|
||||
{{{ template "common/footer" . }}}
|
||||
{% template "common/footer" . %}
|
||||
@@ -1,3 +1,3 @@
|
||||
{{{ template "common/header" . }}}
|
||||
{% template "common/header" . %}
|
||||
|
||||
{{{ template "common/footer" . }}}
|
||||
{% template "common/footer" . %}
|
||||
@@ -1,7 +1,7 @@
|
||||
{{{ template "common/header" . }}}
|
||||
{% template "common/header" . %}
|
||||
|
||||
<div class="ui grid">
|
||||
{{{ template "host/menu" . }}}
|
||||
{% template "host/menu" . %}
|
||||
|
||||
<div class="twelve wide column">
|
||||
<div class="pageHeader">
|
||||
@@ -14,24 +14,25 @@
|
||||
</div>
|
||||
</div>
|
||||
<form class="ui form fluid vertical segment">
|
||||
<input type="hidden" name="id" value="{{{.Host.Id}}}">
|
||||
<input type="hidden" name="id" value="{%.Host.Id%}">
|
||||
<div class="four fields">
|
||||
<div class="field">
|
||||
<label>主机名</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="name" value="{{{.Host.Name}}}" placeholder="192.168.50.154">
|
||||
<input type="text" name="name" value="{%.Host.Name%}" placeholder="127.0.0.1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>端口</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="port" value="{{{.Host.Port}}}" placeholder="5921">
|
||||
<input type="text" name="port" value="{%.Host.Port%}" placeholder="5921">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>主机别名 (方便记忆和引用)</label>
|
||||
<label>节点名称</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="alias" value="{{{.Host.Alias}}}">
|
||||
<input type="text" name="alias" value="{%.Host.Alias%}"
|
||||
placeholder="节点名称如web">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,13 +40,13 @@
|
||||
<div class="field">
|
||||
<label>备注</label>
|
||||
<div class="ui small input">
|
||||
<textarea rows="7" name="remark" >{{{.Host.Remark}}}</textarea>
|
||||
<textarea rows="7" name="remark" >{%.Host.Remark%}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui primary submit button">保存</div>
|
||||
<a class="ui button" onclick="location.href='/task';">取消</a>
|
||||
<a class="ui button" onclick="location.href='/host';">取消</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,4 +113,4 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{{{ template "common/footer" . }}}
|
||||
{% template "common/footer" . %}
|
||||
+27
-21
@@ -1,35 +1,37 @@
|
||||
{{{ template "common/header" . }}}
|
||||
{% template "common/header" . %}
|
||||
|
||||
<div class="ui grid">
|
||||
{{{ template "host/menu" . }}}
|
||||
{% template "host/menu" . %}
|
||||
|
||||
<div class="twelve wide column">
|
||||
<div class="pageHeader">
|
||||
{%if .IsAdmin%}
|
||||
<div class="pageHeader">
|
||||
<div class="segment">
|
||||
<h3 class="ui dividing header">
|
||||
<a href="/host/create">
|
||||
<i class="large add icon"></i>
|
||||
<div class="content">
|
||||
添加主机
|
||||
添加节点
|
||||
</div>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
{%end%}
|
||||
<form class="ui form">
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<input type="text" placeholder="ID" name="id" value="{{{if gt .Params.Id 0}}}{{{.Params.Id}}}{{{end}}}">
|
||||
<input type="text" placeholder="ID" name="id" value="{%if gt .Params.Id 0%}{%.Params.Id%}{%end%}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<input type="text" placeholder="主机名" name="name" value="{{{.Params.Name}}}">
|
||||
<input type="text" placeholder="主机名" name="name" value="{%.Params.Name%}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="ui linkedin submit button">搜索</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<table class="ui striped table">
|
||||
<table class="ui celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
@@ -41,32 +43,36 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{{range $i, $v := .Hosts}}}
|
||||
{%range $i, $v := .Hosts%}
|
||||
<tr>
|
||||
<td>{{{.Id}}}</td>
|
||||
<td>{{{.Name}}}</td>
|
||||
<td>{{{.Alias}}}</td>
|
||||
<td>{{{.Port}}}</td>
|
||||
<td>{{{.Remark}}}</td>
|
||||
<td>{%.Id%}</td>
|
||||
<td>{%.Name%}</td>
|
||||
<td>{%.Alias%}</td>
|
||||
<td>{%.Port%}</td>
|
||||
<td>{%.Remark%}</td>
|
||||
<td class="operation">
|
||||
<a class="ui purple button" href="/host/edit/{{{.Id}}}">编辑</a>
|
||||
<button class="ui positive button" onclick="util.removeConfirm('/host/remove/{{{.Id}}}')">删除</button><br>
|
||||
{%if $.IsAdmin%}
|
||||
<a class="ui purple button" href="/host/edit/{%.Id%}">编辑</a>
|
||||
<button class="ui positive button" onclick="util.removeConfirm('/host/remove/{%.Id%}')">删除</button><br>
|
||||
{%end%}
|
||||
<div style="margin-top: 5px;">
|
||||
<a class="ui twitter button" href="/task?host_id={{{.Id}}}">查看任务</a>
|
||||
<button class="ui blue button" @click="ping({{{.Id}}})">连接测试</button>
|
||||
<a class="ui twitter button" href="/task?host_id={%.Id%}">查看任务</a>
|
||||
{%if $.IsAdmin%}
|
||||
<button class="ui blue button" @click="ping({%.Id%})">连接测试</button>
|
||||
{%end%}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{{end}}}
|
||||
{%end%}
|
||||
</tbody>
|
||||
</table>
|
||||
{{{ template "common/pagination" .}}}
|
||||
{% template "common/pagination" .%}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
var Vue = new Vue({
|
||||
el: '.ui.striped.table',
|
||||
el: '.ui.celled.table',
|
||||
methods: {
|
||||
ping: function(id) {
|
||||
util.get("/host/ping/" + id, function(code, message) {
|
||||
@@ -77,4 +83,4 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{{{ template "common/footer" . }}}
|
||||
{% template "common/footer" . %}
|
||||
@@ -1,8 +1,8 @@
|
||||
<div class="four wide column">
|
||||
<div class="verticalMenu">
|
||||
<div class="ui vertical pointing menu fluid">
|
||||
<a class="{{{if eq .URI "/host"}}}active teal{{{end}}} item" href="/host">
|
||||
<i class="linux icon"></i> 主机列表
|
||||
<a class="{%if eq .URI "/host"%}active teal{%end%} item" href="/host">
|
||||
<i class="linux icon"></i> 节点列表
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{{ template "common/header" . }}}
|
||||
{% template "common/header" . %}
|
||||
|
||||
<div class="ui grid">
|
||||
{{{ template "install/menu" . }}}
|
||||
{% template "install/menu" . %}
|
||||
<div class="ten wide column">
|
||||
<form class="ui form">
|
||||
<div class="ui blue center aligned segment">
|
||||
@@ -186,4 +186,4 @@
|
||||
|
||||
</script>
|
||||
|
||||
{{{ template "common/footer" . }}}
|
||||
{% template "common/footer" . %}
|
||||
@@ -1,7 +1,7 @@
|
||||
{{{ template "common/header" . }}}
|
||||
{% template "common/header" . %}
|
||||
|
||||
<div class="ui grid">
|
||||
{{{ template "manage/menu" . }}}
|
||||
{% template "manage/menu" . %}
|
||||
|
||||
<div class="twelve wide column">
|
||||
<div class="pageHeader">
|
||||
@@ -22,18 +22,18 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{{range $i, $v := .LoginLogs}}}
|
||||
{%range $i, $v := .LoginLogs%}
|
||||
<tr>
|
||||
<td>{{{.Username}}}</td>
|
||||
<td>{{{.Ip}}}</td>
|
||||
<td>{{{.Created}}}</td>
|
||||
<td>{%.Username%}</td>
|
||||
<td>{%.Ip%}</td>
|
||||
<td>{%.Created%}</td>
|
||||
</tr>
|
||||
{{{end}}}
|
||||
{%end%}
|
||||
</tbody>
|
||||
</table>
|
||||
{{{ template "common/pagination" .}}}
|
||||
{% template "common/pagination" .%}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{{{ template "common/footer" . }}}
|
||||
{% template "common/footer" . %}
|
||||
+13
-13
@@ -1,12 +1,12 @@
|
||||
{{{ template "common/header" . }}}
|
||||
{% template "common/header" . %}
|
||||
<div class="ui grid">
|
||||
{{{template "manage/menu" .}}}
|
||||
{%template "manage/menu" .%}
|
||||
<div class="twelve wide column">
|
||||
<div class="pageHeader">
|
||||
<div class="segment">
|
||||
<h3 class="ui dividing header">
|
||||
<div class="content">
|
||||
{{{.Title}}}
|
||||
{%.Title%}
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
@@ -19,7 +19,7 @@
|
||||
SMTP服务器
|
||||
</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="host" value="{{{.Mail.Host}}}">
|
||||
<input type="text" name="host" value="{%.Mail.Host%}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
@@ -27,7 +27,7 @@
|
||||
端口
|
||||
</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="port" value="{{{if gt .Mail.Port 0}}}{{{.Mail.Port}}}{{{end}}}">
|
||||
<input type="text" name="port" value="{%if gt .Mail.Port 0%}{%.Mail.Port%}{%end%}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
@@ -35,7 +35,7 @@
|
||||
用户名
|
||||
</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="user" value="{{{.Mail.User}}}">
|
||||
<input type="text" name="user" value="{%.Mail.User%}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
@@ -43,25 +43,25 @@
|
||||
密码
|
||||
</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="password" value="{{{.Mail.Password}}}">
|
||||
<input type="text" name="password" value="{%.Mail.Password%}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui primary button">保存</button>
|
||||
{{{if .Mail.Host}}}
|
||||
{%if .Mail.Host%}
|
||||
<a class="ui green button" onclick="clearMailServer()">删除</a>
|
||||
{{{end}}}
|
||||
{%end%}
|
||||
<br><br><br>
|
||||
<div>
|
||||
<div class="content">邮箱用户</div><p></p>
|
||||
<div class="fields">
|
||||
{{{range $i, $v := .Mail.MailUsers}}}
|
||||
{%range $i, $v := .Mail.MailUsers%}
|
||||
<div class="field">
|
||||
<div class="ui segment">
|
||||
{{{.Username}}}-{{{.Email}}} <div class="ui blue button" onclick="removeMailUser({{{.Id}}})">删除</div>
|
||||
{%.Username%}-{%.Email%} <div class="ui blue button" onclick="removeMailUser({%.Id%})">删除</div>
|
||||
</div>
|
||||
</div>
|
||||
{{{end}}}
|
||||
{%end%}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -198,4 +198,4 @@
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{{ template "common/footer" . }}}
|
||||
{% template "common/footer" . %}
|
||||
@@ -1,13 +1,13 @@
|
||||
<div class="three wide column">
|
||||
<div class="verticalMenu">
|
||||
<div class="ui vertical pointing menu fluid">
|
||||
<a class="{{{if eq .URI "/manage/slack/edit"}}}active teal{{{end}}} item" href="/manage/slack/edit">
|
||||
<a class="{%if eq .URI "/manage/slack/edit"%}active teal{%end%} item" href="/manage/slack/edit">
|
||||
<i class="slack icon"></i> Slack配置
|
||||
</a>
|
||||
<a class="{{{if eq .URI "/manage/mail/edit"}}}active teal{{{end}}} item" href="/manage/mail/edit">
|
||||
<a class="{%if eq .URI "/manage/mail/edit"%}active teal{%end%} item" href="/manage/mail/edit">
|
||||
<i class="slack icon"></i> 邮件配置
|
||||
</a>
|
||||
<a class="{{{if eq .URI "/manage/login-log"}}}active teal{{{end}}} item" href="/manage/login-log">
|
||||
<a class="{%if eq .URI "/manage/login-log"%}active teal{%end%} item" href="/manage/login-log">
|
||||
<i class="slack icon"></i> 登录日志
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{{{ template "common/header" . }}}
|
||||
{% template "common/header" . %}
|
||||
<div class="ui grid">
|
||||
{{{template "manage/menu" .}}}
|
||||
{%template "manage/menu" .%}
|
||||
<div class="twelve wide column">
|
||||
<div class="pageHeader">
|
||||
<div class="segment">
|
||||
<h3 class="ui dividing header">
|
||||
<div class="content">
|
||||
{{{.Title}}}
|
||||
{%.Title%}
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="content">Slack WebHook URL</div>
|
||||
</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" id="url" value="{{{.Slack.Url}}}">
|
||||
<input type="text" id="url" value="{%.Slack.Url%}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui primary button" @click="updateUrl">保存</div>
|
||||
@@ -27,13 +27,13 @@
|
||||
<div>
|
||||
<div class="content">Slack Channel(配置任务通知时,可选择多Channel)</div><p></p>
|
||||
<div class="fields">
|
||||
{{{range $i, $v := .Slack.Channels}}}
|
||||
{%range $i, $v := .Slack.Channels%}
|
||||
<div class="field">
|
||||
<div class="ui segment">
|
||||
{{{.Name}}} <div class="ui blue button" @click="removeChannel({{{.Id}}})">删除</div>
|
||||
{%.Name%} <div class="ui blue button" @click="removeChannel({%.Id%})">删除</div>
|
||||
</div>
|
||||
</div>
|
||||
{{{end}}}
|
||||
{%end%}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui facebook button" @click="createChannel">新增Channel</div>
|
||||
@@ -85,4 +85,4 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{{ template "common/footer" . }}}
|
||||
{% template "common/footer" . %}
|
||||
+156
-58
@@ -1,8 +1,9 @@
|
||||
{{{ template "common/header" . }}}
|
||||
{% template "common/header" . %}
|
||||
<div class="ui grid">
|
||||
{{{template "task/menu" .}}}
|
||||
{%template "task/menu" .%}
|
||||
<div class="twelve wide column">
|
||||
<div class="pageHeader">
|
||||
{%if .IsAdmin%}
|
||||
<div class="pageHeader">
|
||||
<div class="segment">
|
||||
<h3 class="ui dividing header">
|
||||
<a href="/task/create">
|
||||
@@ -14,34 +15,40 @@
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
{%end%}
|
||||
<form class="ui form">
|
||||
<div class="six fields search">
|
||||
<div class="one wide field">
|
||||
<input type="text" placeholder="任务ID" name="id" value="{{{if gt .Params.Id 0}}}{{{.Params.Id}}}{{{end}}}">
|
||||
<input type="text" placeholder="任务ID" name="id" value="{%if gt .Params.Id 0%}{%.Params.Id%}{%end%}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<input type="text" placeholder="任务名称" name="name" value="{{{.Params.Name}}}">
|
||||
<input type="text" placeholder="任务名称" name="name" value="{%.Params.Name%}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<input type="text" placeholder="标签名称" name="tag" value="{%.Params.Tag%}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="six fields search">
|
||||
<div class="field">
|
||||
<select name="host_id" id="hostId">
|
||||
<option value="">选择主机</option>
|
||||
{{{range $i, $v := .Hosts}}}
|
||||
<option value="{{{.Id}}}" {{{if eq $.Params.HostId .Id }}} selected {{{end}}} >{{{.Alias}}}-{{{.Name}}}</option>
|
||||
{{{end}}}
|
||||
<option value="">选择节点</option>
|
||||
{%range $i, $v := .Hosts%}
|
||||
<option value="{%.Id%}" {%if eq $.Params.HostId .Id %} selected {%end%} >{%.Alias%}-{%.Name%}</option>
|
||||
{%end%}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<select name="protocol" id="protocol">
|
||||
<option value="0">执行方式</option>
|
||||
<option value="2" {{{if eq .Params.Protocol 2}}}selected{{{end}}} data-match="host_id" data-validate-type="selectProtocol">SHELL</option>
|
||||
<option value="1" {{{if eq .Params.Protocol 1}}}selected{{{end}}}>HTTP</option>
|
||||
<option value="2" {%if eq .Params.Protocol 2%}selected{%end%} data-match="host_id" data-validate-type="selectProtocol">SHELL</option>
|
||||
<option value="1" {%if eq .Params.Protocol 1%}selected{%end%}>HTTP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<select name="status">
|
||||
<option value="0">状态</option>
|
||||
<option value="1" {{{if eq .Params.Status 0}}}selected{{{end}}} >停止</option>
|
||||
<option value="2" {{{if eq .Params.Status 1}}}selected{{{end}}}>激活</option>
|
||||
<option value="1" {%if eq .Params.Status 0%}selected{%end%} >停止</option>
|
||||
<option value="2" {%if eq .Params.Status 1%}selected{%end%}>激活</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
@@ -49,84 +56,143 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<table class="ui pink table task-list">
|
||||
{%if .IsAdmin%}
|
||||
<div class="field">
|
||||
<select id="batch-operation">
|
||||
<option value="0">批量操作</option>
|
||||
<option value="1">激活</option>
|
||||
<option value="2">停止</option>
|
||||
<option value="3">删除</option>
|
||||
</select>
|
||||
</div>
|
||||
{%end%}
|
||||
<br>
|
||||
<table class="ui celled table task-list">
|
||||
<thead>
|
||||
<tr>
|
||||
{%if .IsAdmin%}
|
||||
<th>
|
||||
<input type="checkbox" onclick="checkAll(this)" style="width:25px;height: 25px;">
|
||||
</th>
|
||||
{%end%}
|
||||
<th>任务ID</th>
|
||||
<th>任务名称</th>
|
||||
<th>任务类型</th>
|
||||
<th>标签</th>
|
||||
<th>cron表达式</th>
|
||||
<th>执行方式</th>
|
||||
<th>超时时间</th>
|
||||
<th>重试次数</th>
|
||||
<th>单实例运行</th>
|
||||
<th>主机</th>
|
||||
<th>任务节点</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{{range $i, $v := .Tasks}}}
|
||||
{%range $i, $v := .Tasks%}
|
||||
<tr>
|
||||
<td>{{{.Id}}}</td>
|
||||
<td>{{{.Task.Name}}}</td>
|
||||
<td>{{{if eq .Level 1}}}主任务{{{else}}}子任务{{{end}}}</td>
|
||||
<td>{{{.Spec}}}</td>
|
||||
<td>{{{if eq .Protocol 1}}} HTTP {{{else if eq .Protocol 2}}} SHELL {{{end}}}</td>
|
||||
<td>{{{if eq .Timeout -1}}}后台运行{{{else if gt .Timeout 0}}}{{{.Timeout}}}秒{{{else}}}不限制{{{end}}}</td>
|
||||
<td>{{{.RetryTimes}}}</td>
|
||||
<td>{{{if gt .Multi 0}}}否{{{else}}}是{{{end}}}</td>
|
||||
<td>{{{.Alias}}}-{{{.Name}}}</td>
|
||||
{%if $.IsAdmin%}
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
class="sub-check"
|
||||
data-id="{%.Id%}"
|
||||
style="width:25px;height: 25px;">
|
||||
</td>
|
||||
{%end%}
|
||||
<td>{%.Id%}</td>
|
||||
<td>{%.Name%}</td>
|
||||
<td>{%if eq .Level 1%}主任务{%else%}子任务{%end%}</td>
|
||||
<td>{%.Tag%}</td>
|
||||
<td>{%.Spec%}</td>
|
||||
<td>{%if eq .Protocol 1%} HTTP {%else if eq .Protocol 2%} SHELL {%end%}</td>
|
||||
<td>{%if eq .Timeout -1%}后台运行{%else if gt .Timeout 0%}{%.Timeout%}秒{%else%}不限制{%end%}</td>
|
||||
<td>{%.RetryTimes%}</td>
|
||||
<td>{%if gt .Multi 0%}否{%else%}是{%end%}</td>
|
||||
<td>
|
||||
{{{if eq .Level 1}}}
|
||||
{{{if eq .Status 1}}}<span style="color: green;">激活</span>{{{else}}}<span style="color: red;">停止<span>{{{end}}}
|
||||
{{{end}}}
|
||||
{%range $k, $h := .Hosts%}
|
||||
{%$h.Alias%}<br>
|
||||
{%end%}
|
||||
</td>
|
||||
<td>
|
||||
{%if eq .Level 1%}
|
||||
{%if eq .Status 1%}<span><i class="checkmark big icon"></i></span>{%else%}<span><i class="minus big icon"></i><span>{%end%}
|
||||
{%end%}
|
||||
</td>
|
||||
<td>
|
||||
<div class="ui buttons operation">
|
||||
<a class="ui purple button" href="/task/edit/{{{.Id}}}">编辑</a>
|
||||
{{{if eq .Level 1}}}
|
||||
{{{if eq .Status 1}}}
|
||||
<button class="ui primary button" @click="changeStatus({{{.Id}}},{{{.Status}}})">停止</button>
|
||||
{{{else}}}
|
||||
<button class="ui blue button" @click="changeStatus({{{.Id}}},{{{.Status}}})">激活 </button>
|
||||
{{{end}}}
|
||||
{{{end}}}
|
||||
<button class="ui positive button" @click="remove({{{.Id}}})">删除</button> <br>
|
||||
<button class="ui twitter button" @click="run({{{.Id}}})">手动运行</button>
|
||||
<a class="ui instagram button" href="/task/log?task_id={{{.Id}}}">查看日志</a>
|
||||
{%if $.IsAdmin%}
|
||||
<a href="/task/edit/{%.Id%}" ><i class="edit big icon" title="编辑"></i></a>
|
||||
{%if eq .Level 1%}
|
||||
{%if eq .Status 1%}
|
||||
<a href="javascript:void(0);" @click="changeStatus({%.Id%},{%.Status%})"><i class="pause circle big icon" title="停止"></i></a>
|
||||
{%else%}
|
||||
<a href="javascript:void(0);" @click="changeStatus({%.Id%},{%.Status%})"><i class="play big icon" title="激活"></i></a>
|
||||
{%end%}
|
||||
{%end%}
|
||||
<a href="javascript:void(0);" @click="remove({%.Id%})"><i class="remove big icon" title="删除"></i></a>
|
||||
<a href="javascript:void(0);" @click="run({%.Id%})"><i class="rocket big icon" title="手动执行"></i></a>
|
||||
{%end%}
|
||||
|
||||
<a href="/task/log?task_id={%.Id%}"><i class="bar chart icon big" title="查看日志"></i></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{{end}}}
|
||||
{%end%}
|
||||
</tbody>
|
||||
</table>
|
||||
{{{ template "common/pagination" .}}}
|
||||
{% template "common/pagination" .%}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
$('.ui.checkbox').checkbox();
|
||||
|
||||
$('#batch-operation').change(function() {
|
||||
var type = $(this).val();
|
||||
if (type == 0) {
|
||||
return;
|
||||
}
|
||||
var ids = [];
|
||||
$('.sub-check:checked').each(function() {
|
||||
ids.push($(this).data('id'));
|
||||
});
|
||||
if (ids.length == 0) {
|
||||
swal('错误提示', '至少选择一个任务', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
util.confirm("确定要执行此操作吗", function () {
|
||||
$.ajaxSetup({
|
||||
async: false
|
||||
});
|
||||
switch (type) {
|
||||
case "1":
|
||||
for (i in ids) {
|
||||
changeStatus(ids[i], false, true);
|
||||
}
|
||||
break;
|
||||
case "2":
|
||||
for (i in ids) {
|
||||
changeStatus(ids[i], true, true);
|
||||
}
|
||||
break;
|
||||
case "3":
|
||||
for (i in ids) {
|
||||
remove(ids[i], true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
|
||||
var vue = new Vue(
|
||||
{
|
||||
el: '.task-list',
|
||||
methods: {
|
||||
changeStatus: function (id ,status) {
|
||||
var url = '';
|
||||
if (status) {
|
||||
url = '/task/disable';
|
||||
} else {
|
||||
url = '/task/enable';
|
||||
}
|
||||
url += '/' + id;
|
||||
util.post(url,{}, function() {
|
||||
location.reload();
|
||||
});
|
||||
},
|
||||
remove: function(id) {
|
||||
util.removeConfirm('/task/remove/' + id);
|
||||
},
|
||||
changeStatus: changeStatus,
|
||||
remove: remove,
|
||||
run: function(id) {
|
||||
util.get("/task/run/" + id, function(code, message) {
|
||||
swal('操作成功', message, 'success');
|
||||
@@ -136,9 +202,41 @@
|
||||
}
|
||||
);
|
||||
|
||||
function checkAll(ele) {
|
||||
if ($(ele).is(":checked")) {
|
||||
$('.sub-check').prop("checked", true);
|
||||
} else {
|
||||
$('.sub-check').prop("checked", false);
|
||||
}
|
||||
}
|
||||
|
||||
function remove(id, stopReload) {
|
||||
var url = '/task/remove/' + id;
|
||||
if (stopReload === undefined) {
|
||||
util.removeConfirm(url);
|
||||
return;
|
||||
}
|
||||
util.post(url, {}, function () {
|
||||
if (stopReload === undefined) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function changeStatus(id ,status, stopReload) {
|
||||
var url = '';
|
||||
if (status) {
|
||||
url = '/task/disable';
|
||||
} else {
|
||||
url = '/task/enable';
|
||||
}
|
||||
url += '/' + id;
|
||||
util.post(url,{}, function() {
|
||||
if (stopReload === undefined) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{{{ template "common/footer" . }}}
|
||||
{% template "common/footer" . %}
|
||||
|
||||
+54
-41
@@ -1,4 +1,4 @@
|
||||
{{{ template "common/header" . }}}
|
||||
{% template "common/header" . %}
|
||||
<style type="text/css">
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
@@ -10,9 +10,10 @@
|
||||
</style>
|
||||
<div class="ui grid">
|
||||
<!--the vertical menu-->
|
||||
{{{ template "task/menu" . }}}
|
||||
{% template "task/menu" . %}
|
||||
|
||||
<div class="twelve wide column">
|
||||
{%if .IsAdmin%}
|
||||
<div class="pageHeader">
|
||||
<div class="segment">
|
||||
<h3 class="ui dividing header">
|
||||
@@ -22,25 +23,26 @@
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
{%end%}
|
||||
<form class="ui form">
|
||||
<div class="six fields search">
|
||||
<div class="field">
|
||||
<input type="text" placeholder="任务ID" name="task_id" value="{{{if gt .Params.TaskId 0}}}{{{.Params.TaskId}}}{{{end}}}">
|
||||
<input type="text" placeholder="任务ID" name="task_id" value="{%if gt .Params.TaskId 0%}{%.Params.TaskId%}{%end%}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<select name="protocol" id="protocol">
|
||||
<option value="0">执行方式</option>
|
||||
<option value="2" {{{if eq .Params.Protocol 2}}}selected{{{end}}} data-match="host_id" data-validate-type="selectProtocol">SHELL</option>
|
||||
<option value="1" {{{if eq .Params.Protocol 1}}}selected{{{end}}}>HTTP</option>
|
||||
<option value="2" {%if eq .Params.Protocol 2%}selected{%end%} data-match="host_id" data-validate-type="selectProtocol">SHELL</option>
|
||||
<option value="1" {%if eq .Params.Protocol 1%}selected{%end%}>HTTP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<select name="status">
|
||||
<option value="0">状态</option>
|
||||
<option value="1" {{{if eq .Params.Status 0}}}selected{{{end}}} >失败</option>
|
||||
<option value="2" {{{if eq .Params.Status 1}}}selected{{{end}}}>执行中</option>
|
||||
<option value="3" {{{if eq .Params.Status 2}}}selected{{{end}}}>成功</option>
|
||||
<option value="4" {{{if eq .Params.Status 3}}}selected{{{end}}}>取消</option>
|
||||
<option value="1" {%if eq .Params.Status 0%}selected{%end%} >失败</option>
|
||||
<option value="2" {%if eq .Params.Status 1%}selected{%end%}>执行中</option>
|
||||
<option value="3" {%if eq .Params.Status 2%}selected{%end%}>成功</option>
|
||||
<option value="4" {%if eq .Params.Status 3%}selected{%end%}>取消</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
@@ -48,62 +50,69 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<table class="ui pink table">
|
||||
<table class="ui celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>任务ID</th>
|
||||
<th>任务名称</th>
|
||||
<th>cron表达式</th>
|
||||
<th>协议</th>
|
||||
<th>执行方式</th>
|
||||
<th>重试次数</th>
|
||||
<th>主机</th>
|
||||
<th>任务节点</th>
|
||||
<th>执行时长</th>
|
||||
<th>状态</th>
|
||||
<th>执行结果</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{{range $i, $v := .Logs}}}
|
||||
{%range $i, $v := .Logs%}
|
||||
<tr>
|
||||
<td><a href="/task?id={{{.TaskId}}}">{{{.TaskId}}}</a></td>
|
||||
<td>{{{.Name}}}</td>
|
||||
<td>{{{.Spec}}}</td>
|
||||
<td>{{{if eq .Protocol 1}}} HTTP {{{else if eq .Protocol 2}}} SHELL {{{end}}}</td>
|
||||
<td>{{{.RetryTimes}}}</td>
|
||||
<td>{{{.Hostname}}}</td>
|
||||
<td><a href="/task?id={%.TaskId%}">{%.TaskId%}</a></td>
|
||||
<td>{%.Name%}</td>
|
||||
<td>{%.Spec%}</td>
|
||||
<td>{%if eq .Protocol 1%} HTTP {%else if eq .Protocol 2%} SHELL {%end%}</td>
|
||||
<td>{%.RetryTimes%}</td>
|
||||
<td>{%unescape .Hostname%}</td>
|
||||
<td>
|
||||
{{{if and (ne .Status 3) (ne .Status 4)}}}
|
||||
{{{if gt .TotalTime 0}}}{{{.TotalTime}}}秒{{{else}}}1秒{{{end}}}<br>
|
||||
开始时间: {{{.StartTime.Format "2006-01-02 15:04:05" }}}<br>
|
||||
{{{if ne .Status 1}}}
|
||||
结束时间: {{{.EndTime.Format "2006-01-02 15:04:05" }}}
|
||||
{{{end}}}
|
||||
{{{end}}}
|
||||
{%if and (ne .Status 3) (ne .Status 4)%}
|
||||
{%if gt .TotalTime 0%}{%.TotalTime%}秒{%else%}1秒{%end%}<br>
|
||||
开始时间: {%.StartTime.Format "2006-01-02 15:04:05" %}<br>
|
||||
{%if ne .Status 1%}
|
||||
结束时间: {%.EndTime.Format "2006-01-02 15:04:05" %}
|
||||
{%end%}
|
||||
{%end%}
|
||||
</td>
|
||||
<td>
|
||||
{{{if eq .Status 2}}}
|
||||
{%if eq .Status 2%}
|
||||
成功
|
||||
{{{else if eq .Status 1}}}
|
||||
{%else if eq .Status 1%}
|
||||
<span style="color:green">执行中</span>
|
||||
{{{else if eq .Status 0}}}
|
||||
{%else if eq .Status 0%}
|
||||
<span style="color:red">失败</span>
|
||||
{{{else if eq .Status 3}}}
|
||||
{%else if eq .Status 3%}
|
||||
<span style="color:#4499EE">取消</span>
|
||||
{{{end}}}
|
||||
{%end%}
|
||||
</td>
|
||||
<td>
|
||||
{{{if or (eq .Status 2) (eq .Status 0)}}}
|
||||
{%if or (eq .Status 2) (eq .Status 0)%}
|
||||
<button class="ui small primary button"
|
||||
onclick="showResult('{{{.Name}}}', '{{{.Command}}}', '{{{.Result}}}')"
|
||||
onclick="showResult('{%.Name%}', '{%.Command%}', '{%.Result%}')"
|
||||
>查看结果
|
||||
</button>
|
||||
{{{end}}}
|
||||
{%end%}
|
||||
|
||||
|
||||
{%if $.IsAdmin%}
|
||||
{%if and (eq .Status 1) (eq .Protocol 2) %}
|
||||
<button class="ui small blue button" onclick="stopTask({%.Id%}, {%.TaskId%})">停止任务</button>
|
||||
{%end%}
|
||||
{%end%}
|
||||
</td>
|
||||
</tr>
|
||||
{{{end}}}
|
||||
{%end%}
|
||||
</tbody>
|
||||
</table>
|
||||
{{{ template "common/pagination" .}}}
|
||||
{% template "common/pagination" .%}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -127,10 +136,6 @@
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
function showTime(startTime, endTime, status) {
|
||||
|
||||
}
|
||||
|
||||
function showResult(name, command,result) {
|
||||
$('.message').html($('#task-result').html());
|
||||
new Vue(
|
||||
@@ -150,6 +155,14 @@
|
||||
}).modal('refresh').modal('show');
|
||||
}
|
||||
|
||||
function stopTask(id, taskId) {
|
||||
util.confirm("确定要停止任务吗", function () {
|
||||
util.post("/task/log/stop/", {id: id, task_id:taskId}, function (code, message) {
|
||||
swal('提示', message, 'info');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
util.confirm("确定要删除所有日志吗?", function() {
|
||||
util.post("/task/log/clear",{}, function() {
|
||||
@@ -158,4 +171,4 @@
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{{ template "common/footer" . }}}
|
||||
{% template "common/footer" . %}
|
||||
@@ -1,10 +1,10 @@
|
||||
<div class="three wide column">
|
||||
<div class="verticalMenu">
|
||||
<div class="ui vertical pointing menu fluid">
|
||||
<a class="{{{if eq .URI "/task"}}}active teal{{{end}}} item" href="/task">
|
||||
<a class="{%if eq .URI "/task"%}active teal{%end%} item" href="/task">
|
||||
<i class="tasks icon"></i> 定时任务列表
|
||||
</a>
|
||||
<a class="item {{{if eq .URI "/task/log"}}}active teal{{{end}}} " href="/task/log">
|
||||
<a class="item {%if eq .URI "/task/log"%}active teal{%end%} " href="/task/log">
|
||||
<i class="bar chart icon"></i> 定时任务日志
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
{{{ template "common/header" . }}}
|
||||
{% template "common/header" . %}
|
||||
<div class="ui grid">
|
||||
{{{template "task/menu" .}}}
|
||||
{%template "task/menu" .%}
|
||||
<div class="twelve wide column">
|
||||
<div class="pageHeader">
|
||||
<div class="segment">
|
||||
<h3 class="ui dividing header">
|
||||
<div class="content">
|
||||
{{{.Title}}}
|
||||
{%.Title%}
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<form class="ui form fluid vertical segment">
|
||||
<input type="hidden" name="id" value="{{{.Task.Id}}}">
|
||||
<input type="hidden" name="id" value="{%.Task.Id%}">
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>
|
||||
<div class="content">任务名称</div>
|
||||
</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="name" placeholder="订单量统计" value="{{{.Task.Task.Name}}}">
|
||||
<input type="text" name="name" placeholder="任务名称" value="{%.Task.Name%}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>
|
||||
<div class="content">标签名称</div>
|
||||
</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="tag" placeholder="标签用于任务分类" value="{%.Task.Tag%}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -32,9 +41,9 @@
|
||||
任务类型新增后不能变更
|
||||
</div>
|
||||
</label>
|
||||
<select name="level" id="level" {{{if .Task}}}disabled="disabled"{{{end}}}>
|
||||
<option value="1" {{{if .Task}}} {{{if eq .Task.Level 1}}}selected{{{end}}} {{{end}}}>主任务</option>
|
||||
<option value="2" {{{if .Task}}} {{{if eq .Task.Level 2}}}selected{{{end}}} {{{end}}}>子任务</option>
|
||||
<select name="level" id="level" {%if .Task%}disabled="disabled"{%end%}>
|
||||
<option value="1" {%if .Task%} {%if eq .Task.Level 1%}selected{%end%} {%end%}>主任务</option>
|
||||
<option value="2" {%if .Task%} {%if eq .Task.Level 2%}selected{%end%} {%end%}>子任务</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,8 +58,8 @@
|
||||
</div>
|
||||
</label>
|
||||
<select name="dependency_status" id="dependency_status">
|
||||
<option value="1" {{{if .Task}}} {{{if eq .Task.DependencyStatus 1}}}selected{{{end}}} {{{end}}}>强依赖</option>
|
||||
<option value="2" {{{if .Task}}} {{{if eq .Task.DependencyStatus 2}}}selected{{{end}}} {{{end}}}>弱依赖</option>
|
||||
<option value="1" {%if .Task%} {%if eq .Task.DependencyStatus 1%}selected{%end%} {%end%}>强依赖</option>
|
||||
<option value="2" {%if .Task%} {%if eq .Task.DependencyStatus 2%}selected{%end%} {%end%}>弱依赖</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
@@ -62,7 +71,7 @@
|
||||
</div>
|
||||
</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="dependency_task_id" placeholder="可选" value="{{{.Task.DependencyTaskId}}}">
|
||||
<input type="text" name="dependency_task_id" placeholder="可选" value="{%.Task.DependencyTaskId%}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,7 +83,7 @@
|
||||
</div>
|
||||
</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="spec" value="{{{.Task.Spec}}}" placeholder="秒 分 时 天 月 周"/>
|
||||
<input type="text" name="spec" value="{%.Task.Spec%}" placeholder="秒 分 时 天 月 周"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,54 +92,74 @@
|
||||
<div class="field">
|
||||
<label>执行方式</label>
|
||||
<select name="protocol" id="protocol">
|
||||
<option value="2" {{{if .Task}}} {{{if eq .Task.Protocol 2}}}selected{{{end}}} {{{end}}} data-match="host_id" data-validate-type="selectProtocol">SHELL</option>
|
||||
<option value="1" {{{if .Task}}} {{{if eq .Task.Protocol 1}}}selected{{{end}}} {{{end}}}>HTTP</option>
|
||||
<option value="2" {%if .Task%} {%if eq .Task.Protocol 2%}selected{%end%} {%end%}
|
||||
data-validate-type="selectProtocol">SHELL</option>
|
||||
<option value="1" {%if .Task%} {%if eq .Task.Protocol 1%}selected{%end%} {%end%}>HTTP</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="three fields" id="hostField">
|
||||
<div class="fields" id="hostField">
|
||||
<div class="field">
|
||||
<label>任务执行器</label>
|
||||
<select name="host_id" id="hostId">
|
||||
<option value="">选择主机</option>
|
||||
{{{range $i, $v := .Hosts}}}
|
||||
<option value="{{{.Id}}}" {{{if $.Task}}}{{{if eq $.Task.HostId .Id }}} selected {{{end}}} {{{end}}}>{{{.Alias}}}-{{{.Name}}}</option>
|
||||
{{{end}}}
|
||||
</select> <a class="ui blue button" href="/host/create" target="_blank">添加主机</a>
|
||||
<label>选择任务节点</label>
|
||||
<div id="hostId">
|
||||
{%range $i, $v := .Hosts%}
|
||||
<label>
|
||||
<input type="checkbox" value="{%.Id%}" {%if $.Task%}{%if $v.Selected%} checked {%end%}{%end%} style="width:25px;height: 25px;">{%.Alias%}-{%.Name%}
|
||||
{%if (HostFormat $i) %}<br>{%end%}
|
||||
</label>
|
||||
{%end%}
|
||||
</div> <br> <a class="ui blue button" href="/host/create" target="_blank">添加节点</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="two fields" id="http-method" style="display: none">
|
||||
<div class="field">
|
||||
<label>请求方法</label>
|
||||
<select name="http_method">
|
||||
<option value="1" {%if .Task%} {%if eq .Task.HttpMethod 1%}selected{%end%} {%end%}>GET</option>
|
||||
<option value="2" {%if .Task%} {%if eq .Task.HttpMethod 2%}selected{%end%} {%end%}
|
||||
data-validate-type="selectProtocol">POST</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>命令</label>
|
||||
<textarea rows="5" name="command" placeholder="请输入系统命令" id="command">{{{.Task.Command}}}</textarea>
|
||||
<textarea rows="5" name="command" placeholder="请输入系统命令" id="command">{%.Task.Command%}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="three fields">
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>任务超时时间(秒, 0-86400)</label>
|
||||
<input type="text" name="timeout" placeholder="默认0, 不限制" value="{{{if .Task}}} {{{.Task.Timeout}}} {{{else}}} 0 {{{end}}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>任务失败重试次数 (0-10)</label>
|
||||
<input type="text" name="retry_times" placeholder="默认0, 不重试" value="{{{if .Task}}} {{{.Task.RetryTimes}}} {{{else}}} 0 {{{end}}}">
|
||||
<input type="text" name="timeout" placeholder="默认0, 不限制" value="{%if .Task%} {%.Task.Timeout%} {%else%}0{%end%}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>允许多实例同时运行</label>
|
||||
<select name="multi">
|
||||
<option value="2" {{{if .Task}}} {{{if eq .Task.Multi 0}}}selected{{{end}}} {{{end}}}>否</option>
|
||||
<option value="1"{{{if .Task}}} {{{if eq .Task.Multi 1}}}selected{{{end}}} {{{end}}}>是</option>
|
||||
<option value="2" {%if .Task%} {%if eq .Task.Multi 0%}selected{%end%} {%end%}>否</option>
|
||||
<option value="1"{%if .Task%} {%if eq .Task.Multi 1%}selected{%end%} {%end%}>是</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>任务失败重试次数 (0-10)</label>
|
||||
<input type="text" name="retry_times" placeholder="默认0, 不重试" value="{%if .Task%} {%.Task.RetryTimes%} {%else%}0{%end%}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>任务失败重试间隔时间 (秒, 0-3600)</label>
|
||||
<input type="text" name="retry_interval" placeholder="默认0, 执行默认重试策略" value="{%if .Task%} {%.Task.RetryInterval%} {%else%}0{%end%}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>任务通知</label>
|
||||
<select name="notify_status" id="task-status">
|
||||
<option value="1"{{{if .Task}}} {{{if eq .Task.NotifyStatus 0}}}selected{{{end}}} {{{end}}}>不通知</option>
|
||||
<option value="2" {{{if .Task}}} {{{if eq .Task.NotifyStatus 1}}}selected{{{end}}} {{{end}}}>失败通知</option>
|
||||
<option value="3" {{{if .Task}}} {{{if eq .Task.NotifyStatus 2}}}selected{{{end}}} {{{end}}}>执行结束通知</option>
|
||||
<option value="1"{%if .Task%} {%if eq .Task.NotifyStatus 0%}selected{%end%} {%end%}>不通知</option>
|
||||
<option value="2" {%if .Task%} {%if eq .Task.NotifyStatus 1%}selected{%end%} {%end%}>失败通知</option>
|
||||
<option value="3" {%if .Task%} {%if eq .Task.NotifyStatus 2%}selected{%end%} {%end%}>执行结束通知</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,9 +167,9 @@
|
||||
<div class="field" >
|
||||
<label>通知类型</label>
|
||||
<select name="notify_type">
|
||||
<option value="1"{{{if .Task}}} {{{if eq .Task.NotifyType 0}}}selected{{{end}}} {{{end}}}>请选择</option>
|
||||
<option value="2" {{{if .Task}}} {{{if eq .Task.NotifyType 1}}}selected{{{end}}} {{{end}}}>邮件</option>
|
||||
<option value="3" {{{if .Task}}} {{{if eq .Task.NotifyType 2}}}selected{{{end}}} {{{end}}}>Slack</option>
|
||||
<option value="1"{%if .Task%} {%if eq .Task.NotifyType 0%}selected{%end%} {%end%}>请选择</option>
|
||||
<option value="2" {%if .Task%} {%if eq .Task.NotifyType 1%}selected{%end%} {%end%}>邮件</option>
|
||||
<option value="3" {%if .Task%} {%if eq .Task.NotifyType 2%}selected{%end%} {%end%}>Slack</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,7 +177,7 @@
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>备注</label>
|
||||
<textarea rows="5" name="remark" placeholder="统计昨天的订单量">{{{.Task.Remark}}}</textarea>
|
||||
<textarea rows="5" name="remark" placeholder="任务备注信息">{%.Task.Remark%}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui primary submit button">保存</div> <a class="ui button" onclick="location.href='/task';">取消</a>
|
||||
@@ -227,11 +256,11 @@
|
||||
}
|
||||
|
||||
function showNotify() {
|
||||
var notifyStatus = {{{.Task.NotifyStatus}}};
|
||||
var notifyStatus = {%.Task.NotifyStatus%};
|
||||
if (notifyStatus > 0) {
|
||||
$('#task-notify-type').show();
|
||||
}
|
||||
var notifyReceiverIds = '{{{.Task.NotifyReceiverId}}}'.split(',');
|
||||
var notifyReceiverIds = '{%.Task.NotifyReceiverId%}'.split(',');
|
||||
changeNotify(notifyReceiverIds);
|
||||
}
|
||||
|
||||
@@ -280,10 +309,12 @@
|
||||
var protocol = $('#protocol').val();
|
||||
if (protocol == 2) {
|
||||
$('#hostField').show();
|
||||
$('#http-method').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
$('#hostField').hide();
|
||||
$('#http-method').show();
|
||||
}
|
||||
|
||||
$('.ui.checkbox')
|
||||
@@ -313,6 +344,15 @@
|
||||
return receivers.join(",");
|
||||
}
|
||||
|
||||
function parseHostId() {
|
||||
var hostIds = [];
|
||||
$('#hostId input:checked').each(function () {
|
||||
hostIds.push($(this).val());
|
||||
});
|
||||
|
||||
return hostIds.join(",");
|
||||
}
|
||||
|
||||
function changeLevel() {
|
||||
var selected = $('#level').val();
|
||||
if (selected == 1) {
|
||||
@@ -336,6 +376,12 @@
|
||||
return false;
|
||||
}
|
||||
fields.notify_receiver_id = parseNotifyReceiver();
|
||||
fields.host_id = parseHostId();
|
||||
if (fields.protocol == 2 && fields.host_id == "") {
|
||||
swal('错误提示', '请选择任务节点');
|
||||
return false;
|
||||
}
|
||||
|
||||
util.post('/task/store', fields, function(code, message) {
|
||||
location.href = "/task"
|
||||
});
|
||||
@@ -396,6 +442,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
retryInterval: {
|
||||
identifier : 'retry_interval',
|
||||
rules: [
|
||||
{
|
||||
type : 'integer[0..3600]',
|
||||
prompt : '重试间隔时间0-3600'
|
||||
}
|
||||
]
|
||||
},
|
||||
remark: {
|
||||
identifier : 'remark',
|
||||
rules: [
|
||||
@@ -410,4 +465,4 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{{{ template "common/footer" . }}}
|
||||
{% template "common/footer" . %}
|
||||
@@ -0,0 +1,73 @@
|
||||
{% template "common/header" . %}
|
||||
<div class="ui small modal">
|
||||
<div class="header">修改密码</div>
|
||||
<div class="content">
|
||||
<form class="ui form">
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>原密码</label>
|
||||
<input class="small input" type="password" name="old_password" placeholder="原密码">
|
||||
</div>
|
||||
</div>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>新密码</label>
|
||||
<input type="password" name="new_password" placeholder="新密码">
|
||||
</div>
|
||||
</div>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>确认新密码</label>
|
||||
<input type="password" name="confirm_new_password" placeholder="确认新密码">
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui button primary button" >确定</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
$('.ui.modal').modal('setting', 'closable', false).modal('show');
|
||||
$('.ui.form').form(
|
||||
{
|
||||
onSuccess: function(event, fields) {
|
||||
util.post('/user/editMyPassword', fields, function(code, message) {
|
||||
swal("操作成功", '修改成功', 'success');
|
||||
location.href = "/task"
|
||||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
fields: {
|
||||
oldPassword: {
|
||||
identifier : 'old_password',
|
||||
rules: [
|
||||
{
|
||||
type : 'empty',
|
||||
prompt : '请输入原密码'
|
||||
}
|
||||
]
|
||||
},
|
||||
newPassword: {
|
||||
identifier : 'new_password',
|
||||
rules: [
|
||||
{
|
||||
type : 'empty',
|
||||
prompt : '请输入新密码'
|
||||
}
|
||||
]
|
||||
},
|
||||
confirmNewPassword: {
|
||||
identifier : 'confirm_new_password',
|
||||
rules: [
|
||||
{
|
||||
type : 'match[new_password]',
|
||||
prompt : '两次输入密码不匹配'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
inline : true
|
||||
});
|
||||
</script>
|
||||
{% template "common/footer" . %}
|
||||
@@ -1,14 +1,8 @@
|
||||
{{{ template "common/header" . }}}
|
||||
{% template "common/header" . %}
|
||||
<div class="ui small modal">
|
||||
<div class="header">修改密码</div>
|
||||
<div class="content">
|
||||
<form class="ui form">
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>原密码</label>
|
||||
<input class="small input" type="password" name="old_password" placeholder="原密码">
|
||||
</div>
|
||||
</div>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>新密码</label>
|
||||
@@ -21,7 +15,7 @@
|
||||
<input type="password" name="confirm_new_password" placeholder="确认新密码">
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui button primary button" >登录</button>
|
||||
<button class="ui button primary button" >确定</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,23 +25,14 @@
|
||||
$('.ui.form').form(
|
||||
{
|
||||
onSuccess: function(event, fields) {
|
||||
util.post('/user/editPassword', fields, function(code, message) {
|
||||
util.post('/user/editPassword/{%.Id%}', fields, function(code, message) {
|
||||
swal("操作成功", '修改成功', 'success');
|
||||
location.href = "/task"
|
||||
location.href = "/user"
|
||||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
fields: {
|
||||
oldPassword: {
|
||||
identifier : 'old_password',
|
||||
rules: [
|
||||
{
|
||||
type : 'empty',
|
||||
prompt : '请输入原密码'
|
||||
}
|
||||
]
|
||||
},
|
||||
newPassword: {
|
||||
identifier : 'new_password',
|
||||
rules: [
|
||||
@@ -70,4 +55,4 @@
|
||||
inline : true
|
||||
});
|
||||
</script>
|
||||
{{{ template "common/footer" . }}}
|
||||
{% template "common/footer" . %}
|
||||
@@ -0,0 +1,73 @@
|
||||
{% template "common/header" . %}
|
||||
|
||||
<div class="ui grid">
|
||||
{% template "user/menu" . %}
|
||||
|
||||
<div class="twelve wide column">
|
||||
<div class="pageHeader">
|
||||
<div class="segment">
|
||||
<h3 class="ui dividing header">
|
||||
<a href="/user/create">
|
||||
<i class="large add icon"></i>
|
||||
<div class="content">
|
||||
添加用户
|
||||
</div>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户ID</th>
|
||||
<th>用户名</th>
|
||||
<th>邮箱</th>
|
||||
<th>角色</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%range $i, $v := .Users%}
|
||||
<tr>
|
||||
<td>{%.Id%}</td>
|
||||
<td>{%.Name%}</td>
|
||||
<td>{%.Email%}</td>
|
||||
<td>{%if .IsAdmin%}管理员{%else%}普通用户{%end%}</td>
|
||||
<td>{%if .Status%}启用{%else%}禁用{%end%}</td>
|
||||
<td class="operation">
|
||||
<a class="ui purple button" href="/user/edit/{%.Id%}">编辑</a>
|
||||
{%if eq .Status 1%}
|
||||
<button class="ui button red" onclick="changeStatus({%.Id%},{%.Status%})">禁用</button>
|
||||
{%else%}
|
||||
<button class="ui button twitter" onclick="changeStatus({%.Id%},{%.Status%})">启用</button>
|
||||
{%end%}
|
||||
<a href="/user/editPassword/{%.Id%}">
|
||||
<button class="ui button facebook">修改密码</button>
|
||||
</a>
|
||||
<button class="ui positive button" onclick="util.removeConfirm('/user/remove/{%.Id%}')">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
{%end%}
|
||||
</tbody>
|
||||
</table>
|
||||
{% template "common/pagination" .%}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function changeStatus(id ,status) {
|
||||
var url = '';
|
||||
if (status) {
|
||||
url = '/user/disable';
|
||||
} else {
|
||||
url = '/user/enable';
|
||||
}
|
||||
url += '/' + id;
|
||||
util.post(url,{}, function() {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{% template "common/footer" . %}
|
||||
@@ -1,4 +1,4 @@
|
||||
{{{ template "common/header" . }}}
|
||||
{% template "common/header" . %}
|
||||
<div class="ui small modal">
|
||||
<div class="header">用户登录</div>
|
||||
<div class="content">
|
||||
@@ -13,6 +13,12 @@
|
||||
<input type="password" name="password" placeholder="密码">
|
||||
</div>
|
||||
</div>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
{%.Captcha.CreateHtml%}
|
||||
<input type="text" name="captcha" placeholder="验证码">
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui button primary button" >登录</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -25,6 +31,10 @@
|
||||
onSuccess: function(event, fields) {
|
||||
util.post('/user/login', fields, function(code, message) {
|
||||
location.href = "/"
|
||||
}, function (code, message) {
|
||||
if (code == 5) {
|
||||
$('.captcha-img').trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
@@ -47,9 +57,18 @@
|
||||
prompt : '请输入密码'
|
||||
}
|
||||
]
|
||||
},
|
||||
Captcha: {
|
||||
identifier : 'captcha',
|
||||
rules: [
|
||||
{
|
||||
type : 'empty',
|
||||
prompt : '请输入验证码'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
inline : true
|
||||
});
|
||||
</script>
|
||||
{{{ template "common/footer" . }}}
|
||||
{% template "common/footer" . %}
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="four wide column">
|
||||
<div class="verticalMenu">
|
||||
<div class="ui vertical pointing menu fluid">
|
||||
<a class="{%if eq .URI "/user"%}active teal{%end%} item" href="/user">
|
||||
<i class="user icon"></i> 用户列表
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,142 @@
|
||||
{% template "common/header" . %}
|
||||
|
||||
<div class="ui grid">
|
||||
{% template "user/menu" . %}
|
||||
|
||||
<div class="twelve wide column">
|
||||
<div class="pageHeader">
|
||||
<div class="segment">
|
||||
<h3 class="ui dividing header">
|
||||
<div class="content">
|
||||
添加用户
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<form class="ui form fluid vertical segment">
|
||||
<input type="hidden" name="id" value="{%.User.Id%}">
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>用户名</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="name" value="{%.User.Name%}" placeholder="请输入用户名">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>邮箱</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="email" value="{%.User.Email%}" placeholder="请输入邮箱">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{%if eq .User.Id 0%}
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" placeholder="请输入密码">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>确认密码</label>
|
||||
<input type="password" name="confirm_password" placeholder="请再次输入密码">
|
||||
</div>
|
||||
</div>
|
||||
{%end%}
|
||||
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>角色</label>
|
||||
<input type="radio" name="is_admin" value="0" {%if eq .User.IsAdmin 0%}checked{%end%}> 普通用户
|
||||
<input type="radio" name="is_admin" value="1" {%if eq .User.IsAdmin 1%}checked{%end%}> 管理员
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>状态</label>
|
||||
<input type="radio" name="status" value="1" {%if eq .User.Status 1%}checked{%end%}> 启用
|
||||
<input type="radio" name="status" value="0"
|
||||
{%if eq .User.Status 0%}checked{%end%}
|
||||
> 禁用
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui primary submit button">保存</div>
|
||||
<a class="ui button" onclick="location.href='/user';">取消</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
var $uiForm = $('.ui.form');
|
||||
$($uiForm).form(
|
||||
{
|
||||
onSuccess: function(event, fields) {
|
||||
util.post('/user/store', fields, function(code, message) {
|
||||
location.href = "/user"
|
||||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
fields: {
|
||||
name: {
|
||||
identifier : 'name',
|
||||
rules: [
|
||||
{
|
||||
type : 'empty',
|
||||
prompt : '请输入用户名'
|
||||
},
|
||||
{
|
||||
type : 'minLength[3]',
|
||||
prompt : '长度不能少于3'
|
||||
},
|
||||
{
|
||||
type : 'maxLength[32]',
|
||||
prompt : '长度不能超过32'
|
||||
}
|
||||
]
|
||||
},
|
||||
password: {
|
||||
identifier : 'password',
|
||||
rules: [
|
||||
{
|
||||
type : 'empty',
|
||||
prompt : '请输入密码'
|
||||
},
|
||||
{
|
||||
type : 'minLength[6]',
|
||||
prompt : '长度不能少于6位'
|
||||
}
|
||||
]
|
||||
},
|
||||
confirmPassword: {
|
||||
identifier : 'confirm_password',
|
||||
rules: [
|
||||
{
|
||||
type : 'empty',
|
||||
prompt : '请再次输入密码'
|
||||
},
|
||||
{
|
||||
type : 'match[password]',
|
||||
prompt : '两次输入密码不匹配'
|
||||
}
|
||||
]
|
||||
},
|
||||
email: {
|
||||
identifier : 'email',
|
||||
rules: [
|
||||
{
|
||||
type : 'empty',
|
||||
prompt : '请输入邮箱'
|
||||
},
|
||||
{
|
||||
type : 'email',
|
||||
prompt : '邮箱格式错误'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
inline : true
|
||||
});
|
||||
</script>
|
||||
|
||||
{% template "common/footer" . %}
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# set -x -u
|
||||
# 上传二进制包到七牛
|
||||
|
||||
if [[ -z $QINIU_ACCESS_KEY || -z $QINIU_SECRET_KEY || -z $QINIU_URL ]];then
|
||||
echo 'QINIU_ACCESS_KEY | QINIU_SECRET_KEY | QINIU_URL is need'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 打包
|
||||
for i in linux darwin windows
|
||||
do
|
||||
./build.sh -p $i
|
||||
if [[ $? != 0 ]];then
|
||||
break
|
||||
fi
|
||||
./build_node.sh -p $i
|
||||
if [[ $? != 0 ]];then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# 身份认证
|
||||
qrsctl login $QINIU_ACCESS_KEY $QINIU_SECRET_KEY
|
||||
|
||||
# 上传
|
||||
for i in `ls gocron*.gz gocron*.zip`
|
||||
do
|
||||
# 上传文件 qrsctl put bucket key srcFile
|
||||
KEY=gocron/$i
|
||||
qrsctl put github $KEY $i
|
||||
if [[ $? != 0 ]];then
|
||||
break
|
||||
fi
|
||||
echo "刷新七牛CDN-" $QINIU_URL/$KEY
|
||||
qrsctl cdn/refresh $QINIU_URL/$KEY
|
||||
rm $i
|
||||
done
|
||||
|
||||
echo '打包并上传成功'
|
||||
+2
-2
@@ -14,12 +14,12 @@
|
||||
|
||||
package com
|
||||
|
||||
// PowInt is int type of math.Pow function.
|
||||
// PowInt is int type of math.Pow function.
|
||||
func PowInt(x int, y int) int {
|
||||
if y <= 0 {
|
||||
return 1
|
||||
} else {
|
||||
if y % 2 == 0 {
|
||||
if y%2 == 0 {
|
||||
sqrt := PowInt(x, y/2)
|
||||
return sqrt * sqrt
|
||||
} else {
|
||||
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, "control" means (i) the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
"submitted" means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution.
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
If the Work includes a "NOTICE" text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions.
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
6. Trademarks.
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
8. Limitation of Liability.
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability.
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets "[]" replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same "printed page" as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# cache [](https://travis-ci.org/go-macaron/cache) [](http://gocover.io/github.com/go-macaron/cache)
|
||||
|
||||
Middleware cache provides cache management for [Macaron](https://github.com/go-macaron/macaron). It can use many cache adapters, including memory, file, Redis, Memcache, PostgreSQL, MySQL, Ledis and Nodb.
|
||||
|
||||
### Installation
|
||||
|
||||
go get github.com/go-macaron/cache
|
||||
|
||||
## Getting Help
|
||||
|
||||
- [API Reference](https://gowalker.org/github.com/go-macaron/cache)
|
||||
- [Documentation](http://go-macaron.com/docs/middlewares/cache)
|
||||
|
||||
## Credits
|
||||
|
||||
This package is a modified version of [beego/cache](https://github.com/astaxie/beego/tree/master/cache).
|
||||
|
||||
## License
|
||||
|
||||
This project is under the Apache License, Version 2.0. See the [LICENSE](LICENSE) file for the full license text.
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
// Copyright 2013 Beego Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
// Package cache is a middleware that provides the cache management of Macaron.
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
const _VERSION = "0.3.0"
|
||||
|
||||
func Version() string {
|
||||
return _VERSION
|
||||
}
|
||||
|
||||
// Cache is the interface that operates the cache data.
|
||||
type Cache interface {
|
||||
// Put puts value into cache with key and expire time.
|
||||
Put(key string, val interface{}, timeout int64) error
|
||||
// Get gets cached value by given key.
|
||||
Get(key string) interface{}
|
||||
// Delete deletes cached value by given key.
|
||||
Delete(key string) error
|
||||
// Incr increases cached int-type value by given key as a counter.
|
||||
Incr(key string) error
|
||||
// Decr decreases cached int-type value by given key as a counter.
|
||||
Decr(key string) error
|
||||
// IsExist returns true if cached value exists.
|
||||
IsExist(key string) bool
|
||||
// Flush deletes all cached data.
|
||||
Flush() error
|
||||
// StartAndGC starts GC routine based on config string settings.
|
||||
StartAndGC(opt Options) error
|
||||
}
|
||||
|
||||
// Options represents a struct for specifying configuration options for the cache middleware.
|
||||
type Options struct {
|
||||
// Name of adapter. Default is "memory".
|
||||
Adapter string
|
||||
// Adapter configuration, it's corresponding to adapter.
|
||||
AdapterConfig string
|
||||
// GC interval time in seconds. Default is 60.
|
||||
Interval int
|
||||
// Occupy entire database. Default is false.
|
||||
OccupyMode bool
|
||||
// Configuration section name. Default is "cache".
|
||||
Section string
|
||||
}
|
||||
|
||||
func prepareOptions(options []Options) Options {
|
||||
var opt Options
|
||||
if len(options) > 0 {
|
||||
opt = options[0]
|
||||
}
|
||||
if len(opt.Section) == 0 {
|
||||
opt.Section = "cache"
|
||||
}
|
||||
sec := macaron.Config().Section(opt.Section)
|
||||
|
||||
if len(opt.Adapter) == 0 {
|
||||
opt.Adapter = sec.Key("ADAPTER").MustString("memory")
|
||||
}
|
||||
if opt.Interval == 0 {
|
||||
opt.Interval = sec.Key("INTERVAL").MustInt(60)
|
||||
}
|
||||
if len(opt.AdapterConfig) == 0 {
|
||||
opt.AdapterConfig = sec.Key("ADAPTER_CONFIG").MustString("data/caches")
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// NewCacher creates and returns a new cacher by given adapter name and configuration.
|
||||
// It panics when given adapter isn't registered and starts GC automatically.
|
||||
func NewCacher(name string, opt Options) (Cache, error) {
|
||||
adapter, ok := adapters[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cache: unknown adapter '%s'(forgot to import?)", name)
|
||||
}
|
||||
return adapter, adapter.StartAndGC(opt)
|
||||
}
|
||||
|
||||
// Cacher is a middleware that maps a cache.Cache service into the Macaron handler chain.
|
||||
// An single variadic cache.Options struct can be optionally provided to configure.
|
||||
func Cacher(options ...Options) macaron.Handler {
|
||||
opt := prepareOptions(options)
|
||||
cache, err := NewCacher(opt.Adapter, opt)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return func(ctx *macaron.Context) {
|
||||
ctx.Map(cache)
|
||||
}
|
||||
}
|
||||
|
||||
var adapters = make(map[string]Cache)
|
||||
|
||||
// Register registers a adapter.
|
||||
func Register(name string, adapter Cache) {
|
||||
if adapter == nil {
|
||||
panic("cache: cannot register adapter with nil value")
|
||||
}
|
||||
if _, dup := adapters[name]; dup {
|
||||
panic(fmt.Errorf("cache: cannot register adapter '%s' twice", name))
|
||||
}
|
||||
adapters[name] = adapter
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
// Copyright 2013 Beego Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Unknwon/com"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
// Item represents a cache item.
|
||||
type Item struct {
|
||||
Val interface{}
|
||||
Created int64
|
||||
Expire int64
|
||||
}
|
||||
|
||||
func (item *Item) hasExpired() bool {
|
||||
return item.Expire > 0 &&
|
||||
(time.Now().Unix()-item.Created) >= item.Expire
|
||||
}
|
||||
|
||||
// FileCacher represents a file cache adapter implementation.
|
||||
type FileCacher struct {
|
||||
lock sync.Mutex
|
||||
rootPath string
|
||||
interval int // GC interval.
|
||||
}
|
||||
|
||||
// NewFileCacher creates and returns a new file cacher.
|
||||
func NewFileCacher() *FileCacher {
|
||||
return &FileCacher{}
|
||||
}
|
||||
|
||||
func (c *FileCacher) filepath(key string) string {
|
||||
m := md5.Sum([]byte(key))
|
||||
hash := hex.EncodeToString(m[:])
|
||||
return filepath.Join(c.rootPath, string(hash[0]), string(hash[1]), hash)
|
||||
}
|
||||
|
||||
// Put puts value into cache with key and expire time.
|
||||
// If expired is 0, it will be deleted by next GC operation.
|
||||
func (c *FileCacher) Put(key string, val interface{}, expire int64) error {
|
||||
filename := c.filepath(key)
|
||||
item := &Item{val, time.Now().Unix(), expire}
|
||||
data, err := EncodeGob(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
os.MkdirAll(filepath.Dir(filename), os.ModePerm)
|
||||
return ioutil.WriteFile(filename, data, os.ModePerm)
|
||||
}
|
||||
|
||||
func (c *FileCacher) read(key string) (*Item, error) {
|
||||
filename := c.filepath(key)
|
||||
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
item := new(Item)
|
||||
return item, DecodeGob(data, item)
|
||||
}
|
||||
|
||||
// Get gets cached value by given key.
|
||||
func (c *FileCacher) Get(key string) interface{} {
|
||||
item, err := c.read(key)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if item.hasExpired() {
|
||||
os.Remove(c.filepath(key))
|
||||
return nil
|
||||
}
|
||||
return item.Val
|
||||
}
|
||||
|
||||
// Delete deletes cached value by given key.
|
||||
func (c *FileCacher) Delete(key string) error {
|
||||
return os.Remove(c.filepath(key))
|
||||
}
|
||||
|
||||
// Incr increases cached int-type value by given key as a counter.
|
||||
func (c *FileCacher) Incr(key string) error {
|
||||
item, err := c.read(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
item.Val, err = Incr(item.Val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Put(key, item.Val, item.Expire)
|
||||
}
|
||||
|
||||
// Decrease cached int value.
|
||||
func (c *FileCacher) Decr(key string) error {
|
||||
item, err := c.read(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
item.Val, err = Decr(item.Val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Put(key, item.Val, item.Expire)
|
||||
}
|
||||
|
||||
// IsExist returns true if cached value exists.
|
||||
func (c *FileCacher) IsExist(key string) bool {
|
||||
return com.IsExist(c.filepath(key))
|
||||
}
|
||||
|
||||
// Flush deletes all cached data.
|
||||
func (c *FileCacher) Flush() error {
|
||||
return os.RemoveAll(c.rootPath)
|
||||
}
|
||||
|
||||
func (c *FileCacher) startGC() {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
if c.interval < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
if err := filepath.Walk(c.rootPath, func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("Walk: %v", err)
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
fmt.Errorf("ReadFile: %v", err)
|
||||
}
|
||||
|
||||
item := new(Item)
|
||||
if err = DecodeGob(data, item); err != nil {
|
||||
return err
|
||||
}
|
||||
if item.hasExpired() {
|
||||
if err = os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("Remove: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Printf("error garbage collecting cache files: %v", err)
|
||||
}
|
||||
|
||||
time.AfterFunc(time.Duration(c.interval)*time.Second, func() { c.startGC() })
|
||||
}
|
||||
|
||||
// StartAndGC starts GC routine based on config string settings.
|
||||
func (c *FileCacher) StartAndGC(opt Options) error {
|
||||
c.lock.Lock()
|
||||
c.rootPath = opt.AdapterConfig
|
||||
c.interval = opt.Interval
|
||||
|
||||
if !filepath.IsAbs(c.rootPath) {
|
||||
c.rootPath = filepath.Join(macaron.Root, c.rootPath)
|
||||
}
|
||||
c.lock.Unlock()
|
||||
|
||||
if err := os.MkdirAll(c.rootPath, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go c.startGC()
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register("file", NewFileCacher())
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
// Copyright 2013 Beego Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MemoryItem represents a memory cache item.
|
||||
type MemoryItem struct {
|
||||
val interface{}
|
||||
created int64
|
||||
expire int64
|
||||
}
|
||||
|
||||
func (item *MemoryItem) hasExpired() bool {
|
||||
return item.expire > 0 &&
|
||||
(time.Now().Unix()-item.created) >= item.expire
|
||||
}
|
||||
|
||||
// MemoryCacher represents a memory cache adapter implementation.
|
||||
type MemoryCacher struct {
|
||||
lock sync.RWMutex
|
||||
items map[string]*MemoryItem
|
||||
interval int // GC interval.
|
||||
}
|
||||
|
||||
// NewMemoryCacher creates and returns a new memory cacher.
|
||||
func NewMemoryCacher() *MemoryCacher {
|
||||
return &MemoryCacher{items: make(map[string]*MemoryItem)}
|
||||
}
|
||||
|
||||
// Put puts value into cache with key and expire time.
|
||||
// If expired is 0, it will be deleted by next GC operation.
|
||||
func (c *MemoryCacher) Put(key string, val interface{}, expire int64) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.items[key] = &MemoryItem{
|
||||
val: val,
|
||||
created: time.Now().Unix(),
|
||||
expire: expire,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get gets cached value by given key.
|
||||
func (c *MemoryCacher) Get(key string) interface{} {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
item, ok := c.items[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if item.hasExpired() {
|
||||
go c.Delete(key)
|
||||
return nil
|
||||
}
|
||||
return item.val
|
||||
}
|
||||
|
||||
// Delete deletes cached value by given key.
|
||||
func (c *MemoryCacher) Delete(key string) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
delete(c.items, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Incr increases cached int-type value by given key as a counter.
|
||||
func (c *MemoryCacher) Incr(key string) (err error) {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
item, ok := c.items[key]
|
||||
if !ok {
|
||||
return errors.New("key not exist")
|
||||
}
|
||||
item.val, err = Incr(item.val)
|
||||
return err
|
||||
}
|
||||
|
||||
// Decr decreases cached int-type value by given key as a counter.
|
||||
func (c *MemoryCacher) Decr(key string) (err error) {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
item, ok := c.items[key]
|
||||
if !ok {
|
||||
return errors.New("key not exist")
|
||||
}
|
||||
|
||||
item.val, err = Decr(item.val)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsExist returns true if cached value exists.
|
||||
func (c *MemoryCacher) IsExist(key string) bool {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
_, ok := c.items[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Flush deletes all cached data.
|
||||
func (c *MemoryCacher) Flush() error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.items = make(map[string]*MemoryItem)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *MemoryCacher) checkRawExpiration(key string) {
|
||||
item, ok := c.items[key]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if item.hasExpired() {
|
||||
delete(c.items, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MemoryCacher) checkExpiration(key string) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.checkRawExpiration(key)
|
||||
}
|
||||
|
||||
func (c *MemoryCacher) startGC() {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
if c.interval < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
if c.items != nil {
|
||||
for key, _ := range c.items {
|
||||
c.checkRawExpiration(key)
|
||||
}
|
||||
}
|
||||
|
||||
time.AfterFunc(time.Duration(c.interval)*time.Second, func() { c.startGC() })
|
||||
}
|
||||
|
||||
// StartAndGC starts GC routine based on config string settings.
|
||||
func (c *MemoryCacher) StartAndGC(opt Options) error {
|
||||
c.lock.Lock()
|
||||
c.interval = opt.Interval
|
||||
c.lock.Unlock()
|
||||
|
||||
go c.startGC()
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register("memory", NewMemoryCacher())
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
)
|
||||
|
||||
func EncodeGob(item *Item) ([]byte, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
err := gob.NewEncoder(buf).Encode(item)
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
func DecodeGob(data []byte, out *Item) error {
|
||||
buf := bytes.NewBuffer(data)
|
||||
return gob.NewDecoder(buf).Decode(&out)
|
||||
}
|
||||
|
||||
func Incr(val interface{}) (interface{}, error) {
|
||||
switch val.(type) {
|
||||
case int:
|
||||
val = val.(int) + 1
|
||||
case int32:
|
||||
val = val.(int32) + 1
|
||||
case int64:
|
||||
val = val.(int64) + 1
|
||||
case uint:
|
||||
val = val.(uint) + 1
|
||||
case uint32:
|
||||
val = val.(uint32) + 1
|
||||
case uint64:
|
||||
val = val.(uint64) + 1
|
||||
default:
|
||||
return val, errors.New("item value is not int-type")
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func Decr(val interface{}) (interface{}, error) {
|
||||
switch val.(type) {
|
||||
case int:
|
||||
val = val.(int) - 1
|
||||
case int32:
|
||||
val = val.(int32) - 1
|
||||
case int64:
|
||||
val = val.(int64) - 1
|
||||
case uint:
|
||||
if val.(uint) > 0 {
|
||||
val = val.(uint) - 1
|
||||
} else {
|
||||
return val, errors.New("item value is less than 0")
|
||||
}
|
||||
case uint32:
|
||||
if val.(uint32) > 0 {
|
||||
val = val.(uint32) - 1
|
||||
} else {
|
||||
return val, errors.New("item value is less than 0")
|
||||
}
|
||||
case uint64:
|
||||
if val.(uint64) > 0 {
|
||||
val = val.(uint64) - 1
|
||||
} else {
|
||||
return val, errors.New("item value is less than 0")
|
||||
}
|
||||
default:
|
||||
return val, errors.New("item value is not int-type")
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, "control" means (i) the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
"submitted" means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution.
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
If the Work includes a "NOTICE" text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions.
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
6. Trademarks.
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
8. Limitation of Liability.
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability.
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets "[]" replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same "printed page" as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# captcha [](https://travis-ci.org/go-macaron/captcha)
|
||||
|
||||
Middleware captcha provides captcha service for [Macaron](https://github.com/go-macaron/macaron).
|
||||
|
||||
### Installation
|
||||
|
||||
go get github.com/go-macaron/captcha
|
||||
|
||||
## Getting Help
|
||||
|
||||
- [API Reference](https://gowalker.org/github.com/go-macaron/captcha)
|
||||
- [Documentation](http://go-macaron.com/docs/middlewares/captcha)
|
||||
|
||||
## License
|
||||
|
||||
This project is under the Apache License, Version 2.0. See the [LICENSE](LICENSE) file for the full license text.
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
// Copyright 2013 Beego Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
// Package captcha a middleware that provides captcha service for Macaron.
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/Unknwon/com"
|
||||
"github.com/go-macaron/cache"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
const _VERSION = "0.1.0"
|
||||
|
||||
func Version() string {
|
||||
return _VERSION
|
||||
}
|
||||
|
||||
var (
|
||||
defaultChars = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
)
|
||||
|
||||
// Captcha represents a captcha service.
|
||||
type Captcha struct {
|
||||
store cache.Cache
|
||||
SubURL string
|
||||
URLPrefix string
|
||||
FieldIdName string
|
||||
FieldCaptchaName string
|
||||
StdWidth int
|
||||
StdHeight int
|
||||
ChallengeNums int
|
||||
Expiration int64
|
||||
CachePrefix string
|
||||
}
|
||||
|
||||
// generate key string
|
||||
func (c *Captcha) key(id string) string {
|
||||
return c.CachePrefix + id
|
||||
}
|
||||
|
||||
// generate rand chars with default chars
|
||||
func (c *Captcha) genRandChars() string {
|
||||
return string(com.RandomCreateBytes(c.ChallengeNums, defaultChars...))
|
||||
}
|
||||
|
||||
// CreateHTML outputs HTML for display and fetch new captcha images.
|
||||
func (c *Captcha) CreateHTML() template.HTML {
|
||||
value, err := c.CreateCaptcha()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("fail to create captcha: %v", err))
|
||||
}
|
||||
return template.HTML(fmt.Sprintf(`<input type="hidden" name="%[1]s" value="%[2]s">
|
||||
<a class="captcha" href="javascript:" tabindex="-1">
|
||||
<img onclick="this.src=('%[3]s%[4]s%[2]s.png?reload='+(new Date()).getTime())" class="captcha-img" src="%[3]s%[4]s%[2]s.png">
|
||||
</a>`, c.FieldIdName, value, c.SubURL, c.URLPrefix))
|
||||
}
|
||||
|
||||
// DEPRECATED
|
||||
func (c *Captcha) CreateHtml() template.HTML {
|
||||
return c.CreateHTML()
|
||||
}
|
||||
|
||||
// create a new captcha id
|
||||
func (c *Captcha) CreateCaptcha() (string, error) {
|
||||
id := string(com.RandomCreateBytes(15))
|
||||
if err := c.store.Put(c.key(id), c.genRandChars(), c.Expiration); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// verify from a request
|
||||
func (c *Captcha) VerifyReq(req macaron.Request) bool {
|
||||
req.ParseForm()
|
||||
return c.Verify(req.Form.Get(c.FieldIdName), req.Form.Get(c.FieldCaptchaName))
|
||||
}
|
||||
|
||||
// direct verify id and challenge string
|
||||
func (c *Captcha) Verify(id string, challenge string) bool {
|
||||
if len(challenge) == 0 || len(id) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
var chars string
|
||||
|
||||
key := c.key(id)
|
||||
|
||||
if v, ok := c.store.Get(key).(string); ok {
|
||||
chars = v
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
defer c.store.Delete(key)
|
||||
|
||||
if len(chars) != len(challenge) {
|
||||
return false
|
||||
}
|
||||
|
||||
// verify challenge
|
||||
for i, c := range []byte(chars) {
|
||||
if c != challenge[i]-48 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
// Suburl path. Default is empty.
|
||||
SubURL string
|
||||
// URL prefix of getting captcha pictures. Default is "/captcha/".
|
||||
URLPrefix string
|
||||
// Hidden input element ID. Default is "captcha_id".
|
||||
FieldIdName string
|
||||
// User input value element name in request form. Default is "captcha".
|
||||
FieldCaptchaName string
|
||||
// Challenge number. Default is 6.
|
||||
ChallengeNums int
|
||||
// Captcha image width. Default is 240.
|
||||
Width int
|
||||
// Captcha image height. Default is 80.
|
||||
Height int
|
||||
// Captcha expiration time in seconds. Default is 600.
|
||||
Expiration int64
|
||||
// Cache key prefix captcha characters. Default is "captcha_".
|
||||
CachePrefix string
|
||||
}
|
||||
|
||||
func prepareOptions(options []Options) Options {
|
||||
var opt Options
|
||||
if len(options) > 0 {
|
||||
opt = options[0]
|
||||
}
|
||||
|
||||
opt.SubURL = strings.TrimSuffix(opt.SubURL, "/")
|
||||
|
||||
// Defaults.
|
||||
if len(opt.URLPrefix) == 0 {
|
||||
opt.URLPrefix = "/captcha/"
|
||||
} else if opt.URLPrefix[len(opt.URLPrefix)-1] != '/' {
|
||||
opt.URLPrefix += "/"
|
||||
}
|
||||
if len(opt.FieldIdName) == 0 {
|
||||
opt.FieldIdName = "captcha_id"
|
||||
}
|
||||
if len(opt.FieldCaptchaName) == 0 {
|
||||
opt.FieldCaptchaName = "captcha"
|
||||
}
|
||||
if opt.ChallengeNums == 0 {
|
||||
opt.ChallengeNums = 6
|
||||
}
|
||||
if opt.Width == 0 {
|
||||
opt.Width = stdWidth
|
||||
}
|
||||
if opt.Height == 0 {
|
||||
opt.Height = stdHeight
|
||||
}
|
||||
if opt.Expiration == 0 {
|
||||
opt.Expiration = 600
|
||||
}
|
||||
if len(opt.CachePrefix) == 0 {
|
||||
opt.CachePrefix = "captcha_"
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// NewCaptcha initializes and returns a captcha with given options.
|
||||
func NewCaptcha(opt Options) *Captcha {
|
||||
return &Captcha{
|
||||
SubURL: opt.SubURL,
|
||||
URLPrefix: opt.URLPrefix,
|
||||
FieldIdName: opt.FieldIdName,
|
||||
FieldCaptchaName: opt.FieldCaptchaName,
|
||||
StdWidth: opt.Width,
|
||||
StdHeight: opt.Height,
|
||||
ChallengeNums: opt.ChallengeNums,
|
||||
Expiration: opt.Expiration,
|
||||
CachePrefix: opt.CachePrefix,
|
||||
}
|
||||
}
|
||||
|
||||
// Captchaer is a middleware that maps a captcha.Captcha service into the Macaron handler chain.
|
||||
// An single variadic captcha.Options struct can be optionally provided to configure.
|
||||
// This should be register after cache.Cacher.
|
||||
func Captchaer(options ...Options) macaron.Handler {
|
||||
return func(ctx *macaron.Context, cache cache.Cache) {
|
||||
cpt := NewCaptcha(prepareOptions(options))
|
||||
cpt.store = cache
|
||||
|
||||
if strings.HasPrefix(ctx.Req.URL.Path, cpt.URLPrefix) {
|
||||
var chars string
|
||||
id := path.Base(ctx.Req.URL.Path)
|
||||
if i := strings.Index(id, "."); i > -1 {
|
||||
id = id[:i]
|
||||
}
|
||||
key := cpt.key(id)
|
||||
|
||||
// Reload captcha.
|
||||
if len(ctx.Query("reload")) > 0 {
|
||||
chars = cpt.genRandChars()
|
||||
if err := cpt.store.Put(key, chars, cpt.Expiration); err != nil {
|
||||
ctx.Status(500)
|
||||
ctx.Write([]byte("captcha reload error"))
|
||||
panic(fmt.Errorf("reload captcha: %v", err))
|
||||
}
|
||||
} else {
|
||||
if v, ok := cpt.store.Get(key).(string); ok {
|
||||
chars = v
|
||||
} else {
|
||||
ctx.Status(404)
|
||||
ctx.Write([]byte("captcha not found"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := NewImage([]byte(chars), cpt.StdWidth, cpt.StdHeight).WriteTo(ctx.Resp); err != nil {
|
||||
panic(fmt.Errorf("write captcha: %v", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Captcha"] = cpt
|
||||
ctx.Map(cpt)
|
||||
}
|
||||
}
|
||||
+498
@@ -0,0 +1,498 @@
|
||||
// Copyright 2013 Beego Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"io"
|
||||
"math"
|
||||
)
|
||||
|
||||
const (
|
||||
fontWidth = 11
|
||||
fontHeight = 18
|
||||
blackChar = 1
|
||||
|
||||
// Standard width and height of a captcha image.
|
||||
stdWidth = 240
|
||||
stdHeight = 80
|
||||
|
||||
// Maximum absolute skew factor of a single digit.
|
||||
maxSkew = 0.7
|
||||
// Number of background circles.
|
||||
circleCount = 20
|
||||
)
|
||||
|
||||
var font = [][]byte{
|
||||
{ // 0
|
||||
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
|
||||
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
},
|
||||
{ // 1
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
},
|
||||
{ // 2
|
||||
0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0,
|
||||
0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0,
|
||||
0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
},
|
||||
{ // 3
|
||||
0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
|
||||
0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
},
|
||||
{ // 4
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
},
|
||||
{ // 5
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
},
|
||||
{ // 6
|
||||
0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0,
|
||||
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0,
|
||||
0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0,
|
||||
1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0,
|
||||
1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0,
|
||||
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
|
||||
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
},
|
||||
{ // 7
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
|
||||
},
|
||||
{ // 8
|
||||
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0,
|
||||
0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0,
|
||||
0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0,
|
||||
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
|
||||
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
|
||||
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
},
|
||||
{ // 9
|
||||
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
|
||||
0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1,
|
||||
0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
|
||||
},
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
*image.Paletted
|
||||
numWidth int
|
||||
numHeight int
|
||||
dotSize int
|
||||
}
|
||||
|
||||
var prng = &siprng{}
|
||||
|
||||
// randIntn returns a pseudorandom non-negative int in range [0, n).
|
||||
func randIntn(n int) int {
|
||||
return prng.Intn(n)
|
||||
}
|
||||
|
||||
// randInt returns a pseudorandom int in range [from, to].
|
||||
func randInt(from, to int) int {
|
||||
return prng.Intn(to+1-from) + from
|
||||
}
|
||||
|
||||
// randFloat returns a pseudorandom float64 in range [from, to].
|
||||
func randFloat(from, to float64) float64 {
|
||||
return (to-from)*prng.Float64() + from
|
||||
}
|
||||
|
||||
func randomPalette() color.Palette {
|
||||
p := make([]color.Color, circleCount+1)
|
||||
// Transparent color.
|
||||
p[0] = color.RGBA{0xFF, 0xFF, 0xFF, 0x00}
|
||||
// Primary color.
|
||||
prim := color.RGBA{
|
||||
uint8(randIntn(129)),
|
||||
uint8(randIntn(129)),
|
||||
uint8(randIntn(129)),
|
||||
0xFF,
|
||||
}
|
||||
p[1] = prim
|
||||
// Circle colors.
|
||||
for i := 2; i <= circleCount; i++ {
|
||||
p[i] = randomBrightness(prim, 255)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// NewImage returns a new captcha image of the given width and height with the
|
||||
// given digits, where each digit must be in range 0-9.
|
||||
func NewImage(digits []byte, width, height int) *Image {
|
||||
m := new(Image)
|
||||
m.Paletted = image.NewPaletted(image.Rect(0, 0, width, height), randomPalette())
|
||||
m.calculateSizes(width, height, len(digits))
|
||||
// Randomly position captcha inside the image.
|
||||
maxx := width - (m.numWidth+m.dotSize)*len(digits) - m.dotSize
|
||||
maxy := height - m.numHeight - m.dotSize*2
|
||||
var border int
|
||||
if width > height {
|
||||
border = height / 5
|
||||
} else {
|
||||
border = width / 5
|
||||
}
|
||||
x := randInt(border, maxx-border)
|
||||
y := randInt(border, maxy-border)
|
||||
// Draw digits.
|
||||
for _, n := range digits {
|
||||
m.drawDigit(font[n], x, y)
|
||||
x += m.numWidth + m.dotSize
|
||||
}
|
||||
// Draw strike-through line.
|
||||
m.strikeThrough()
|
||||
// Apply wave distortion.
|
||||
m.distort(randFloat(5, 10), randFloat(100, 200))
|
||||
// Fill image with random circles.
|
||||
m.fillWithCircles(circleCount, m.dotSize)
|
||||
return m
|
||||
}
|
||||
|
||||
// encodedPNG encodes an image to PNG and returns
|
||||
// the result as a byte slice.
|
||||
func (m *Image) encodedPNG() []byte {
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, m.Paletted); err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// WriteTo writes captcha image in PNG format into the given writer.
|
||||
func (m *Image) WriteTo(w io.Writer) (int64, error) {
|
||||
n, err := w.Write(m.encodedPNG())
|
||||
return int64(n), err
|
||||
}
|
||||
|
||||
func (m *Image) calculateSizes(width, height, ncount int) {
|
||||
// Goal: fit all digits inside the image.
|
||||
var border int
|
||||
if width > height {
|
||||
border = height / 4
|
||||
} else {
|
||||
border = width / 4
|
||||
}
|
||||
// Convert everything to floats for calculations.
|
||||
w := float64(width - border*2)
|
||||
h := float64(height - border*2)
|
||||
// fw takes into account 1-dot spacing between digits.
|
||||
fw := float64(fontWidth + 1)
|
||||
fh := float64(fontHeight)
|
||||
nc := float64(ncount)
|
||||
// Calculate the width of a single digit taking into account only the
|
||||
// width of the image.
|
||||
nw := w / nc
|
||||
// Calculate the height of a digit from this width.
|
||||
nh := nw * fh / fw
|
||||
// Digit too high?
|
||||
if nh > h {
|
||||
// Fit digits based on height.
|
||||
nh = h
|
||||
nw = fw / fh * nh
|
||||
}
|
||||
// Calculate dot size.
|
||||
m.dotSize = int(nh / fh)
|
||||
// Save everything, making the actual width smaller by 1 dot to account
|
||||
// for spacing between digits.
|
||||
m.numWidth = int(nw) - m.dotSize
|
||||
m.numHeight = int(nh)
|
||||
}
|
||||
|
||||
func (m *Image) drawHorizLine(fromX, toX, y int, colorIdx uint8) {
|
||||
for x := fromX; x <= toX; x++ {
|
||||
m.SetColorIndex(x, y, colorIdx)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Image) drawCircle(x, y, radius int, colorIdx uint8) {
|
||||
f := 1 - radius
|
||||
dfx := 1
|
||||
dfy := -2 * radius
|
||||
xo := 0
|
||||
yo := radius
|
||||
|
||||
m.SetColorIndex(x, y+radius, colorIdx)
|
||||
m.SetColorIndex(x, y-radius, colorIdx)
|
||||
m.drawHorizLine(x-radius, x+radius, y, colorIdx)
|
||||
|
||||
for xo < yo {
|
||||
if f >= 0 {
|
||||
yo--
|
||||
dfy += 2
|
||||
f += dfy
|
||||
}
|
||||
xo++
|
||||
dfx += 2
|
||||
f += dfx
|
||||
m.drawHorizLine(x-xo, x+xo, y+yo, colorIdx)
|
||||
m.drawHorizLine(x-xo, x+xo, y-yo, colorIdx)
|
||||
m.drawHorizLine(x-yo, x+yo, y+xo, colorIdx)
|
||||
m.drawHorizLine(x-yo, x+yo, y-xo, colorIdx)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Image) fillWithCircles(n, maxradius int) {
|
||||
maxx := m.Bounds().Max.X
|
||||
maxy := m.Bounds().Max.Y
|
||||
for i := 0; i < n; i++ {
|
||||
colorIdx := uint8(randInt(1, circleCount-1))
|
||||
r := randInt(1, maxradius)
|
||||
m.drawCircle(randInt(r, maxx-r), randInt(r, maxy-r), r, colorIdx)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Image) strikeThrough() {
|
||||
maxx := m.Bounds().Max.X
|
||||
maxy := m.Bounds().Max.Y
|
||||
y := randInt(maxy/3, maxy-maxy/3)
|
||||
amplitude := randFloat(5, 20)
|
||||
period := randFloat(80, 180)
|
||||
dx := 2.0 * math.Pi / period
|
||||
for x := 0; x < maxx; x++ {
|
||||
xo := amplitude * math.Cos(float64(y)*dx)
|
||||
yo := amplitude * math.Sin(float64(x)*dx)
|
||||
for yn := 0; yn < m.dotSize; yn++ {
|
||||
r := randInt(0, m.dotSize)
|
||||
m.drawCircle(x+int(xo), y+int(yo)+(yn*m.dotSize), r/2, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Image) drawDigit(digit []byte, x, y int) {
|
||||
skf := randFloat(-maxSkew, maxSkew)
|
||||
xs := float64(x)
|
||||
r := m.dotSize / 2
|
||||
y += randInt(-r, r)
|
||||
for yo := 0; yo < fontHeight; yo++ {
|
||||
for xo := 0; xo < fontWidth; xo++ {
|
||||
if digit[yo*fontWidth+xo] != blackChar {
|
||||
continue
|
||||
}
|
||||
m.drawCircle(x+xo*m.dotSize, y+yo*m.dotSize, r, 1)
|
||||
}
|
||||
xs += skf
|
||||
x = int(xs)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Image) distort(amplude float64, period float64) {
|
||||
w := m.Bounds().Max.X
|
||||
h := m.Bounds().Max.Y
|
||||
|
||||
oldm := m.Paletted
|
||||
newm := image.NewPaletted(image.Rect(0, 0, w, h), oldm.Palette)
|
||||
|
||||
dx := 2.0 * math.Pi / period
|
||||
for x := 0; x < w; x++ {
|
||||
for y := 0; y < h; y++ {
|
||||
xo := amplude * math.Sin(float64(y)*dx)
|
||||
yo := amplude * math.Cos(float64(x)*dx)
|
||||
newm.SetColorIndex(x, y, oldm.ColorIndexAt(x+int(xo), y+int(yo)))
|
||||
}
|
||||
}
|
||||
m.Paletted = newm
|
||||
}
|
||||
|
||||
func randomBrightness(c color.RGBA, max uint8) color.RGBA {
|
||||
minc := min3(c.R, c.G, c.B)
|
||||
maxc := max3(c.R, c.G, c.B)
|
||||
if maxc > max {
|
||||
return c
|
||||
}
|
||||
n := randIntn(int(max-maxc)) - int(minc)
|
||||
return color.RGBA{
|
||||
uint8(int(c.R) + n),
|
||||
uint8(int(c.G) + n),
|
||||
uint8(int(c.B) + n),
|
||||
uint8(c.A),
|
||||
}
|
||||
}
|
||||
|
||||
func min3(x, y, z uint8) (m uint8) {
|
||||
m = x
|
||||
if y < m {
|
||||
m = y
|
||||
}
|
||||
if z < m {
|
||||
m = z
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func max3(x, y, z uint8) (m uint8) {
|
||||
m = x
|
||||
if y > m {
|
||||
m = y
|
||||
}
|
||||
if z > m {
|
||||
m = z
|
||||
}
|
||||
return
|
||||
}
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
// Copyright 2013 Beego Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// siprng is PRNG based on SipHash-2-4.
|
||||
type siprng struct {
|
||||
mu sync.Mutex
|
||||
k0, k1, ctr uint64
|
||||
}
|
||||
|
||||
// siphash implements SipHash-2-4, accepting a uint64 as a message.
|
||||
func siphash(k0, k1, m uint64) uint64 {
|
||||
// Initialization.
|
||||
v0 := k0 ^ 0x736f6d6570736575
|
||||
v1 := k1 ^ 0x646f72616e646f6d
|
||||
v2 := k0 ^ 0x6c7967656e657261
|
||||
v3 := k1 ^ 0x7465646279746573
|
||||
t := uint64(8) << 56
|
||||
|
||||
// Compression.
|
||||
v3 ^= m
|
||||
|
||||
// Round 1.
|
||||
v0 += v1
|
||||
v1 = v1<<13 | v1>>(64-13)
|
||||
v1 ^= v0
|
||||
v0 = v0<<32 | v0>>(64-32)
|
||||
|
||||
v2 += v3
|
||||
v3 = v3<<16 | v3>>(64-16)
|
||||
v3 ^= v2
|
||||
|
||||
v0 += v3
|
||||
v3 = v3<<21 | v3>>(64-21)
|
||||
v3 ^= v0
|
||||
|
||||
v2 += v1
|
||||
v1 = v1<<17 | v1>>(64-17)
|
||||
v1 ^= v2
|
||||
v2 = v2<<32 | v2>>(64-32)
|
||||
|
||||
// Round 2.
|
||||
v0 += v1
|
||||
v1 = v1<<13 | v1>>(64-13)
|
||||
v1 ^= v0
|
||||
v0 = v0<<32 | v0>>(64-32)
|
||||
|
||||
v2 += v3
|
||||
v3 = v3<<16 | v3>>(64-16)
|
||||
v3 ^= v2
|
||||
|
||||
v0 += v3
|
||||
v3 = v3<<21 | v3>>(64-21)
|
||||
v3 ^= v0
|
||||
|
||||
v2 += v1
|
||||
v1 = v1<<17 | v1>>(64-17)
|
||||
v1 ^= v2
|
||||
v2 = v2<<32 | v2>>(64-32)
|
||||
|
||||
v0 ^= m
|
||||
|
||||
// Compress last block.
|
||||
v3 ^= t
|
||||
|
||||
// Round 1.
|
||||
v0 += v1
|
||||
v1 = v1<<13 | v1>>(64-13)
|
||||
v1 ^= v0
|
||||
v0 = v0<<32 | v0>>(64-32)
|
||||
|
||||
v2 += v3
|
||||
v3 = v3<<16 | v3>>(64-16)
|
||||
v3 ^= v2
|
||||
|
||||
v0 += v3
|
||||
v3 = v3<<21 | v3>>(64-21)
|
||||
v3 ^= v0
|
||||
|
||||
v2 += v1
|
||||
v1 = v1<<17 | v1>>(64-17)
|
||||
v1 ^= v2
|
||||
v2 = v2<<32 | v2>>(64-32)
|
||||
|
||||
// Round 2.
|
||||
v0 += v1
|
||||
v1 = v1<<13 | v1>>(64-13)
|
||||
v1 ^= v0
|
||||
v0 = v0<<32 | v0>>(64-32)
|
||||
|
||||
v2 += v3
|
||||
v3 = v3<<16 | v3>>(64-16)
|
||||
v3 ^= v2
|
||||
|
||||
v0 += v3
|
||||
v3 = v3<<21 | v3>>(64-21)
|
||||
v3 ^= v0
|
||||
|
||||
v2 += v1
|
||||
v1 = v1<<17 | v1>>(64-17)
|
||||
v1 ^= v2
|
||||
v2 = v2<<32 | v2>>(64-32)
|
||||
|
||||
v0 ^= t
|
||||
|
||||
// Finalization.
|
||||
v2 ^= 0xff
|
||||
|
||||
// Round 1.
|
||||
v0 += v1
|
||||
v1 = v1<<13 | v1>>(64-13)
|
||||
v1 ^= v0
|
||||
v0 = v0<<32 | v0>>(64-32)
|
||||
|
||||
v2 += v3
|
||||
v3 = v3<<16 | v3>>(64-16)
|
||||
v3 ^= v2
|
||||
|
||||
v0 += v3
|
||||
v3 = v3<<21 | v3>>(64-21)
|
||||
v3 ^= v0
|
||||
|
||||
v2 += v1
|
||||
v1 = v1<<17 | v1>>(64-17)
|
||||
v1 ^= v2
|
||||
v2 = v2<<32 | v2>>(64-32)
|
||||
|
||||
// Round 2.
|
||||
v0 += v1
|
||||
v1 = v1<<13 | v1>>(64-13)
|
||||
v1 ^= v0
|
||||
v0 = v0<<32 | v0>>(64-32)
|
||||
|
||||
v2 += v3
|
||||
v3 = v3<<16 | v3>>(64-16)
|
||||
v3 ^= v2
|
||||
|
||||
v0 += v3
|
||||
v3 = v3<<21 | v3>>(64-21)
|
||||
v3 ^= v0
|
||||
|
||||
v2 += v1
|
||||
v1 = v1<<17 | v1>>(64-17)
|
||||
v1 ^= v2
|
||||
v2 = v2<<32 | v2>>(64-32)
|
||||
|
||||
// Round 3.
|
||||
v0 += v1
|
||||
v1 = v1<<13 | v1>>(64-13)
|
||||
v1 ^= v0
|
||||
v0 = v0<<32 | v0>>(64-32)
|
||||
|
||||
v2 += v3
|
||||
v3 = v3<<16 | v3>>(64-16)
|
||||
v3 ^= v2
|
||||
|
||||
v0 += v3
|
||||
v3 = v3<<21 | v3>>(64-21)
|
||||
v3 ^= v0
|
||||
|
||||
v2 += v1
|
||||
v1 = v1<<17 | v1>>(64-17)
|
||||
v1 ^= v2
|
||||
v2 = v2<<32 | v2>>(64-32)
|
||||
|
||||
// Round 4.
|
||||
v0 += v1
|
||||
v1 = v1<<13 | v1>>(64-13)
|
||||
v1 ^= v0
|
||||
v0 = v0<<32 | v0>>(64-32)
|
||||
|
||||
v2 += v3
|
||||
v3 = v3<<16 | v3>>(64-16)
|
||||
v3 ^= v2
|
||||
|
||||
v0 += v3
|
||||
v3 = v3<<21 | v3>>(64-21)
|
||||
v3 ^= v0
|
||||
|
||||
v2 += v1
|
||||
v1 = v1<<17 | v1>>(64-17)
|
||||
v1 ^= v2
|
||||
v2 = v2<<32 | v2>>(64-32)
|
||||
|
||||
return v0 ^ v1 ^ v2 ^ v3
|
||||
}
|
||||
|
||||
// rekey sets a new PRNG key, which is read from crypto/rand.
|
||||
func (p *siprng) rekey() {
|
||||
var k [16]byte
|
||||
if _, err := io.ReadFull(rand.Reader, k[:]); err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
p.k0 = binary.LittleEndian.Uint64(k[0:8])
|
||||
p.k1 = binary.LittleEndian.Uint64(k[8:16])
|
||||
p.ctr = 1
|
||||
}
|
||||
|
||||
// Uint64 returns a new pseudorandom uint64.
|
||||
// It rekeys PRNG on the first call and every 64 MB of generated data.
|
||||
func (p *siprng) Uint64() uint64 {
|
||||
p.mu.Lock()
|
||||
if p.ctr == 0 || p.ctr > 8*1024*1024 {
|
||||
p.rekey()
|
||||
}
|
||||
v := siphash(p.k0, p.k1, p.ctr)
|
||||
p.ctr++
|
||||
p.mu.Unlock()
|
||||
return v
|
||||
}
|
||||
|
||||
func (p *siprng) Int63() int64 {
|
||||
return int64(p.Uint64() & 0x7fffffffffffffff)
|
||||
}
|
||||
|
||||
func (p *siprng) Uint32() uint32 {
|
||||
return uint32(p.Uint64())
|
||||
}
|
||||
|
||||
func (p *siprng) Int31() int32 {
|
||||
return int32(p.Uint32() & 0x7fffffff)
|
||||
}
|
||||
|
||||
func (p *siprng) Intn(n int) int {
|
||||
if n <= 0 {
|
||||
panic("invalid argument to Intn")
|
||||
}
|
||||
if n <= 1<<31-1 {
|
||||
return int(p.Int31n(int32(n)))
|
||||
}
|
||||
return int(p.Int63n(int64(n)))
|
||||
}
|
||||
|
||||
func (p *siprng) Int63n(n int64) int64 {
|
||||
if n <= 0 {
|
||||
panic("invalid argument to Int63n")
|
||||
}
|
||||
max := int64((1 << 63) - 1 - (1<<63)%uint64(n))
|
||||
v := p.Int63()
|
||||
for v > max {
|
||||
v = p.Int63()
|
||||
}
|
||||
return v % n
|
||||
}
|
||||
|
||||
func (p *siprng) Int31n(n int32) int32 {
|
||||
if n <= 0 {
|
||||
panic("invalid argument to Int31n")
|
||||
}
|
||||
max := int32((1 << 31) - 1 - (1<<31)%uint32(n))
|
||||
v := p.Int31()
|
||||
for v > max {
|
||||
v = p.Int31()
|
||||
}
|
||||
return v % n
|
||||
}
|
||||
|
||||
func (p *siprng) Float64() float64 { return float64(p.Int63()) / (1 << 63) }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user