This commit is contained in:
cupcakearmy 2021-10-31 23:11:57 +01:00
commit 3665cea62d
No known key found for this signature in database
GPG Key ID: D28129AE5654D9D9
9 changed files with 141 additions and 101 deletions

View File

@ -5,13 +5,6 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.5.0] - 2021-11
### Added
- Support for multiple paths
- Improved error handling
## [1.4.1] - 2021-10-31 ## [1.4.1] - 2021-10-31
### Fixes ### Fixes

View File

@ -30,7 +30,7 @@ var backupCmd = &cobra.Command{
} }
location, _ := internal.GetLocation(splitted[0]) location, _ := internal.GetLocation(splitted[0])
errs := location.Backup(false, specificBackend) errs := location.Backup(false, specificBackend)
for err := range errs { for _, err := range errs {
colors.Error.Println(err) colors.Error.Println(err)
errors++ errors++
} }

View File

@ -9,8 +9,9 @@ import (
) )
var restoreCmd = &cobra.Command{ var restoreCmd = &cobra.Command{
Use: "restore", Use: "restore [snapshot id]",
Short: "Restore backup for location", Short: "Restore backup for location",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
err := lock.Lock() err := lock.Lock()
CheckErr(err) CheckErr(err)
@ -24,7 +25,11 @@ var restoreCmd = &cobra.Command{
target, _ := cmd.Flags().GetString("to") target, _ := cmd.Flags().GetString("to")
from, _ := cmd.Flags().GetString("from") from, _ := cmd.Flags().GetString("from")
force, _ := cmd.Flags().GetBool("force") force, _ := cmd.Flags().GetBool("force")
err = l.Restore(target, from, force) snapshot := ""
if len(args) > 0 {
snapshot = args[0]
}
err = l.Restore(target, from, force, snapshot)
CheckErr(err) CheckErr(err)
}, },
} }

View File

@ -1,12 +1,12 @@
# Restore # Restore
```bash ```bash
autorestic restore [-l, --location] [--from backend] [--to <out dir>] [-f, --force] autorestic restore [-l, --location] [--from backend] [--to <out dir>] [-f, --force] [snapshot]
``` ```
This will restore all the locations to the selected target. If for one location there are more than one backends specified autorestic will take the first one. This will restore the location to the selected target. If for one location there are more than one backends specified autorestic will take the first one. If no specific snapshot is specified `autorestic` will use `latest`.
The `--to` path has to be empty as no data will be overwritten by default. If you are sure you can pass the `-f, --force` flag and the data will be overwritten in the destination. However note that this will overwrite all the data existent in the backup, not only the 1 file that is missing e.g. If you are sure you can pass the `-f, --force` flag and the data will be overwritten in the destination. However note that this will overwrite all the data existent in the backup, not only the 1 file that is missing e.g.
## Example ## Example

View File

