Commit 62bf8885 authored by abbycin's avatar abbycin

done

parent 27ed8384
.idea
*.exe
*~
log
\ No newline at end of file
## 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"
port = 5432
db = ["db1", "db2"]
username = "postgres"
password = "postgres"
destination = "/data/archive"
db = ["test", "postgres"]
username = "abby"
password = "abby"
destination = "D:/backup"
keep = 10
# M H d m w
cron = "10 2 * * *"
options = ["-Fc", "--no-onwer"]
cron = "0 30 1 * * *" # run every day at 01:30:00
options = ["-Fc", "--no-owner"]
[[postgresql]]
host = "127.0.0.1"
port = 5433
db = ["db1", "db2"]
username = "admin"
password = "admin"
destination = "/data/backup"
keep = 10
cron = "0 1 * * *"
options = ["--inserts"]
\ No newline at end of file
#[[PGConfig]]
#host = "127.0.0.1"
#port = 5433
#db = ["db1", "db2"]
#username = "admin"
#password = "admin"
#destination = "/data/backup"
#keep = 10
#cron = "0 0 1 * * *" # run every day at 01:00:00
#options = ["--inserts"]
\ No newline at end of file
......@@ -5,16 +5,32 @@ import (
"github.com/kardianos/service"
"github.com/robfig/cron"
"os"
"path/filepath"
"runtime"
"strings"
"time"
)
func InitLogger() {
_ = log.Init(&log.Logger{
FileName: os.TempDir() + "/dbackup.log",
curr, e := filepath.Abs(filepath.Dir(os.Args[0]))
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,
RollInterval: time.Hour * 30,
Level: log.DEBUG,
PanicOnFatal: false,
PanicOnFatal: true,
})
}
......@@ -29,11 +45,13 @@ type Task struct {
}
func (t *Task) Start(srv service.Service) error {
log.Info("starting...")
go t.Run()
return nil
}
func (t* Task) Run() {
log.Info("running...")
t.Cron.Start()
<- t.Ch
log.Info("stopped")
......@@ -43,6 +61,7 @@ func (t* Task) Run() {
func (t *Task) Stop(srv service.Service) error {
log.Info("stopping...")
t.Cron.Stop()
t.Ch <- struct{}{}
return nil
}
......
package detail
import (
"bufio"
log "dbackup/logging"
"fmt"
"github.com/robfig/cron"
"io"
"io/ioutil"
"os"
"os/exec"
user2 "os/user"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
)
......@@ -14,22 +22,111 @@ const (
)
type PGConfig struct {
Host string
Port string
DB string
Username string
Password string
Destination string
Keep int
Cron string
Options []string
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"`
Cron string `toml:"cron"`
Options []string `toml:"options"`
}
type Postgres struct {
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 {
x.Setup()
log.Info("postgres adding task, count: %d", len(x.PGConfig))
for _, cfg := range x.PGConfig {
if e := cfg.AddCron(t.Cron); e != nil {
return e
......@@ -39,13 +136,42 @@ func (x *Postgres) AddTask(t *Task) error {
}
func (x *PGConfig) Run() {
path := fmt.Sprintf(`%s/%v_%v.dump`, x.Destination, x.DB, time.Now().Unix())
options := append(x.GetOptions(), "-Fc", fmt.Sprintf(`-f%v`, path))
out, err := exec.Command(PGDumpCmd, options...).Output()
log.Info("backup postgresql...")
for _, db := range x.DB {
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 {
log.Fatal("%s", out)
log.Fatal("failed: %s, %s", err, out)
} 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 {
}
}
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 c.AddJob(strings.Join(s, " "), x)
}
func (x *PGConfig) GetOptions() []string {
func (x *PGConfig) GetOptions(db string) []string {
options := x.Options
if x.DB != "" {
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))
}
options = append(options, "-d", db, "-h", x.Host, "-p", strconv.Itoa(x.Port), "-U", x.Username)
return options
}
\ No newline at end of file
example.png

94.9 KB

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