diff --git a/.gitignore b/.gitignore index 40ab25b..65d7b07 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ node_modules # Build & Dev test autorestic -data \ No newline at end of file +data +dist \ No newline at end of file diff --git a/cmd/backup.go b/cmd/backup.go index eed8858..212ff61 100644 --- a/cmd/backup.go +++ b/cmd/backup.go @@ -39,25 +39,18 @@ var backupCmd = &cobra.Command{ } defer lock.Unlock() { - backup(internal.GetAllOrLocation(cmd, false), config) + selected, err := internal.GetAllOrSelected(cmd, false) + cobra.CheckErr(err) + for _, name := range selected { + location := config.Locations[name] + fmt.Printf("Backing up: `%s`", name) + location.Backup() + } } }, } func init() { rootCmd.AddCommand(backupCmd) - backupCmd.PersistentFlags().StringSliceP("location", "l", []string{}, "Locations") - backupCmd.PersistentFlags().BoolP("all", "a", false, "Backup all locations") -} - -func backup(locations []string, config *internal.Config) { - for _, name := range locations { - location, ok := config.Locations[name] - if !ok { - fmt.Println(fmt.Errorf("location `%s` does not exist", name)) - } else { - fmt.Printf("Backing up: `%s`", name) - location.Backup() - } - } + internal.AddFlagsToCommand(backupCmd, false) } diff --git a/cmd/exec.go b/cmd/exec.go index c624de8..0174e82 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -31,20 +31,19 @@ var execCmd = &cobra.Command{ if err := config.CheckConfig(); err != nil { panic(err) } - exec(internal.GetAllOrLocation(cmd, true), config, args) + { + selected, err := internal.GetAllOrSelected(cmd, true) + cobra.CheckErr(err) + for _, name := range selected { + fmt.Println(name) + backend := config.Backends[name] + backend.Exec(args) + } + } }, } func init() { rootCmd.AddCommand(execCmd) - execCmd.PersistentFlags().StringSliceP("backend", "b", []string{}, "backends") - execCmd.PersistentFlags().BoolP("all", "a", false, "Exec in all backends") -} - -func exec(backends []string, config *internal.Config, args []string) { - for _, name := range backends { - fmt.Println(name) - backend := config.Backends[name] - backend.Exec(args) - } + internal.AddFlagsToCommand(execCmd, true) } diff --git a/cmd/forget.go b/cmd/forget.go new file mode 100644 index 0000000..8528d7c --- /dev/null +++ b/cmd/forget.go @@ -0,0 +1,49 @@ +/* +Copyright © 2021 NAME HERE + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "github.com/cupcakearmy/autorestic/internal" + "github.com/spf13/cobra" +) + +// forgetCmd represents the forget command +var forgetCmd = &cobra.Command{ + Use: "forget", + Short: "Forget and optionally prune snapshots according the specified policies", + Run: func(cmd *cobra.Command, args []string) { + config := internal.GetConfig() + if err := config.CheckConfig(); err != nil { + panic(err) + } + { + selected, err := internal.GetAllOrSelected(cmd, false) + cobra.CheckErr(err) + prune, _ := cmd.Flags().GetBool("prune") + for _, name := range selected { + location := config.Locations[name] + err := location.Forget(prune) + cobra.CheckErr(err) + } + } + }, +} + +func init() { + rootCmd.AddCommand(forgetCmd) + internal.AddFlagsToCommand(forgetCmd, false) + forgetCmd.Flags().Bool("prune", false, "Also prune repository") +} diff --git a/cmd/install.go b/cmd/install.go new file mode 100644 index 0000000..fa91750 --- /dev/null +++ b/cmd/install.go @@ -0,0 +1,34 @@ +/* +Copyright © 2021 NAME HERE + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "github.com/cupcakearmy/autorestic/internal/bins" + "github.com/spf13/cobra" +) + +var installCmd = &cobra.Command{ + Use: "install", + Short: "A brief description of your command", + Run: func(cmd *cobra.Command, args []string) { + err := bins.InstallRestic() + cobra.CheckErr(err) + }, +} + +func init() { + rootCmd.AddCommand(installCmd) +} diff --git a/cmd/root.go b/cmd/root.go index c484007..4cf1724 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -30,8 +30,9 @@ var cfgFile string // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ - Use: "autorestic", - Short: "CLI Wrapper for restic", + Version: internal.VERSION, + Use: "autorestic", + Short: "CLI Wrapper for restic", } // Execute adds all child commands to the root command and sets flags appropriately. diff --git a/cmd/upgrade.go b/cmd/upgrade.go new file mode 100644 index 0000000..e12c9fd --- /dev/null +++ b/cmd/upgrade.go @@ -0,0 +1,36 @@ +/* +Copyright © 2021 NAME HERE + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "github.com/cupcakearmy/autorestic/internal/bins" + "github.com/spf13/cobra" +) + +var upgradeCmd = &cobra.Command{ + Use: "upgrade", + Short: "A brief description of your command", + Run: func(cmd *cobra.Command, args []string) { + noRestic, _ := cmd.Flags().GetBool("no-restic") + err := bins.Upgrade(!noRestic) + cobra.CheckErr(err) + }, +} + +func init() { + rootCmd.AddCommand(upgradeCmd) + upgradeCmd.Flags().Bool("no-restic", false, "Also update restic. Default: true") +} diff --git a/go.mod b/go.mod index 07ed665..b0202ff 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/cupcakearmy/autorestic go 1.16 require ( + github.com/blang/semver/v4 v4.0.0 github.com/buger/goterm v1.0.0 github.com/mitchellh/go-homedir v1.1.0 github.com/spf13/cobra v1.1.3 diff --git a/go.sum b/go.sum index f29fda4..314d040 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/buger/goterm v1.0.0 h1:ZB6uUlY8+sjJyFGzz2WpRqX2XYPeXVgtZAOJMwOsTWM= github.com/buger/goterm v1.0.0/go.mod h1:16STi3LquiscTIHA8SXUNKEa/Cnu4ZHBH8NsCaWgso0= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -248,7 +250,6 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 h1:rF3Ohx8DRyl8h2zw9qojyLHLhrJpEMgyPOImREEryf0= golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/bins/bins.go b/internal/bins/bins.go new file mode 100644 index 0000000..276e2a5 --- /dev/null +++ b/internal/bins/bins.go @@ -0,0 +1,127 @@ +package bins + +import ( + "compress/bzip2" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path" + "runtime" + "strings" + + "github.com/blang/semver/v4" + "github.com/cupcakearmy/autorestic/internal" +) + +const INSTALL_PATH = "/usr/local/bin" + +type GithubReleaseAsset struct { + Name string `json:"name"` + Link string `json:"browser_download_url"` +} +type GithubRelease struct { + Tag string `json:"tag_name"` + Assets []GithubReleaseAsset `json:"assets"` +} + +func dlJSON(url string) (GithubRelease, error) { + var parsed GithubRelease + resp, err := http.Get(url) + if err != nil { + return parsed, err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return parsed, err + + } + json.Unmarshal(body, &parsed) + return parsed, nil +} + +func InstallRestic() error { + installed := internal.CheckIfCommandIsCallable("restic") + if installed { + fmt.Println("restic already installed") + return nil + } else { + body, err := dlJSON("https://api.github.com/repos/restic/restic/releases/latest") + if err != nil { + return err + } + ending := fmt.Sprintf("_%s_%s.bz2", runtime.GOOS, runtime.GOARCH) + for _, asset := range body.Assets { + if strings.HasSuffix(asset.Name, ending) { + // Found + fmt.Println(asset.Link) + + // Download archive + resp, err := http.Get(asset.Link) + if err != nil { + return err + } + defer resp.Body.Close() + + // Uncompress + bz := bzip2.NewReader(resp.Body) + + // Save binary + file, err := os.Create(path.Join(INSTALL_PATH, "restic")) + if err != nil { + return err + } + file.Chmod(0755) + defer file.Close() + io.Copy(file, bz) + + fmt.Printf("Successfully installed restic under %s\n", INSTALL_PATH) + return nil + } + } + return errors.New("could not find right binary for your system, please install restic manually. https://bit.ly/2Y1Rzai") + } +} + +func upgradeRestic() error { + out, err := internal.ExecuteCommand(internal.ExecuteOptions{ + Command: "restic", + }, "self-update") + fmt.Println(out) + return err +} + +func Upgrade(restic bool) error { + // Upgrade restic + if restic { + InstallRestic() + upgradeRestic() + } + + // Upgrade self + current, err := semver.ParseTolerant(internal.VERSION) + if err != nil { + return err + } + fmt.Println(current) + + body, err := dlJSON("https://api.github.com/repos/cupcakearmy/autorestic/releases/latest") + if err != nil { + return err + } + latest, err := semver.ParseTolerant(body.Tag) + if err != nil { + return err + } + if current.GT(latest) { + + fmt.Println("Updated autorestic") + } else { + fmt.Println("Already up to date") + } + return nil +} diff --git a/internal/config.go b/internal/config.go index 4544f54..485cee4 100644 --- a/internal/config.go +++ b/internal/config.go @@ -12,6 +12,8 @@ import ( "github.com/spf13/viper" ) +const VERSION = "1.0.0" + type Config struct { Locations map[string]Location `mapstructure:"locations"` Backends map[string]Backend `mapstructure:"backends"` @@ -60,7 +62,7 @@ func (c Config) CheckConfig() error { return nil } -func GetAllOrLocation(cmd *cobra.Command, backends bool) []string { +func GetAllOrSelected(cmd *cobra.Command, backends bool) ([]string, error) { var list []string if backends { for key := range config.Backends { @@ -73,7 +75,7 @@ func GetAllOrLocation(cmd *cobra.Command, backends bool) []string { } all, _ := cmd.Flags().GetBool("all") if all { - return list + return list, nil } else { var selected []string if backends { @@ -92,9 +94,22 @@ func GetAllOrLocation(cmd *cobra.Command, backends bool) []string { } } if !found { - panic("invalid key") + if backends { + return nil, fmt.Errorf("invalid backend \"%s\"", s) + } else { + return nil, fmt.Errorf("invalid location \"%s\"", s) + } } } - return selected + return selected, nil + } +} + +func AddFlagsToCommand(cmd *cobra.Command, backend bool) { + cmd.PersistentFlags().BoolP("all", "a", false, "Backup all locations") + if backend { + cmd.PersistentFlags().StringSliceP("backend", "b", []string{}, "backends") + } else { + cmd.PersistentFlags().StringSliceP("location", "l", []string{}, "Locations") } } diff --git a/internal/location.go b/internal/location.go index bf461a6..74ab21e 100644 --- a/internal/location.go +++ b/internal/location.go @@ -83,3 +83,27 @@ func (l Location) Backup() error { } return nil } + +func (l Location) Forget(prune bool) error { + c := GetConfig() + from := GetPathRelativeToConfig(l.From) + for _, to := range l.To { + backend := c.Backends[to] + options := ExecuteOptions{ + Envs: backend.getEnv(), + Dir: from, + } + flags := l.getOptions("forget") + cmd := []string{"forget", "--path", from} + if prune { + cmd = append(cmd, "--prune") + } + cmd = append(cmd, flags...) + out, err := ExecuteResticCommand(options, cmd...) + fmt.Println(out) + if err != nil { + return err + } + } + return nil +}