@ -20,6 +20,6 @@ If you are on macOS you can install through brew: `brew install autorestic`.
### AUR ### AUR
If you are on Arch there is an [AUR Package](https://aur.archlinux.org/packages/autorestic-bin/) by @n194. If you are on Arch there is an [AUR Package](https://aur.archlinux.org/packages/autorestic-bin/) (looking for maintainers).
> :ToCPrevNext > :ToCPrevNext

View File

@ -157,25 +157,32 @@ func (b Backend) ExecDocker(l Location, args []string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
volume := l.getVolumeName() volume := l.From[0]
path, _ := l.getPath()
options := ExecuteOptions{ options := ExecuteOptions{
Command: "docker", Command: "docker",
Envs: env, Envs: env,
} }
dir := "/data"
docker := []string{ docker := []string{
"run", "--rm", "run", "--rm",
"--entrypoint", "ash", "--entrypoint", "ash",
"--workdir", path, "--workdir", dir,
"--volume", volume + ":" + path, "--volume", volume + ":" + dir,
} }
// Use of docker host, not the container host
if hostname, err := os.Hostname(); err == nil { if hostname, err := os.Hostname(); err == nil {
docker = append(docker, "--hostname", hostname) docker = append(docker, "--hostname", hostname)
} }
if b.Type == "local" { switch b.Type {
case "local":
actual := env["RESTIC_REPOSITORY"] actual := env["RESTIC_REPOSITORY"]
docker = append(docker, "--volume", actual+":"+"/repo") docker = append(docker, "--volume", actual+":"+"/repo")
env["RESTIC_REPOSITORY"] = "/repo" env["RESTIC_REPOSITORY"] = "/repo"
case "b2":
case "s3":
// No additional setup needed
default:
return "", fmt.Errorf("Backend type \"%s\" is not supported as volume endpoint", b.Type)
} }
for key, value := range env { for key, value := range env {
docker = append(docker, "--env", key+"="+value) docker = append(docker, "--env", key+"="+value)

View File

@ -16,7 +16,7 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
const VERSION = "1.4.0" const VERSION = "1.4.1"
var CI bool = false var CI bool = false
var VERBOSE bool = false var VERBOSE bool = false
@ -55,6 +55,7 @@ func GetConfig() *Config {
config = &Config{} config = &Config{}
if err := viper.UnmarshalExact(config); err != nil { if err := viper.UnmarshalExact(config); err != nil {
colors.Error.Println(err)
colors.Error.Println("Could not parse config file!") colors.Error.Println("Could not parse config file!")
lock.Unlock() lock.Unlock()
os.Exit(1) os.Exit(1)
@ -81,7 +82,11 @@ func (c *Config) Describe() {
var tmp string var tmp string
colors.PrimaryPrint(`Location: "%s"`, name) colors.PrimaryPrint(`Location: "%s"`, name)
colors.PrintDescription("From", l.From) tmp = ""
for _, path := range l.From {
tmp += fmt.Sprintf("\t%s %s\n", colors.Success.Sprint("←"), path)
}
colors.PrintDescription("From", tmp)
tmp = "" tmp = ""
for _, to := range l.To { for _, to := range l.To {
@ -253,12 +258,7 @@ func appendOptionsToSlice(str *[]string, options OptionMap) {
*str = append(*str, optionToString(key)) *str = append(*str, optionToString(key))
continue continue
} }
// String *str = append(*str, optionToString(key), fmt.Sprint(value))
asString, ok := value.(string)
if ok {
*str = append(*str, optionToString(key), asString)
continue
}
} }
} }
} }
@ -274,3 +274,15 @@ func getOptions(options Options, key string) []string {
} }
return selected return selected
} }
func combineOptions(key string, l Location, b Backend) []string {
// Priority: location > backend > global
var options []string
gFlags := getOptions(GetConfig().Global, key)
bFlags := getOptions(b.Options, key)
lFlags := getOptions(l.Options, key)
options = append(options, gFlags...)
options = append(options, bFlags...)
options = append(options, lFlags...)
return options
}

View File

