mirror of
https://github.com/cupcakearmy/autorestic.git
synced 2025-04-01 15:22:43 +00:00
PreValidate can be used to mount remote directories (e.g. via NFS) so they must executed first before running any restic commands. This was done for the backup command in 13aa560, but not for check. This commit fixes that.
344 lines
8.4 KiB
Go
344 lines
8.4 KiB
Go
package internal
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/cupcakearmy/autorestic/internal/colors"
|
|
"github.com/cupcakearmy/autorestic/internal/flags"
|
|
"github.com/cupcakearmy/autorestic/internal/lock"
|
|
"github.com/joho/godotenv"
|
|
"github.com/mitchellh/go-homedir"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
const VERSION = "1.8.3"
|
|
|
|
type OptionMap map[string][]interface{}
|
|
type Options map[string]OptionMap
|
|
|
|
type Config struct {
|
|
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
|
|
var config *Config
|
|
|
|
func exitConfig(err error, msg string) {
|
|
if err != nil {
|
|
colors.Error.Println(err)
|
|
}
|
|
if msg != "" {
|
|
colors.Error.Println(msg)
|
|
}
|
|
lock.Unlock()
|
|
os.Exit(1)
|
|
}
|
|
|
|
func GetConfig() *Config {
|
|
|
|
if config == nil {
|
|
once.Do(func() {
|
|
if err := viper.ReadInConfig(); err == nil {
|
|
absConfig, _ := filepath.Abs(viper.ConfigFileUsed())
|
|
if !flags.CRON_LEAN {
|
|
colors.Faint.Println("Using config: \t", absConfig)
|
|
}
|
|
// Load env file
|
|
envFile := filepath.Join(filepath.Dir(absConfig), ".autorestic.env")
|
|
err = godotenv.Load(envFile)
|
|
if err == nil && !flags.CRON_LEAN {
|
|
colors.Faint.Println("Using env:\t", envFile)
|
|
}
|
|
} else {
|
|
text := err.Error()
|
|
if strings.Contains(text, "no such file or directory") {
|
|
cfgFileName := ".autorestic"
|
|
colors.Error.Println(
|
|
fmt.Sprintf(
|
|
"cannot find configuration file '%s.yml' or '%s.yaml'.",
|
|
cfgFileName, cfgFileName))
|
|
} else {
|
|
colors.Error.Println("could not load config file\n" + text)
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
|
|
var versionConfig interface{}
|
|
viper.UnmarshalKey("version", &versionConfig)
|
|
if versionConfig == nil {
|
|
exitConfig(nil, "no version specified in config file. please see docs on how to migrate")
|
|
}
|
|
version, ok := versionConfig.(int)
|
|
if !ok {
|
|
exitConfig(nil, "version specified in config file is not an int")
|
|
} else {
|
|
// Check for version
|
|
if version != 2 {
|
|
exitConfig(nil, "unsupported config version number. please check the docs for migration\nhttps://autorestic.vercel.app/migration/")
|
|
}
|
|
}
|
|
|
|
config = &Config{}
|
|
if err := viper.UnmarshalExact(config); err != nil {
|
|
exitConfig(err, "Could not parse config file!")
|
|
}
|
|
})
|
|
}
|
|
return config
|
|
}
|
|
|
|
func GetPathRelativeToConfig(p string) (string, error) {
|
|
if path.IsAbs(p) {
|
|
return p, nil
|
|
} else if strings.HasPrefix(p, "~") {
|
|
home, err := homedir.Dir()
|
|
return path.Join(home, strings.TrimPrefix(p, "~")), err
|
|
} else {
|
|
return path.Join(path.Dir(viper.ConfigFileUsed()), p), nil
|
|
}
|
|
}
|
|
|
|
func (c *Config) Describe() {
|
|
// Locations
|
|
for name, l := range c.Locations {
|
|
var tmp string
|
|
colors.PrimaryPrint(`Location: "%s"`, name)
|
|
|
|
tmp = ""
|
|
for _, path := range l.From {
|
|
tmp += fmt.Sprintf("\t%s %s\n", colors.Success.Sprint("←"), path)
|
|
}
|
|
colors.PrintDescription("From", tmp)
|
|
|
|
tmp = ""
|
|
for _, to := range l.To {
|
|
tmp += fmt.Sprintf("\t%s %s\n", colors.Success.Sprint("→"), to)
|
|
}
|
|
colors.PrintDescription("To", tmp)
|
|
|
|
if l.Cron != "" {
|
|
colors.PrintDescription("Cron", l.Cron)
|
|
}
|
|
|
|
tmp = ""
|
|
hooks := map[string][]string{
|
|
"PreValidate": l.Hooks.PreValidate,
|
|
"Before": l.Hooks.Before,
|
|
"After": l.Hooks.After,
|
|
"Failure": l.Hooks.Failure,
|
|
"Success": l.Hooks.Success,
|
|
}
|
|
for hook, commands := range hooks {
|
|
if len(commands) > 0 {
|
|
tmp += "\n\t" + hook
|
|
for _, cmd := range commands {
|
|
tmp += colors.Faint.Sprintf("\n\t ▶ %s", cmd)
|
|
}
|
|
}
|
|
}
|
|
if tmp != "" {
|
|
colors.PrintDescription("Hooks", tmp)
|
|
}
|
|
|
|
if len(l.Options) > 0 {
|
|
tmp = ""
|
|
for t, options := range l.Options {
|
|
tmp += "\n\t" + t
|
|
for option, values := range options {
|
|
for _, value := range values {
|
|
tmp += colors.Faint.Sprintf("\n\t ✧ --%s=%s", option, value)
|
|
}
|
|
}
|
|
}
|
|
colors.PrintDescription("Options", tmp)
|
|
}
|
|
}
|
|
|
|
// Backends
|
|
for name, b := range c.Backends {
|
|
colors.PrimaryPrint("Backend: \"%s\"", name)
|
|
colors.PrintDescription("Type", b.Type)
|
|
colors.PrintDescription("Path", b.Path)
|
|
|
|
if len(b.Env) > 0 {
|
|
tmp := ""
|
|
for option, value := range b.Env {
|
|
tmp += fmt.Sprintf("\n\t%s %s %s", colors.Success.Sprint("✧"), strings.ToUpper(option), colors.Faint.Sprint(value))
|
|
}
|
|
colors.PrintDescription("Env", tmp)
|
|
}
|
|
}
|
|
}
|
|
|
|
func CheckConfig() error {
|
|
c := GetConfig()
|
|
if c == nil {
|
|
return fmt.Errorf("config could not be loaded/found")
|
|
}
|
|
if !CheckIfResticIsCallable() {
|
|
return fmt.Errorf(`%s was not found. Install either with "autorestic install" or manually`, flags.RESTIC_BIN)
|
|
}
|
|
|
|
cwd, _ := GetPathRelativeToConfig(".")
|
|
for name, location := range c.Locations {
|
|
location.name = name
|
|
|
|
// Hooks before location validation
|
|
options := ExecuteOptions{
|
|
Command: "bash",
|
|
Dir: cwd,
|
|
Envs: map[string]string{
|
|
"AUTORESTIC_LOCATION": location.name,
|
|
},
|
|
}
|
|
if err := location.ExecuteHooks(location.Hooks.PreValidate, options); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := location.validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for name, backend := range c.Backends {
|
|
backend.name = name
|
|
if err := backend.validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GetAllOrSelected(cmd *cobra.Command, backends bool) ([]string, error) {
|
|
var list []string
|
|
if backends {
|
|
for name := range config.Backends {
|
|
list = append(list, name)
|
|
}
|
|
} else {
|
|
for name := range config.Locations {
|
|
list = append(list, name)
|
|
}
|
|
}
|
|
|
|
all, _ := cmd.Flags().GetBool("all")
|
|
if all {
|
|
return list, nil
|
|
}
|
|
|
|
var selected []string
|
|
if backends {
|
|
selected, _ = cmd.Flags().GetStringSlice("backend")
|
|
} else {
|
|
selected, _ = cmd.Flags().GetStringSlice("location")
|
|
}
|
|
for _, s := range selected {
|
|
var splitted = strings.Split(s, "@")
|
|
for _, l := range list {
|
|
if l == splitted[0] {
|
|
goto found
|
|
}
|
|
}
|
|
if backends {
|
|
return nil, fmt.Errorf("invalid backend \"%s\"", s)
|
|
} else {
|
|
return nil, fmt.Errorf("invalid location \"%s\"", s)
|
|
}
|
|
found:
|
|
}
|
|
|
|
if len(selected) == 0 {
|
|
return selected, fmt.Errorf("nothing selected, aborting")
|
|
}
|
|
return selected, nil
|
|
}
|
|
|
|
func AddFlagsToCommand(cmd *cobra.Command, backend bool) {
|
|
var usage string
|
|
if backend {
|
|
usage = "all backends"
|
|
} else {
|
|
usage = "all locations"
|
|
}
|
|
cmd.PersistentFlags().BoolP("all", "a", false, usage)
|
|
if backend {
|
|
cmd.PersistentFlags().StringSliceP("backend", "b", []string{}, "select backends")
|
|
} else {
|
|
cmd.PersistentFlags().StringSliceP("location", "l", []string{}, "select locations")
|
|
}
|
|
}
|
|
|
|
func (c *Config) SaveConfig() error {
|
|
file := viper.ConfigFileUsed()
|
|
if err := CopyFile(file, file+".old"); err != nil {
|
|
return err
|
|
}
|
|
colors.Secondary.Println("Saved a backup copy of your file next to the original.")
|
|
|
|
viper.Set("backends", c.Backends)
|
|
viper.Set("locations", c.Locations)
|
|
|
|
return viper.WriteConfig()
|
|
}
|
|
|
|
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 {
|
|
*str = append(*str, optionToString(key))
|
|
continue
|
|
}
|
|
*str = append(*str, optionToString(key), fmt.Sprint(value))
|
|
}
|
|
}
|
|
}
|
|
|
|
func getOptions(options Options, keys []string) []string {
|
|
var selected []string
|
|
for _, key := range keys {
|
|
appendOptionsToSlice(&selected, options[key])
|
|
}
|
|
return selected
|
|
}
|
|
|
|
func combineBackendOptions(key string, b Backend) []string {
|
|
// Priority: backend > global
|
|
var options []string
|
|
gFlags := getOptions(GetConfig().Global, []string{key})
|
|
bFlags := getOptions(b.Options, []string{"all", key})
|
|
options = append(options, gFlags...)
|
|
options = append(options, bFlags...)
|
|
return options
|
|
}
|
|
|
|
func combineAllOptions(key string, l Location, b Backend) []string {
|
|
// Priority: location > backend > global
|
|
var options []string
|
|
gFlags := getOptions(GetConfig().Global, []string{key})
|
|
bFlags := getOptions(b.Options, []string{"all", key})
|
|
lFlags := getOptions(l.Options, []string{"all", key})
|
|
options = append(options, gFlags...)
|
|
options = append(options, bFlags...)
|
|
options = append(options, lFlags...)
|
|
return options
|
|
}
|