mirror of
https://github.com/cupcakearmy/autorestic.git
synced 2024-11-05 13:14:48 +01:00
13aa560fda
Fix #332. This adds a new "PreValidate" hook that is executed before checking the backup location. This allows, for example, mounting a remote source to make the directories of the location available. "PreValidate" is added as a new hook to avoid any breakage that might have been caused by changing the behaviour of the "before" hook. Documentataion updates included.
457 lines
11 KiB
Go
457 lines
11 KiB
Go
package internal
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cupcakearmy/autorestic/internal/colors"
|
|
"github.com/cupcakearmy/autorestic/internal/flags"
|
|
"github.com/cupcakearmy/autorestic/internal/lock"
|
|
"github.com/cupcakearmy/autorestic/internal/metadata"
|
|
"github.com/robfig/cron"
|
|
)
|
|
|
|
type LocationType string
|
|
|
|
const (
|
|
TypeLocal LocationType = "local"
|
|
TypeVolume LocationType = "volume"
|
|
)
|
|
|
|
type HookArray = []string
|
|
|
|
type LocationForgetOption string
|
|
|
|
const (
|
|
LocationForgetYes LocationForgetOption = "yes"
|
|
LocationForgetNo LocationForgetOption = "no"
|
|
LocationForgetPrune LocationForgetOption = "prune"
|
|
)
|
|
|
|
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"`
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
func GetLocation(name string) (Location, bool) {
|
|
l, ok := GetConfig().Locations[name]
|
|
l.name = name
|
|
return l, ok
|
|
}
|
|
|
|
func (l Location) validate() error {
|
|
if len(l.From) == 0 {
|
|
return fmt.Errorf(`Location "%s" is missing "from" key`, l.name)
|
|
}
|
|
t, err := l.getType()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch t {
|
|
case TypeLocal:
|
|
for _, path := range l.From {
|
|
if from, err := GetPathRelativeToConfig(path); err != nil {
|
|
return err
|
|
} else {
|
|
if _, err := os.Stat(from); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
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 {
|
|
return fmt.Errorf(`location "%s" has no "to" targets`, l.name)
|
|
}
|
|
// Check if backends are all valid
|
|
for _, to := range l.To {
|
|
_, ok := GetBackend(to)
|
|
if !ok {
|
|
return fmt.Errorf(`location "%s" has an invalid backend "%s"`, l.name, to)
|
|
}
|
|
}
|
|
|
|
// Check copy option
|
|
for copyFrom, copyTo := range l.CopyOption {
|
|
if _, ok := GetBackend(copyFrom); !ok {
|
|
return fmt.Errorf(`location "%s" has an invalid backend "%s" in copy option`, l.name, copyFrom)
|
|
}
|
|
if !ArrayContains(l.To, copyFrom) {
|
|
return fmt.Errorf(`location "%s" has an invalid copy from "%s"`, l.name, copyFrom)
|
|
}
|
|
for _, copyToTarget := range copyTo {
|
|
if _, ok := GetBackend(copyToTarget); !ok {
|
|
return fmt.Errorf(`location "%s" has an invalid backend "%s" in copy option`, l.name, copyToTarget)
|
|
}
|
|
if ArrayContains(l.To, copyToTarget) {
|
|
return fmt.Errorf(`location "%s" cannot copy to "%s" as it's already a target`, l.name, copyToTarget)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if forget type is correct
|
|
if l.ForgetOption != "" {
|
|
if l.ForgetOption != LocationForgetYes && l.ForgetOption != LocationForgetNo && l.ForgetOption != LocationForgetPrune {
|
|
return fmt.Errorf("invalid value for forget option: %s", l.ForgetOption)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (l Location) ExecuteHooks(commands []string, options ExecuteOptions) error {
|
|
if len(commands) == 0 {
|
|
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")
|
|
for _, command := range commands {
|
|
colors.Body.Println("> " + command)
|
|
_, out, err := ExecuteCommand(options, "-c", command)
|
|
if err != nil {
|
|
colors.Error.Println(out)
|
|
return err
|
|
}
|
|
}
|
|
colors.Body.Println("")
|
|
return nil
|
|
}
|
|
|
|
func (l Location) getType() (LocationType, error) {
|
|
t := strings.ToLower(l.Type)
|
|
if t == "" || t == "local" {
|
|
return TypeLocal, nil
|
|
} else if t == "volume" {
|
|
return TypeVolume, nil
|
|
}
|
|
return "", fmt.Errorf("invalid location type \"%s\"", l.Type)
|
|
}
|
|
|
|
func buildTag(parts ...string) string {
|
|
parts = append([]string{"ar"}, parts...)
|
|
return strings.Join(parts, ":")
|
|
}
|
|
|
|
func (l Location) getLocationTags() string {
|
|
return buildTag("location", l.name)
|
|
}
|
|
|
|
func (l Location) Backup(cron bool, specificBackend string) []error {
|
|
var errors []error
|
|
var backends []string
|
|
colors.PrimaryPrint(" Backing up location \"%s\" ", l.name)
|
|
t, err := l.getType()
|
|
if err != nil {
|
|
errors = append(errors, err)
|
|
return errors
|
|
}
|
|
cwd, _ := GetPathRelativeToConfig(".")
|
|
options := ExecuteOptions{
|
|
Command: "bash",
|
|
Dir: cwd,
|
|
Envs: map[string]string{
|
|
"AUTORESTIC_LOCATION": l.name,
|
|
},
|
|
}
|
|
|
|
// Hooks before location validation
|
|
if err := l.ExecuteHooks(l.Hooks.PreValidate, options); err != nil {
|
|
errors = append(errors, err)
|
|
goto after
|
|
}
|
|
|
|
if err := l.validate(); err != nil {
|
|
errors = append(errors, err)
|
|
goto after
|
|
}
|
|
|
|
// Hooks after location validation
|
|
if err := l.ExecuteHooks(l.Hooks.Before, options); err != nil {
|
|
errors = append(errors, err)
|
|
goto after
|
|
}
|
|
|
|
// Backup
|
|
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()
|
|
if err != nil {
|
|
errors = append(errors, err)
|
|
continue
|
|
}
|
|
|
|
cmd := []string{"backup"}
|
|
cmd = append(cmd, combineAllOptions("backup", l, backend)...)
|
|
if cron {
|
|
cmd = append(cmd, "--tag", buildTag("cron"))
|
|
}
|
|
cmd = append(cmd, "--tag", l.getLocationTags())
|
|
backupOptions := ExecuteOptions{
|
|
Envs: env,
|
|
}
|
|
|
|
var code int = 0
|
|
var out string
|
|
switch t {
|
|
case TypeLocal:
|
|
for _, from := range l.From {
|
|
path, err := GetPathRelativeToConfig(from)
|
|
if err != nil {
|
|
errors = append(errors, err)
|
|
goto after
|
|
}
|
|
cmd = append(cmd, path)
|
|
}
|
|
code, out, err = ExecuteResticCommand(backupOptions, cmd...)
|
|
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")
|
|
code, out, err = backend.ExecDocker(l, cmd)
|
|
}
|
|
|
|
// Extract metadata
|
|
md := metadata.ExtractMetadataFromBackupLog(out)
|
|
md.ExitCode = fmt.Sprint(code)
|
|
mdEnv := metadata.MakeEnvFromMetadata(&md)
|
|
for k, v := range mdEnv {
|
|
options.Envs[k+"_"+fmt.Sprint(i)] = v
|
|
options.Envs[k+"_"+strings.ToUpper(backend.name)] = v
|
|
}
|
|
|
|
// If error save it and continue
|
|
if err != nil {
|
|
colors.Error.Println(out)
|
|
errors = append(errors, fmt.Errorf("%s@%s:\n%s%s", l.name, backend.name, out, err))
|
|
continue
|
|
}
|
|
|
|
// Copy
|
|
if md.SnapshotID != "" {
|
|
for copyFrom, copyTo := range l.CopyOption {
|
|
b1, _ := GetBackend(copyFrom)
|
|
for _, copyToTarget := range copyTo {
|
|
b2, _ := GetBackend(copyToTarget)
|
|
colors.Secondary.Println("Copying " + copyFrom + " → " + copyToTarget)
|
|
env, _ := b1.getEnv()
|
|
env2, _ := b2.getEnv()
|
|
// Add the second repo to the env with a "2" suffix
|
|
for k, v := range env2 {
|
|
env[k+"2"] = v
|
|
}
|
|
_, _, err := ExecuteResticCommand(ExecuteOptions{
|
|
Envs: env,
|
|
}, "copy", md.SnapshotID)
|
|
|
|
if err != nil {
|
|
errors = append(errors, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// After backup hooks
|
|
if err := l.ExecuteHooks(l.Hooks.After, options); err != nil {
|
|
errors = append(errors, err)
|
|
}
|
|
|
|
after:
|
|
// Success/failure hooks
|
|
var commands []string
|
|
var isSuccess = len(errors) == 0
|
|
if isSuccess {
|
|
commands = l.Hooks.Success
|
|
} else {
|
|
commands = l.Hooks.Failure
|
|
}
|
|
if err := l.ExecuteHooks(commands, options); err != nil {
|
|
errors = append(errors, err)
|
|
}
|
|
|
|
// Forget and optionally prune
|
|
if isSuccess && l.ForgetOption != "" && l.ForgetOption != LocationForgetNo {
|
|
err := l.Forget(l.ForgetOption == LocationForgetPrune, false)
|
|
if err != nil {
|
|
errors = append(errors, err)
|
|
}
|
|
}
|
|
|
|
if len(errors) == 0 {
|
|
colors.Success.Println("Done")
|
|
}
|
|
return errors
|
|
}
|
|
|
|
func (l Location) Forget(prune bool, dry bool) error {
|
|
colors.PrimaryPrint("Forgetting for location \"%s\"", l.name)
|
|
|
|
backendsToForget := l.To
|
|
for _, copyBackends := range l.CopyOption {
|
|
backendsToForget = append(backendsToForget, copyBackends...)
|
|
}
|
|
|
|
for _, to := range backendsToForget {
|
|
backend, _ := GetBackend(to)
|
|
colors.Secondary.Printf("For backend \"%s\"\n", backend.name)
|
|
env, err := backend.getEnv()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
options := ExecuteOptions{
|
|
Envs: env,
|
|
}
|
|
cmd := []string{"forget", "--tag", l.getLocationTags()}
|
|
if prune {
|
|
cmd = append(cmd, "--prune")
|
|
}
|
|
if dry {
|
|
cmd = append(cmd, "--dry-run")
|
|
}
|
|
cmd = append(cmd, combineAllOptions("forget", l, backend)...)
|
|
_, _, err = ExecuteResticCommand(options, cmd...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
colors.Success.Println("Done")
|
|
return nil
|
|
}
|
|
|
|
func (l Location) hasBackend(backend string) bool {
|
|
for _, b := range l.To {
|
|
if b == backend {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func buildRestoreCommand(l Location, to string, snapshot string, options []string) []string {
|
|
base := []string{"restore", "--target", to, "--tag", l.getLocationTags(), snapshot}
|
|
base = append(base, options...)
|
|
return base
|
|
}
|
|
|
|
func (l Location) Restore(to, from string, force bool, snapshot string, options []string) error {
|
|
if from == "" {
|
|
from = l.To[0]
|
|
} else if !l.hasBackend(from) {
|
|
return fmt.Errorf("invalid backend: \"%s\"", from)
|
|
}
|
|
|
|
if snapshot == "" {
|
|
snapshot = "latest"
|
|
}
|
|
|
|
colors.PrimaryPrint("Restoring location \"%s\"", l.name)
|
|
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:
|
|
to, err = filepath.Abs(to)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Check if target is empty
|
|
if !force {
|
|
notEmptyError := fmt.Errorf("target %s is not empty", to)
|
|
_, err = os.Stat(to)
|
|
if err == nil {
|
|
files, err := ioutil.ReadDir(to)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(files) > 0 {
|
|
return notEmptyError
|
|
}
|
|
} else {
|
|
if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
err = backend.Exec(buildRestoreCommand(l, to, snapshot, options))
|
|
case TypeVolume:
|
|
_, _, err = backend.ExecDocker(l, buildRestoreCommand(l, "/", snapshot, options))
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
colors.Success.Println("Done")
|
|
return nil
|
|
}
|
|
|
|
func (l Location) RunCron() error {
|
|
if l.Cron == "" {
|
|
return nil
|
|
}
|
|
|
|
schedule, err := cron.ParseStandard(l.Cron)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
last := time.Unix(lock.GetCron(l.name), 0)
|
|
next := schedule.Next(last)
|
|
now := time.Now()
|
|
if now.After(next) {
|
|
lock.SetCron(l.name, now.Unix())
|
|
l.Backup(true, "")
|
|
} else {
|
|
if !flags.CRON_LEAN {
|
|
colors.Body.Printf("Skipping \"%s\", not due yet.\n", l.name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|