@ -19,12 +19,12 @@ type LocationType string
const ( const (
TypeLocal LocationType = "local" TypeLocal LocationType = "local"
TypeVolume LocationType = "volume" TypeVolume LocationType = "volume"
VolumePrefix string = "volume:"
) )
type HookArray = []string type HookArray = []string
type Hooks struct { type Hooks struct {
Dir string `yaml:"dir"`
Before HookArray `yaml:"before,omitempty"` Before HookArray `yaml:"before,omitempty"`
After HookArray `yaml:"after,omitempty"` After HookArray `yaml:"after,omitempty"`
Success HookArray `yaml:"success,omitempty"` Success HookArray `yaml:"success,omitempty"`
@ -33,7 +33,8 @@ type Hooks struct {
type Location struct { type Location struct {
name string `yaml:",omitempty"` name string `yaml:",omitempty"`
From string `yaml:"from,omitempty"` From []string `yaml:"from,omitempty"`
Type string `yaml:"type,omitempty"`
To []string `yaml:"to,omitempty"` To []string `yaml:"to,omitempty"`
Hooks Hooks `yaml:"hooks,omitempty"` Hooks Hooks `yaml:"hooks,omitempty"`
Cron string `yaml:"cron,omitempty"` Cron string `yaml:"cron,omitempty"`
@ -47,11 +48,17 @@ func GetLocation(name string) (Location, bool) {
} }
func (l Location) validate() error { func (l Location) validate() error {
if l.From == "" { if len(l.From) == 0 {
return fmt.Errorf(`Location "%s" is missing "from" key`, l.name) return fmt.Errorf(`Location "%s" is missing "from" key`, l.name)
} }
if l.getType() == TypeLocal { t, err := l.getType()
if from, err := GetPathRelativeToConfig(l.From); err != nil { if err != nil {
return err
}
switch t {
case TypeLocal:
for _, path := range l.From {
if from, err := GetPathRelativeToConfig(path); err != nil {
return err return err
} else { } else {
if stat, err := os.Stat(from); err != nil { if stat, err := os.Stat(from); err != nil {
@ -63,6 +70,11 @@ func (l Location) validate() error {
} }
} }
} }
case TypeVolume:
if len(l.From) > 1 {
return fmt.Errorf(`location "%s" has more than one docker volume`, l.name)
}
}
if len(l.To) == 0 { if len(l.To) == 0 {
return fmt.Errorf(`Location "%s" has no "to" targets`, l.name) return fmt.Errorf(`Location "%s" has no "to" targets`, l.name)
@ -77,10 +89,17 @@ func (l Location) validate() error {
return nil return nil
} }
func ExecuteHooks(commands []string, options ExecuteOptions) error { func (l Location) ExecuteHooks(commands []string, options ExecuteOptions) error {
if len(commands) == 0 { if len(commands) == 0 {
return nil return nil
} }
if l.Hooks.Dir != "" {
if dir, err := GetPathRelativeToConfig(l.Hooks.Dir); err != nil {
return err
} else {
options.Dir = dir
}
}
colors.Secondary.Println("\nRunning hooks") colors.Secondary.Println("\nRunning hooks")
for _, command := range commands { for _, command := range commands {
colors.Body.Println("> " + command) colors.Body.Println("> " + command)
@ -97,39 +116,38 @@ func ExecuteHooks(commands []string, options ExecuteOptions) error {
return nil return nil
} }
func (l Location) getType() LocationType { func (l Location) getType() (LocationType, error) {
if strings.HasPrefix(l.From, VolumePrefix) { t := strings.ToLower(l.Type)
return TypeVolume if t == "" || t == "local" {
return TypeLocal, nil
} else if t == "volume" {
return TypeVolume, nil
} }
return TypeLocal return "", fmt.Errorf("invalid location type \"%s\"", l.Type)
} }
func (l Location) getVolumeName() string { func (l Location) getTag(parts ...string) string {
return strings.TrimPrefix(l.From, VolumePrefix) parts = append([]string{"ar"}, parts...)
return strings.Join(parts, ":")
} }
func (l Location) getPath() (string, error) { func (l Location) getLocationTag() string {
t := l.getType() return l.getTag("location", l.name)
switch t {
case TypeLocal:
if path, err := GetPathRelativeToConfig(l.From); err != nil {
return "", err
} else {
return path, nil
}
case TypeVolume:
return "/volume/" + l.name + "/" + l.getVolumeName(), nil
}
return "", fmt.Errorf("could not get path for location \"%s\"", l.name)
} }
func (l Location) Backup(cron bool, specificBackend string) []error { func (l Location) Backup(cron bool, specificBackend string) []error {
var errors []error var errors []error
var backends []string var backends []string
colors.PrimaryPrint(" Backing up location \"%s\" ", l.name) colors.PrimaryPrint(" Backing up location \"%s\" ", l.name)
t := l.getType() t, err := l.getType()
if err != nil {
errors = append(errors, err)
return errors
}
cwd, _ := GetPathRelativeToConfig(".")
options := ExecuteOptions{ options := ExecuteOptions{
Command: "bash", Command: "bash",
Dir: cwd,
Envs: map[string]string{ Envs: map[string]string{
"AUTORESTIC_LOCATION": l.name, "AUTORESTIC_LOCATION": l.name,
}, },
@ -137,18 +155,11 @@ func (l Location) Backup(cron bool, specificBackend string) []error {
if err := l.validate(); err != nil { if err := l.validate(); err != nil {
errors = append(errors, err) errors = append(errors, err)
colors.Error.Print(err)
goto after goto after
} }
if t == TypeLocal {
dir, _ := GetPathRelativeToConfig(l.From)
colors.Faint.Printf("Executing under: \"%s\"\n", dir)
options.Dir = dir
}
// Hooks // Hooks
if err := ExecuteHooks(l.Hooks.Before, options); err != nil { if err := l.ExecuteHooks(l.Hooks.Before, options); err != nil {
errors = append(errors, err) errors = append(errors, err)
goto after goto after
} }
@ -173,30 +184,38 @@ func (l Location) Backup(cron bool, specificBackend string) []error {
continue continue
} }
lFlags := getOptions(l.Options, "backup")
bFlags := getOptions(backend.Options, "backup")
cmd := []string{"backup"} cmd := []string{"backup"}
cmd = append(cmd, lFlags...) cmd = append(cmd, combineOptions("backup", l, backend)...)
cmd = append(cmd, bFlags...)
if cron { if cron {
cmd = append(cmd, "--tag", "cron") cmd = append(cmd, "--tag", l.getTag("cron"))
} }
cmd = append(cmd, ".") cmd = append(cmd, "--tag", l.getLocationTag())
backupOptions := ExecuteOptions{ backupOptions := ExecuteOptions{
Dir: options.Dir,
Envs: env, Envs: env,
} }
var out string var out string
switch t { switch t {
case TypeLocal: case TypeLocal:
for _, from := range l.From {
path, err := GetPathRelativeToConfig(from)
if err != nil {
errors = append(errors, err)
goto after
}
cmd = append(cmd, path)
}
out, err = ExecuteResticCommand(backupOptions, cmd...) out, err = ExecuteResticCommand(backupOptions, cmd...)
case TypeVolume: case TypeVolume:
ok := CheckIfVolumeExists(l.From[0])
if !ok {
errors = append(errors, fmt.Errorf("volume \"%s\" does not exist", l.From[0]))
continue
}
cmd = append(cmd, "/data")
out, err = backend.ExecDocker(l, cmd) out, err = backend.ExecDocker(l, cmd)
} }
if err != nil { if err != nil {
colors.Error.Println(out)
errors = append(errors, err) errors = append(errors, err)
continue continue
} }
@ -213,7 +232,7 @@ func (l Location) Backup(cron bool, specificBackend string) []error {
} }
// After hooks // After hooks
if err := ExecuteHooks(l.Hooks.After, options); err != nil { if err := l.ExecuteHooks(l.Hooks.After, options); err != nil {
errors = append(errors, err) errors = append(errors, err)
} }
@ -224,22 +243,19 @@ after:
} else { } else {
commands = l.Hooks.Success commands = l.Hooks.Success
} }
if err := ExecuteHooks(commands, options); err != nil { if err := l.ExecuteHooks(commands, options); err != nil {
errors = append(errors, err) errors = append(errors, err)
} }
if len(errors) == 0 {
colors.Success.Println("Done") colors.Success.Println("Done")
}
return errors return errors
} }
func (l Location) Forget(prune bool, dry bool) error { func (l Location) Forget(prune bool, dry bool) error {
colors.PrimaryPrint("Forgetting for location \"%s\"", l.name) colors.PrimaryPrint("Forgetting for location \"%s\"", l.name)
path, err := l.getPath()
if err != nil {
return err
}
for _, to := range l.To { for _, to := range l.To {
backend, _ := GetBackend(to) backend, _ := GetBackend(to)
colors.Secondary.Printf("For backend \"%s\"\n", backend.name) colors.Secondary.Printf("For backend \"%s\"\n", backend.name)
@ -250,17 +266,14 @@ func (l Location) Forget(prune bool, dry bool) error {
options := ExecuteOptions{ options := ExecuteOptions{
Envs: env, Envs: env,
} }
lFlags := getOptions(l.Options, "forget") cmd := []string{"forget", "--tag", l.getLocationTag()}
bFlags := getOptions(backend.Options, "forget")
cmd := []string{"forget", "--path", path}
if prune { if prune {
cmd = append(cmd, "--prune") cmd = append(cmd, "--prune")
} }
if dry { if dry {
cmd = append(cmd, "--dry-run") cmd = append(cmd, "--dry-run")
} }
cmd = append(cmd, lFlags...) cmd = append(cmd, combineOptions("forget", l, backend)...)
cmd = append(cmd, bFlags...)
out, err := ExecuteResticCommand(options, cmd...) out, err := ExecuteResticCommand(options, cmd...)
if VERBOSE { if VERBOSE {
colors.Faint.Println(out) colors.Faint.Println(out)
@ -282,7 +295,7 @@ func (l Location) hasBackend(backend string) bool {
return false return false
} }
func (l Location) Restore(to, from string, force bool) error { func (l Location) Restore(to, from string, force bool, snapshot string) error {
if from == "" { if from == "" {
from = l.To[0] from = l.To[0]
} else if !l.hasBackend(from) { } else if !l.hasBackend(from) {
@ -293,16 +306,20 @@ func (l Location) Restore(to, from string, force bool) error {
if err != nil { if err != nil {
return err return err
} }
colors.PrimaryPrint("Restoring location \"%s\"", l.name)
backend, _ := GetBackend(from) if snapshot == "" {
path, err := l.getPath() snapshot = "latest"
if err != nil {
return nil
} }
colors.Secondary.Println("Restoring lastest snapshot")
colors.Body.Printf("%s → %s.\n", from, path) colors.PrimaryPrint("Restoring location \"%s\"", l.name)
switch l.getType() { backend, _ := GetBackend(from)
colors.Secondary.Printf("Restoring %s@%s → %s\n", snapshot, backend.name, to)
t, err := l.getType()
if err != nil {
return err
}
switch t {
case TypeLocal: case TypeLocal:
// Check if target is empty // Check if target is empty
if !force { if !force {
@ -322,9 +339,9 @@ func (l Location) Restore(to, from string, force bool) error {
} }
} }
} }
err = backend.Exec([]string{"restore", "--target", to, "--path", path, "latest"}) err = backend.Exec([]string{"restore", "--target", to, "--tag", l.getLocationTag(), snapshot})
case TypeVolume: case TypeVolume:
_, err = backend.ExecDocker(l, []string{"restore", "--target", ".", "--path", path, "latest"}) _, err = backend.ExecDocker(l, []string{"restore", "--target", "/", "--tag", l.getLocationTag(), snapshot})
} }
if err != nil { if err != nil {
return err return err

View File

@ -77,3 +77,9 @@ func CopyFile(from, to string) error {
} }
return nil return nil
} }
func CheckIfVolumeExists(volume string) bool {
out, err := ExecuteCommand(ExecuteOptions{Command: "docker"}, "volume", "inspect", volume)
fmt.Println(out)
return err == nil
}