...
 
Commits (12)
......@@ -4,3 +4,4 @@ script:
before_install:
- export TZ=Europe/Berlin
- sudo apt-get install -y zsh
- go get github.com/daviddengcn/go-colortext
......@@ -2,6 +2,7 @@ BIN= snaprd
PREFIX= /usr/local
${BIN}: *.go Makefile
go get github.com/daviddengcn/go-colortext
go build -o ${BIN}
checkfmt:
......
![](snaprd_logo_sq.png?raw=true)
snaprd - backup utility
=======================
......@@ -94,6 +96,40 @@ Basic operation:
2016-09-14 Wednesday 20:32:29 (1s, 10m0s)
```
See a full list of options available to the run command:
$ snaprd run -h
Usage of run:
-maxKeep int
how many snapshots to keep in highest (oldest) interval. Use 0 to keep all
-minGbSpace int
if set, keep at least x GiB of the snapshots filesystem free
-minPercSpace float
if set, keep at least x% of the snapshots filesystem free
-noLogDate
if set, does not print date and time in the log output. Useful if output is redirected to syslog
-noPurge
if set, obsolete snapshots will not be deleted (minimum space requirements will still be honoured)
-noWait
if set, skip the initial waiting time before the first snapshot
-notify string
specify an email address to send reports
-origin string
data source (default "/tmp/snaprd_test/")
-r string
(shorthand for -repository) (default "/tmp/snaprd_dest")
-repository string
where to store snapshots (default "/tmp/snaprd_dest")
-rsyncOpts value
additional options for rsync
-rsyncPath string
path to rsync binary (default "/usr/bin/rsync")
-schedFile string
path to external schedules (default "/etc/snaprd.schedules")
-schedule string
one of longterm,shortterm (default "longterm")
E-Mail Notification
-------------------
......
......@@ -18,3 +18,20 @@ type realClock struct{}
func (realClock) Now() time.Time {
return time.Now()
}
type skewClock struct {
skew time.Duration
}
func (cl *skewClock) Now() time.Time {
return time.Now().Add(-cl.skew)
}
func newSkewClock(i int64) *skewClock {
d := time.Now().Sub(time.Unix(i, 0))
return &skewClock{skew: d}
}
func (cl *skewClock) forward(d time.Duration) {
cl.skew -= d
}
......@@ -54,6 +54,7 @@ type Config struct {
MinPercSpace float64
MinGiBSpace int
Notify string
noColor bool
}
// WriteCache writes the global configuration to disk as a json file.
......@@ -214,6 +215,9 @@ func loadConfig() (*Config, error) {
flags.StringVar(&(config.SchedFile),
"schedFile", defaultSchedFileName,
"path to external schedules")
flags.BoolVar(&(config.noColor),
"noColor", false,
"do not colorize list output")
if err := flags.Parse(os.Args[2:]); err != nil {
return nil, err
......
......@@ -9,7 +9,9 @@ import (
"errors"
"flag"
"fmt"
"github.com/daviddengcn/go-colortext"
"io"
"io/ioutil"
"log"
"os"
"os/signal"
......@@ -226,13 +228,21 @@ func subcmdList(cl clock) {
snapshots := snapshots.interval(intervals, n, cl)
debugf("snapshots in interval %d: %s", n, snapshots)
if n < len(intervals)-2 {
ct.Foreground(ct.Yellow, false)
fmt.Printf("### From %s ago, %d/%d\n", intervals.offset(n+1), len(snapshots), intervals.goal(n))
ct.ResetColor()
} else {
if config.MaxKeep == 0 {
fmt.Printf("### From past, %d/∞\n", len(snapshots))
} else {
ct.Foreground(ct.Yellow, false)
if config.MaxKeep != 0 {
fmt.Printf("### From past, %d/%d\n", len(snapshots), config.MaxKeep)
} else if config.MinPercSpace != 0 {
fmt.Printf("### From past, %d/(keep %.1f%% free)\n", len(snapshots), config.MinPercSpace)
} else if config.MinGiBSpace != 0 {
fmt.Printf("### From past, %d/(keep %dGiB free)\n", len(snapshots), config.MinGiBSpace)
} else {
fmt.Printf("### From past, %d/∞\n", len(snapshots))
}
ct.ResetColor()
}
for i, sn := range snapshots {
stime := sn.startTime.Format("2006-01-02 Monday 15:04:05")
......@@ -277,7 +287,12 @@ func mainExitCode(logIO io.Writer) int {
return 2
}
case "list":
if config.noColor {
ct.Writer = ioutil.Discard
}
ct.Foreground(ct.Green, false)
fmt.Printf("### Repository: %s, Origin: %s, Schedule: %s\n", config.repository, config.Origin, config.Schedule)
ct.ResetColor()
subcmdList(nil)
case "scheds":
schedules.list()
......@@ -288,7 +303,9 @@ func mainExitCode(logIO io.Writer) int {
func main() {
rio := newRingIO(os.Stderr, 25, 100)
exitCode := mainExitCode(rio)
if exitCode != 0 && config.Notify != "" {
// do not send a notification when error code is 0 or 1 (error in flag handling)
// because in the case 1 we can not access the config yet.
if exitCode > 1 && config.Notify != "" {
FailureMail(exitCode, rio)
}
os.Exit(exitCode)
......
......@@ -34,7 +34,10 @@ func prune(q chan *snapshot, cl clock) {
(config.MaxKeep != 0) {
debugf("%d snapshots in oldest interval", len(iv))
log.Printf("mark oldest as obsolete: %s", iv[0])
iv[0].transObsolete()
err := iv[0].transObsolete()
if err != nil {
log.Printf("could not transition snapshot: %s", err)
}
q <- iv[0]
pruneAgain = true
}
......@@ -44,7 +47,10 @@ func prune(q chan *snapshot, cl clock) {
dist := iv[youngest].startTime.Sub(iv[secondYoungest].startTime)
if dist.Seconds() < intervals[i].Seconds() {
log.Printf("mark as obsolete: %s", iv[youngest].Name())
iv[youngest].transObsolete()
err := iv[youngest].transObsolete()
if err != nil {
log.Printf("could not transition snapshot: %s", err)
}
q <- iv[youngest]
pruneAgain = true
}
......
......@@ -27,23 +27,6 @@ var mockSnapshots = []string{
"1400337721-1400337722-complete",
}
type skewClock struct {
skew time.Duration
}
func (cl *skewClock) Now() time.Time {
return time.Now().Add(-cl.skew)
}
func newSkewClock(i int64) *skewClock {
d := time.Now().Sub(time.Unix(i, 0))
return &skewClock{skew: d}
}
func (cl *skewClock) forward(d time.Duration) {
cl.skew -= d
}
func mockConfig() {
tmpRepository, err := ioutil.TempDir("", "snaprd_testing")
if err != nil {
......
......@@ -47,6 +47,7 @@ func createRsyncCommand(sn *snapshot, base *snapshot) *exec.Cmd {
args = append(args, config.RsyncPath)
args = append(args, "--delete")
args = append(args, "-a")
args = append(args, "--stats")
args = append(args, config.RsyncOpts...)
if base != nil {
args = append(args, "--link-dest="+base.FullName())
......@@ -144,7 +145,10 @@ func createSnapshot(base *snapshot) (*snapshot, error) {
return nil, fmt.Errorf("rsync failed: %s", err)
}
}
newSn.transComplete(cl)
err = newSn.transComplete(cl)
if err != nil {
return nil, err
}
log.Println("finished:", newSn.Name())
return newSn, nil
}
......
......@@ -21,7 +21,7 @@ func TestCreateRsyncCommand(t *testing.T) {
config.ReadCache()
cmd := createRsyncCommand(testSnapshots[1], testSnapshots[0])
got := cmd.Args
wanted := []string{"/usr/bin/rsync", "--delete", "-a",
wanted := []string{"/usr/bin/rsync", "--delete", "-a", "--stats",
"--link-dest=testdata/.data/1400337531-1400338693-complete",
"/tmp/snaprd_test/",
"testdata/.data/1400534523-0-incomplete"}
......
......@@ -85,11 +85,11 @@ func (s *snapshot) FullName() string {
}
// transComplete transitions the receiver to complete state.
func (s *snapshot) transComplete(cl clock) {
func (s *snapshot) transComplete(cl clock) error {
oldName := s.FullName()
etime := cl.Now()
if etime.Before(s.startTime) {
log.Fatal("endTime before startTime!")
return errors.New("endTime before startTime!")
}
// make all snapshots at least 1 second long
if etime.Sub(s.startTime).Seconds() < 1 {
......@@ -97,42 +97,52 @@ func (s *snapshot) transComplete(cl clock) {
}
s.endTime = etime
s.state = stateComplete
debugf("renaming complete snapshot %s -> %s", oldName, s.FullName())
err := os.Rename(oldName, s.FullName())
if err != nil {
log.Fatal(err)
newName := s.FullName()
debugf("renaming complete snapshot %s -> %s", oldName, newName)
if oldName != newName {
err := os.Rename(oldName, newName)
if err != nil {
return err
}
}
updateSymlinks()
overwriteSymlink(filepath.Join(dataSubdir, s.Name()), filepath.Join(config.repository, "latest"))
return nil
}
// transObsolete transitions the receiver to obsolete state.
func (s *snapshot) transObsolete() {
func (s *snapshot) transObsolete() error {
oldName := s.FullName()
s.state = stateObsolete
newName := s.FullName()
err := os.Rename(oldName, newName)
updateSymlinks()
if err != nil {
log.Fatal(err)
if oldName != newName {
err := os.Rename(oldName, newName)
if err != nil {
return err
}
}
updateSymlinks()
return nil
}
// transPurging transitions the receiver to purging state.
func (s *snapshot) transPurging() {
func (s *snapshot) transPurging() error {
oldName := s.FullName()
s.state = statePurging
newName := s.FullName()
err := os.Rename(oldName, newName)
if err != nil {
log.Fatal(err)
if oldName != newName {
err := os.Rename(oldName, newName)
if err != nil {
return err
}
}
return nil
}
// transIncomplete generates a new incomplete snapshot based on a previous one.
// Can be used to try to use previous incomplete snapshots, or even to reuse
// obsolete ones.
func (s *snapshot) transIncomplete(cl clock) {
func (s *snapshot) transIncomplete(cl clock) error {
oldName := s.FullName()
s.startTime = cl.Now()
s.endTime = time.Time{}
......@@ -142,17 +152,21 @@ func (s *snapshot) transIncomplete(cl clock) {
if oldName != newName {
err := os.Rename(oldName, newName)
if err != nil {
log.Fatal(err)
return err
}
}
return nil
}
// purge deletes the receiver snapshot from disk.
func (s *snapshot) purge() {
s.transPurging()
err := s.transPurging()
if err != nil {
log.Printf("error peparing %s for purging: %s", s.Name(), err)
}
path := s.FullName()
log.Println("purging", s.Name())
err := os.RemoveAll(path)
err = os.RemoveAll(path)
if err != nil {
log.Printf("error when purging \"%s\" (ignored): %s", s.Name(), err)
}
......