From b2e6741ffa9fe36f63d78197704e5426c00b0052 Mon Sep 17 00:00:00 2001
From: abbycin <abbytsing@gmail.com>
Date: Tue, 10 Mar 2020 14:33:53 +0800
Subject: [PATCH] windows can also run as serivce now

---
 README.md            |  20 +++----
 conf.toml            |   7 +--
 detail/common.go     |  17 +++---
 detail/postgresql.go | 124 ++++++++++++-------------------------------
 go.mod               |   1 +
 go.sum               |   2 +
 logging/logging.go   |   3 +-
 main.go              |  23 ++++----
 8 files changed, 75 insertions(+), 122 deletions(-)

diff --git a/README.md b/README.md
index fcad2de..a643da1 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,7 @@
 配置使用TOML格式,示例如下:
 ```toml
 [[PGConfig]]
+pg_home = "C:/Program Files/PostgreSQL/10/bin" # pg 安装目录
 host = "127.0.0.1"
 port = 5432
 db = ["test", "postgres"]
@@ -24,32 +25,31 @@ username = "admin"
 password = "admin"
 destination = "/data/backup"
 keep = 10
-cron = "0 0 1 * * *" # 每天1点0分0秒运行
+cron = "* 1 * * *" # 每天1点0分0秒运行
 options = ["--inserts"]
 ```
 
