mirror of
https://github.com/cupcakearmy/autorestic.git
synced 2025-12-08 05:14:59 +00:00
1.6.0 (#180)
* fix for #178 * restore options * error codes * update docs * forget docs * add option to auto forget * add copy option * update go version to enable generics * copy docs * changelog & version bump
This commit is contained in:
@@ -13,18 +13,18 @@ import (
|
||||
)
|
||||
|
||||
type BackendRest struct {
|
||||
User string `yaml:"user,omitempty"`
|
||||
Password string `yaml:"password,omitempty"`
|
||||
User string `mapstructure:"user,omitempty"`
|
||||
Password string `mapstructure:"password,omitempty"`
|
||||
}
|
||||
|
||||
type Backend struct {
|
||||
name string
|
||||
Type string `yaml:"type,omitempty"`
|
||||
Path string `yaml:"path,omitempty"`
|
||||
Key string `yaml:"key,omitempty"`
|
||||
Env map[string]string `yaml:"env,omitempty"`
|
||||
Rest BackendRest `yaml:"rest,omitempty"`
|
||||
Options Options `yaml:"options,omitempty"`
|
||||
Type string `mapstructure:"type,omitempty"`
|
||||
Path string `mapstructure:"path,omitempty"`
|
||||
Key string `mapstructure:"key,omitempty"`
|
||||
Env map[string]string `mapstructure:"env,omitempty"`
|
||||
Rest BackendRest `mapstructure:"rest,omitempty"`
|
||||
Options Options `mapstructure:"options,omitempty"`
|
||||
}
|
||||
|
||||
func GetBackend(name string) (Backend, bool) {
|
||||
@@ -122,13 +122,13 @@ func (b Backend) validate() error {
|
||||
}
|
||||
options := ExecuteOptions{Envs: env}
|
||||
// Check if already initialized
|
||||
_, err = ExecuteResticCommand(options, "snapshots")
|
||||
_, _, err = ExecuteResticCommand(options, "snapshots")
|
||||
if err == nil {
|
||||
return nil
|
||||
} else {
|
||||
// If not initialize
|
||||
colors.Body.Printf("Initializing backend \"%s\"...\n", b.name)
|
||||
out, err := ExecuteResticCommand(options, "init")
|
||||
_, out, err := ExecuteResticCommand(options, "init")
|
||||
if flags.VERBOSE {
|
||||
colors.Faint.Println(out)
|
||||
}
|
||||
@@ -142,7 +142,7 @@ func (b Backend) Exec(args []string) error {
|
||||
return err
|
||||
}
|
||||
options := ExecuteOptions{Envs: env}
|
||||
out, err := ExecuteResticCommand(options, args...)
|
||||
_, out, err := ExecuteResticCommand(options, args...)
|
||||
if err != nil {
|
||||
colors.Error.Println(out)
|
||||
return err
|
||||
@@ -153,10 +153,10 @@ func (b Backend) Exec(args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b Backend) ExecDocker(l Location, args []string) (string, error) {
|
||||
func (b Backend) ExecDocker(l Location, args []string) (int, string, error) {
|
||||
env, err := b.getEnv()
|
||||
if err != nil {
|
||||
return "", err
|
||||
return -1, "", err
|
||||
}
|
||||
volume := l.From[0]
|
||||
options := ExecuteOptions{
|
||||
@@ -188,20 +188,19 @@ func (b Backend) ExecDocker(l Location, args []string) (string, error) {
|
||||
// No additional setup needed
|
||||
case "rclone":
|
||||
// Read host rclone config and mount it into the container
|
||||
configFile, err := ExecuteCommand(ExecuteOptions{Command: "rclone"}, "config", "file")
|
||||
code, configFile, err := ExecuteCommand(ExecuteOptions{Command: "rclone"}, "config", "file")
|
||||
if err != nil {
|
||||
return "", err
|
||||
return code, "", err
|
||||
}
|
||||
splitted := strings.Split(strings.TrimSpace(configFile), "\n")
|
||||
configFilePath := splitted[len(splitted)-1]
|
||||
docker = append(docker, "--volume", configFilePath+":"+"/root/.config/rclone/rclone.conf:ro")
|
||||
default:
|
||||
return "", fmt.Errorf("Backend type \"%s\" is not supported as volume endpoint", b.Type)
|
||||
return -1, "", fmt.Errorf("Backend type \"%s\" is not supported as volume endpoint", b.Type)
|
||||
}
|
||||
for key, value := range env {
|
||||
docker = append(docker, "--env", key+"="+value)
|
||||
}
|
||||
docker = append(docker, "cupcakearmy/autorestic:"+VERSION, "-c", strings.Join(args, " "))
|
||||
out, err := ExecuteCommand(options, docker...)
|
||||
return out, err
|
||||
return ExecuteCommand(options, docker...)
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ func InstallRestic() error {
|
||||
}
|
||||
|
||||
func upgradeRestic() error {
|
||||
out, err := internal.ExecuteCommand(internal.ExecuteOptions{
|
||||
_, out, err := internal.ExecuteCommand(internal.ExecuteOptions{
|
||||
Command: "restic",
|
||||
}, "self-update")
|
||||
colors.Faint.Println(out)
|
||||
|
||||
@@ -17,17 +17,17 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const VERSION = "1.5.8"
|
||||
const VERSION = "1.6.0"
|
||||
|
||||
type OptionMap map[string][]interface{}
|
||||
type Options map[string]OptionMap
|
||||
|
||||
type Config struct {
|
||||
Version string `yaml:"version"`
|
||||
Extras interface{} `yaml:"extras"`
|
||||
Locations map[string]Location `yaml:"locations"`
|
||||
Backends map[string]Backend `yaml:"backends"`
|
||||
Global Options `yaml:"global"`
|
||||
Version string `mapstructure:"version"`
|
||||
Extras interface{} `mapstructure:"extras"`
|
||||
Locations map[string]Location `mapstructure:"locations"`
|
||||
Backends map[string]Backend `mapstructure:"backends"`
|
||||
Global Options `mapstructure:"global"`
|
||||
}
|
||||
|
||||
var once sync.Once
|
||||
@@ -56,7 +56,7 @@ func GetConfig() *Config {
|
||||
// Load env file
|
||||
envFile := filepath.Join(filepath.Dir(absConfig), ".autorestic.env")
|
||||
err = godotenv.Load(envFile)
|
||||
if err == nil {
|
||||
if err == nil && !flags.CRON_LEAN {
|
||||
colors.Faint.Println("Using env:\t", envFile)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -24,22 +24,34 @@ const (
|
||||
|
||||
type HookArray = []string
|
||||
|
||||
type LocationForgetOption string
|
||||
|
||||
const (
|
||||
LocationForgetYes LocationForgetOption = "yes"
|
||||
LocationForgetNo LocationForgetOption = "no"
|
||||
LocationForgetPrune LocationForgetOption = "prune"
|
||||
)
|
||||
|
||||
type Hooks struct {
|
||||
Dir string `yaml:"dir"`
|
||||
Before HookArray `yaml:"before,omitempty"`
|
||||
After HookArray `yaml:"after,omitempty"`
|
||||
Success HookArray `yaml:"success,omitempty"`
|
||||
Failure HookArray `yaml:"failure,omitempty"`
|
||||
Dir string `mapstructure:"dir"`
|
||||
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 `yaml:",omitempty"`
|
||||
From []string `yaml:"from,omitempty"`
|
||||
Type string `yaml:"type,omitempty"`
|
||||
To []string `yaml:"to,omitempty"`
|
||||
Hooks Hooks `yaml:"hooks,omitempty"`
|
||||
Cron string `yaml:"cron,omitempty"`
|
||||
Options Options `yaml:"options,omitempty"`
|
||||
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) {
|
||||
@@ -78,13 +90,38 @@ func (l Location) validate() error {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
// Check if backends are all valid
|
||||
for _, to := range l.To {
|
||||
_, ok := GetBackend(to)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid backend `%s`", to)
|
||||
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
|
||||
@@ -104,7 +141,7 @@ func (l Location) ExecuteHooks(commands []string, options ExecuteOptions) error
|
||||
colors.Secondary.Println("\nRunning hooks")
|
||||
for _, command := range commands {
|
||||
colors.Body.Println("> " + command)
|
||||
out, err := ExecuteCommand(options, "-c", command)
|
||||
_, out, err := ExecuteCommand(options, "-c", command)
|
||||
if err != nil {
|
||||
colors.Error.Println(out)
|
||||
return err
|
||||
@@ -195,6 +232,7 @@ func (l Location) Backup(cron bool, specificBackend string) []error {
|
||||
Envs: env,
|
||||
}
|
||||
|
||||
var code int = 0
|
||||
var out string
|
||||
switch t {
|
||||
case TypeLocal:
|
||||
@@ -206,7 +244,7 @@ func (l Location) Backup(cron bool, specificBackend string) []error {
|
||||
}
|
||||
cmd = append(cmd, path)
|
||||
}
|
||||
out, err = ExecuteResticCommand(backupOptions, cmd...)
|
||||
code, out, err = ExecuteResticCommand(backupOptions, cmd...)
|
||||
case TypeVolume:
|
||||
ok := CheckIfVolumeExists(l.From[0])
|
||||
if !ok {
|
||||
@@ -214,20 +252,53 @@ func (l Location) Backup(cron bool, specificBackend string) []error {
|
||||
continue
|
||||
}
|
||||
cmd = append(cmd, "/data")
|
||||
out, err = backend.ExecDocker(l, cmd)
|
||||
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
|
||||
}
|
||||
|
||||
md := metadata.ExtractMetadataFromBackupLog(out)
|
||||
mdEnv := metadata.MakeEnvFromMetadata(&md)
|
||||
for k, v := range mdEnv {
|
||||
options.Envs[k+"_"+fmt.Sprint(i)] = v
|
||||
options.Envs[k+"_"+strings.ToUpper(backend.name)] = v
|
||||
// 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
|
||||
}
|
||||
_, out, err := ExecuteResticCommand(ExecuteOptions{
|
||||
Envs: env,
|
||||
}, "copy", md.SnapshotID)
|
||||
|
||||
if flags.VERBOSE {
|
||||
colors.Faint.Println(out)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if flags.VERBOSE {
|
||||
colors.Faint.Println(out)
|
||||
}
|
||||
@@ -240,15 +311,21 @@ func (l Location) Backup(cron bool, specificBackend string) []error {
|
||||
|
||||
after:
|
||||
var commands []string
|
||||
if len(errors) > 0 {
|
||||
commands = l.Hooks.Failure
|
||||
} else {
|
||||
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 {
|
||||
l.Forget(l.ForgetOption == LocationForgetPrune, false)
|
||||
}
|
||||
|
||||
if len(errors) == 0 {
|
||||
colors.Success.Println("Done")
|
||||
}
|
||||
@@ -276,7 +353,7 @@ func (l Location) Forget(prune bool, dry bool) error {
|
||||
cmd = append(cmd, "--dry-run")
|
||||
}
|
||||
cmd = append(cmd, combineOptions("forget", l, backend)...)
|
||||
out, err := ExecuteResticCommand(options, cmd...)
|
||||
_, out, err := ExecuteResticCommand(options, cmd...)
|
||||
if flags.VERBOSE {
|
||||
colors.Faint.Println(out)
|
||||
}
|
||||
@@ -297,18 +374,19 @@ func (l Location) hasBackend(backend string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (l Location) Restore(to, from string, force bool, snapshot string) error {
|
||||
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)
|
||||
}
|
||||
|
||||
to, err := filepath.Abs(to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if snapshot == "" {
|
||||
snapshot = "latest"
|
||||
}
|
||||
@@ -323,6 +401,10 @@ func (l Location) Restore(to, from string, force bool, snapshot string) error {
|
||||
}
|
||||
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)
|
||||
@@ -341,9 +423,9 @@ func (l Location) Restore(to, from string, force bool, snapshot string) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
err = backend.Exec([]string{"restore", "--target", to, "--tag", l.getLocationTags(), snapshot})
|
||||
err = backend.Exec(buildRestoreCommand(l, to, snapshot, options))
|
||||
case TypeVolume:
|
||||
_, err = backend.ExecDocker(l, []string{"restore", "--target", "/", "--tag", l.getLocationTags(), snapshot})
|
||||
_, _, err = backend.ExecDocker(l, buildRestoreCommand(l, "/", snapshot, options))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -21,6 +21,7 @@ type BackupLogMetadata struct {
|
||||
AddedSize string
|
||||
Processed BackupLogMetadataProcessed
|
||||
SnapshotID string
|
||||
ExitCode string
|
||||
}
|
||||
|
||||
type MetadatExtractor interface {
|
||||
@@ -67,6 +68,7 @@ func MakeEnvFromMetadata(metadata *BackupLogMetadata) map[string]string {
|
||||
env[prefix+"PROCESSED_FILES"] = metadata.Processed.Files
|
||||
env[prefix+"PROCESSED_SIZE"] = metadata.Processed.Size
|
||||
env[prefix+"PROCESSED_DURATION"] = metadata.Processed.Duration
|
||||
env[prefix+"EXIT_CODE"] = metadata.ExitCode
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ type ExecuteOptions struct {
|
||||
Dir string
|
||||
}
|
||||
|
||||
func ExecuteCommand(options ExecuteOptions, args ...string) (string, error) {
|
||||
func ExecuteCommand(options ExecuteOptions, args ...string) (int, string, error) {
|
||||
cmd := exec.Command(options.Command, args...)
|
||||
env := os.Environ()
|
||||
for k, v := range options.Envs {
|
||||
@@ -47,12 +47,16 @@ func ExecuteCommand(options ExecuteOptions, args ...string) (string, error) {
|
||||
cmd.Stderr = &error
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return error.String(), err
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
return exitError.ExitCode(), error.String(), err
|
||||
} else {
|
||||
return -1, error.String(), err
|
||||
}
|
||||
}
|
||||
return out.String(), nil
|
||||
return 0, out.String(), nil
|
||||
}
|
||||
|
||||
func ExecuteResticCommand(options ExecuteOptions, args ...string) (string, error) {
|
||||
func ExecuteResticCommand(options ExecuteOptions, args ...string) (int, string, error) {
|
||||
options.Command = RESTIC_BIN
|
||||
var c = GetConfig()
|
||||
var optionsAsString = getOptions(c.Global, "")
|
||||
@@ -80,6 +84,15 @@ func CopyFile(from, to string) error {
|
||||
}
|
||||
|
||||
func CheckIfVolumeExists(volume string) bool {
|
||||
_, err := ExecuteCommand(ExecuteOptions{Command: "docker"}, "volume", "inspect", volume)
|
||||
_, _, err := ExecuteCommand(ExecuteOptions{Command: "docker"}, "volume", "inspect", volume)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func ArrayContains[T comparable](arr []T, needle T) bool {
|
||||
for _, item := range arr {
|
||||
if item == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user