Compare commits

...

6 Commits
v0.0.3 ... main

Author SHA1 Message Date
d334256d0b
Fix skipping file access error 2023-04-11 10:40:24 +05:00
3a37c29de4 Autodetect datetime format 2023-04-08 14:02:36 +05:00
dc3eb7d9ef Remove uncompleted file on any error 2023-04-08 14:01:44 +05:00
ca8193ad06 Fix typo 2023-04-08 14:01:16 +05:00
465cfae874
Add usage and basic config examples 2023-04-07 14:58:30 +05:00
d34d504754 Update klauspost/compress dependency
provides little compression ratio increase
2023-04-05 21:13:13 +05:00
8 changed files with 108 additions and 29 deletions

View File

@ -1,2 +1,58 @@
# backuper # backuper
## Usage
### Incremental backup
```sh
backuper i <config file path>
```
### Full backup
```sh
backuper f <config file path>
```
### Search files in backup
```sh
backuper s <config file path> <mask>
```
### Recover files from backup
```sh
backuper r <config file path> <mask> <files datetime> <path to recover>
```
Examples:
```sh
# Recover Go files relevant as of 01.01.2023 to /home/user/go directory
backuper r config.conf "*.go" "01.01.2023" "/home/user/go"
```
### Test backup for errors
```sh
backuper t <config file path>
```
## Basic config example
Backup config files from `/etc` and sqlite files from `/var`:
```toml
FileName = "backup"
[[Patterns]]
Path = "/etc"
FileNamePatternList = ["*.conf", "*.toml", "*.ini", "*.yaml"]
Recursive = true
[[Patterns]]
Path = "/var"
FileNamePatternList = ["*.sqlite"]
Recursive = true
```

View File

@ -46,6 +46,8 @@ func (b *Config) fileList(fileNames chan FileInfo) {
if b.StopOnAnyError { if b.StopOnAnyError {
return fmt.Errorf("get file info error: %v", err) return fmt.Errorf("get file info error: %v", err)
} }
return nil
} }
file := FileInfo{ file := FileInfo{

2
go.mod
View File

@ -4,7 +4,7 @@ go 1.20
require ( require (
github.com/BurntSushi/toml v1.2.1 github.com/BurntSushi/toml v1.2.1
github.com/klauspost/compress v1.16.3 github.com/klauspost/compress v1.16.4
github.com/stretchr/testify v1.8.2 github.com/stretchr/testify v1.8.2
github.com/tidwall/match v1.1.1 github.com/tidwall/match v1.1.1
) )

4
go.sum
View File

@ -3,8 +3,8 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= github.com/klauspost/compress v1.16.4 h1:91KN02FnsOYhuunwU4ssRe8lc2JosWmizWa91B5v1PU=
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.16.4/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

View File

@ -62,7 +62,8 @@ func (index Index) Save(fileName string) error {
enc, err := zstd.NewWriter(f, zstd.WithEncoderLevel(zstd.SpeedBestCompression)) enc, err := zstd.NewWriter(f, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
if err != nil { if err != nil {
f.Close() // TODO: удалить частичный файл? f.Close()
os.Remove(fileName)
return err return err
} }
@ -82,7 +83,8 @@ func (index Index) Save(fileName string) error {
err := csvWriter.Write([]string{fileName, historyItem.ArchiveFileName, strconv.Itoa(int(historyItem.ModificationTime.Unix()))}) err := csvWriter.Write([]string{fileName, historyItem.ArchiveFileName, strconv.Itoa(int(historyItem.ModificationTime.Unix()))})
if err != nil { if err != nil {
enc.Close() enc.Close()
f.Close() // TODO: удалить частичный файл? f.Close()
os.Remove(fileName)
return err return err
} }
} }
@ -91,13 +93,15 @@ func (index Index) Save(fileName string) error {
csvWriter.Flush() csvWriter.Flush()
if err := csvWriter.Error(); err != nil { if err := csvWriter.Error(); err != nil {
enc.Close() enc.Close()
f.Close() // TODO: удалить частичный файл? f.Close()
os.Remove(fileName)
return err return err
} }
err = enc.Close() err = enc.Close()
if err != nil { if err != nil {
f.Close() // TODO: удалить частичный файл? f.Close()
os.Remove(fileName)
return err return err
} }

23
main.go
View File

@ -4,7 +4,6 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"time"
) )
func init() { func init() {
@ -56,25 +55,9 @@ func main() {
log.Fatalln(err) log.Fatalln(err)
} }
var t time.Time t, err := parseTime(os.Args[4])
switch len(os.Args[4]) { if err != nil {
case len("02.01.2006"): config.fatalln(err)
t, err = time.Parse("02.01.2006 15:04", os.Args[4])
if err != nil {
config.fatalln("time parse error:", err)
}
case len("02.01.2006 15:04"):
t, err = time.Parse("02.01.2006 15:04", os.Args[4])
if err != nil {
config.fatalln("time parse error:", err)
}
case len("02.01.2006 15:04:05"):
t, err = time.Parse("02.01.2006 15:04:05", os.Args[4])
if err != nil {
config.fatalln("time parse error:", err)
}
default:
config.fatalln(`wrong time format, must be ["DD.MM.YYYY", "DD.MM.YYYY hh:mm", "DD.MM.YYYY hh:mm:ss"]`)
} }
plan, err := config.extractionPlan(os.Args[3], t) plan, err := config.extractionPlan(os.Args[3], t)

View File

@ -1,9 +1,11 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"github.com/tidwall/match" "github.com/tidwall/match"
) )
@ -32,7 +34,7 @@ func sizeToApproxHuman(s int64) string {
return fmt.Sprintf("%d B", s) return fmt.Sprintf("%d B", s)
} }
// clean убирает невозможнын комбинации символов из пути // clean убирает невозможные комбинации символов из пути
func clean(s string) string { func clean(s string) string {
s = strings.ReplaceAll(s, ":", "") s = strings.ReplaceAll(s, ":", "")
s = strings.ReplaceAll(s, `\\`, `\`) s = strings.ReplaceAll(s, `\\`, `\`)
@ -71,3 +73,16 @@ func isFilePathMatchPatterns(patterns []string, fileName string) bool {
return false return false
} }
func parseTime(s string) (time.Time, error) {
switch len(s) {
case len("02.01.2006"):
return time.ParseInLocation("02.01.2006", s, time.Local)
case len("02.01.2006 15:04"):
return time.ParseInLocation("02.01.2006 15:04", s, time.Local)
case len("02.01.2006 15:04:05"):
return time.ParseInLocation("02.01.2006 15:04:05", s, time.Local)
}
return time.Time{}, errors.New("unknown time format")
}

View File

@ -2,6 +2,7 @@ package main
import ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -10,3 +11,21 @@ func TestSizeToApproxHuman(t *testing.T) {
assert.Equal(t, "1.0 KiB", sizeToApproxHuman(1024)) assert.Equal(t, "1.0 KiB", sizeToApproxHuman(1024))
assert.Equal(t, "1.1 KiB", sizeToApproxHuman(1126)) assert.Equal(t, "1.1 KiB", sizeToApproxHuman(1126))
} }
func TestParseTime(t *testing.T) {
tests := []struct {
input string
expected time.Time
}{
{"02.01.2006", time.Date(2006, 01, 02, 0, 0, 0, 0, time.Local)},
{"02.01.2006 15:04", time.Date(2006, 01, 02, 15, 4, 0, 0, time.Local)},
{"02.01.2006 15:04:05", time.Date(2006, 01, 02, 15, 4, 5, 0, time.Local)},
}
for _, test := range tests {
got, err := parseTime(test.input)
assert.NoError(t, err)
assert.Equal(t, test.expected, got)
}
}