diff --git a/Dockerfile b/Dockerfile index b0a9234..5fa1a9b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ RUN go mod download COPY . . RUN go build -FROM restic/restic:0.17.1 +FROM restic/restic:0.17.2 RUN apk add --no-cache rclone bash curl docker-cli COPY --from=builder /app/autorestic /usr/bin/autorestic ENTRYPOINT [] diff --git a/install.sh b/install.sh index f70a584..68d70f4 100755 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ #!/bin/bash - +set -e -o pipefail shopt -s nocaseglob OUT_FILE=/usr/local/bin/autorestic diff --git a/internal/backend.go b/internal/backend.go index fd76990..61e4201 100644 --- a/internal/backend.go +++ b/internal/backend.go @@ -14,20 +14,20 @@ import ( ) type BackendRest struct { - User string `mapstructure:"user,omitempty"` - Password string `mapstructure:"password,omitempty"` + User string `mapstructure:"user,omitempty" yaml:"user,omitempty"` + Password string `mapstructure:"password,omitempty" yaml:"password,omitempty"` } type Backend struct { name string - Type string `mapstructure:"type,omitempty"` - Path string `mapstructure:"path,omitempty"` - Key string `mapstructure:"key,omitempty"` - RequireKey bool `mapstructure:"requireKey,omitempty"` - Init bool `mapstructure:"init,omitempty"` - Env map[string]string `mapstructure:"env,omitempty"` - Rest BackendRest `mapstructure:"rest,omitempty"` - Options Options `mapstructure:"options,omitempty"` + Type string `mapstructure:"type,omitempty" yaml:"type,omitempty"` + Path string `mapstructure:"path,omitempty" yaml:"path,omitempty"` + Key string `mapstructure:"key,omitempty" yaml:"key,omitempty"` + RequireKey bool `mapstructure:"requireKey,omitempty" yaml:"requireKey,omitempty"` + Init bool `mapstructure:"init,omitempty" yaml:"init,omitempty"` + Env map[string]string `mapstructure:"env,omitempty" yaml:"env,omitempty"` + Rest BackendRest `mapstructure:"rest,omitempty" yaml:"rest,omitempty"` + Options Options `mapstructure:"options,omitempty" yaml:"options,omitempty"` } func GetBackend(name string) (Backend, bool) { diff --git a/internal/config.go b/internal/config.go index 24d948c..c5430d3 100644 --- a/internal/config.go +++ b/internal/config.go @@ -23,11 +23,11 @@ type OptionMap map[string][]interface{} type Options map[string]OptionMap type Config struct { - Version string `mapstructure:"version"` - Extras interface{} `mapstructure:"extras"` - Locations map[string]Location `mapstructure:"locations"` - Backends map[string]Backend `mapstructure:"backends"` - Global Options `mapstructure:"global"` + Version string `mapstructure:"version" yaml:"version"` + Extras interface{} `mapstructure:"extras" yaml:"extras"` + Locations map[string]Location `mapstructure:"locations" yaml:"locations"` + Backends map[string]Backend `mapstructure:"backends" yaml:"backends"` + Global Options `mapstructure:"global" yaml:"global"` } var once sync.Once diff --git a/internal/config_test.go b/internal/config_test.go index 0d55f8e..99185e8 100644 --- a/internal/config_test.go +++ b/internal/config_test.go @@ -1,10 +1,15 @@ package internal import ( + "path" "reflect" "strconv" "strings" + "sync" "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" ) func TestOptionToString(t *testing.T) { @@ -143,6 +148,48 @@ func TestGetOptionsMultipleKeys(t *testing.T) { reflect.DeepEqual(result, expected) } +func TestSaveConfigProducesReadableConfig(t *testing.T) { + workDir := t.TempDir() + viper.SetConfigFile(path.Join(workDir, ".autorestic.yml")) + + // Required to appease the config reader + viper.Set("version", 2) + + c := Config{ + Version: "2", + Locations: map[string]Location{ + "test": { + Type: "local", + name: "test", + From: []string{"in-dir"}, + To: []string{"test"}, + // ForgetOption & ConfigOption have previously marshalled in a way that + // can't get read correctly + ForgetOption: "foo", + CopyOption: map[string][]string{"foo": {"bar"}}, + }, + }, + Backends: map[string]Backend{ + "test": { + name: "test", + Type: "local", + Path: "backup-target", + Key: "supersecret", + }, + }, + } + + err := c.SaveConfig() + assert.NoError(t, err) + + // Ensure we the config reading logic actually runs + config = nil + once = sync.Once{} + readConfig := GetConfig() + assert.NotNil(t, readConfig) + assert.Equal(t, c, *readConfig) +} + func assertEqual[T comparable](t testing.TB, result, expected T) { t.Helper() diff --git a/internal/cron.go b/internal/cron.go index 56ae3d5..055e192 100644 --- a/internal/cron.go +++ b/internal/cron.go @@ -1,12 +1,22 @@ package internal +import ( + "errors" + "fmt" +) + func RunCron() error { c := GetConfig() + var errs []error for name, l := range c.Locations { l.name = name if err := l.RunCron(); err != nil { - return err + errs = append(errs, err) } } + + if len(errs) > 0 { + return fmt.Errorf("Encountered errors during cron process:\n%w", errors.Join(errs...)) + } return nil } diff --git a/internal/location.go b/internal/location.go index e8cc84a..6aa425e 100644 --- a/internal/location.go +++ b/internal/location.go @@ -1,6 +1,7 @@ package internal import ( + "errors" "fmt" "io/ioutil" "os" @@ -33,26 +34,26 @@ const ( ) type Hooks struct { - Dir string `mapstructure:"dir"` - PreValidate HookArray `mapstructure:"prevalidate,omitempty"` - Before HookArray `mapstructure:"before,omitempty"` - After HookArray `mapstructure:"after,omitempty"` - Success HookArray `mapstructure:"success,omitempty"` - Failure HookArray `mapstructure:"failure,omitempty"` + Dir string `mapstructure:"dir" yaml:"dir"` + PreValidate HookArray `mapstructure:"prevalidate,omitempty" yaml:"prevalidate,omitempty"` + Before HookArray `mapstructure:"before,omitempty" yaml:"before,omitempty"` + After HookArray `mapstructure:"after,omitempty" yaml:"after,omitempty"` + Success HookArray `mapstructure:"success,omitempty" yaml:"success,omitempty"` + Failure HookArray `mapstructure:"failure,omitempty" yaml:"failure,omitempty"` } type LocationCopy = map[string][]string type Location struct { - name string `mapstructure:",omitempty"` - From []string `mapstructure:"from,omitempty"` - Type string `mapstructure:"type,omitempty"` - To []string `mapstructure:"to,omitempty"` - Hooks Hooks `mapstructure:"hooks,omitempty"` - Cron string `mapstructure:"cron,omitempty"` - Options Options `mapstructure:"options,omitempty"` - ForgetOption LocationForgetOption `mapstructure:"forget,omitempty"` - CopyOption LocationCopy `mapstructure:"copy,omitempty"` + name string `mapstructure:",omitempty" yaml:",omitempty"` + From []string `mapstructure:"from,omitempty" yaml:"from,omitempty"` + Type string `mapstructure:"type,omitempty" yaml:"type,omitempty"` + To []string `mapstructure:"to,omitempty" yaml:"to,omitempty"` + Hooks Hooks `mapstructure:"hooks,omitempty" yaml:"hooks,omitempty"` + Cron string `mapstructure:"cron,omitempty" yaml:"cron,omitempty"` + Options Options `mapstructure:"options,omitempty" yaml:"options,omitempty"` + ForgetOption LocationForgetOption `mapstructure:"forget,omitempty" yaml:"forget,omitempty"` + CopyOption LocationCopy `mapstructure:"copy,omitempty" yaml:"copy,omitempty"` } func GetLocation(name string) (Location, bool) { @@ -454,7 +455,10 @@ func (l Location) RunCron() error { now := time.Now() if now.After(next) { lock.SetCron(l.name, now.Unix()) - l.Backup(true, "") + errs := l.Backup(true, "") + if len(errs) > 0 { + return fmt.Errorf("Failed to backup location \"%s\":\n%w", l.name, errors.Join(errs...)) + } } else { if !flags.CRON_LEAN { colors.Body.Printf("Skipping \"%s\", not due yet.\n", l.name)