diff --git a/Makefile b/Makefile index 8aad006..14b186e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,4 @@ -bindata: - go-bindata help.txt +default: ci: publish @@ -7,12 +6,6 @@ publish: curl -sSLo golang.sh https://raw.githubusercontent.com/Luzifer/github-publish/master/golang.sh bash golang.sh -setup-testenv: - go get github.com/onsi/ginkgo/ginkgo - go get github.com/onsi/gomega - go get github.com/alecthomas/gometalinter - gometalinter --install --update - test: go test -v diff --git a/bindata.go b/bindata.go deleted file mode 100644 index cc2ae2b..0000000 --- a/bindata.go +++ /dev/null @@ -1,293 +0,0 @@ -// Code generated by go-bindata. DO NOT EDIT. -// sources: -// help.txt - -package main - - -import ( - "bytes" - "compress/gzip" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" - "time" -) - -func bindataRead(data []byte, name string) ([]byte, error) { - gz, err := gzip.NewReader(bytes.NewBuffer(data)) - if err != nil { - return nil, fmt.Errorf("Read %q: %v", name, err) - } - - var buf bytes.Buffer - _, err = io.Copy(&buf, gz) - clErr := gz.Close() - - if err != nil { - return nil, fmt.Errorf("Read %q: %v", name, err) - } - if clErr != nil { - return nil, err - } - - return buf.Bytes(), nil -} - - -type asset struct { - bytes []byte - info fileInfoEx -} - -type fileInfoEx interface { - os.FileInfo - MD5Checksum() string -} - -type bindataFileInfo struct { - name string - size int64 - mode os.FileMode - modTime time.Time - md5checksum string -} - -func (fi bindataFileInfo) Name() string { - return fi.name -} -func (fi bindataFileInfo) Size() int64 { - return fi.size -} -func (fi bindataFileInfo) Mode() os.FileMode { - return fi.mode -} -func (fi bindataFileInfo) ModTime() time.Time { - return fi.modTime -} -func (fi bindataFileInfo) MD5Checksum() string { - return fi.md5checksum -} -func (fi bindataFileInfo) IsDir() bool { - return false -} -func (fi bindataFileInfo) Sys() interface{} { - return nil -} - -var _bindataHelptxt = []byte( - "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xac\x94\xb1\x72\xdb\x30\x0c\x86\x77\x3d\x05\xc6\x76\x50\xbc\x67\xeb\x25" + - "\x97\xa9\x43\xaf\xbd\x5e\x87\x5c\x06\x84\x82\x24\x5e\x28\x42\x07\x80\x8e\xed\xa1\xcf\xde\xa3\xc8\xd8\xb2\x5d\xdf" + - "\x75\x28\x36\x91\xc0\xc7\x5f\x3f\x40\xfe\x12\x9c\x67\x12\x40\xe1\x14\x3b\xe8\xd2\x1c\xbc\xf3\xb6\x07\x63\x70\x42" + - "\x68\x04\xaf\xe8\xde\xd2\x0c\xef\xde\x46\x08\xa4\x0a\x23\x1e\x0e\x81\x9a\xe6\xa7\xe2\x40\xf7\x0d\x9c\xaa\xda\x9a" + - "\xfb\xec\x78\x9a\x30\x76\x2f\x4d\xf3\x65\x8b\x3e\xe0\x6b\x20\x78\x28\x6b\x9a\x2b\x6a\xde\x06\x7c\x74\x02\x97\xf1" + - "\x70\x76\x30\x3a\xc7\xd2\xf9\x38\x64\x4d\x36\x1e\xd7\x25\x05\xd2\x06\xa0\x4f\x21\x5c\x21\x8e\xf1\xc4\xe2\x48\x97" + - "\xba\xe5\x7f\x3c\x47\xe0\x1e\xb0\x94\x15\x54\x03\xe0\x02\x61\x4c\xf3\x0d\xc8\x23\x05\x32\x5a\x20\xb4\x33\xc1\x48" + - "\x9c\x74\x65\x56\xef\x8b\x94\xe0\xd5\x5a\x37\x62\x1c\xa8\x6b\x97\xc5\x35\xe5\xab\x57\x2b\x4a\xca\x56\x4d\x04\xf5" + - "\xd1\x11\x04\x54\x3b\xe9\x29\xa4\x24\x42\xd1\xfe\x81\xc4\xd1\xd0\x47\xea\xc0\xc7\x95\x45\x0d\x80\x90\x1a\x0b\xc1" + - "\x73\x4e\x84\x19\x6d\x7c\x81\x67\x43\x19\xc8\x5e\x00\xbe\x97\x5d\xcd\x12\x86\x50\x68\xb0\x81\xce\xcb\xe2\xf5\x92" + - "\x96\xbf\xc8\x19\xcb\x7e\x8d\x3b\x22\xd6\x71\xc4\xd1\x96\x64\x6f\xe3\x47\xcf\xae\x39\x6a\x68\x49\x6f\xf5\xec\x47" + - "\x9a\x26\x14\x7f\x28\x8e\xd7\x5c\xee\xcf\x9a\x4f\x33\xab\xaf\xb4\x2d\x89\xef\xf7\xb7\x68\x0f\x3c\xcd\x98\x55\xd5" + - "\xd2\x6c\x16\x45\x53\xc0\x01\x7d\x54\x83\xc0\x0e\x43\xed\x61\xf3\x14\x70\x58\x46\xb4\x6d\x1d\xc7\xde\x0f\x6d\x35" + - "\xa5\xed\xcf\xa1\x79\x2f\x49\x19\xa8\x9e\x05\x6c\xf4\xeb\x91\x78\x2f\x17\xab\xb9\x3d\x99\x25\x3e\x3d\x52\x8f\x29" + - "\xd8\x3d\xfc\xde\xdc\x95\x13\x37\x97\xf7\xe9\x6e\x8f\x53\xf8\xbc\x68\x0a\xec\xde\x8e\x8a\x2e\xc6\xfe\x29\x2f\x1b" + - "\xc3\xc8\xa1\x5b\xbc\xca\xc9\x27\x6d\x55\x11\xd0\x8e\x5c\xca\xb2\xff\x8f\xb6\x7c\x48\xd1\xd6\xd1\x6b\x1a\xb2\xae" + - "\xee\x1a\xf5\x4d\x7c\xb4\x95\x3d\xf5\x81\xd0\xac\x97\x93\xcd\xc9\x0a\x42\xac\x95\x14\x33\x24\x5e\x22\x1e\x19\x10" + - "\x8c\xb4\x64\xe4\xe7\x88\x93\xd5\x4b\xa4\x4b\xf5\x96\x44\x73\x3b\xfe\x16\x8b\x80\xfa\x0e\x94\x6b\x05\xb3\xf0\x20" + - "\x38\xc1\x47\x19\xc6\x0e\x68\xe7\x4d\x9b\x3f\x01\x00\x00\xff\xff\x42\x8c\xc7\xe4\x16\x05\x00\x00") - -func bindataHelptxtBytes() ([]byte, error) { - return bindataRead( - _bindataHelptxt, - "help.txt", - ) -} - - - -func bindataHelptxt() (*asset, error) { - bytes, err := bindataHelptxtBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{ - name: "help.txt", - size: 1302, - md5checksum: "", - mode: os.FileMode(420), - modTime: time.Unix(1466894900, 0), - } - - a := &asset{bytes: bytes, info: info} - - return a, nil -} - - -// -// Asset loads and returns the asset for the given name. -// It returns an error if the asset could not be found or -// could not be loaded. -// -func Asset(name string) ([]byte, error) { - cannonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[cannonicalName]; ok { - a, err := f() - if err != nil { - return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) - } - return a.bytes, nil - } - return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} -} - -// -// MustAsset is like Asset but panics when Asset would return an error. -// It simplifies safe initialization of global variables. -// nolint: deadcode -// -func MustAsset(name string) []byte { - a, err := Asset(name) - if err != nil { - panic("asset: Asset(" + name + "): " + err.Error()) - } - - return a -} - -// -// AssetInfo loads and returns the asset info for the given name. -// It returns an error if the asset could not be found or could not be loaded. -// -func AssetInfo(name string) (os.FileInfo, error) { - cannonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[cannonicalName]; ok { - a, err := f() - if err != nil { - return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) - } - return a.info, nil - } - return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} -} - -// -// AssetNames returns the names of the assets. -// nolint: deadcode -// -func AssetNames() []string { - names := make([]string, 0, len(_bindata)) - for name := range _bindata { - names = append(names, name) - } - return names -} - -// -// _bindata is a table, holding each asset generator, mapped to its name. -// -var _bindata = map[string]func() (*asset, error){ - "help.txt": bindataHelptxt, -} - -// -// AssetDir returns the file names below a certain -// directory embedded in the file by go-bindata. -// For example if you run go-bindata on data/... and data contains the -// following hierarchy: -// data/ -// foo.txt -// img/ -// a.png -// b.png -// then AssetDir("data") would return []string{"foo.txt", "img"} -// AssetDir("data/img") would return []string{"a.png", "b.png"} -// AssetDir("foo.txt") and AssetDir("notexist") would return an error -// AssetDir("") will return []string{"data"}. -// -func AssetDir(name string) ([]string, error) { - node := _bintree - if len(name) != 0 { - cannonicalName := strings.Replace(name, "\\", "/", -1) - pathList := strings.Split(cannonicalName, "/") - for _, p := range pathList { - node = node.Children[p] - if node == nil { - return nil, &os.PathError{ - Op: "open", - Path: name, - Err: os.ErrNotExist, - } - } - } - } - if node.Func != nil { - return nil, &os.PathError{ - Op: "open", - Path: name, - Err: os.ErrNotExist, - } - } - rv := make([]string, 0, len(node.Children)) - for childName := range node.Children { - rv = append(rv, childName) - } - return rv, nil -} - - -type bintree struct { - Func func() (*asset, error) - Children map[string]*bintree -} - -var _bintree = &bintree{Func: nil, Children: map[string]*bintree{ - "help.txt": {Func: bindataHelptxt, Children: map[string]*bintree{}}, -}} - -// RestoreAsset restores an asset under the given directory -func RestoreAsset(dir, name string) error { - data, err := Asset(name) - if err != nil { - return err - } - info, err := AssetInfo(name) - if err != nil { - return err - } - err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) - if err != nil { - return err - } - err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) - if err != nil { - return err - } - return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) -} - -// RestoreAssets restores an asset under the given directory recursively -func RestoreAssets(dir, name string) error { - children, err := AssetDir(name) - // File - if err != nil { - return RestoreAsset(dir, name) - } - // Dir - for _, child := range children { - err = RestoreAssets(dir, filepath.Join(name, child)) - if err != nil { - return err - } - } - return nil -} - -func _filePath(dir, name string) string { - cannonicalName := strings.Replace(name, "\\", "/", -1) - return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) -} diff --git a/configfile.go b/configfile.go index ef205f0..5a44a15 100644 --- a/configfile.go +++ b/configfile.go @@ -2,16 +2,15 @@ package main import ( "bytes" - "errors" "fmt" "io" - "io/ioutil" "os" "regexp" "strconv" "text/template" valid "github.com/asaskevich/govalidator" + "github.com/pkg/errors" "gopkg.in/yaml.v2" ) @@ -96,11 +95,11 @@ type configFile struct { func init() { valid.CustomTypeTagMap.Set("customFileExistsValidator", valid.CustomTypeValidator(func(i interface{}, context interface{}) bool { - switch v := i.(type) { // this validates a field against the value in another field, i.e. dependent validation - case string: + if v, ok := i.(string); ok { _, err := os.Stat(v) return v == "" || err == nil } + return false })) } @@ -108,7 +107,7 @@ func init() { func (c *configFile) validate() error { result, err := valid.ValidateStruct(c) if !result || err != nil { - return err + return errors.Wrap(err, "validating config") } if c.Encryption.Enable && c.Encryption.GPGSignKey != "" && c.Encryption.Passphrase == "" { @@ -146,18 +145,18 @@ func getTemplateFuncMap() template.FuncMap { } func loadConfigFile(in io.Reader) (*configFile, error) { - fileContent, err := ioutil.ReadAll(in) + fileContent, err := io.ReadAll(in) if err != nil { - return nil, err + return nil, errors.Wrap(err, "reading config file content") } buf := bytes.NewBuffer([]byte{}) tpl, err := template.New("config file").Funcs(getTemplateFuncMap()).Parse(string(fileContent)) if err != nil { - return nil, err + return nil, errors.Wrap(err, "parsing config file as template") } if err := tpl.Execute(buf, nil); err != nil { - return nil, err + return nil, errors.Wrap(err, "rendering config file template") } hostname, _ := os.Hostname() // #nosec G104 @@ -166,12 +165,13 @@ func loadConfigFile(in io.Reader) (*configFile, error) { Hostname: hostname, } if err := yaml.Unmarshal(buf.Bytes(), res); err != nil { - return nil, err + return nil, errors.Wrap(err, "unmarshalling config") } return res, res.validate() } +//nolint:funlen // Is just a list of parameter groups func (c *configFile) GenerateCommand(argv []string, time string) (commandLine []string, env []string, logfilter *regexp.Regexp, err error) { var ( tmpEnv []string @@ -186,58 +186,71 @@ func (c *configFile) GenerateCommand(argv []string, time string) (commandLine [] root = c.RootPath dest = c.Destination commandLine, env, err = c.generateFullCommand(option, time, root, dest, addTime, "") + case commandListChangedFiles: - option = "" + option = "inc" root = c.RootPath dest = c.Destination commandLine, env, err = c.generateFullCommand(option, time, root, dest, addTime, "") commandLine = append([]string{"--dry-run", "--verbosity", "8"}, commandLine...) logfilter = regexp.MustCompile(`^[ADM] `) + case commandFullBackup: option = command root = c.RootPath dest = c.Destination commandLine, env, err = c.generateFullCommand(option, time, root, dest, addTime, "") + case commandIncrBackup: option = command root = c.RootPath dest = c.Destination commandLine, env, err = c.generateFullCommand(option, time, root, dest, addTime, "") + case commandCleanup: option = command commandLine, env, err = c.generateLiteCommand(option, time, addTime) + case commandList: option = command commandLine, env, err = c.generateLiteCommand(option, time, addTime) + case commandRestore: addTime = true option = command root = c.Destination restoreFile := "" - if len(argv) == 3 { + switch len(argv) { + case 3: //nolint:gomnd // Simple count of arguments restoreFile = argv[1] dest = argv[2] - } else if len(argv) == 2 { + + case 2: //nolint:gomnd // Simple count of arguments dest = argv[1] - } else { + + default: err = errors.New("You need to specify one or more parameters: See help message") return commandLine, env, logfilter, err } commandLine, env, err = c.generateFullCommand(option, time, root, dest, addTime, restoreFile) + case commandStatus: option = "collection-status" commandLine, env, err = c.generateLiteCommand(option, time, addTime) + case commandVerify: option = command root = c.Destination dest = c.RootPath commandLine, env, err = c.generateFullCommand(option, time, root, dest, addTime, "") + case commandRemove: commandLine, env, err = c.generateRemoveCommand() + default: - err = fmt.Errorf("Did not understand command '%s', please see 'help' for details what to do", command) + err = fmt.Errorf("did not understand command '%s', please see 'help' for details what to do", command) return commandLine, env, logfilter, err } @@ -251,7 +264,7 @@ func (c *configFile) GenerateCommand(argv []string, time string) (commandLine [] return commandLine, env, logfilter, err } -func (c *configFile) cleanSlice(in []string) []string { +func (*configFile) cleanSlice(in []string) []string { out := []string{} for _, i := range in { @@ -305,6 +318,7 @@ func (c *configFile) generateRemoveCommand() ([]string, []string, error) { return commandLine, env, nil } +//revive:disable-next-line:flag-parameter // Keeping for the sake of simplicity func (c *configFile) generateLiteCommand(option, time string, addTime bool) ([]string, []string, error) { var commandLine, env, tmpArg, tmpEnv []string // Assemble command @@ -324,6 +338,7 @@ func (c *configFile) generateLiteCommand(option, time string, addTime bool) ([]s return commandLine, env, nil } +//revive:disable-next-line:flag-parameter // Keeping for the sake of simplicity func (c *configFile) generateFullCommand(option, time, root, dest string, addTime bool, restoreFile string) ([]string, []string, error) { var commandLine, env, tmpArg, tmpEnv []string // Assemble command diff --git a/configfile_test.go b/configfile_test.go index 575bc06..c18ee68 100644 --- a/configfile_test.go +++ b/configfile_test.go @@ -8,7 +8,7 @@ import ( ) var _ = Describe("Configfile", func() { - var config = `--- + config := `--- root: / hostname: testing dest: s3+http://my-backup/myhost/ @@ -62,6 +62,7 @@ logdir: /var/log/duplicity/ It("should have generated the expected commandLine", func() { Expect(commandLine).To(Equal([]string{ + "inc", "--full-if-older-than", "7D", "--s3-use-new-style", "--include=/data", @@ -299,5 +300,4 @@ logdir: /var/log/duplicity/ })) }) }) - }) diff --git a/duplicity_backup_suite_test.go b/duplicity_backup_suite_test.go index d3beaba..a505af9 100644 --- a/duplicity_backup_suite_test.go +++ b/duplicity_backup_suite_test.go @@ -1,10 +1,10 @@ package main_test import ( + "testing" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - - "testing" ) func TestDuplicityBackup(t *testing.T) { diff --git a/go.mod b/go.mod index c2ccc55..6a535f1 100644 --- a/go.mod +++ b/go.mod @@ -4,19 +4,21 @@ go 1.21.0 require ( github.com/Luzifer/go_helpers v1.4.0 - github.com/Luzifer/rconfig v1.2.0 + github.com/Luzifer/go_helpers/v2 v2.20.0 + github.com/Luzifer/rconfig/v2 v2.4.0 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/mitchellh/go-homedir v1.1.0 github.com/nightlyone/lockfile v1.0.0 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.27.10 + github.com/pkg/errors v0.9.1 github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 github.com/sirupsen/logrus v1.9.3 gopkg.in/yaml.v2 v2.4.0 ) require ( - github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/nxadm/tail v1.4.8 // indirect github.com/spf13/pflag v1.0.5 // indirect diff --git a/go.sum b/go.sum index 66ef482..2bc6000 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,18 @@ github.com/Luzifer/go_helpers v1.4.0 h1:Pmm058SbYewfnpP1CHda/zERoAqYoZFiBHF4l8k03Ko= github.com/Luzifer/go_helpers v1.4.0/go.mod h1:5yUSe0FS7lIx1Uzmt0R3tdPFrSSaPfiCqaIA6u0Zn4Y= -github.com/Luzifer/rconfig v1.2.0 h1:waD1sqasGVSQSrExpLrQ9Q1JmMaltrS391VdOjWXP/I= -github.com/Luzifer/rconfig v1.2.0/go.mod h1:9pet6z2+mm/UAB0jF/rf0s62USfHNolzgR6Q4KpsJI0= +github.com/Luzifer/go_helpers/v2 v2.20.0 h1:OyCUs7TFGwfJpGqD21KEKKOXy92jetw2l7dlmG7HZnA= +github.com/Luzifer/go_helpers/v2 v2.20.0/go.mod h1:KPGjImwm51SmOTZMd9XUsT241gHYJuEyLrS/omQ4/Dw= +github.com/Luzifer/rconfig/v2 v2.4.0 h1:MAdymTlExAZ8mx5VG8xOFAtFQSpWBipKYQHPOmYTn9o= +github.com/Luzifer/rconfig/v2 v2.4.0/go.mod h1:hWF3ZVSusbYlg5bEvCwalEyUSY+0JPJWUiIu7rBmav8= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= @@ -34,8 +37,8 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/nightlyone/lockfile v1.0.0 h1:RHep2cFKK4PonZJDdEl4GmkabuhbsRMgk/k3uAmxBiA= @@ -53,6 +56,8 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo= @@ -63,8 +68,9 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -90,6 +96,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/main.go b/main.go index a292899..b013bfd 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + _ "embed" "fmt" "os" "os/exec" @@ -9,14 +10,21 @@ import ( "strings" "time" - "github.com/Luzifer/go_helpers/str" - "github.com/Luzifer/go_helpers/which" - "github.com/Luzifer/rconfig" + "github.com/Luzifer/go_helpers/v2/env" + "github.com/Luzifer/go_helpers/v2/str" + "github.com/Luzifer/go_helpers/v2/which" + "github.com/Luzifer/rconfig/v2" + "github.com/pkg/errors" "github.com/rifflock/lfshook" "github.com/mitchellh/go-homedir" "github.com/nightlyone/lockfile" - log "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus" +) + +const ( + logDirPerms = 0o750 + messageChanSize = 10 ) var ( @@ -35,100 +43,114 @@ var ( duplicityBinary string + //go:embed help.txt + helpText string + version = "dev" ) -func initCFG() { +func initApp() error { + rconfig.AutoEnv(true) if err := rconfig.Parse(&cfg); err != nil { - log.WithError(err).Fatal("Error while parsing arguments") + logrus.WithError(err).Fatal("Error while parsing arguments") } - if cfg.VersionAndExit { - fmt.Printf("duplicity-backup %s\n", version) - os.Exit(0) + l, err := logrus.ParseLevel(cfg.LogLevel) + if err != nil { + return errors.Wrap(err, "parsing log-level") } + logrus.SetLevel(l) - if logLevel, err := log.ParseLevel(cfg.LogLevel); err == nil { - log.SetLevel(logLevel) - } else { - log.Fatalf("Unable to parse log level: %s", err) - } - - var err error if cfg.ConfigFile, err = homedir.Expand(cfg.ConfigFile); err != nil { - log.WithError(err).Fatal("Unable to expand config-file") + return errors.Wrap(err, "expanding config-file path") } if cfg.LockFile, err = homedir.Expand(cfg.LockFile); err != nil { - log.WithError(err).Fatal("Unable to expand lock") + return errors.Wrap(err, "expanding lock-file path") } if duplicityBinary, err = which.FindInPath("duplicity"); err != nil { - log.WithError(err).Fatal("Did not find duplicity binary in $PATH, please install it") + return errors.Wrap(err, "finding duplicity binary in $PATH") } + + return nil } +//nolint:gocyclo // Slightly too complex, makes no sense to split func main() { - initCFG() - var ( err error config *configFile ) + if err = initApp(); err != nil { + logrus.WithError(err).Fatal("initializing app") + } + + if cfg.VersionAndExit { + logrus.WithField("version", version).Info("duplicity-backup") + os.Exit(0) + } + lock, err := lockfile.New(cfg.LockFile) if err != nil { - log.WithError(err).Fatal("Could not initialize lockfile") + logrus.WithError(err).Fatal("initializing lockfile") } // If no command is passed assume we're requesting "help" argv := rconfig.Args() if len(argv) == 1 || argv[1] == "help" { - helptext, _ := Asset("help.txt") // #nosec G104 - fmt.Println(string(helptext)) + if _, err = os.Stderr.WriteString(helpText); err != nil { + logrus.WithError(err).Fatal("printing help to stderr") + } return } // Get configuration configSource, err := os.Open(cfg.ConfigFile) if err != nil { - log.WithError(err).Fatalf("Unable to open configuration file %s", cfg.ConfigFile) + logrus.WithError(err).Fatalf("opening configuration file %s", cfg.ConfigFile) } - defer configSource.Close() + defer configSource.Close() //nolint:errcheck // If this errors the file will be closed by process exit + config, err = loadConfigFile(configSource) if err != nil { - log.WithError(err).Fatal("Unable to read configuration file") + logrus.WithError(err).Fatal("reading configuration file") } // Initialize logfile - if err = os.MkdirAll(config.LogDirectory, 0750); err != nil { - log.WithError(err).Fatal("Unable to create log dir") + if err = os.MkdirAll(config.LogDirectory, logDirPerms); err != nil { + logrus.WithError(err).Fatal("creating log dir") } logFilePath := path.Join(config.LogDirectory, time.Now().Format("duplicity-backup_2006-01-02_15-04-05.txt")) - logFile, err := os.Create(logFilePath) + logFile, err := os.Create(logFilePath) //#nosec:G304 // That's a log file we just created the path for if err != nil { - log.WithError(err).Fatalf("Unable to open logfile %s", logFilePath) + logrus.WithError(err).Fatalf("opening logfile %s", logFilePath) } - defer logFile.Close() + defer logFile.Close() //nolint:errcheck // If this errors the file will be closed by process exit // Hook into logging and write to file - log.AddHook(lfshook.NewHook(logFile, nil)) + logrus.AddHook(lfshook.NewHook(logFile, nil)) - log.Infof("++++ duplicity-backup %s started with command '%s'", version, argv[1]) + logrus.Infof("++++ duplicity-backup %s started with command '%s'", version, argv[1]) if err := lock.TryLock(); err != nil { - log.WithError(err).Error("Could not acquire lock") + logrus.WithError(err).Error("acquiring lock") return } - defer lock.Unlock() + defer func() { + if err = lock.Unlock(); err != nil { + logrus.WithError(err).Error("releasing log") + } + }() if err := execute(config, argv[1:]); err != nil { return } if config.Cleanup.Type != "none" && str.StringInSlice(argv[1], removeCommands) { - log.Info("++++ Starting removal of old backups") + logrus.Info("++++ Starting removal of old backups") if err := execute(config, []string{commandRemove}); err != nil { return @@ -136,12 +158,12 @@ func main() { } if err := config.Notify(argv[1], true, nil); err != nil { - log.WithError(err).Error("Error sending notifications") + logrus.WithError(err).Error("sending notifications") } else { - log.Info("Notifications sent") + logrus.Info("notifications sent") } - log.Info("++++ Backup finished successfully") + logrus.Info("++++ Backup finished successfully") } func execute(config *configFile, argv []string) error { @@ -150,15 +172,16 @@ func execute(config *configFile, argv []string) error { commandLine, tmpEnv []string logFilter *regexp.Regexp ) + commandLine, tmpEnv, logFilter, err = config.GenerateCommand(argv, cfg.RestoreTime) if err != nil { - log.WithError(err).Error("Unable to generate command") + logrus.WithError(err).Error("generating command") return err } - env := envListToMap(os.Environ()) - for k, v := range envListToMap(tmpEnv) { - env[k] = v + procEnv := env.ListToMap(os.Environ()) + for k, v := range env.ListToMap(tmpEnv) { + procEnv[k] = v } // Ensure duplicity is talking to us @@ -168,13 +191,13 @@ func execute(config *configFile, argv []string) error { commandLine = append([]string{"--dry-run"}, commandLine...) } - log.Debugf("Command: %s %s", duplicityBinary, strings.Join(commandLine, " ")) + logrus.Debugf("Command: %s %s", duplicityBinary, strings.Join(commandLine, " ")) - msgChan := make(chan string, 10) + msgChan := make(chan string, messageChanSize) go func(c chan string, logFilter *regexp.Regexp) { for l := range c { if logFilter == nil || logFilter.MatchString(l) { - log.Info(l) + logrus.Info(l) } } }(msgChan, logFilter) @@ -183,45 +206,24 @@ func execute(config *configFile, argv []string) error { cmd := exec.Command(duplicityBinary, commandLine...) // #nosec G204 cmd.Stdout = output cmd.Stderr = output - cmd.Env = envMapToList(env) + cmd.Env = env.MapToList(procEnv) err = cmd.Run() close(msgChan) if err != nil { - log.Error("Execution of duplicity command was unsuccessful! (exit-code was non-zero)") + logrus.Error("Execution of duplicity command was unsuccessful! (exit-code was non-zero)") } else { - log.Info("Execution of duplicity command was successful.") + logrus.Info("Execution of duplicity command was successful.") } if err != nil { - if nErr := config.Notify(argv[0], false, fmt.Errorf("Could not create backup: %s", err)); nErr != nil { - log.WithError(err).Error("Error sending notifications") + if nErr := config.Notify(argv[0], false, fmt.Errorf("creating backup: %s", err)); nErr != nil { + logrus.WithError(err).Error("Error sending notifications") } else { - log.Info("Notifications sent") + logrus.Info("Notifications sent") } } - return err -} - -func envListToMap(list []string) map[string]string { - out := map[string]string{} - for _, entry := range list { - if len(entry) == 0 || entry[0] == '#' { - continue - } - - parts := strings.SplitN(entry, "=", 2) - out[parts[0]] = parts[1] - } - return out -} - -func envMapToList(envMap map[string]string) []string { - out := []string{} - for k, v := range envMap { - out = append(out, k+"="+v) - } - return out + return errors.Wrap(err, "running duplicity") } diff --git a/notification.go b/notification.go index 0e5de3e..7b44035 100644 --- a/notification.go +++ b/notification.go @@ -2,13 +2,18 @@ package main import ( "bytes" + "context" "encoding/json" "fmt" "net/http" + "time" "github.com/Luzifer/go_helpers/str" + "github.com/pkg/errors" ) +const notifyRequestTimeout = 2 * time.Second + func (c *configFile) Notify(command string, success bool, err error) error { if !str.StringInSlice(command, notifyCommands) { return nil @@ -37,7 +42,7 @@ func (c *configFile) Notify(command string, success bool, err error) error { estr = fmt.Sprintf("%s\n- %s", estr, e) } - return fmt.Errorf("%d notifiers failed:%s", len(errs), estr) + return errors.Errorf("%d notifiers failed:%s", len(errs), estr) } type mondashResult struct { @@ -50,6 +55,7 @@ type mondashResult struct { HideValue bool `json:"hide_value"` } +//revive:disable-next-line:flag-parameter // not a flag parameter func (c *configFile) notifyMonDash(success bool, err error) error { if c.Notifications.MonDash.BoardURL == "" { return nil @@ -73,7 +79,7 @@ func (c *configFile) notifyMonDash(success bool, err error) error { buf := bytes.NewBuffer([]byte{}) if err = json.NewEncoder(buf).Encode(monitoringResult); err != nil { - return err + return errors.Wrap(err, "encoding request payload") } url := fmt.Sprintf("%s/duplicity-%s", @@ -81,17 +87,25 @@ func (c *configFile) notifyMonDash(success bool, err error) error { c.Hostname, ) - req, _ := http.NewRequest(http.MethodPut, url, buf) // #nosec G104 + ctx, cancel := context.WithTimeout(context.Background(), notifyRequestTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, buf) + if err != nil { + return errors.Wrap(err, "creating request") + } + req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", c.Notifications.MonDash.Token) + res, err := http.DefaultClient.Do(req) if err != nil { - return err + return errors.Wrap(err, "executing request") } - defer res.Body.Close() + defer res.Body.Close() //nolint:errcheck // Will be cleaned by process exit shortly after - if res.StatusCode != 200 { - return fmt.Errorf("Received unexpected status code: %d", res.StatusCode) + if res.StatusCode != http.StatusOK { + return errors.Errorf("unexpected status code: %d", res.StatusCode) } return nil @@ -104,6 +118,7 @@ type slackResult struct { Text string `json:"text"` } +//revive:disable-next-line:flag-parameter // not a flag parameter func (c *configFile) notifySlack(success bool, err error) error { if c.Notifications.Slack.HookURL == "" { return nil @@ -123,17 +138,27 @@ func (c *configFile) notifySlack(success bool, err error) error { buf := bytes.NewBuffer([]byte{}) if err = json.NewEncoder(buf).Encode(sr); err != nil { - return err + return errors.Wrap(err, "encoding payload") } - res, err := http.Post(c.Notifications.Slack.HookURL, "application/json", buf) + ctx, cancel := context.WithTimeout(context.Background(), notifyRequestTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.Notifications.Slack.HookURL, buf) if err != nil { - return err + return errors.Wrap(err, "creating request") } - defer res.Body.Close() - if res.StatusCode != 200 { - return fmt.Errorf("Received unexpected status code: %d", res.StatusCode) + req.Header.Set("Content-Type", "application/json") + + res, err := http.DefaultClient.Do(req) + if err != nil { + return errors.Wrap(err, "executing request") + } + defer res.Body.Close() //nolint:errcheck // Will be cleaned by process exit shortly after + + if res.StatusCode != http.StatusOK { + return errors.Errorf("unexpected status code: %d", res.StatusCode) } return nil