Commit 50918756 authored by Administrator's avatar Administrator
Browse files

Merge branch 'xqk-next' into 'next'

新增插件功能、redis 新增ZRevRangeWithScores方法

See merge request !1
parents 4a6972d2 0190a63c
package main
import (
"flag"
"net/http"
"git.zc0901.com/go/god/example/wechat/pkg"
"git.zc0901.com/go/god/api"
"git.zc0901.com/go/god/lib/conf"
"git.zc0901.com/go/god/lib/logx"
)
var (
configFile = flag.String("f", "config.yaml", "API 配置文件")
handlers *pkg.OpenHandlers
)
func init() {
handlers = pkg.NewOpenHandlers()
}
func main() {
flag.Parse()
var apiConf api.ServerConf
conf.MustLoad(*configFile, &apiConf)
server := api.MustNewServer(apiConf)
defer server.Stop()
server.AddRoute(api.Route{
Method: http.MethodGet,
Path: "/home",
Handler: homeHandler,
})
// 授权事件接收配置
server.AddRoute(api.Route{
Method: http.MethodPost,
Path: "/oplatform/wxbef357be217c23c5/notify",
Handler: handlers.Notify,
})
// 获取 component_verify_ticket 后,查看平台令牌
server.AddRoute(api.Route{
Method: http.MethodGet,
Path: "/oplatform/wxbef357be217c23c5/accesstoken",
Handler: handlers.AccessToken,
})
// 生成PC版/移动版平台授权码 ?isMobile=1
server.AddRoute(api.Route{
Method: http.MethodGet,
Path: "/oplatform/wxbef357be217c23c5/auth",
Handler: handlers.Auth,
})
// 授权方授权后跳转网址
server.AddRoute(api.Route{
Method: http.MethodGet,
Path: "/oplatform/wxbef357be217c23c5/redirect",
Handler: handlers.Redirect,
})
// 根据微信重定向带过来的授权码查询授权信息
server.AddRoute(api.Route{
Method: http.MethodGet,
Path: "/oplatform/wxbef357be217c23c5/queryauth",
Handler: handlers.QueryAuth,
})
logx.Infof("微信响应 API NewServer 已启动 —— %v:%v", apiConf.Host, apiConf.Port)
server.Start()
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write([]byte("hello world"))
}
//import (
// "flag"
// "net/http"
//
// "git.zc0901.com/go/god/example/wechat/pkg"
//
// "git.zc0901.com/go/god/api"
// "git.zc0901.com/go/god/lib/conf"
// "git.zc0901.com/go/god/lib/logx"
//)
//
//var (
// configFile = flag.String("f", "config.yaml", "API 配置文件")
// handlers *pkg.OpenHandlers
//)
//
//func init() {
// handlers = pkg.NewOpenHandlers()
//}
//
//func main() {
// flag.Parse()
// var apiConf api.ServerConf
// conf.MustLoad(*configFile, &apiConf)
//
// server := api.MustNewServer(apiConf)
// defer server.Stop()
//
// server.AddRoute(api.Route{
// Method: http.MethodGet,
// Path: "/home",
// Handler: homeHandler,
// })
//
// // 授权事件接收配置
// server.AddRoute(api.Route{
// Method: http.MethodPost,
// Path: "/oplatform/wxbef357be217c23c5/notify",
// Handler: handlers.Notify,
// })
//
// // 获取 component_verify_ticket 后,查看平台令牌
// server.AddRoute(api.Route{
// Method: http.MethodGet,
// Path: "/oplatform/wxbef357be217c23c5/accesstoken",
// Handler: handlers.AccessToken,
// })
//
// // 生成PC版/移动版平台授权码 ?isMobile=1
// server.AddRoute(api.Route{
// Method: http.MethodGet,
// Path: "/oplatform/wxbef357be217c23c5/auth",
// Handler: handlers.Auth,
// })
//
// // 授权方授权后跳转网址
// server.AddRoute(api.Route{
// Method: http.MethodGet,
// Path: "/oplatform/wxbef357be217c23c5/redirect",
// Handler: handlers.Redirect,
// })
//
// // 根据微信重定向带过来的授权码查询授权信息
// server.AddRoute(api.Route{
// Method: http.MethodGet,
// Path: "/oplatform/wxbef357be217c23c5/queryauth",
// Handler: handlers.QueryAuth,
// })
//
// logx.Infof("微信响应 API NewServer 已启动 —— %v:%v", apiConf.Host, apiConf.Port)
// server.Start()
//}
//
//func homeHandler(w http.ResponseWriter, r *http.Request) {
// w.WriteHeader(200)
//
// w.Write([]byte("hello world"))
//}
package pkg
import (
"fmt"
"net/http"
"git.zc0901.com/go/god/lib/g"
"git.zc0901.com/go/god/lib/gconv"
"git.zc0901.com/go/god/lib/store/kv"
"git.zc0901.com/go/god/lib/store/redis"
"git.zc0901.com/go/god/api/httpx"
"git.zc0901.com/go/god/lib/logx"
cacheRedis "git.zc0901.com/go/god/lib/store/cache"
"git.zc0901.com/go/god/lib/wechat/cache"
"git.zc0901.com/go/god/lib/wechat/msg"
"git.zc0901.com/go/god/lib/wechat/openplatform"
"git.zc0901.com/go/god/lib/wechat/openplatform/config"
)
type OpenHandlers struct {
open *openplatform.OpenPlatform
}
func NewOpenHandlers() *OpenHandlers {
store := kv.NewStore([]cacheRedis.Conf{
{
Conf: redis.Conf{
Host: "vps:6382",
Password: "4a5d4787a82c660ee18719f51ff40d9a669a4958",
Mode: redis.StandaloneMode,
},
Weight: 100,
},
})
o := openplatform.New(&config.Config{
AppID: "wxbef357be217c23c5",
AppSecret: "403d127716317ea23c8db1a1107b14fc",
Token: "imola1999zhuke2012dhome2020",
EncodingAESKey: "imola1999azhuke2012adhome2020a18611914900aa",
Cache: cache.NewRedis(store),
})
return &OpenHandlers{o}
}
func (h *OpenHandlers) Notify(w http.ResponseWriter, r *http.Request) {
logx.Debugf("请求方法: %v", r.Method)
server := openplatform.GetServer(h.open.Context, w, r)
// 设置消息响应处理器
server.SetMessageHandler(func(m *msg.Msg) (*msg.Response, error) {
if m.InfoType == "component_verify_ticket" {
accessToken, err := h.open.SetAccessToken(m.ComponentVerifyTicket)
if err != nil {
return nil, err
}
logx.Debug("平台访问令牌", accessToken)
} else {
fmt.Println("消息钩子", m)
}
return nil, nil
})
// 处理微信的请求信息
err := server.Serve()
if err != nil {
logx.Error("微信响应服务器错误:", err)
return
}
// 发送回复的消息
err = server.Send()
if err != nil {
logx.Errorf("响应微信错误,err=%+v", err)
return
}
}
func (h *OpenHandlers) AccessToken(w http.ResponseWriter, r *http.Request) {
accessToken, err := h.open.AccessToken()
if err != nil {
httpx.Error(w, err)
return
}
httpx.OkJson(w, map[string]string{
"token": accessToken,
})
}
func (h *OpenHandlers) Auth(w http.ResponseWriter, r *http.Request) {
isMobile := false
if vs, ok := r.URL.Query()["isMobile"]; ok && len(vs) == 1 {
isMobile = gconv.Bool(vs[0])
}
apiHost := "http://zs.ngrok.zc0901.com"
redirect := fmt.Sprintf("%s/oplatform/%s/redirect", apiHost, h.open.AppID)
var authUrl string
var err error
if isMobile {
authUrl, err = h.open.MobileAuthURL(redirect, 2, "")
} else {
authUrl, err = h.open.PcAuthURL(redirect, 2, "")
}
if err != nil {
w.WriteHeader(200)
w.Write([]byte("系统异常: " + err.Error()))
return
}
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(fmt.Sprintf("<script>location.href=\"%s\"</script>", authUrl)))
}
func (h *OpenHandlers) Redirect(w http.ResponseWriter, r *http.Request) {
var authCode, expiresIn string
// 授权码
if vs := r.URL.Query()["auth_code"]; len(vs) == 1 {
authCode = vs[0]
}
// 过期时间
if vs := r.URL.Query()["expires_in"]; len(vs) == 1 {
expiresIn = vs[0]
}
http.Redirect(w, r, "/home?auth_code="+authCode+"&expires_in="+expiresIn, 302)
}
func (h *OpenHandlers) QueryAuth(w http.ResponseWriter, r *http.Request) {
var authCode string
// 授权码
if vs := r.URL.Query()["auth_code"]; len(vs) == 1 {
authCode = vs[0]
}
authInfo, err := h.open.QueryAuth(authCode)
if err != nil {
httpx.Error(w, err)
return
}
httpx.OkJson(w, g.Map{
"authInfo": authInfo,
})
}
//import (
// "fmt"
// "net/http"
//
// "git.zc0901.com/go/god/lib/g"
//
// "git.zc0901.com/go/god/lib/gconv"
//
// "git.zc0901.com/go/god/lib/store/kv"
// "git.zc0901.com/go/god/lib/store/redis"
//
// "git.zc0901.com/go/god/api/httpx"
//
// "git.zc0901.com/go/god/lib/logx"
// cacheRedis "git.zc0901.com/go/god/lib/store/cache"
// "git.zc0901.com/go/god/lib/wechat/cache"
// "git.zc0901.com/go/god/lib/wechat/msg"
// "git.zc0901.com/go/god/lib/wechat/openplatform"
// "git.zc0901.com/go/god/lib/wechat/openplatform/config"
//)
//
//type OpenHandlers struct {
// open *openplatform.OpenPlatform
//}
//
//func NewOpenHandlers() *OpenHandlers {
// store := kv.NewStore([]cacheRedis.Conf{
// {
// Conf: redis.Conf{
// Host: "vps:6382",
// Password: "4a5d4787a82c660ee18719f51ff40d9a669a4958",
// Mode: redis.StandaloneMode,
// },
// Weight: 100,
// },
// })
//
// o := openplatform.New(&config.Config{
// AppID: "wxbef357be217c23c5",
// AppSecret: "403d127716317ea23c8db1a1107b14fc",
// Token: "imola1999zhuke2012dhome2020",
// EncodingAESKey: "imola1999azhuke2012adhome2020a18611914900aa",
// Cache: cache.NewRedis(store),
// })
// return &OpenHandlers{o}
//}
//
//func (h *OpenHandlers) Notify(w http.ResponseWriter, r *http.Request) {
// logx.Debugf("请求方法: %v", r.Method)
// server := openplatform.GetServer(h.open.Context, w, r)
//
// // 设置消息响应处理器
// server.SetMessageHandler(func(m *msg.Msg) (*msg.Response, error) {
// if m.InfoType == "component_verify_ticket" {
// accessToken, err := h.open.SetAccessToken(m.ComponentVerifyTicket)
// if err != nil {
// return nil, err
// }
// logx.Debug("平台访问令牌", accessToken)
// } else {
// fmt.Println("消息钩子", m)
// }
// return nil, nil
// })
//
// // 处理微信的请求信息
// err := server.Serve()
// if err != nil {
// logx.Error("微信响应服务器错误:", err)
// return
// }
//
// // 发送回复的消息
// err = server.Send()
// if err != nil {
// logx.Errorf("响应微信错误,err=%+v", err)
// return
// }
//}
//
//func (h *OpenHandlers) AccessToken(w http.ResponseWriter, r *http.Request) {
// accessToken, err := h.open.AccessToken()
// if err != nil {
// httpx.Error(w, err)
// return
// }
// httpx.OkJson(w, map[string]string{
// "token": accessToken,
// })
//}
//
//func (h *OpenHandlers) Auth(w http.ResponseWriter, r *http.Request) {
// isMobile := false
// if vs, ok := r.URL.Query()["isMobile"]; ok && len(vs) == 1 {
// isMobile = gconv.Bool(vs[0])
// }
//
// apiHost := "http://zs.ngrok.zc0901.com"
// redirect := fmt.Sprintf("%s/oplatform/%s/redirect", apiHost, h.open.AppID)
//
// var authUrl string
// var err error
//
// if isMobile {
// authUrl, err = h.open.MobileAuthURL(redirect, 2, "")
// } else {
// authUrl, err = h.open.PcAuthURL(redirect, 2, "")
// }
// if err != nil {
// w.WriteHeader(200)
// w.Write([]byte("系统异常: " + err.Error()))
// return
// }
// w.Header().Set("Content-Type", "text/html")
// w.Write([]byte(fmt.Sprintf("<script>location.href=\"%s\"</script>", authUrl)))
//}
//
//func (h *OpenHandlers) Redirect(w http.ResponseWriter, r *http.Request) {
// var authCode, expiresIn string
//
// // 授权码
// if vs := r.URL.Query()["auth_code"]; len(vs) == 1 {
// authCode = vs[0]
// }
//
// // 过期时间
// if vs := r.URL.Query()["expires_in"]; len(vs) == 1 {
// expiresIn = vs[0]
// }
//
// http.Redirect(w, r, "/home?auth_code="+authCode+"&expires_in="+expiresIn, 302)
//}
//
//func (h *OpenHandlers) QueryAuth(w http.ResponseWriter, r *http.Request) {
// var authCode string
//
// // 授权码
// if vs := r.URL.Query()["auth_code"]; len(vs) == 1 {
// authCode = vs[0]
// }
//
// authInfo, err := h.open.QueryAuth(authCode)
// if err != nil {
// httpx.Error(w, err)
// return
// }
// httpx.OkJson(w, g.Map{
// "authInfo": authInfo,
// })
//}
......@@ -51,6 +51,8 @@ require (
rogchap.com/v8go v0.7.0
)
require github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220209173558-ad29539cd2e9
require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
......
......@@ -70,6 +70,8 @@ github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220209173558-ad29539cd2e9 h1:zvkJv+9Pxm1nnEMcKnShREt4qtduHKz4iw4AB4ul0Ao=
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220209173558-ad29539cd2e9/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
......
......@@ -67,6 +67,7 @@ type (
ZIncrBy(key string, increment int64, field string) (int64, error)
ZRange(key string, start, stop int64) ([]string, error)
ZRangeWithScores(key string, start, stop int64) ([]redis.Pair, error)
ZRevRangeWithScores(key string, start, stop int64) ([]redis.Pair, error)
ZRangeByScoreWithScores(key string, start, stop int64) ([]redis.Pair, error)
ZRangeByScoreWithScoresAndLimit(key string, start, stop int64, page, size int) ([]redis.Pair, error)
ZRank(key, field string) (int64, error)
......@@ -649,6 +650,15 @@ func (cs clusterStore) ZRangeWithScores(key string, start, stop int64) ([]redis.
return node.ZRangeWithScores(key, start, stop)
}
func (cs clusterStore) ZRevRangeWithScores(key string, start, stop int64) ([]redis.Pair, error) {
node, err := cs.getRedis(key)
if err != nil {
return nil, err
}
return node.ZRevRangeWithScores(key, start, stop)
}
func (cs clusterStore) ZRangeByScoreWithScores(key string, start, stop int64) ([]redis.Pair, error) {
node, err := cs.getRedis(key)
if err != nil {
......
......@@ -4,4 +4,10 @@ mac:
GOOS=darwin go build -ldflags="-s -w" -ldflags="-X 'main.BuildTime=$(version)'" -o god god.go
#$(if $(shell command -v upx), upx god)
mv god ~/go/bin/
#mv god /usr/local/bin/
mac_amd64:
GOARCH=amd64 CGO_ENABLED=0 GOOS=darwin go build -ldflags="-s -w" -ldflags="-X 'main.BuildTime=$(version)'" -o god god.go
#$(if $(shell command -v upx), upx god)
mv god ~/go/bin/
#mv god /usr/local/bin/
\ No newline at end of file
......@@ -54,6 +54,7 @@ type (
Path string
RequestType Type
ResponseType Type
Handler string
}
Service struct {
......
......@@ -2,6 +2,7 @@ package main
import (
"fmt"
"git.zc0901.com/go/god/tools/god/plugin"
"os"
"runtime"
"time"
......@@ -187,6 +188,29 @@ var (
},
Action: gogen.GoCommand,
},
{
Name: "plugin",
Usage: "生成自定义文件",
Flags: []cli.Flag{
cli.StringFlag{
Name: "plugin, p",
Usage: "插件文件",
},
cli.StringFlag{
Name: "dir",
Usage: "目标目录",
},
cli.StringFlag{
Name: "api",
Usage: "api文件",
},
cli.StringFlag{
Name: "style",
Usage: "the file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md]",
},
},
Action: plugin.Command,
},
},
},
}
......
package main
import (
"fmt"
"git.zc0901.com/go/god/tools/god/plugin"
)
func main() {
// 命令行测试 echo '{"apiFilePath":"/Users/xqk/GolandProjects/dhome_old/dhome/api/api/activity.api","style":"","dir":"/Users/xqk/GolandProjects"}' | go run godplugin.go
p, err := plugin.NewPlugin()
if err != nil {
panic(err)
}
if p.Api != nil {
fmt.Printf("api: %+v \n", p.Api)
}
fmt.Println("Enjoy anything you want.")
}
package plugin
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"git.zc0901.com/go/god/tools/god/api/parser"
"git.zc0901.com/go/god/tools/god/rpc/execx"
"git.zc0901.com/go/god/tools/god/util"
"github.com/urfave/cli"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
)
const pluginArg = "_plugin"
// Plugin defines an api plugin
type Plugin struct {
Api *parser.Parser
ApiFilePath string
Style string
Dir string
}
// Command is the entry of god api plugin
func Command(c *cli.Context) error {
ex, err := os.Executable()
if err != nil {
panic(err)
}
plugin := c.String("plugin")
if len(plugin) == 0 {
return errors.New("missing plugin")
}
transferData, err := prepareArgs(c)
if err != nil {
return err
}
bin, args := getPluginAndArgs(plugin)
bin, download, err := getCommand(bin)
if err != nil {
return err
}
if download {
defer func() {
_ = os.Remove(bin)
}()
}
content, err := execx.Run(bin+" "+args, filepath.Dir(ex), bytes.NewBuffer(transferData))
if err != nil {
return err
}
fmt.Println(content)
return nil
}
func prepareArgs(c *cli.Context) ([]byte, error) {
apiPath := c.String("api")
var transferData Plugin
if len(apiPath) > 0 && util.FileExists(apiPath) {
api, err := parser.NewParser(apiPath)
if err != nil {
return nil, err
}
transferData.Api = api
}
absApiFilePath, err := filepath.Abs(apiPath)
if err != nil {
return nil, err
}
transferData.ApiFilePath = absApiFilePath
dirAbs, err := filepath.Abs(c.String("dir"))
if err != nil {
return nil, err
}
transferData.Dir = dirAbs
transferData.Style = c.String("style")
data, err := json.Marshal(transferData)
if err != nil {
return nil, err
}
return data, nil
}
func getCommand(arg string) (string, bool, error) {
p, err := exec.LookPath(arg)
if err == nil {
abs, err := filepath.Abs(p)
if err != nil {
return "", false, err
}
return abs, false, nil
}
defaultErr := errors.New("invalid plugin value " + arg)
if strings.HasPrefix(arg, "http") {
items := strings.Split(arg, "/")
if len(items) == 0 {
return "", false, defaultErr
}
filename, err := filepath.Abs(pluginArg + items[len(items)-1])
if err != nil {
return "", false, err
}
err = downloadFile(filename, arg)
if err != nil {
return "", false, err
}
err = os.Chmod(filename, os.ModePerm)
if err != nil {
return "", false, err
}
return filename, true, nil
}
return arg, false, nil
}
func downloadFile(filepath, url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
out, err := os.Create(filepath)
if err != nil {
return err
}
defer func() {
_ = out.Close()
}()
_, err = io.Copy(out, resp.Body)
return err
}
// NewPlugin returns contextual resources when written in other languages
func NewPlugin() (*Plugin, error) {
var plugin Plugin
content, err := ioutil.ReadAll(os.Stdin)
if err != nil {
return nil, err
}
var info struct {
ApiFilePath string
Style string
Dir string
}
err = json.Unmarshal(content, &info)
if err != nil {
return nil, err
}
plugin.ApiFilePath = info.ApiFilePath
plugin.Style = info.Style
plugin.Dir = info.Dir
api, err := parser.NewParser(info.ApiFilePath)
if err != nil {
return nil, err
}
plugin.Api = api
return &plugin, nil
}
func getPluginAndArgs(arg string) (string, string) {
i := strings.Index(arg, "=")
if i <= 0 {
return arg, ""
}
return trimQuote(arg[:i]), trimQuote(arg[i+1:])
}
func trimQuote(in string) string {
in = strings.Trim(in, `"`)
in = strings.Trim(in, `'`)
in = strings.Trim(in, "`")
return in
}
package plugin
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestGetPluginAndArgs(t *testing.T) {
bin, args := getPluginAndArgs("android")
assert.Equal(t, "android", bin)
assert.Equal(t, "", args)
bin, args = getPluginAndArgs("android=")
assert.Equal(t, "android", bin)
assert.Equal(t, "", args)
bin, args = getPluginAndArgs("android=-javaPackage com.tal")
assert.Equal(t, "android", bin)
assert.Equal(t, "-javaPackage com.tal", args)
bin, args = getPluginAndArgs("android=-javaPackage com.tal --lambda")
assert.Equal(t, "android", bin)
assert.Equal(t, "-javaPackage com.tal --lambda", args)
bin, args = getPluginAndArgs(`https://test-xjy-file.obs.cn-east-2.myhuaweicloud.com/202012/8a7ab6e1-e639-49d1-89cf-2ae6127a1e90n=-v 1`)
assert.Equal(t, "https://test-xjy-file.obs.cn-east-2.myhuaweicloud.com/202012/8a7ab6e1-e639-49d1-89cf-2ae6127a1e90n", bin)
assert.Equal(t, "-v 1", args)
}
......@@ -12,7 +12,7 @@ import (
"git.zc0901.com/go/god/tools/god/vars"
)
func Run(arg string, dir string) (string, error) {
func Run(arg string, dir string, in ...*bytes.Buffer) (string, error) {
goos := runtime.GOOS
var cmd *exec.Cmd
switch goos {
......@@ -28,6 +28,9 @@ func Run(arg string, dir string) (string, error) {
}
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
if len(in) > 0 {
cmd.Stdin = in[0]
}
cmd.Stdout = stdout
cmd.Stderr = stderr
err := cmd.Run()
......
......@@ -61,6 +61,12 @@ func Clean(category string) error {
return os.RemoveAll(dir)
}
// FileExists returns true if the specified file is exists.
func FileExists(file string) bool {
_, err := os.Stat(file)
return err == nil
}
func LoadTemplate(category, file, builtin string) (string, error) {
dir, err := GetTemplateDir(category)
if err != nil {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment