diff --git a/CHANGELOG.md b/CHANGELOG.md index bf2ecb0..9341c44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.4.0] - 2021-10-30 + +### Added + +- Allow specify to specify a backend for location backup +- Global restic flags +- Generic ENV support for backends + +### Changed + +- Install now only requires `wget` +- Env variable for the `KEY` has been renamed from `AUTORESTIC_[BACKEND NAME]_KEY` -> `AUTORESTIC_[BACKEND NAME]_RESTIC_PASSWORD` + +### Fixed + +- Error handling during upgrade & uninstall + ## [1.3.0] - 2021-10-26 ### Added diff --git a/cmd/backup.go b/cmd/backup.go index 53c875d..e33af34 100644 --- a/cmd/backup.go +++ b/cmd/backup.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "strings" "github.com/cupcakearmy/autorestic/internal" "github.com/cupcakearmy/autorestic/internal/colors" @@ -22,8 +23,13 @@ var backupCmd = &cobra.Command{ CheckErr(err) errors := 0 for _, name := range selected { - location, _ := internal.GetLocation(name) - errs := location.Backup(false) + var splitted = strings.Split(name, "@") + var specificBackend = "" + if len(splitted) > 1 { + specificBackend = splitted[1] + } + location, _ := internal.GetLocation(splitted[0]) + errs := location.Backup(false, specificBackend) for err := range errs { colors.Error.Println(err) errors++ diff --git a/cmd/uninstall.go b/cmd/uninstall.go index 50f774c..e3e955f 100644 --- a/cmd/uninstall.go +++ b/cmd/uninstall.go @@ -9,12 +9,12 @@ var uninstallCmd = &cobra.Command{ Use: "uninstall", Short: "Uninstall restic and autorestic", Run: func(cmd *cobra.Command, args []string) { - noRestic, _ := cmd.Flags().GetBool("no-restic") - bins.Uninstall(!noRestic) + restic, _ := cmd.Flags().GetBool("restic") + bins.Uninstall(restic) }, } func init() { rootCmd.AddCommand(uninstallCmd) - uninstallCmd.Flags().Bool("no-restic", false, "do not uninstall restic.") + uninstallCmd.Flags().Bool("restic", false, "also uninstall restic.") } diff --git a/cmd/upgrade.go b/cmd/upgrade.go index f2a0dc7..b6bc15e 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -9,13 +9,13 @@ var upgradeCmd = &cobra.Command{ Use: "upgrade", Short: "Upgrade autorestic and restic", Run: func(cmd *cobra.Command, args []string) { - noRestic, _ := cmd.Flags().GetBool("no-restic") - err := bins.Upgrade(!noRestic) + restic, _ := cmd.Flags().GetBool("restic") + err := bins.Upgrade(restic) CheckErr(err) }, } func init() { rootCmd.AddCommand(upgradeCmd) - upgradeCmd.Flags().Bool("no-restic", false, "also update restic") + upgradeCmd.Flags().Bool("restic", true, "also update restic") } diff --git a/docs/markdown/backend/available.md b/docs/markdown/backend/available.md index 88bccf0..cf692c4 100644 --- a/docs/markdown/backend/available.md +++ b/docs/markdown/backend/available.md @@ -4,6 +4,8 @@ In theory [all the restic backends](https://restic.readthedocs.io/en/stable/030_ Those tested are the following: +> ℹ️ You can also [specify the `env` variables in a config file](/backend/env) to separate them from the config file. + ## Local ```yaml diff --git a/docs/markdown/backend/env.md b/docs/markdown/backend/env.md index 4831c13..f66784e 100644 --- a/docs/markdown/backend/env.md +++ b/docs/markdown/backend/env.md @@ -1,36 +1,67 @@ # Environment -> ⚠ Available since version `v1.3.0` +> ⚠ Available since version `v1.4.0` Sometimes it's favorable not having the encryption keys in the config files. -For that `autorestic` allows passing the backend keys as `ENV` variables, or through an env file. +For that `autorestic` allows passing the env variables to backend password as `ENV` variables, or through an env file. +You can also pass whatever `env` variable to restic by prefixing it with `AUTORESTIC_[BACKEND NAME]_`. -The syntax for the `ENV` variables is as follows: `AUTORESTIC_[BACKEND NAME]_KEY`. +> ℹ️ Env variables and file overwrite the config file in the following order: +> +> Env Variables > Env File (`.autorestic.env`) > Config file (`.autorestic.yaml`) + +## Env file + +Alternatively `autorestic` can load an env file, located next to `.autorestic.yml` called `.autorestic.env`. + +``` +AUTORESTIC_FOO_RESTIC_PASSWORD=secret123 +``` + +### Example with repository password + +The syntax for the `ENV` variables is as follows: `AUTORESTIC_[BACKEND NAME]_RESTIC_PASSWORD`. ```yaml | autorestic.yaml backend: foo: type: ... path: ... - key: secret123 # => AUTORESTIC_FOO_KEY=secret123 + key: secret123 # => AUTORESTIC_FOO_RESTIC_PASSWORD=secret123 ``` -## Example - This means we could remove `key: secret123` from `.autorestic.yaml` and execute as follows: ```bash -AUTORESTIC_FOO_KEY=secret123 autorestic backup ... +AUTORESTIC_FOO_RESTIC_PASSWORD=secret123 autorestic backup ... ``` -## Env file +### Example with Backblaze B2 -Alternatively `autorestic` can load an env file, located next to `autorestic.yml` called `.autorestic.env`. - -```| .autorestic.env -AUTORESTIC_FOO_KEY=secret123 +```yaml | autorestic.yaml +backends: + bb: + type: b2 + path: myBucket + key: myPassword + env: + B2_ACCOUNT_ID: 123 + B2_ACCOUNT_KEY: 456 ``` -after that you can simply use `autorestic` as your are used to. +You could create an `.autorestic.env` or pass the following `ENV` variables to autorestic: + +``` +AUTORESTIC_BB_RESTIC_PASSWORD=myPassword +AUTORESTIC_BB_B2_ACCOUNT_ID=123 +AUTORESTIC_BB_B2_ACCOUNT_KEY=456 +``` + +```yaml | autorestic.yaml +backends: + bb: + type: b2 + path: myBucket +``` > :ToCPrevNext diff --git a/docs/markdown/backend/options.md b/docs/markdown/backend/options.md index 2a49a56..63227dc 100644 --- a/docs/markdown/backend/options.md +++ b/docs/markdown/backend/options.md @@ -1,8 +1,6 @@ # Options -For the `backup` and `forget` commands you can pass any native flags to `restic`. - -> It is also possible to set options for an [a specific location](/location/options). +> ℹ️ For more detail see the [location docs](/location/options) for options, as they are the same. ```yaml backend: @@ -18,8 +16,4 @@ backend: In this example, whenever `autorestic` runs `restic backup` it will append a `--tag abc --tag` to the native command. -For more detail see the [location docs](/location/options) for options, as they are the same. - -> For flags without arguments you can set them to `true`. They will be handled accordingly. - > :ToCPrevNext diff --git a/docs/markdown/cli/backup.md b/docs/markdown/cli/backup.md index ddbdd0e..f260fcb 100644 --- a/docs/markdown/cli/backup.md +++ b/docs/markdown/cli/backup.md @@ -14,4 +14,12 @@ autorestic backup -a autorestic backup -l foo -l bar ``` +## Specific location + +`autorestic` also allows selecting specific backends for a location with the `location@backend` syntax. + +```bash +autorestic backup -l location@backend +``` + > :ToCPrevNext diff --git a/docs/markdown/installation.md b/docs/markdown/installation.md index 7f7e3fc..a6a9a2a 100644 --- a/docs/markdown/installation.md +++ b/docs/markdown/installation.md @@ -2,10 +2,10 @@ Linux & macOS. Windows is not supported. If you have problems installing please open an issue :) -Autorestic requires `curl`, `wget` and `bzip2` to be installed. For most systems these should be already installed. +Autorestic requires `bash`, `wget` and `bzip2` to be installed. For most systems these should be already installed. ```bash -curl -s https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash +wget -qO - https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash ``` ## Alternatives diff --git a/docs/markdown/location/options.md b/docs/markdown/location/options.md index 7ac90c6..16d0260 100644 --- a/docs/markdown/location/options.md +++ b/docs/markdown/location/options.md @@ -1,8 +1,32 @@ # Options -For the `backup` and `forget` commands you can pass any native flags to `restic`. +For the `backup` and `forget` commands you can pass any native flags to `restic`. In addition you can specify flags for every command with `all`. -> It is also possible to set options for an [entire backend](/backend/options). +If flags don't start with `-` they will get prefixed with `--`. + +Flags without arguments can be set to `true`. They will be handled accordingly. + +> ℹ️ It is also possible to set options for an [entire backend](/backend/options) or globally (see below). + +```yaml +locations: + foo: + # ... + options: + all: + some-flag: 123 + # Equivalent to + --some-flag: 123 + backup: + boolean-flag: true + tag: + - foo + - bar +``` + +## Example + +In this example, whenever `autorestic` runs `restic backup` it will append a `--tag abc --tag` to the native command. ```yaml locations: @@ -16,8 +40,22 @@ locations: - bar ``` -In this example, whenever `autorestic` runs `restic backup` it will append a `--tag abc --tag` to the native command. +## Global Options -> For flags without arguments you can set them to `true`. They will be handled accordingly. +It is possible to specify global flags that will be run every time restic is invoked. To do so specify them under `global` in your config file. + +```yaml +global: + all: + cache-dir: ~/restic + backup: + tag: + - foo + +backends: + # ... +locations: + # ... +``` > :ToCPrevNext diff --git a/docs/markdown/quick.md b/docs/markdown/quick.md index 231d184..574bc03 100644 --- a/docs/markdown/quick.md +++ b/docs/markdown/quick.md @@ -3,7 +3,7 @@ ## Installation ```bash -curl -s https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash +wget -qO - https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash ``` See [installation](/installation) for alternative options. diff --git a/install.sh b/install.sh index de473a4..2638498 100755 --- a/install.sh +++ b/install.sh @@ -31,7 +31,7 @@ else fi echo $ARCH -curl -s https://api.github.com/repos/cupcakearmy/autorestic/releases/latest \ +wget -qO - https://api.github.com/repos/cupcakearmy/autorestic/releases/latest \ | grep "browser_download_url.*_${OS}_${ARCH}" \ | cut -d : -f 2,3 \ | tr -d \" \ diff --git a/internal/backend.go b/internal/backend.go index 8116d44..e60c709 100644 --- a/internal/backend.go +++ b/internal/backend.go @@ -58,20 +58,27 @@ func (b Backend) generateRepo() (string, error) { func (b Backend) getEnv() (map[string]string, error) { env := make(map[string]string) + // Key if b.Key != "" { env["RESTIC_PASSWORD"] = b.Key - } else { - key, err := b.getKey() - if err != nil { - return nil, err - } - env["RESTIC_PASSWORD"] = key } + + // From config file repo, err := b.generateRepo() env["RESTIC_REPOSITORY"] = repo for key, value := range b.Env { env[strings.ToUpper(key)] = value } + + // From Envfile and passed as env + var prefix = "AUTORESTIC_" + strings.ToUpper(b.name) + "_" + for _, variable := range os.Environ() { + var splitted = strings.SplitN(variable, "=", 2) + if strings.HasPrefix(splitted[0], prefix) { + env[strings.TrimPrefix(splitted[0], prefix)] = splitted[1] + } + } + return env, err } @@ -85,17 +92,6 @@ func generateRandomKey() string { return key } -func (b Backend) getKey() (string, error) { - if b.Key != "" { - return b.Key, nil - } - keyName := "AUTORESTIC_" + strings.ToUpper(b.name) + "_KEY" - if key, found := os.LookupEnv(keyName); found { - return key, nil - } - return "", fmt.Errorf("no key found for backend \"%s\"", b.name) -} - func (b Backend) validate() error { if b.Type == "" { return fmt.Errorf(`Backend "%s" has no "type"`, b.name) @@ -105,8 +101,9 @@ func (b Backend) validate() error { } if b.Key == "" { // Check if key is set in environment - if _, err := b.getKey(); err != nil { - // If not generate a new one + env, _ := b.getEnv() + if _, found := env["RESTIC_PASSWORD"]; !found { + // No key set in config file or env => generate random key and save file key := generateRandomKey() b.Key = key c := GetConfig() diff --git a/internal/bins/bins.go b/internal/bins/bins.go index 87e9488..27c640c 100644 --- a/internal/bins/bins.go +++ b/internal/bins/bins.go @@ -47,11 +47,11 @@ func dlJSON(url string) (GithubRelease, error) { func Uninstall(restic bool) error { if err := os.Remove(path.Join(INSTALL_PATH, "autorestic")); err != nil { - colors.Error.Println(err) + return err } if restic { if err := os.Remove(path.Join(INSTALL_PATH, "restic")); err != nil { - colors.Error.Println(err) + return err } } return nil @@ -79,11 +79,15 @@ func downloadAndInstallAsset(body GithubRelease, name string) error { return err } defer tmp.Close() - tmp.Chmod(0755) - io.Copy(tmp, bz) + if err := tmp.Chmod(0755); err != nil { + return err + } + if _, err := io.Copy(tmp, bz); err != nil { + return err + } to := path.Join(INSTALL_PATH, name) - os.Remove(to) // Delete if current, ignore error if file does not exits. + defer os.Remove(to) // Delete if current, ignore error if file does not exits. if err := os.Rename(tmp.Name(), to); err != nil { return nil } @@ -121,9 +125,11 @@ func Upgrade(restic bool) error { // Upgrade restic if restic { if err := InstallRestic(); err != nil { - colors.Error.Println(err) + return err + } + if err := upgradeRestic(); err != nil { + return err } - upgradeRestic() } // Upgrade self @@ -140,7 +146,9 @@ func Upgrade(restic bool) error { return err } if current.LT(latest) { - downloadAndInstallAsset(body, "autorestic") + if err := downloadAndInstallAsset(body, "autorestic"); err != nil { + return err + } colors.Success.Println("Updated autorestic") } else { colors.Body.Println("Already up to date") diff --git a/internal/config.go b/internal/config.go index f63b351..3cf5cca 100644 --- a/internal/config.go +++ b/internal/config.go @@ -16,16 +16,20 @@ import ( "github.com/spf13/viper" ) -const VERSION = "1.3.0" +const VERSION = "1.4.0" var CI bool = false var VERBOSE bool = false var CRON_LEAN bool = false +type OptionMap map[string][]interface{} +type Options map[string]OptionMap + type Config struct { Extras interface{} `yaml:"extras"` Locations map[string]Location `yaml:"locations"` Backends map[string]Backend `yaml:"backends"` + Global Options `yaml:"global"` } var once sync.Once @@ -185,20 +189,18 @@ func GetAllOrSelected(cmd *cobra.Command, backends bool) ([]string, error) { selected, _ = cmd.Flags().GetStringSlice("location") } for _, s := range selected { - found := false + var splitted = strings.Split(s, "@") for _, l := range list { - if l == s { - found = true - break + if l == splitted[0] { + goto found } } - if !found { - if backends { - return nil, fmt.Errorf("invalid backend \"%s\"", s) - } else { - return nil, fmt.Errorf("invalid location \"%s\"", s) - } + if backends { + return nil, fmt.Errorf("invalid backend \"%s\"", s) + } else { + return nil, fmt.Errorf("invalid location \"%s\"", s) } + found: } if len(selected) == 0 { @@ -235,23 +237,40 @@ func (c *Config) SaveConfig() error { return viper.WriteConfig() } -func getOptions(options Options, key string) []string { - var selected []string - for k, values := range options[key] { +func optionToString(option string) string { + if !strings.HasPrefix(option, "-") { + return "--" + option + } + return option +} + +func appendOptionsToSlice(str *[]string, options OptionMap) { + for key, values := range options { for _, value := range values { // Bool asBool, ok := value.(bool) if ok && asBool { - selected = append(selected, fmt.Sprintf("--%s", k)) + *str = append(*str, optionToString(key)) continue } // String asString, ok := value.(string) if ok { - selected = append(selected, fmt.Sprintf("--%s", k), asString) + *str = append(*str, optionToString(key), asString) continue } } } +} + +func getOptions(options Options, key string) []string { + var selected []string + var keys = []string{"all"} + if key != "" { + keys = append(keys, key) + } + for _, key := range keys { + appendOptionsToSlice(&selected, options[key]) + } return selected } diff --git a/internal/location.go b/internal/location.go index 79f9bce..cf75ce1 100644 --- a/internal/location.go +++ b/internal/location.go @@ -31,8 +31,6 @@ type Hooks struct { Failure HookArray `yaml:"failure,omitempty"` } -type Options map[string]map[string][]interface{} - type Location struct { name string `yaml:",omitempty"` From string `yaml:"from,omitempty"` @@ -125,8 +123,9 @@ func (l Location) getPath() (string, error) { return "", fmt.Errorf("could not get path for location \"%s\"", l.name) } -func (l Location) Backup(cron bool) []error { +func (l Location) Backup(cron bool, specificBackend string) []error { var errors []error + var backends []string colors.PrimaryPrint(" Backing up location \"%s\" ", l.name) t := l.getType() options := ExecuteOptions{ @@ -155,7 +154,17 @@ func (l Location) Backup(cron bool) []error { } // Backup - for i, to := range l.To { + if specificBackend == "" { + backends = l.To + } else { + if l.hasBackend(specificBackend) { + backends = []string{specificBackend} + } else { + errors = append(errors, fmt.Errorf("backup location \"%s\" has no backend \"%s\"", l.name, specificBackend)) + return errors + } + } + for i, to := range backends { backend, _ := GetBackend(to) colors.Secondary.Printf("Backend: %s\n", backend.name) env, err := backend.getEnv() @@ -338,7 +347,7 @@ func (l Location) RunCron() error { now := time.Now() if now.After(next) { lock.SetCron(l.name, now.Unix()) - l.Backup(true) + l.Backup(true, "") } else { if !CRON_LEAN { colors.Body.Printf("Skipping \"%s\", not due yet.\n", l.name) diff --git a/internal/utils.go b/internal/utils.go index 9a73a05..7756677 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -53,6 +53,9 @@ func ExecuteCommand(options ExecuteOptions, args ...string) (string, error) { func ExecuteResticCommand(options ExecuteOptions, args ...string) (string, error) { options.Command = RESTIC_BIN + var c = GetConfig() + var optionsAsString = getOptions(c.Global, "") + args = append(optionsAsString, args...) return ExecuteCommand(options, args...) }