-配置中`keep`表示保留最近 N 天, `cron`表示何时执行备份任务,6个`*` 分别表示: 秒, 分, 时, 本月第几天,月,本月第几天,举例如下:
+配置中`keep`表示保留最近 N 小时, `cron`表示何时执行备份任务,5个`*` 分别表示: 分, 时, 本月第几天,月,本周第几天,举例如下:
 
 
-- `33 1 * * * *` 每个小时的 1 分 33 秒执行  
-- `*/1 * * * * *` 每秒执行  
-- `0 */1 * * * * ` 每分钟执行  
-- `23 30 1 * * *`  每天 1 时 30 分 23 秒执行  
+- `*/1 * * * *` 每1分钟执行  
+- `0 */1 * * *` 每小1时执行  
+- `0 0 */1 * * ` 每月1号执行  
+- `23 2 1 * *`  每月1号2时23分执行  
 
 更多参考 [这里](https://godoc.org/github.com/robfig/cron)  
 
 ## 用法  
 1, Windows/Linux下直接运行  
-2, Linux下可选择安装为服务 `./dbackup -service install` (以root用户运行)  
+2, 可选择安装为服务 `./dbackup -service install` (Linux下以root用户运行)  
   可用参数有 `-service start`, `-service stop`, `-service restart`, `-service install`, `-service uninstall`  
   
 程序会读取程序所在目录下的`conf.toml`配置,并在当前目录下创建`log`目录用于记录日志  
 
 ## 注意
-1,必须保证`pg_dump` 在环境变量中  
+1, 如果可以找到`pg_dump`那们配置中的`pg_home`可以设置为空(即pg_home = "")  
 2, 程序只会使用必要的参数用于连接数据库,额外的参数可以通过`options`传递  
-3, 输出备份文件名为 `db_yyyymmdd-HHMMSS.dump` 且不可配置,存放位置为 `destination/db/db_yyyymmdd-HHMMSS.dump`      
-3, windows中程序不能作为服务运行(因为pg_dump找不到pgpass)   
+3, 输出备份文件名为 `db_yyyymmdd-HHMMSS.dump` 且不可配置,存放位置为 `destination/db/db_yyyymmdd-HHMMSS.dump`         
 
 ## 示例  
 
diff --git a/conf.toml b/conf.toml
index c973614..a8b656e 100644
--- a/conf.toml
+++ b/conf.toml
@@ -1,12 +1,13 @@
 [[PGConfig]]
+pg_home = "C:/Program Files/PostgreSQL/10/bin" # where postgresql installed
 host = "127.0.0.1"
 port = 5432
 db = ["test", "postgres"]
 username = "abby"
 password = "abby"
 destination = "D:/backup"
-keep = 10
-cron = "0 30 1 * * *" # run every day at 01:30:00
+keep = 10 # keep latest 10 hours dump, set -1 to disable cleanup
+cron = "1 * * * *" # run every 1 minute
 options = ["-Fc", "--no-owner"]
 
 #[[PGConfig]]
@@ -17,5 +18,5 @@ options = ["-Fc", "--no-owner"]
 #password = "admin"
 #destination = "/data/backup"
 #keep = 10
-#cron = "0 0 1 * * *" # run every day at 01:00:00
+#cron = "0 1 * * *" # run every day at 01:00:00
 #options = ["--inserts"]
\ No newline at end of file
diff --git a/detail/common.go b/detail/common.go
index cf2ce20..d5c4f02 100644
--- a/detail/common.go
+++ b/detail/common.go
@@ -3,7 +3,7 @@ package detail
 import (
 	"dbackup/logging"
 	"github.com/kardianos/service"
-	"github.com/robfig/cron"
+	"github.com/robfig/cron/v3"
 	"os"
 	"path/filepath"
 	"runtime"
@@ -26,21 +26,20 @@ func InitLogger() {
 	}
 
 	e = log.Init(&log.Logger{
-		FileName: curr,
-		RollSize: 1 << 30,
+		FileName:     curr,
+		RollSize:     1 << 30,
 		RollInterval: time.Hour * 30,
-		Level: log.DEBUG,
+		Level:        log.DEBUG,
 		PanicOnFatal: true,
 	})
 }
 
-
 type Job interface {
 	AddTask(*Task) error
 }
 
 type Task struct {
-	Ch chan struct{}
+	Ch   chan struct{}
 	Cron *cron.Cron
 }
 
@@ -50,10 +49,10 @@ func (t *Task) Start(srv service.Service) error {
 	return nil
 }
 
-func (t* Task) Run() {
+func (t *Task) Run() {
 	log.Info("running...")
 	t.Cron.Start()
-	<- t.Ch
+	<-t.Ch
 	log.Info("stopped")
 	t.Cron.Stop()
 
@@ -68,4 +67,4 @@ func (t *Task) Stop(srv service.Service) error {
 
 func (t *Task) AddTask(task Job) error {
 	return task.AddTask(t)
-}
\ No newline at end of file
+}
diff --git a/detail/postgresql.go b/detail/postgresql.go
index 43f0272..dc6ed52 100644
--- a/detail/postgresql.go
+++ b/detail/postgresql.go
@@ -1,16 +1,12 @@
 package detail
 
 import (
-	"bufio"
 	log "dbackup/logging"
 	"fmt"
-	"github.com/robfig/cron"
-	"io"
+	"github.com/robfig/cron/v3"
 	"io/ioutil"
 	"os"
 	"os/exec"
-	user2 "os/user"
-	"path/filepath"
 	"runtime"
 	"strconv"
 	"strings"
@@ -22,13 +18,14 @@ const (
 )
 
 type PGConfig struct {
+	PgHome      string   `toml:"pg_home"`
 	Host        string   `toml:"host"`
 	Port        int      `toml:"port"`
 	DB          []string `toml:"db"`
 	Username    string   `toml:"username"`
 	Password    string   `toml:"password"`
 	Destination string   `toml:"destination"`
-	Keep        int64      `toml:"keep"`
+	Keep        int64    `toml:"keep"`
 	Cron        string   `toml:"cron"`
 	Options     []string `toml:"options"`
 }
@@ -43,85 +40,11 @@ func (x *Postgres) Setup() {
 			log.Fatal("invalid destination: %s, must use Unix Path Seperator `/`")
 		}
 		for _, i := range c.DB {
-			if e := os.MkdirAll(c.Destination + "/" + i, os.ModePerm); e != nil {
+			if e := os.MkdirAll(c.Destination+"/"+i, os.ModePerm); e != nil {
 				log.Fatal("can't mkdir: %s, err: %s", c.Destination, e)
 			}
 		}
 	}
-
-	user, e := user2.Current()
-
-	if e != nil {
-		log.Fatal("can't get current user: %s", e)
-	}
-
-	var pgpass string
-
-	if runtime.GOOS == "windows" {
-		pgpass = filepath.Join(os.Getenv("AppData"), "postgresql")
-		_ = os.MkdirAll(pgpass, os.ModePerm)
-		pgpass = filepath.Join(pgpass, "pgpass.conf")
-
-	} else if runtime.GOOS == "linux" {
-
-		pgpass = filepath.Join(user.HomeDir, ".pgpass")
-
-	} else {
-		log.Fatal("un-support os")
-	}
-
-	f, e := os.OpenFile(pgpass, os.O_RDWR, 0)
-	if e != nil {
-		f, e = os.Create(pgpass)
-		if e != nil {
-			log.Fatal("can't create %s, err: %s", f, e)
-		}
-
-		if runtime.GOOS == "linux" {
-			_ = os.Chmod(pgpass, 0600)
-		}
-	}
-
-	set := make(map[string]bool)
-
-	scanner := bufio.NewScanner(f)
-	for scanner.Scan() {
-		ff := scanner.Text()
-		log.Info("==>> %s", ff)
-		set[ff] = true
-	}
-
-	if err := scanner.Err(); err != nil {
-		f.Close()
-		log.Fatal("can't read pgpass: %s", err)
-	}
-
-	for _, cfg := range x.PGConfig {
-		for _, db := range cfg.DB {
-			line := fmt.Sprintf("%s:%d:%s:%s:%s", cfg.Host, cfg.Port, db, cfg.Username, cfg.Password)
-			set[line] = true
-		}
-	}
-
-	e = f.Truncate(0)
-	f.Seek(0, io.SeekStart)
-	for k, _ := range set {
-		_, _ = fmt.Fprintf(f, "%s\n", k)
-	}
-
-	f.Close()
-
-	// NOTE: don't work when run as service
-	// hard code, fuck windows
-	// assume that %APPDATA% is C:\Users\Administrator\AppData\Roaming
-	//if runtime.GOOS == "windows" {
-	//	r := "C:/Users/Administrator/AppData/Roaming/postgresql"
-	//	_ = os.MkdirAll(r, os.ModePerm)
-	//	e = os.Rename(pgpass, r + "/pgpass.conf")
-	//	if e != nil {
-	//		log.Fatal("can't rename: %s", e)
-	//	}
-	//}
 }
 
 func (x *Postgres) AddTask(t *Task) error {
@@ -142,17 +65,37 @@ func (x *PGConfig) Run() {
 		options := append(x.GetOptions(db), "-f", path) //fmt.Sprintf(`-f%v`, path))
 		cmd := fmt.Sprintf("%s %s", PGDumpCmd, strings.Join(options, " "))
 		log.Info("running task: `%s`", cmd)
-		out, err := exec.Command(PGDumpCmd, options...).CombinedOutput()
-		if err != nil {
-			log.Fatal("failed: %s, %s", err, out)
+		if runtime.GOOS == "windows" {
+			cmd = fmt.Sprintf(`set PGPASSWORD=%s&& %s`, x.Password, cmd)
+			mo := exec.Command("cmd", "/C", cmd)
+			mo.Env = []string{fmt.Sprintf(`PATH=%s`, x.PgHome)}
+			out, err := mo.CombinedOutput()
+			if err != nil {
+				log.Fatal("failed: %s, %s", err, out)
+			} else {
+				log.Info("ok")
+				x.CleanUp(db)
+			}
 		} else {
-			log.Info("ok")
-			x.CleanUp(db)
+			cmd = fmt.Sprintf(`PGPASSWORD="%s" %s`, x.Password, cmd)
+			mo := exec.Command("bash", "-c", cmd)
+			mo.Env = []string{fmt.Sprintf("PATH=%s:%s", os.Getenv("PATH"), x.PgHome)}
+			out, err := mo.CombinedOutput()
+
+			if err != nil {
+				log.Fatal("failed: %s, %s", err, out)
+			} else {
+				log.Info("ok")
+				x.CleanUp(db)
+			}
 		}
 	}
 }
 
 func (x *PGConfig) CleanUp(db string) {
+	if x.Keep < 0 {
+		return
+	}
 	root := fmt.Sprintf("%s/%s", x.Destination, db)
 	info, e := ioutil.ReadDir(root)
 	if e != nil {
@@ -167,7 +110,7 @@ func (x *PGConfig) CleanUp(db string) {
 				log.Error("can't get last modify time for: %s, err: %s", f, e)
 				continue
 			}
-			if time.Since(o.ModTime()) >= time.Duration(x.Keep * int64(time.Hour)) {
+			if time.Since(o.ModTime()) >= time.Duration(x.Keep*int64(time.Hour)) {
 				log.Info("remove expired backup: %s", i.Name())
 				_ = os.Remove(f)
 			}
@@ -186,10 +129,11 @@ func (x *PGConfig) AddCron(c *cron.Cron) error {
 	}
 	s = s[0:i]
 	log.Info("%s", s)
-	if len(s) != 6 {
+	if len(s) != 5 {
 		return fmt.Errorf("invalid cron format: %s", x.Cron)
 	}
-	return c.AddJob(strings.Join(s, " "), x)
+	_, e := c.AddJob(strings.Join(s, " "), x)
+	return e
 }
 
 func (x *PGConfig) GetOptions(db string) []string {
@@ -198,4 +142,4 @@ func (x *PGConfig) GetOptions(db string) []string {
 	options = append(options, "-d", db, "-h", x.Host, "-p", strconv.Itoa(x.Port), "-U", x.Username)
 
 	return options
-}
\ No newline at end of file
+}
diff --git a/go.mod b/go.mod
index 4ee0d5d..100d4ff 100644
--- a/go.mod
+++ b/go.mod
@@ -6,4 +6,5 @@ require (
 	github.com/BurntSushi/toml v0.3.1
 	github.com/kardianos/service v1.0.0
 	github.com/robfig/cron v1.2.0
+	github.com/robfig/cron/v3 v3.0.1
 )
diff --git a/go.sum b/go.sum
index 4f2d2ff..529b8b9 100644
--- a/go.sum
+++ b/go.sum
@@ -6,5 +6,7 @@ github.com/kardianos/service v1.0.0 h1:HgQS3mFfOlyntWX8Oke98JcJLqt1DBcHR4kxShpYe
 github.com/kardianos/service v1.0.0/go.mod h1:8CzDhVuCuugtsHyZoTvsOBuvonN/UDBvl0kH+BUxvbo=
 github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
 github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
+github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
+github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
 golang.org/x/sys v0.0.0-20190204203706-41f3e6584952 h1:FDfvYgoVsA7TTZSbgiqjAbfPbK47CNHdWl3h/PJtii0=
 golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
diff --git a/logging/logging.go b/logging/logging.go
index fa72f25..39ab204 100644
--- a/logging/logging.go
+++ b/logging/logging.go
@@ -151,8 +151,9 @@ func Error(f string, args ...interface{}) {
 
 func Fatal(f string, args ...interface{}) {
 	backend.dispatch(FATAL, f, args...)
+	fmt.Printf(f, args...)
 	Release()
 	if backend.PanicOnFatal {
-		panic("fatal error")
+		os.Exit(1)
 	}
 }
diff --git a/main.go b/main.go
index e210357..55353e4 100644
--- a/main.go
+++ b/main.go
@@ -4,20 +4,26 @@ import (
 	"dbackup/detail"
 	log "dbackup/logging"
 	"flag"
+	"fmt"
 	"github.com/BurntSushi/toml"
 	"github.com/kardianos/service"
-	"github.com/robfig/cron"
+	"github.com/robfig/cron/v3"
 	"os"
 	"path/filepath"
 )
 
 func main() {
 	svcFlag := flag.String("service", "", "Control the system service.")
+	version := flag.Bool("version", false, "print version")
 	flag.Parse()
+	if *version {
+		fmt.Println("1.0.1")
+		os.Exit(0)
+	}
 	detail.InitLogger()
 
 	p := &detail.Task{
-		Ch: make(chan struct{}),
+		Ch:   make(chan struct{}),
 		Cron: cron.New(),
 	}
 
@@ -51,8 +57,7 @@ func main() {
 	if len(*svcFlag) != 0 {
 		err := service.Control(s, *svcFlag)
 		if err != nil {
-			log.Fatal("valid actions: %q\n", service.ControlAction)
-			log.Fatal("%s", err)
+			log.Fatal("error: %v\nvalid actions: %q\n", err, service.ControlAction)
 		}
 		return
 	}
@@ -65,12 +70,12 @@ func main() {
 }
 
 func addTask(t *detail.Task) {
-	tmp, err := filepath.Abs(filepath.Dir(os.Args[0]))
+	x, err := os.Executable()
 	if err != nil {
-		log.Fatal("can't get current work directory: %s", err)
+		log.Fatal("can't get executable path: %v", err)
 	}
-
-	cfg := filepath.Join(tmp, "conf.toml")
+	exePath := filepath.Dir(x)
+	cfg := filepath.Join(exePath, "conf.toml")
 
 	var pg detail.Postgres
 	if _, err := toml.DecodeFile(cfg, &pg); err != nil {
@@ -80,4 +85,4 @@ func addTask(t *detail.Task) {
 	if err := pg.AddTask(t); err != nil {
 		log.Fatal("%s", err)
 	}
-}
\ No newline at end of file
+}
-- 
2.21.0