Compare commits

...

29 Commits

Author SHA1 Message Date
c82db56069 Update config.go 2023-01-19 09:44:05 +01:00
27821dc3ef changelog 2023-01-18 22:45:46 +01:00
866975d32d update deps 2023-01-18 22:43:56 +01:00
dependabot[bot]
955ac0e323 Bump restic/restic from 0.14.0 to 0.15.0 (#283)
Bumps restic/restic from 0.14.0 to 0.15.0.

---
updated-dependencies:
- dependency-name: restic/restic
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-18 22:39:38 +01:00
Mariusz Kozakowski
1512db5b55 Fix regexp for AddedExtractor (#284)
Co-authored-by: mariom <11mariom@gmail.com>
2023-01-18 22:39:12 +01:00
dependabot[bot]
046331748c Bump golang from 1.18-alpine to 1.19-alpine (#268)
Bumps golang from 1.18-alpine to 1.19-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-09 17:31:54 +01:00
72a40eaaa2 update deps 2022-12-09 17:29:07 +01:00
ec61effe22 Merge branch 'master' of https://github.com/cupcakearmy/autorestic 2022-12-09 17:24:07 +01:00
d6acab94a5 use restic as base image 2022-12-09 17:24:03 +01:00
Daniel Brennand
d0d2fcf0f2 docs: add dbrennand.autorestic role (#250) 2022-10-29 21:32:48 +02:00
58fd41fafb consistent casing (#247) 2022-10-18 16:23:27 +02:00
Šimon Woidig
d0c4a32879 Update install.sh (#246)
Add check for bzip2 command existance
2022-10-18 16:17:35 +02:00
74979e9a2a Update config.go 2022-10-17 15:03:17 +02:00
Romain de Laage
3732dcf6ff Check for errors on forget after having backuped (#241) 2022-10-17 15:02:47 +02:00
Andreas Wagner
874ed52e3b add more architectures for linux build process (#243)
* add more architectures for linux build process

equivalent to 7d665fa1f4/helpers/build-release-binaries/main.go

* add solaris and s390x

Co-authored-by: Nicco <hi@nicco.io>
2022-10-06 15:41:01 +01:00
4d9a2b828e Update config.go 2022-09-13 15:16:21 +02:00
Chosto
b830667264 Use options when calling check or init (#199) 2022-09-13 15:15:02 +02:00
John Burkhardt
83eeb847ac Add command line flag to override docker image. (#233) 2022-09-13 15:04:12 +02:00
shahvirb
a89ba5a40a Update forget.md keep-within argument to be '14d' (#236)
keep-within duration can only have units 'y', 'm', 'd', and 'h'. The current documentation of '2w' yields:
```invalid argument "2w" for "--keep-within" flag: invalid unit 'w' found after number 2```

https://restic.readthedocs.io/en/latest/060_forget.html#removing-snapshots-according-to-a-policy
2022-09-13 14:59:46 +02:00
Romain de Laage
6990bf6adc Check for errors and forward on exec command (#227)
fix #226
2022-08-26 17:09:26 +02:00
Romain de Laage
2f407cf211 Add forget for copy backend (#223)
fix #221
2022-08-24 12:09:32 +02:00
489f3078fe bump version 2022-08-24 12:03:11 +02:00
Romain de Laage
e07dd0d991 Don't check if path is a directory (#220)
We only run a stat on the path to check there is no error
2022-08-22 17:28:38 +02:00
kenc
2b9dc9f17c Add test for locking behaviour (#211) 2022-06-27 09:03:34 +02:00
kenc
465bc037c2 Add lock tests (#209)
* Add lock tests

* Refactor setLock to accept key value pairs

This allows SetCron and Lock to use the same function setLockValue. It
also removes the need to call getLock explicitly in tests by returning
the lock object.
2022-06-06 12:59:47 +02:00
kenc
37a043afff Add location unit tests (#208) 2022-06-02 17:05:44 +02:00
kenc
e91b632181 Add backend unit tests (#207) 2022-06-01 14:58:38 +02:00
kenc
2b30998b9a Add options parsing unit tests (#205)
* Add options parsing unit tests

* Refactor into subtests
2022-05-30 12:56:25 +02:00
Varac
49b37a0a9a Add varacs ansible role (#201)
Co-authored-by: Nicco <hi@nicco.io>
2022-05-24 13:34:10 +02:00
25 changed files with 4375 additions and 8010 deletions

8
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -5,6 +5,17 @@ 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.7.4] - 2023-01-18
### Fixed
- Transformer for extracting information. @11mariom
### Changed
- Bump docker restic version.
- Docs dependencies updated.
## [1.7.1] - 2022-04-27 ## [1.7.1] - 2022-04-27
### Fixed ### Fixed

View File

@@ -1,4 +1,4 @@
FROM golang:1.18-alpine as builder FROM golang:1.19-alpine as builder
WORKDIR /app WORKDIR /app
COPY go.* . COPY go.* .
@@ -6,7 +6,8 @@ RUN go mod download
COPY . . COPY . .
RUN go build RUN go build
FROM alpine FROM restic/restic:0.15.0
RUN apk add --no-cache restic rclone bash openssh RUN apk add --no-cache rclone bash
COPY --from=builder /app/autorestic /usr/bin/autorestic COPY --from=builder /app/autorestic /usr/bin/autorestic
ENTRYPOINT []
CMD [ "autorestic" ] CMD [ "autorestic" ]

View File

@@ -17,11 +17,14 @@ import (
var DIR, _ = filepath.Abs("./dist") var DIR, _ = filepath.Abs("./dist")
var targets = map[string][]string{ var targets = map[string][]string{
// "aix": {"ppc64"}, // Not supported by fsnotify
"darwin": {"amd64", "arm64"}, "darwin": {"amd64", "arm64"},
"freebsd": {"386", "amd64", "arm"}, "freebsd": {"386", "amd64", "arm"},
"linux": {"386", "amd64", "arm", "arm64"}, "linux": {"386", "amd64", "arm", "arm64", "ppc64le", "mips", "mipsle", "mips64", "mips64le", "s390x"},
"netbsd": {"386", "amd64"}, "netbsd": {"386", "amd64"},
"openbsd": {"386", "amd64"}, "openbsd": {"386", "amd64"},
// "windows": {"386", "amd64"}, // Not supported by autorestic
"solaris": {"amd64"},
} }
type buildOptions struct { type buildOptions struct {

View File

@@ -1,6 +1,8 @@
package cmd package cmd
import ( import (
"fmt"
"github.com/cupcakearmy/autorestic/internal" "github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors" "github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/lock" "github.com/cupcakearmy/autorestic/internal/lock"
@@ -18,10 +20,23 @@ var execCmd = &cobra.Command{
selected, err := internal.GetAllOrSelected(cmd, true) selected, err := internal.GetAllOrSelected(cmd, true)
CheckErr(err) CheckErr(err)
var errors []error
for _, name := range selected { for _, name := range selected {
colors.PrimaryPrint(" Executing on \"%s\" ", name) colors.PrimaryPrint(" Executing on \"%s\" ", name)
backend, _ := internal.GetBackend(name) backend, _ := internal.GetBackend(name)
backend.Exec(args) err := backend.Exec(args)
if err != nil {
errors = append(errors, err)
}
}
if len(errors) > 0 {
for _, err := range errors {
colors.Error.Printf("%s\n\n", err)
}
CheckErr(fmt.Errorf("%d errors were found", len(errors)))
} }
}, },
} }

View File

@@ -41,6 +41,7 @@ func init() {
rootCmd.PersistentFlags().BoolVar(&flags.CI, "ci", false, "CI mode disabled interactive mode and colors and enables verbosity") rootCmd.PersistentFlags().BoolVar(&flags.CI, "ci", false, "CI mode disabled interactive mode and colors and enables verbosity")
rootCmd.PersistentFlags().BoolVarP(&flags.VERBOSE, "verbose", "v", false, "verbose mode") rootCmd.PersistentFlags().BoolVarP(&flags.VERBOSE, "verbose", "v", false, "verbose mode")
rootCmd.PersistentFlags().StringVar(&flags.RESTIC_BIN, "restic-bin", "restic", "specify custom restic binary") rootCmd.PersistentFlags().StringVar(&flags.RESTIC_BIN, "restic-bin", "restic", "specify custom restic binary")
rootCmd.PersistentFlags().StringVar(&flags.DOCKER_IMAGE, "docker-image", "cupcakearmy/autorestic:"+internal.VERSION, "specify a custom docker image")
cobra.OnInitialize(initConfig) cobra.OnInitialize(initConfig)
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{ {
"dependencies": { "dependencies": {
"@codedoc/core": "^0.2.24" "@codedoc/core": "^0.3.2"
} }
} }

View File

@@ -2,10 +2,12 @@
A list of community driven projects. (No official affiliation) A list of community driven projects. (No official affiliation)
- SystemD Units: https://gitlab.com/py_crash/autorestic-systemd-units - SystemD Units: <https://gitlab.com/py_crash/autorestic-systemd-units>
- Docker image: https://github.com/pascaliske/docker-autorestic - Docker image: <https://github.com/pascaliske/docker-autorestic>
- Ansible Role: https://github.com/adsanz/ansible-restic-role - Ansible Role: <https://github.com/adsanz/ansible-restic-role>
- Ansible Role: https://github.com/ItsNotGoodName/ansible-role-autorestic - Ansible Role: <https://github.com/ItsNotGoodName/ansible-role-autorestic>
- Ansible Role: https://github.com/FuzzyMistborn/ansible-role-autorestic - Ansible Role: <https://github.com/FuzzyMistborn/ansible-role-autorestic>
- Ansible Role: <https://0xacab.org/varac-projects/ansible-role-autorestic>
- Ansible Role: <https://github.com/dbrennand/ansible-role-autorestic>
> :ToCPrevNext > :ToCPrevNext

View File

@@ -5,7 +5,7 @@ Linux & macOS. Windows is not supported. If you have problems installing please
Autorestic requires `bash`, `wget` and `bzip2` to be installed. For most systems these should be already installed. Autorestic requires `bash`, `wget` and `bzip2` to be installed. For most systems these should be already installed.
```bash ```bash
wget -qO - https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash wget -qO - https://raw.githubusercontent.com/cupcakearmy/autorestic/master/install.sh | bash
``` ```
## Alternatives ## Alternatives

View File

@@ -21,7 +21,7 @@ locations:
keep-weekly: 1 # keep 1 last weekly snapshots keep-weekly: 1 # keep 1 last weekly snapshots
keep-monthly: 12 # keep 12 last monthly snapshots keep-monthly: 12 # keep 12 last monthly snapshots
keep-yearly: 7 # keep 7 last yearly snapshots keep-yearly: 7 # keep 7 last yearly snapshots
keep-within: '2w' # keep snapshots from the last 2 weeks keep-within: '14d' # keep snapshots from the last 14 days
``` ```
## Globally ## Globally

View File

@@ -3,7 +3,7 @@
## Installation ## Installation
```bash ```bash
wget -qO - https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash wget -qO - https://raw.githubusercontent.com/cupcakearmy/autorestic/master/install.sh | bash
``` ```
See [installation](/installation) for alternative options. See [installation](/installation) for alternative options.

1872
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,6 @@
"dev": "codedoc serve" "dev": "codedoc serve"
}, },
"dependencies": { "dependencies": {
"@codedoc/cli": "^0.2.8" "@codedoc/cli": "^0.3.0"
} }
} }

View File

@@ -31,6 +31,11 @@ else
fi fi
echo $ARCH echo $ARCH
if ! command -v bzip2 &>/dev/null; then
echo "Missing bzip2 command. Please install the bzip2 package for your system."
exit 1
fi
wget -qO - https://api.github.com/repos/cupcakearmy/autorestic/releases/latest \ wget -qO - https://api.github.com/repos/cupcakearmy/autorestic/releases/latest \
| grep "browser_download_url.*_${OS}_${ARCH}" \ | grep "browser_download_url.*_${OS}_${ARCH}" \
| cut -d : -f 2,3 \ | cut -d : -f 2,3 \

View File

@@ -9,6 +9,7 @@ import (
"strings" "strings"
"github.com/cupcakearmy/autorestic/internal/colors" "github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/flags"
) )
type BackendRest struct { type BackendRest struct {
@@ -121,13 +122,17 @@ func (b Backend) validate() error {
} }
options := ExecuteOptions{Envs: env, Silent: true} options := ExecuteOptions{Envs: env, Silent: true}
// Check if already initialized // Check if already initialized
_, _, err = ExecuteResticCommand(options, "check") cmd := []string{"check"}
cmd = append(cmd, combineBackendOptions("check", b)...)
_, _, err = ExecuteResticCommand(options, cmd...)
if err == nil { if err == nil {
return nil return nil
} else { } else {
// If not initialize // If not initialize
colors.Body.Printf("Initializing backend \"%s\"...\n", b.name) colors.Body.Printf("Initializing backend \"%s\"...\n", b.name)
_, _, err := ExecuteResticCommand(options, "init") cmd := []string{"init"}
cmd = append(cmd, combineBackendOptions("init", b)...)
_, _, err := ExecuteResticCommand(options, cmd...)
return err return err
} }
} }
@@ -160,7 +165,6 @@ func (b Backend) ExecDocker(l Location, args []string) (int, string, error) {
args = append([]string{"restic"}, args...) args = append([]string{"restic"}, args...)
docker := []string{ docker := []string{
"run", "--rm", "run", "--rm",
"--pull", "always",
"--entrypoint", "ash", "--entrypoint", "ash",
"--workdir", dir, "--workdir", dir,
"--volume", volume + ":" + dir, "--volume", volume + ":" + dir,
@@ -194,6 +198,7 @@ func (b Backend) ExecDocker(l Location, args []string) (int, string, error) {
for key, value := range env { for key, value := range env {
docker = append(docker, "--env", key+"="+value) docker = append(docker, "--env", key+"="+value)
} }
docker = append(docker, "cupcakearmy/autorestic:"+VERSION, "-c", strings.Join(args, " "))
docker = append(docker, flags.DOCKER_IMAGE, "-c", strings.Join(args, " "))
return ExecuteCommand(options, docker...) return ExecuteCommand(options, docker...)
} }

225
internal/backend_test.go Normal file
View File

@@ -0,0 +1,225 @@
package internal
import (
"fmt"
"os"
"testing"
"github.com/spf13/viper"
)
func TestGenerateRepo(t *testing.T) {
t.Run("empty backend", func(t *testing.T) {
b := Backend{
name: "empty backend",
Type: "",
}
_, err := b.generateRepo()
if err == nil {
t.Errorf("Error expected for empty backend type")
}
})
t.Run("local backend", func(t *testing.T) {
b := Backend{
name: "local backend",
Type: "local",
Path: "/foo/bar",
}
result, err := b.generateRepo()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result, "/foo/bar")
})
t.Run("local backend with homedir prefix", func(t *testing.T) {
b := Backend{
name: "local backend",
Type: "local",
Path: "~/foo/bar",
}
result, err := b.generateRepo()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result, fmt.Sprintf("%s/foo/bar", os.Getenv("HOME")))
})
t.Run("local backend with config file", func(t *testing.T) {
// config file path should always be present from initConfig
viper.SetConfigFile("/tmp/.autorestic.yml")
defer viper.Reset()
b := Backend{
name: "local backend",
Type: "local",
}
result, err := b.generateRepo()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result, "/tmp")
})
t.Run("rest backend with valid path", func(t *testing.T) {
b := Backend{
name: "rest backend",
Type: "rest",
Path: "http://localhost:8000/foo",
}
result, err := b.generateRepo()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result, "rest:http://localhost:8000/foo")
})
t.Run("rest backend with user", func(t *testing.T) {
b := Backend{
name: "rest backend",
Type: "rest",
Path: "http://localhost:8000/foo",
Rest: BackendRest{
User: "user",
Password: "",
},
}
result, err := b.generateRepo()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result, "rest:http://user@localhost:8000/foo")
})
t.Run("rest backend with user and password", func(t *testing.T) {
b := Backend{
name: "rest backend",
Type: "rest",
Path: "http://localhost:8000/foo",
Rest: BackendRest{
User: "user",
Password: "pass",
},
}
result, err := b.generateRepo()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result, "rest:http://user:pass@localhost:8000/foo")
})
backendTests := []struct {
name string
backend Backend
want string
}{
{name: "b2 backend", backend: Backend{name: "b2", Type: "b2", Path: "foo"}, want: "b2:foo"},
{name: "azure backend", backend: Backend{name: "azure", Type: "azure", Path: "foo"}, want: "azure:foo"},
{name: "gs backend", backend: Backend{name: "gs", Type: "gs", Path: "foo"}, want: "gs:foo"},
{name: "s3 backend", backend: Backend{name: "s3", Type: "s3", Path: "foo"}, want: "s3:foo"},
{name: "sftp backend", backend: Backend{name: "sftp", Type: "sftp", Path: "foo"}, want: "sftp:foo"},
{name: "rclone backend", backend: Backend{name: "rclone", Type: "rclone", Path: "foo"}, want: "rclone:foo"},
}
for _, tt := range backendTests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.backend.generateRepo()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, got, tt.want)
})
}
}
func TestGetEnv(t *testing.T) {
t.Run("env in key field", func(t *testing.T) {
b := Backend{
name: "",
Type: "local",
Path: "/foo/bar",
Key: "secret123",
}
result, err := b.getEnv()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result["RESTIC_REPOSITORY"], "/foo/bar")
assertEqual(t, result["RESTIC_PASSWORD"], "secret123")
})
t.Run("env in config file", func(t *testing.T) {
b := Backend{
name: "",
Type: "local",
Path: "/foo/bar",
Env: map[string]string{
"B2_ACCOUNT_ID": "foo123",
"B2_ACCOUNT_KEY": "foo456",
},
}
result, err := b.getEnv()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result["RESTIC_REPOSITORY"], "/foo/bar")
assertEqual(t, result["RESTIC_PASSWORD"], "")
assertEqual(t, result["B2_ACCOUNT_ID"], "foo123")
assertEqual(t, result["B2_ACCOUNT_KEY"], "foo456")
})
t.Run("env in Envfile or env vars", func(t *testing.T) {
// generate env variables
// TODO better way to teardown
defer os.Unsetenv("AUTORESTIC_FOO_RESTIC_PASSWORD")
defer os.Unsetenv("AUTORESTIC_FOO_B2_ACCOUNT_ID")
defer os.Unsetenv("AUTORESTIC_FOO_B2_ACCOUNT_KEY")
os.Setenv("AUTORESTIC_FOO_RESTIC_PASSWORD", "secret123")
os.Setenv("AUTORESTIC_FOO_B2_ACCOUNT_ID", "foo123")
os.Setenv("AUTORESTIC_FOO_B2_ACCOUNT_KEY", "foo456")
b := Backend{
name: "foo",
Type: "local",
Path: "/foo/bar",
}
result, err := b.getEnv()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result["RESTIC_REPOSITORY"], "/foo/bar")
assertEqual(t, result["RESTIC_PASSWORD"], "secret123")
assertEqual(t, result["B2_ACCOUNT_ID"], "foo123")
assertEqual(t, result["B2_ACCOUNT_KEY"], "foo456")
})
}
func TestValidate(t *testing.T) {
t.Run("no type given", func(t *testing.T) {
b := Backend{
name: "foo",
Type: "",
Path: "/foo/bar",
}
err := b.validate()
if err == nil {
t.Error("expected to get error")
}
assertEqual(t, err.Error(), "Backend \"foo\" has no \"type\"")
})
t.Run("no path given", func(t *testing.T) {
b := Backend{
name: "foo",
Type: "local",
Path: "",
}
err := b.validate()
if err == nil {
t.Error("expected to get error")
}
assertEqual(t, err.Error(), "Backend \"foo\" has no \"path\"")
})
}

View File

@@ -17,7 +17,7 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
const VERSION = "1.7.1" const VERSION = "1.7.5"
type OptionMap map[string][]interface{} type OptionMap map[string][]interface{}
type Options map[string]OptionMap type Options map[string]OptionMap
@@ -303,7 +303,17 @@ func getOptions(options Options, keys []string) []string {
return selected return selected
} }
func combineOptions(key string, l Location, b Backend) []string { 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 // Priority: location > backend > global
var options []string var options []string
gFlags := getOptions(GetConfig().Global, []string{key}) gFlags := getOptions(GetConfig().Global, []string{key})

164
internal/config_test.go Normal file
View File

@@ -0,0 +1,164 @@
package internal
import (
"reflect"
"strconv"
"strings"
"testing"
)
func TestOptionToString(t *testing.T) {
t.Run("no prefix", func(t *testing.T) {
opt := "test"
result := optionToString(opt)
assertEqual(t, result, "--test")
})
t.Run("single prefix", func(t *testing.T) {
opt := "-test"
result := optionToString(opt)
assertEqual(t, result, "-test")
})
t.Run("double prefix", func(t *testing.T) {
opt := "--test"
result := optionToString(opt)
assertEqual(t, result, "--test")
})
}
func TestAppendOneOptionToSlice(t *testing.T) {
t.Run("string flag", func(t *testing.T) {
result := []string{}
optionMap := OptionMap{"string-flag": []interface{}{"/root"}}
appendOptionsToSlice(&result, optionMap)
expected := []string{
"--string-flag", "/root",
}
assertSliceEqual(t, result, expected)
})
t.Run("bool flag", func(t *testing.T) {
result := []string{}
optionMap := OptionMap{"boolean-flag": []interface{}{true}}
appendOptionsToSlice(&result, optionMap)
expected := []string{
"--boolean-flag",
}
assertSliceEqual(t, result, expected)
})
t.Run("int flag", func(t *testing.T) {
result := []string{}
optionMap := OptionMap{"int-flag": []interface{}{123}}
appendOptionsToSlice(&result, optionMap)
expected := []string{
"--int-flag", "123",
}
assertSliceEqual(t, result, expected)
})
}
func TestAppendMultipleOptionsToSlice(t *testing.T) {
result := []string{}
optionMap := OptionMap{
"string-flag": []interface{}{"/root"},
"int-flag": []interface{}{123},
}
appendOptionsToSlice(&result, optionMap)
expected := []string{
"--string-flag", "/root",
"--int-flag", "123",
}
if len(result) != len(expected) {
t.Errorf("got length %d, want length %d", len(result), len(expected))
}
// checks that expected option comes after flag, regardless of key order in map
for i, v := range expected {
v = strings.TrimPrefix(v, "--")
if value, ok := optionMap[v]; ok {
if val, ok := value[0].(int); ok {
if expected[i+1] != strconv.Itoa(val) {
t.Errorf("Flags and options order are mismatched. got %v, want %v", result, expected)
}
}
}
}
}
func TestAppendOptionWithMultipleValuesToSlice(t *testing.T) {
result := []string{}
optionMap := OptionMap{
"string-flag": []interface{}{"/root", "/bin"},
}
appendOptionsToSlice(&result, optionMap)
expected := []string{
"--string-flag", "/root",
"--string-flag", "/bin",
}
assertSliceEqual(t, result, expected)
}
func TestGetOptionsOneKey(t *testing.T) {
optionMap := OptionMap{
"string-flag": []interface{}{"/root"},
}
options := Options{"backend": optionMap}
keys := []string{"backend"}
result := getOptions(options, keys)
expected := []string{
"--string-flag", "/root",
}
assertSliceEqual(t, result, expected)
}
func TestGetOptionsMultipleKeys(t *testing.T) {
firstOptionMap := OptionMap{
"string-flag": []interface{}{"/root"},
}
secondOptionMap := OptionMap{
"boolean-flag": []interface{}{true},
"int-flag": []interface{}{123},
}
options := Options{
"all": firstOptionMap,
"forget": secondOptionMap,
}
keys := []string{"all", "forget"}
result := getOptions(options, keys)
expected := []string{
"--string-flag", "/root",
"--boolean-flag",
"--int-flag", "123",
}
reflect.DeepEqual(result, expected)
}
func assertEqual[T comparable](t testing.TB, result, expected T) {
t.Helper()
if result != expected {
t.Errorf("got %v, want %v", result, expected)
}
}
func assertSliceEqual(t testing.TB, result, expected []string) {
t.Helper()
if len(result) != len(expected) {
t.Errorf("got length %d, want length %d", len(result), len(expected))
}
for i := range result {
assertEqual(t, result[i], expected[i])
}
}

View File

@@ -5,4 +5,5 @@ var (
VERBOSE bool = false VERBOSE bool = false
CRON_LEAN bool = false CRON_LEAN bool = false
RESTIC_BIN string RESTIC_BIN string
DOCKER_IMAGE string
) )

View File

@@ -74,12 +74,8 @@ func (l Location) validate() error {
if from, err := GetPathRelativeToConfig(path); err != nil { if from, err := GetPathRelativeToConfig(path); err != nil {
return err return err
} else { } else {
if stat, err := os.Stat(from); err != nil { if _, err := os.Stat(from); err != nil {
return err return err
} else {
if !stat.IsDir() {
return fmt.Errorf("\"%s\" is not valid directory for location \"%s\"", from, l.name)
}
} }
} }
} }
@@ -220,7 +216,7 @@ func (l Location) Backup(cron bool, specificBackend string) []error {
} }
cmd := []string{"backup"} cmd := []string{"backup"}
cmd = append(cmd, combineOptions("backup", l, backend)...) cmd = append(cmd, combineAllOptions("backup", l, backend)...)
if cron { if cron {
cmd = append(cmd, "--tag", buildTag("cron")) cmd = append(cmd, "--tag", buildTag("cron"))
} }
@@ -312,7 +308,10 @@ after:
// Forget and optionally prune // Forget and optionally prune
if isSuccess && l.ForgetOption != "" && l.ForgetOption != LocationForgetNo { if isSuccess && l.ForgetOption != "" && l.ForgetOption != LocationForgetNo {
l.Forget(l.ForgetOption == LocationForgetPrune, false) err := l.Forget(l.ForgetOption == LocationForgetPrune, false)
if err != nil {
errors = append(errors, err)
}
} }
if len(errors) == 0 { if len(errors) == 0 {
@@ -324,7 +323,12 @@ after:
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)
for _, to := range l.To { backendsToForget := l.To
for _, copyBackends := range l.CopyOption {
backendsToForget = append(backendsToForget, copyBackends...)
}
for _, to := range backendsToForget {
backend, _ := GetBackend(to) backend, _ := GetBackend(to)
colors.Secondary.Printf("For backend \"%s\"\n", backend.name) colors.Secondary.Printf("For backend \"%s\"\n", backend.name)
env, err := backend.getEnv() env, err := backend.getEnv()
@@ -341,7 +345,7 @@ func (l Location) Forget(prune bool, dry bool) error {
if dry { if dry {
cmd = append(cmd, "--dry-run") cmd = append(cmd, "--dry-run")
} }
cmd = append(cmd, combineOptions("forget", l, backend)...) cmd = append(cmd, combineAllOptions("forget", l, backend)...)
_, _, err = ExecuteResticCommand(options, cmd...) _, _, err = ExecuteResticCommand(options, cmd...)
if err != nil { if err != nil {
return err return err

93
internal/location_test.go Normal file
View File

@@ -0,0 +1,93 @@
package internal
import "testing"
func TestGetType(t *testing.T) {
t.Run("TypeLocal", func(t *testing.T) {
l := Location{
Type: "local",
}
result, err := l.getType()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
assertEqual(t, result, TypeLocal)
})
t.Run("TypeVolume", func(t *testing.T) {
l := Location{
Type: "volume",
}
result, err := l.getType()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
assertEqual(t, result, TypeVolume)
})
t.Run("Empty type", func(t *testing.T) {
l := Location{
Type: "",
}
result, err := l.getType()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
assertEqual(t, result, TypeLocal)
})
t.Run("Invalid type", func(t *testing.T) {
l := Location{
Type: "foo",
}
_, err := l.getType()
if err == nil {
t.Error("expected error")
}
})
}
func TestBuildTag(t *testing.T) {
result := buildTag("foo", "bar")
expected := "ar:foo:bar"
assertEqual(t, result, expected)
}
func TestGetLocationTags(t *testing.T) {
l := Location{
name: "foo",
}
result := l.getLocationTags()
expected := "ar:location:foo"
assertEqual(t, result, expected)
}
func TestHasBackend(t *testing.T) {
t.Run("backend present", func(t *testing.T) {
l := Location{
name: "foo",
To: []string{"foo", "bar"},
}
result := l.hasBackend("foo")
assertEqual(t, result, true)
})
t.Run("backend absent", func(t *testing.T) {
l := Location{
name: "foo",
To: []string{"bar", "baz"},
}
result := l.hasBackend("foo")
assertEqual(t, result, false)
})
}
func TestBuildRestoreCommand(t *testing.T) {
l := Location{
name: "foo",
}
result := buildRestoreCommand(l, "to", "snapshot", []string{"options"})
expected := []string{"restore", "--target", "to", "--tag", "ar:location:foo", "snapshot", "options"}
assertSliceEqual(t, result, expected)
}

View File

@@ -14,6 +14,10 @@ var lock *viper.Viper
var file string var file string
var once sync.Once var once sync.Once
const (
RUNNING = "running"
)
func getLock() *viper.Viper { func getLock() *viper.Viper {
if lock == nil { if lock == nil {
@@ -37,36 +41,38 @@ func getLock() *viper.Viper {
return lock return lock
} }
func setLock(locked bool) error { func setLockValue(key string, value interface{}) (*viper.Viper, error) {
lock := getLock() lock := getLock()
if locked {
running := lock.GetBool("running") if key == RUNNING {
if running { value := value.(bool)
if value && lock.GetBool(key) {
colors.Error.Println("an instance is already running. exiting") colors.Error.Println("an instance is already running. exiting")
os.Exit(1) os.Exit(1)
} }
} }
lock.Set("running", locked)
lock.Set(key, value)
if err := lock.WriteConfigAs(file); err != nil { if err := lock.WriteConfigAs(file); err != nil {
return err return nil, err
} }
return nil return lock, nil
} }
func GetCron(location string) int64 { func GetCron(location string) int64 {
lock := getLock() return getLock().GetInt64("cron." + location)
return lock.GetInt64("cron." + location)
} }
func SetCron(location string, value int64) { func SetCron(location string, value int64) {
lock.Set("cron."+location, value) setLockValue("cron."+location, value)
lock.WriteConfigAs(file)
} }
func Lock() error { func Lock() error {
return setLock(true) _, err := setLockValue(RUNNING, true)
return err
} }
func Unlock() error { func Unlock() error {
return setLock(false) _, err := setLockValue(RUNNING, false)
return err
} }

114
internal/lock/lock_test.go Normal file
View File

@@ -0,0 +1,114 @@
package lock
import (
"log"
"os"
"os/exec"
"strconv"
"testing"
"github.com/spf13/viper"
)
var testDirectory = "autorestic_test_tmp"
// All tests must share the same lock file as it is only initialized once
func setup(t *testing.T) {
d, err := os.MkdirTemp("", testDirectory)
if err != nil {
log.Fatalf("error creating temp dir: %v", err)
return
}
// set config file location
viper.SetConfigFile(d + "/.autorestic.yml")
t.Cleanup(func() {
os.RemoveAll(d)
viper.Reset()
})
}
func TestLock(t *testing.T) {
setup(t)
t.Run("getLock", func(t *testing.T) {
result := getLock().GetBool(RUNNING)
if result {
t.Errorf("got %v, want %v", result, false)
}
})
t.Run("lock", func(t *testing.T) {
lock, err := setLockValue(RUNNING, true)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
result := lock.GetBool(RUNNING)
if !result {
t.Errorf("got %v, want %v", result, true)
}
})
t.Run("unlock", func(t *testing.T) {
lock, err := setLockValue(RUNNING, false)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
result := lock.GetBool(RUNNING)
if result {
t.Errorf("got %v, want %v", result, false)
}
})
// locking a locked instance exits the instance
// this trick to capture os.Exit(1) is discussed here:
// https://talks.golang.org/2014/testing.slide#23
t.Run("lock twice", func(t *testing.T) {
if os.Getenv("CRASH") == "1" {
err := Lock()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
// should fail
Lock()
}
cmd := exec.Command(os.Args[0], "-test.run=TestLock/lock_twice")
cmd.Env = append(os.Environ(), "CRASH=1")
err := cmd.Run()
err, ok := err.(*exec.ExitError)
if !ok {
t.Error("unexpected error")
}
expected := "exit status 1"
if err.Error() != expected {
t.Errorf("got %q, want %q", err.Error(), expected)
}
})
t.Run("set cron", func(t *testing.T) {
expected := int64(5)
SetCron("foo", expected)
result, err := strconv.ParseInt(getLock().GetString("cron.foo"), 10, 64)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if result != expected {
t.Errorf("got %d, want %d", result, expected)
}
})
t.Run("get cron", func(t *testing.T) {
expected := int64(5)
result := GetCron("foo")
if result != expected {
t.Errorf("got %d, want %d", result, expected)
}
})
}

View File

@@ -14,9 +14,9 @@ func (e addedExtractor) Matches(line string) bool {
} }
func (e addedExtractor) Extract(metadata *BackupLogMetadata, line string) { func (e addedExtractor) Extract(metadata *BackupLogMetadata, line string) {
// Sample line: "Added to the repo: 0 B" // Sample line: "Added to the repo: 0 B"
metadata.AddedSize = strings.TrimSpace(e.re.ReplaceAllString(line, "")) metadata.AddedSize = strings.TrimSpace(e.re.ReplaceAllString(line, "$2"))
} }
func NewAddedExtractor() MetadatExtractor { func NewAddedExtractor() MetadatExtractor {
return addedExtractor{regexp.MustCompile(`(?i)^Added to the repo:`)} return addedExtractor{regexp.MustCompile(`(?i)^Added to the repo(sitory)?: ([\d\.]+ \w+)( \([\d\.]+[\w\s]+\))?`)}
} }