Commit 62bf8885 authored by abbycin's avatar abbycin

done

parent 27ed8384
.idea .idea
*.exe *.exe
*~ *~
log
\ No newline at end of file
## dbackup ## dbackup
database backup service 备份数据库服务
## 配置
配置使用TOML格式,示例如下:
```toml
[[PGConfig]]
host = "127.0.0.1"
port = 5432
db = ["test", "postgres"]
username = "abby"
password = "abby"
destination = "D:/backup"
keep = 3
cron = "0 */1 * * * *" # 每分钟的第0秒运行
options = ["-Fc", "--no-owner"]
[[PGConfig]]
host = "127.0.0.1"
port = 5433
db = ["db1", "db2"]
username = "admin"
password = "admin"
destination = "/data/backup"
keep = 10
cron = "0 0 1 * * *" # 每天1点0分0秒运行
options = ["--inserts"]
```
配置中`keep`表示保留最近 N 天, `cron`表示何时执行备份任务,6个`*` 分别表示: 秒, 分, 时, 本月第几天,月,本月第几天,举例如下:
- `33 1 * * * *` 每个小时的 1 分 33 秒执行
- `*/1 * * * * *` 每秒执行
- `0 */1 * * * * ` 每分钟执行
- `23 30 1 * * *` 每天 1 时 30 分 23 秒执行
更多参考 [这里](https://godoc.org/github.com/robfig/cron)
## 用法
1, Windows/Linux下直接运行
2, Linux下可选择安装为服务 `./dbackup -service install` (以root用户运行)
可用参数有 `-service start`, `-service stop`, `-service restart`, `-service install`, `-service uninstall`
程序会读取程序所在目录下的`conf.toml`配置,并在当前目录下创建`log`目录用于记录日志
## 注意
1,必须保证`pg_dump` 在环境变量中
2, 程序只会使用必要的参数用于连接数据库,额外的参数可以通过`options`传递
3, 输出备份文件名为 `db_yyyymmdd-HHMMSS.dump` 且不可配置,存放位置为 `destination/db/db_yyyymmdd-HHMMSS.dump`
3, windows中程序不能作为服务运行(因为pg_dump找不到pgpass)
## 示例
每分钟备份一次,保留最近3分钟的备份
[example](./example.png)
\ No newline at end of file
[[postgresql]] [[PGConfig]]
host = "127.0.0.1" host = "127.0.0.1"
port = 5432 port = 5432
db = ["db1", "db2"] db = ["test", "postgres"]
username = "postgres" username = "abby"
password = "postgres" password = "abby"
destination = "/data/archive" destination = "D:/backup"
keep = 10 keep = 10
# M H d m w cron = "0 30 1 * * *" # run every day at 01:30:00
cron = "10 2 * * *" options = ["-Fc", "--no-owner"]
options = ["-Fc", "--no-onwer"]
[[postgresql]] #[[PGConfig]]
host = "127.0.0.1" #host = "127.0.0.1"
port = 5433 #port = 5433
db = ["db1", "db2"] #db = ["db1", "db2"]
username = "admin" #username = "admin"
password = "admin" #password = "admin"
destination = "/data/backup" #destination = "/data/backup"
keep = 10 #keep = 10
cron = "0 1 * * *" #cron = "0 0 1 * * *" # run every day at 01:00:00
options = ["--inserts"] #options = ["--inserts"]
\ No newline at end of file \ No newline at end of file
...@@ -5,16 +5,32 @@ import ( ...@@ -5,16 +5,32 @@ import (
"github.com/kardianos/service" "github.com/kardianos/service"
"github.com/robfig/cron" "github.com/robfig/cron"
"os" "os"
"path/filepath"
"runtime"
"strings"
"time" "time"
) )
func InitLogger() { func InitLogger() {
_ = log.Init(&log.Logger{ curr, e := filepath.Abs(filepath.Dir(os.Args[0]))
FileName: os.TempDir() + "/dbackup.log", if e != nil {
curr = filepath.Join(os.TempDir(), "dbackup.log")
} else {
curr = filepath.Join(curr, "log")
_ = os.MkdirAll(curr, os.ModePerm)
curr = filepath.Join(curr, "dbackup.log")
}
if runtime.GOOS == "windows" {
curr = strings.ReplaceAll(curr, "\\", "/")
}
e = log.Init(&log.Logger{
FileName: curr,
RollSize: 1 << 30, RollSize: 1 << 30,
RollInterval: time.Hour * 30, RollInterval: time.Hour * 30,
Level: log.DEBUG, Level: log.DEBUG,
PanicOnFatal: false, PanicOnFatal: true,
}) })
} }
...@@ -29,11 +45,13 @@ type Task struct { ...@@ -29,11 +45,13 @@ type Task struct {
} }
func (t *Task) Start(srv service.Service) error { func (t *Task) Start(srv service.Service) error {
log.Info("starting...")
go t.Run()
return nil return nil
} }
func (t* Task) Run() { func (t* Task) Run() {
log.Info("running...")
t.Cron.Start() t.Cron.Start()
<- t.Ch <- t.Ch
log.Info("stopped") log.Info("stopped")
...@@ -43,6 +61,7 @@ func (t* Task) Run() { ...@@ -43,6 +61,7 @@ func (t* Task) Run() {
func (t *Task) Stop(srv service.Service) error { func (t *Task) Stop(srv service.Service) error {
log.Info("stopping...") log.Info("stopping...")
t.Cron.Stop()
t.Ch <- struct{}{} t.Ch <- struct{}{}
return nil return nil
} }
......
package detail package detail
import ( import (
"bufio"
log "dbackup/logging" log "dbackup/logging"
"fmt" "fmt"
"github.com/robfig/cron" "github.com/robfig/cron"
"io"
"io/ioutil"
"os"
"os/exec" "os/exec"
user2 "os/user"
"path/filepath"
"runtime"
"strconv"
"strings" "strings"
"time" "time"
) )
...@@ -14,22 +22,111 @@ const ( ...@@ -14,22 +22,111 @@ const (
) )
type PGConfig struct { type PGConfig struct {
Host string Host string `toml:"host"`
Port string Port int `toml:"port"`
DB string DB []string `toml:"db"`
Username string Username string `toml:"username"`
Password string Password string `toml:"password"`
Destination string Destination string `toml:"destination"`
Keep int Keep int64 `toml:"keep"`
Cron string Cron string `toml:"cron"`
Options []string Options []string `toml:"options"`
} }
type Postgres struct { type Postgres struct {
PGConfig []PGConfig PGConfig []PGConfig
} }
func (x *Postgres) Setup() {
for _, c := range x.PGConfig {
if strings.Index(c.Destination, "\\") != -1 {
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 {
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 { func (x *Postgres) AddTask(t *Task) error {
x.Setup()
log.Info("postgres adding task, count: %d", len(x.PGConfig))
for _, cfg := range x.PGConfig { for _, cfg := range x.PGConfig {
if e := cfg.AddCron(t.Cron); e != nil { if e := cfg.AddCron(t.Cron); e != nil {
return e return e
...@@ -39,13 +136,42 @@ func (x *Postgres) AddTask(t *Task) error { ...@@ -39,13 +136,42 @@ func (x *Postgres) AddTask(t *Task) error {
} }
func (x *PGConfig) Run() { func (x *PGConfig) Run() {
path := fmt.Sprintf(`%s/%v_%v.dump`, x.Destination, x.DB, time.Now().Unix()) log.Info("backup postgresql...")
options := append(x.GetOptions(), "-Fc", fmt.Sprintf(`-f%v`, path)) for _, db := range x.DB {
out, err := exec.Command(PGDumpCmd, options...).Output() path := fmt.Sprintf(`%s/%s/%v_%v.dump`, x.Destination, db, db, time.Now().Format("20060102-150405"))
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 { if err != nil {
log.Fatal("%s", out) log.Fatal("failed: %s, %s", err, out)
} else { } else {
log.Info("%s %s, ok", PGDumpCmd, strings.Join(options, " ")) log.Info("ok")
x.CleanUp(db)
}
}
}
func (x *PGConfig) CleanUp(db string) {
root := fmt.Sprintf("%s/%s", x.Destination, db)
info, e := ioutil.ReadDir(root)
if e != nil {
log.Fatal("can't list dir: %s, err: %s", x.Destination, e)
}
for _, i := range info {
if strings.Index(i.Name(), db) == 0 {
f := fmt.Sprintf("%s/%s", root, i.Name())
o, e := os.Stat(f)
if e != nil {
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)) {
log.Info("remove expired backup: %s", i.Name())
_ = os.Remove(f)
}
}
} }
} }
...@@ -59,30 +185,17 @@ func (x *PGConfig) AddCron(c *cron.Cron) error { ...@@ -59,30 +185,17 @@ func (x *PGConfig) AddCron(c *cron.Cron) error {
} }
} }
s = s[0:i] s = s[0:i]
if len(s) != 5 { log.Info("%s", s)
if len(s) != 6 {
return fmt.Errorf("invalid cron format: %s", x.Cron) return fmt.Errorf("invalid cron format: %s", x.Cron)
} }
return c.AddJob(strings.Join(s, " "), x) return c.AddJob(strings.Join(s, " "), x)
} }
func (x *PGConfig) GetOptions() []string { func (x *PGConfig) GetOptions(db string) []string {
options := x.Options options := x.Options
if x.DB != "" { options = append(options, "-d", db, "-h", x.Host, "-p", strconv.Itoa(x.Port), "-U", x.Username)
options = append(options, fmt.Sprintf(`-d%v`, x.DB))
}
if x.Host != "" {
options = append(options, fmt.Sprintf(`-h%v`, x.Host))
}
if x.Port != "" {
options = append(options, fmt.Sprintf(`-p%v`, x.Port))
}
if x.Username != "" {
options = append(options, fmt.Sprintf(`-U%v`, x.Username))
}
return options return options
} }
\ No newline at end of file
example.png

94.9 KB

...@@ -3,7 +3,7 @@ module dbackup ...@@ -3,7 +3,7 @@ module dbackup
go 1.12 go 1.12
require ( require (
github.com/BurntSushi/toml v0.3.1 // indirect github.com/BurntSushi/toml v0.3.1
github.com/kardianos/service v1.0.0 github.com/kardianos/service v1.0.0
github.com/robfig/cron v1.2.0 github.com/robfig/cron v1.2.0
) )
...@@ -151,6 +151,7 @@ func Error(f string, args ...interface{}) { ...@@ -151,6 +151,7 @@ func Error(f string, args ...interface{}) {
func Fatal(f string, args ...interface{}) { func Fatal(f string, args ...interface{}) {
backend.dispatch(FATAL, f, args...) backend.dispatch(FATAL, f, args...)
Release()
if backend.PanicOnFatal { if backend.PanicOnFatal {
panic("fatal error") panic("fatal error")
} }
......
// +build windows
package main package main
import ( import (
"dbackup/detail" "dbackup/detail"
log "dbackup/logging"
"flag" "flag"
"fmt" "github.com/BurntSushi/toml"
"github.com/kardianos/service" "github.com/kardianos/service"
"github.com/robfig/cron" "github.com/robfig/cron"
"log"
"os" "os"
"path" "path/filepath"
"runtime"
) )
func main() { func main() {
svcFlag := flag.String("service", "", "Control the system service.") svcFlag := flag.String("service", "", "Control the system service.")
flag.Parse() flag.Parse()
detail.InitLogger()
p := &detail.Task{ p := &detail.Task{
Ch: make(chan struct{}), Ch: make(chan struct{}),
...@@ -25,8 +23,6 @@ func main() { ...@@ -25,8 +23,6 @@ func main() {
addTask(p) addTask(p)
detail.InitLogger()
s, e := service.New(p, &service.Config{ s, e := service.New(p, &service.Config{
Name: "test", Name: "test",
DisplayName: "test service", DisplayName: "test service",
...@@ -34,20 +30,20 @@ func main() { ...@@ -34,20 +30,20 @@ func main() {
}) })
if e != nil { if e != nil {
log.Fatal(e) log.Fatal("%s", e)
} }
errs := make(chan error, 5) errs := make(chan error, 5)
lgg, e := s.Logger(errs) lgg, e := s.Logger(errs)
if e != nil { if e != nil {
log.Fatal(e) log.Fatal("%s", e)
} }
go func() { go func() {
for { for {
err := <-errs err := <-errs
if err != nil { if err != nil {
log.Print(err) log.Fatal("%s", err)
} }
} }
}() }()
...@@ -55,8 +51,8 @@ func main() { ...@@ -55,8 +51,8 @@ func main() {
if len(*svcFlag) != 0 { if len(*svcFlag) != 0 {
err := service.Control(s, *svcFlag) err := service.Control(s, *svcFlag)
if err != nil { if err != nil {
log.Printf("Valid actions: %q\n", service.ControlAction) log.Fatal("Valid actions: %q\n", service.ControlAction)
log.Fatal(err) log.Fatal("%s", err)
} }
return return
} }
...@@ -69,19 +65,19 @@ func main() { ...@@ -69,19 +65,19 @@ func main() {
} }
func addTask(t *detail.Task) { func addTask(t *detail.Task) {
bindir := []byte(path.Dir(os.Args[0])) tmp, err := filepath.Abs(filepath.Dir(os.Args[0]))
if runtime.GOOS == "windows" { if err != nil {
i := 0 log.Fatal("can't get current work directory: %s", err)
for j := 0; j < len(bindir); j++ {
if bindir[j] != '\\' {
bindir[i] = bindir[j]
i += 1
}
}
bindir = bindir[0:i]
} }
cfg := fmt.Sprintf("%s/conf.toml", string(bindir)) cfg := filepath.Join(tmp, "conf.toml")
var pg detail.Postgres
if _, err := toml.DecodeFile(cfg, &pg); err != nil {
log.Fatal("%s", err)
}
if err := pg.AddTask(t); err != nil {
log.Fatal("%s", err)
}
} }
\ No newline at end of file
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