Compare commits

...

43 Commits

Author SHA1 Message Date
ddc3accb30 Merge pull request #114 from cupcakearmy/1.3.0
1.3.0
2021-10-26 16:03:09 +02:00
7874512ec0 changelog 2021-10-26 16:02:30 +02:00
0b5f4017e4 version bump 2021-10-26 15:59:49 +02:00
88198c4fcb docs for hook envs 2021-10-26 15:57:47 +02:00
8cd759105f env for hooks 2021-10-26 15:57:40 +02:00
6137e31c3b allow argumentless flags 2021-10-25 18:29:59 +02:00
2789502c89 toc 2021-10-25 18:02:56 +02:00
b87381cd3b docs for envs 2021-10-25 18:02:51 +02:00
048a5ffed8 renamed env 2021-10-25 18:02:45 +02:00
02a8e461d4 ability to use keys from envs 2021-10-25 17:36:06 +02:00
a1abe13a39 docs on cron 2021-10-25 17:07:29 +02:00
ddf287f6f5 Merge pull request #113 from cupcakearmy/1.3.0
1.3.0
2021-10-22 17:59:25 +02:00
56f82ae656 pnpm & typo 2021-10-22 17:57:43 +02:00
95b1ca3297 Merge pull request #108 from FuzzyMistborn/patch-2
Add third ansible role to community docs
2021-10-07 12:14:08 +02:00
Fuzzy
7a8830cb2f Add third ansible role to community docs 2021-10-06 13:24:40 -04:00
959d19cbdb add XDG_CONFIG_HOME to config 2021-10-06 15:50:23 +02:00
9dc7763445 Merge pull request #103 from IronicBadger/master
fixes small typo
2021-09-01 10:31:53 +02:00
Alex Kretzschmar
50984f6771 typo 2021-08-31 18:35:11 -04:00
e80f200873 systemd units 2021-08-06 10:20:40 +02:00
86ae70672a right version 2021-08-05 21:58:48 +02:00
5c0788900f docs 2021-08-05 21:54:40 +02:00
20334a7e83 better config handling 2021-08-05 21:48:02 +02:00
7bebd04482 validate and show cwd 2021-07-12 19:10:36 +02:00
b9b8857bf4 aliases 2021-07-12 19:10:25 +02:00
dd6e618161 typo 2021-07-11 14:03:04 +02:00
a4b54f9f64 check config bugs 2021-07-11 13:51:04 +02:00
c2e88193cd Update contrib.md 2021-06-06 15:51:50 +02:00
a2ef69d96d Merge pull request #87 from somebox/patch-1
correction to B2 config in the example
2021-06-06 15:48:02 +02:00
Jeremy Seitz
d45949b028 correction to B2 config in the example
This example was missing the `env:` block in YAML, which if missing, caused `autorestic check` to compain:

``` * 'Backends[remote]' has invalid keys: b2_account_id, b2_account_key```
2021-06-06 14:27:26 +02:00
77d47cc697 add arm in install script 2021-05-25 22:57:42 +02:00
a3239c0f3b changelog 2021-05-17 22:40:27 +02:00
90cd3171e5 docs 2021-05-17 22:40:23 +02:00
61673bd88b allow options for backend 2021-05-17 22:34:14 +02:00
41736ea3c4 typos 2021-05-07 11:34:46 +02:00
a7779e04bd Merge pull request #75 from FuzzyMistborn/patch-2
Update update command in docs
2021-05-07 11:31:26 +02:00
Fuzzy
1326e7e53c Update update command in docs 2021-05-06 23:05:34 -04:00
478e193d78 forgot version bump 2021-05-06 16:17:45 +02:00
11d4c67dce docs 2021-05-06 16:14:29 +02:00
1643309957 changelog 2021-05-06 15:57:28 +02:00
e05386b0b5 add failure and success hooks 2021-05-06 15:55:32 +02:00
aebaf0a225 Merge branch 'master' of https://github.com/cupcakearmy/autorestic 2021-05-06 15:12:37 +02:00
c090013bf5 Update README.md 2021-05-06 15:12:11 +02:00
88c6949208 custom restic binary 2021-05-06 15:04:35 +02:00
36 changed files with 766 additions and 895 deletions

View File

@@ -5,6 +5,50 @@ 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.3.0] - 2021-10-26
### Added
- Pass restic backup metadata as ENV to hooks
- Support for `XDG_CONFIG_HOME` and `${HOME}/.config` as default locations for `.autorestic.yaml` file.
- Binary restic flags are now supported
- Pass encryption keys from env variables or files.
## [1.2.0] - 2021-08-05
### Added
- Community page
- Support for yaml references and aliases
### Fixed
- Better verbose output for hooks
- Better error message for bad formatted configs
## [1.1.2] - 2021-07-11
### Fixes
Don't check all backend when running `forget` or `exec` commands.
## [1.1.1] - 2021-05-17
### Added
- Options for backends
## [1.1.0] - 2021-05-06
### Added
- use custom restic binary
- success & failure hooks
### Fixed
- don't skip other locations on failure
## [1.0.9] - 2021-05-01
### Fixed

View File

@@ -19,11 +19,12 @@ Releases are automatically built by the github workflow and uploaded to the rele
1. Bump `VERSION` in `internal/config.go`.
2. Update `CHANGELOG.md`
3. Commit to master
4. Create a new release with the `v1.2.3` tag and mark as draft.
4. Create a new release with the `v1.2.3` tag and mark as pre-release.
5. The Github action will build the binaries, upload and mark the release as ready when done.
### Brew
1. Check the checksum with `shasum -a 256 autorestic-1.2.3.tar.gz`
2. Update `url` and `sha256` in the brew repo.
3. Submit PR
1. Download the latest release.
2. Check the checksum with `shasum -a 256 autorestic-1.2.3.tar.gz`
3. Update `url` and `sha256` in the brew repo.
4. Submit PR

View File

@@ -13,6 +13,9 @@
<br><br>
<a target="_blank" href="https://discord.gg/wS7RpYTYd2">
<img src="https://img.shields.io/discord/252403122348097536" alt="discord badge" />
<img src="https://img.shields.io/github/contributors/cupcakearmy/autorestic" alt="contributor badge" />
<img src="https://img.shields.io/github/downloads/cupcakearmy/autorestic/total" alt="downloads badge" />
<img src="https://img.shields.io/github/v/release/cupcakearmy/autorestic" alt="version badge" />
</a>
</p>
</p>
@@ -22,7 +25,7 @@
### 💭 Why / What?
Autorestic is a wrapper around the amazing [restic](https://restic.net/). While being amazing the restic cli can be a bit overwhelming and difficult to manage if you have many different location that you want to backup to multiple locations. This utility is aimed at making this easier 🙂
Autorestic is a wrapper around the amazing [restic](https://restic.net/). While being amazing the restic cli can be a bit overwhelming and difficult to manage if you have many different locations that you want to backup to multiple locations. This utility is aimed at making this easier 🙂.
### 🌈 Features

View File

@@ -13,19 +13,18 @@ var backupCmd = &cobra.Command{
Use: "backup",
Short: "Create backups for given locations",
Run: func(cmd *cobra.Command, args []string) {
internal.GetConfig()
err := lock.Lock()
CheckErr(err)
defer lock.Unlock()
CheckErr(internal.CheckConfig())
selected, err := internal.GetAllOrSelected(cmd, false)
CheckErr(err)
errors := 0
for _, name := range selected {
location, _ := internal.GetLocation(name)
err := location.Backup(false)
if err != nil {
errs := location.Backup(false)
for err := range errs {
colors.Error.Println(err)
errors++
}

View File

@@ -11,12 +11,11 @@ var execCmd = &cobra.Command{
Use: "exec",
Short: "Execute arbitrary native restic commands for given backends",
Run: func(cmd *cobra.Command, args []string) {
internal.GetConfig()
err := lock.Lock()
CheckErr(err)
defer lock.Unlock()
CheckErr(internal.CheckConfig())
selected, err := internal.GetAllOrSelected(cmd, true)
CheckErr(err)
for _, name := range selected {

View File

@@ -10,12 +10,11 @@ var forgetCmd = &cobra.Command{
Use: "forget",
Short: "Forget and optionally prune snapshots according the specified policies",
Run: func(cmd *cobra.Command, args []string) {
internal.GetConfig()
err := lock.Lock()
CheckErr(err)
defer lock.Unlock()
CheckErr(internal.CheckConfig())
selected, err := internal.GetAllOrSelected(cmd, false)
CheckErr(err)
prune, _ := cmd.Flags().GetBool("prune")

View File

@@ -2,6 +2,7 @@ package cmd
import (
"os"
"path/filepath"
"github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors"
@@ -37,6 +38,7 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.autorestic.yml or ./.autorestic.yml)")
rootCmd.PersistentFlags().BoolVar(&internal.CI, "ci", false, "CI mode disabled interactive mode and colors and enables verbosity")
rootCmd.PersistentFlags().BoolVarP(&internal.VERBOSE, "verbose", "v", false, "verbose mode")
rootCmd.PersistentFlags().StringVar(&internal.RESTIC_BIN, "restic-bin", "restic", "specify custom restic binary")
cobra.OnInitialize(initConfig)
}
@@ -49,11 +51,24 @@ func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := homedir.Dir()
CheckErr(err)
viper.AddConfigPath(".")
// Home
if home, err := homedir.Dir(); err != nil {
viper.AddConfigPath(home)
}
// XDG_CONFIG_HOME
{
prefix, found := os.LookupEnv("XDG_CONFIG_HOME")
if !found {
if home, err := homedir.Dir(); err != nil {
prefix = filepath.Join(home, ".config")
}
}
viper.AddConfigPath(filepath.Join(prefix, "autorestic"))
}
viper.SetConfigName(".autorestic")
}
viper.AutomaticEnv()

View File

@@ -22,6 +22,8 @@
>
> [Overview](/backend/overview)
> [Available Backends](/backend/available)
> [Options](/backend/options)
> [Environment](/backend/env)
> :Collapse label=CLI
>
@@ -36,10 +38,10 @@
> [Exec](/cli/exec)
> [Install](/cli/install)
> [Uninstall](/cli/uninstall)
> [Update](/cli/update)
> [Upgrade](/cli/upgrade)
[Examples](/examples)
[QA](/qa)
[Community](/community)
[Contributors](/contrib)

View File

@@ -19,9 +19,9 @@ backends:
backends:
name-of-backend:
type: b2
path: 'backblaze_bucketID'
path: 'bucket_name'
# Or With a path
# path: 'backblaze_bucketID:/some/path'
# path: 'bucket_name:/some/path'
env:
B2_ACCOUNT_ID: 'backblaze_keyID'
B2_ACCOUNT_KEY: 'backblaze_applicationKey'
@@ -29,7 +29,7 @@ backends:
#### API Keys gotcha
If you use a _File name prefix_ when making the application key, do not include a leading slash. Make sure to include this prefix in the path (e.g. `path: 'backblaze_bucketID:my/path'`).
If you use a _File name prefix_ when making the application key, do not include a leading slash. Make sure to include this prefix in the path (e.g. `path: 'bucket_name:my/path'`).
## S3 / Minio

View File

@@ -0,0 +1,36 @@
# Environment
> ⚠ Available since version `v1.3.0`
Sometimes it's favorable not having the encryption keys in the config files.
For that `autorestic` allows passing the backend keys as `ENV` variables, or through an env file.
The syntax for the `ENV` variables is as follows: `AUTORESTIC_[BACKEND NAME]_KEY`.
```yaml | autorestic.yaml
backend:
foo:
type: ...
path: ...
key: secret123 # => AUTORESTIC_FOO_KEY=secret123
```
## Example
This means we could remove `key: secret123` from `.autorestic.yaml` and execute as follows:
```bash
AUTORESTIC_FOO_KEY=secret123 autorestic backup ...
```
## Env file
Alternatively `autorestic` can load an env file, located next to `autorestic.yml` called `.autorestic.env`.
```| .autorestic.env
AUTORESTIC_FOO_KEY=secret123
```
after that you can simply use `autorestic` as your are used to.
> :ToCPrevNext

View File

@@ -0,0 +1,25 @@
# Options
For the `backup` and `forget` commands you can pass any native flags to `restic`.
> It is also possible to set options for an [a specific location](/location/options).
```yaml
backend:
foo:
type: ...
path: ...
options:
backup:
tag:
- foo
- bar
```
In this example, whenever `autorestic` runs `restic backup` it will append a `--tag abc --tag` to the native command.
For more detail see the [location docs](/location/options) for options, as they are the same.
> For flags without arguments you can set them to `true`. They will be handled accordingly.
> :ToCPrevNext

View File

@@ -4,7 +4,7 @@
autorestic forget [-l, --location] [-a, --all] [--dry-run] [--prune]
```
This will prune and remove old data form the backends according to the [keep policy you have specified for the location](/location/forget)
This will prune and remove old data form the backends according to the [keep policy you have specified for the location](/location/forget).
The `--dry-run` flag will do a dry run showing what would have been deleted, but won't touch the actual data.

View File

@@ -27,4 +27,12 @@ Verbose mode will show the output of the native restic commands that are otherwi
autorestic --verbose backup -a
```
## `--restic-bin`
With `--restic-bin` you can specify to run a specific restic binary. This can be useful if you want to [create a custom binary with root access that can be executed by any user](https://restic.readthedocs.io/en/stable/080_examples.html#full-backup-without-root).
```bash
autorestic --restic-bin /some/path/to/my/custom/restic/binary
```
> :ToCPrevNext

View File

@@ -1,11 +0,0 @@
# Update
Autorestic can update itself! Super handy right? Simply run autorestic update and we will check for you if there are updates for restic and autorestic and install them if necessary.
```bash
autorestic update
```
Updates both restic and autorestic automagically.
> :ToCPrevNext

View File

@@ -0,0 +1,11 @@
# Upgrade
Autorestic can upgrade itself! Super handy right? Simply run autorestic upgrade and we will check for you if there are updates for restic and autorestic and install them if necessary.
```bash
autorestic upgrade
```
Updates both restic and autorestic automagically.
> :ToCPrevNext

View File

@@ -0,0 +1,11 @@
# 🏘 Community
A list of community driven projects. (No official affiliation)
- SystemD Units: https://gitlab.com/py_crash/autorestic-systemd-units
- Docker image: https://github.com/pascaliske/docker-autorestic
- Ansible Role: https://github.com/adsanz/ansible-restic-role
- Ansible Role: https://github.com/ItsNotGoodName/ansible-role-autorestic
- Ansible Role: https://github.com/FuzzyMistborn/ansible-role-autorestic
> :ToCPrevNext

View File

@@ -31,6 +31,7 @@ backends:
remote:
type: b2
path: 'myBucket:backup/home'
env:
B2_ACCOUNT_ID: account_id
B2_ACCOUNT_KEY: account_key
@@ -39,4 +40,44 @@ backends:
path: /mnt/my_external_storage
```
## Aliases
A handy tool for more advanced configurations is to use yaml aliases.
These must be specified under the global `extras` key in the `.autorestic.yml` config file.
Aliases allow to reuse snippets of config throughout the same file.
The following example shows how the locations `a` and `b` share the same hooks and forget policies.
```yaml | .autorestic.yml
extras:
hooks: &foo
before:
- echo "Hello"
after:
- echo "kthxbye"
policies: &bar
keep-daily: 14
keep-weekly: 52
backends:
# ...
locations:
a:
from: /data/a
to: some
hooks:
<<: *foo
options:
forget:
<<: *bar
b:
from: data/b
to: some
hooks:
<<: *foo
options:
forget:
<<: *bar
```
> :ToCPrevNext

View File

@@ -12,5 +12,6 @@ This amazing people helped the project!
- @ChanceM - Typos
- @TheForcer - Typos
- @themorlan - Typos
- @somebox - Typos
> :ToCPrevNext

View File

@@ -37,11 +37,17 @@ Then paste this at the bottom of the file and save it. Note that in this specifi
PATH="/usr/local/bin:/usr/bin:/bin"
# Example running every 5 minutes
*/5 * * * * autorestic --ci cron
*/5 * * * * autorestic -c /path/to/my/.autorestic.yml --ci cron
```
> The `--ci` option is not required, but recommended
To debug a cron job you can use
```bash
*/5 * * * * autorestic -c /path/to/my/.autorestic.yml --ci cron > /tmp/autorestic.log 2>&1
```
Now you can add as many `cron` attributes as you wish in the config file ⏱
> Also note that manually triggered backups with `autorestic backup` will not influence the cron timeline, they are willingly not linked.

View File

@@ -2,7 +2,14 @@
If you want to perform some commands before and/or after a backup, you can use hooks.
They consist of a list of `before`/`after` commands that will be executed in the same directory as the target `from`.
They consist of a list of commands that will be executed in the same directory as the target `from`.
The following hooks groups are supported, none are required:
- `before`
- `after`
- `failure`
- `success`
```yml | .autorestic.yml
locations:
@@ -11,10 +18,63 @@ locations:
to: my-backend
hooks:
before:
- echo "Hello"
- echo "Human"
- echo "One"
- echo "Two"
- echo "Three"
after:
- echo "kthxbye"
- echo "Byte"
failure:
- echo "Something went wrong"
success:
- echo "Well done!"
```
## Flowchart
1. `before` hook
2. Run backup
3. `after` hook
4. - `success` hook if no errors were found
- `failure` hook if at least one error was encountered
If the `before` hook encounters errors the backup and `after` hooks will be skipped and only the `failed` hooks will run.
## Environment variables
All hooks are exposed to the `AUTORESTIC_LOCATION` environment variable, which contains the location name.
The `after` and `success` hooks have access to additional information with the following syntax:
```bash
AUTORESTIC_[TYPE]_[I]
AUTORESTIC_[TYPE]_[BACKEND_NAME]
```
Every type of metadata is appended with both the name of the backend associated with and the number in which the backends where executed.
### Available Metadata Types
- `SNAPSHOT_ID`
- `PARENT_SNAPSHOT_ID`
- `FILES_ADDED`
- `FILES_CHANGED`
- `FILES_UNMODIFIED`
- `DIRS_ADDED`
- `DIRS_CHANGED`
- `DIRS_UNMODIFIED`
- `ADDED_SIZE`
- `PROCESSED_FILES`
- `PROCESSED_SIZE`
- `PROCESSED_DURATION`
#### Example
Assuming you have a location `bar` that backs up to a single backend named `foo` you could expect the following env variables:
```bash
AUTORESTIC_LOCATION=bar
AUTORESTIC_FILES_ADDED_0=42
AUTORESTIC_FILES_ADDED_FOO=42
```
> :ToCPrevNext

View File

@@ -2,6 +2,8 @@
For the `backup` and `forget` commands you can pass any native flags to `restic`.
> It is also possible to set options for an [entire backend](/backend/options).
```yaml
locations:
foo:
@@ -16,4 +18,6 @@ locations:
In this example, whenever `autorestic` runs `restic backup` it will append a `--tag abc --tag` to the native command.
> For flags without arguments you can set them to `true`. They will be handled accordingly.
> :ToCPrevNext

View File

@@ -1,4 +1,4 @@
# Update
# Upgrade
## From `0.x` to `1.0`

878
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

1
go.mod
View File

@@ -6,6 +6,7 @@ require (
github.com/blang/semver/v4 v4.0.0
github.com/buger/goterm v1.0.0
github.com/fatih/color v1.10.0
github.com/joho/godotenv v1.4.0
github.com/mitchellh/go-homedir v1.1.0
github.com/robfig/cron v1.2.0
github.com/spf13/cobra v1.1.3

2
go.sum
View File

@@ -101,6 +101,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=

View File

@@ -23,6 +23,8 @@ elif [[ $NATIVE_ARCH == *"arm64"* || $NATIVE_ARCH == *"aarch64"* ]]; then
ARCH=arm64
elif [[ $NATIVE_ARCH == *"x86"* ]]; then
ARCH=386
elif [[ $NATIVE_ARCH == *"armv7"* ]]; then
ARCH=arm
else
echo "Could not determine Architecure automatically, please check the release page manually: https://github.com/cupcakearmy/autorestic/releases"
exit 1

View File

@@ -23,6 +23,7 @@ type Backend struct {
Key string `yaml:"key,omitempty"`
Env map[string]string `yaml:"env,omitempty"`
Rest BackendRest `yaml:"rest,omitempty"`
Options Options `yaml:"options,omitempty"`
}
func GetBackend(name string) (Backend, bool) {
@@ -57,7 +58,15 @@ func (b Backend) generateRepo() (string, error) {
func (b Backend) getEnv() (map[string]string, error) {
env := make(map[string]string)
if b.Key != "" {
env["RESTIC_PASSWORD"] = b.Key
} else {
key, err := b.getKey()
if err != nil {
return nil, err
}
env["RESTIC_PASSWORD"] = key
}
repo, err := b.generateRepo()
env["RESTIC_REPOSITORY"] = repo
for key, value := range b.Env {
@@ -76,6 +85,17 @@ func generateRandomKey() string {
return key
}
func (b Backend) getKey() (string, error) {
if b.Key != "" {
return b.Key, nil
}
keyName := "AUTORESTIC_" + strings.ToUpper(b.name) + "_KEY"
if key, found := os.LookupEnv(keyName); found {
return key, nil
}
return "", fmt.Errorf("no key found for backend \"%s\"", b.name)
}
func (b Backend) validate() error {
if b.Type == "" {
return fmt.Errorf(`Backend "%s" has no "type"`, b.name)
@@ -84,6 +104,9 @@ func (b Backend) validate() error {
return fmt.Errorf(`Backend "%s" has no "path"`, b.name)
}
if b.Key == "" {
// Check if key is set in environment
if _, err := b.getKey(); err != nil {
// If not generate a new one
key := generateRandomKey()
b.Key = key
c := GetConfig()
@@ -94,6 +117,7 @@ func (b Backend) validate() error {
return err
}
}
}
env, err := b.getEnv()
if err != nil {
return err

View File

@@ -2,23 +2,28 @@ package internal
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"sync"
"github.com/cupcakearmy/autorestic/internal/colors"
"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.0.9"
const VERSION = "1.3.0"
var CI bool = false
var VERBOSE bool = false
var CRON_LEAN bool = false
type Config struct {
Extras interface{} `yaml:"extras"`
Locations map[string]Location `yaml:"locations"`
Backends map[string]Backend `yaml:"backends"`
}
@@ -31,7 +36,14 @@ func GetConfig() *Config {
once.Do(func() {
if err := viper.ReadInConfig(); err == nil {
if !CRON_LEAN {
colors.Faint.Println("Using config file:", viper.ConfigFileUsed())
absConfig, _ := filepath.Abs(viper.ConfigFileUsed())
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 {
colors.Faint.Println("Using env:\t", envFile)
}
}
} else {
return
@@ -39,7 +51,9 @@ func GetConfig() *Config {
config = &Config{}
if err := viper.UnmarshalExact(config); err != nil {
panic(err)
colors.Error.Println("Could not parse config file!")
lock.Unlock()
os.Exit(1)
}
})
}
@@ -75,21 +89,22 @@ func (c *Config) Describe() {
colors.PrintDescription("Cron", l.Cron)
}
after, before := len(l.Hooks.After), len(l.Hooks.Before)
if after+before > 0 {
tmp = ""
if before > 0 {
tmp += "\tBefore"
for _, cmd := range l.Hooks.Before {
hooks := map[string][]string{
"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 after > 0 {
tmp += "\n\tAfter"
for _, cmd := range l.Hooks.After {
tmp += colors.Faint.Sprintf("\n\t ▶ %s", cmd)
}
}
if tmp != "" {
colors.PrintDescription("Hooks", tmp)
}
@@ -129,7 +144,7 @@ func CheckConfig() error {
return fmt.Errorf("config could not be loaded/found")
}
if !CheckIfResticIsCallable() {
return fmt.Errorf(`restic was not found. Install either with "autorestic install" or manually`)
return fmt.Errorf(`%s was not found. Install either with "autorestic install" or manually`, RESTIC_BIN)
}
for name, backend := range c.Backends {
backend.name = name
@@ -139,7 +154,7 @@ func CheckConfig() error {
}
for name, location := range c.Locations {
location.name = name
if err := location.validate(c); err != nil {
if err := location.validate(); err != nil {
return err
}
}
@@ -219,3 +234,24 @@ func (c *Config) SaveConfig() error {
return viper.WriteConfig()
}
func getOptions(options Options, key string) []string {
var selected []string
for k, values := range options[key] {
for _, value := range values {
// Bool
asBool, ok := value.(bool)
if ok && asBool {
selected = append(selected, fmt.Sprintf("--%s", k))
continue
}
// String
asString, ok := value.(string)
if ok {
selected = append(selected, fmt.Sprintf("--%s", k), asString)
continue
}
}
}
return selected
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/cupcakearmy/autorestic/internal/metadata"
"github.com/robfig/cron"
)
@@ -24,11 +25,13 @@ const (
type HookArray = []string
type Hooks struct {
Before HookArray `yaml:"before"`
After HookArray `yaml:"after"`
Before HookArray `yaml:"before,omitempty"`
After HookArray `yaml:"after,omitempty"`
Success HookArray `yaml:"success,omitempty"`
Failure HookArray `yaml:"failure,omitempty"`
}
type Options map[string]map[string][]string
type Options map[string]map[string][]interface{}
type Location struct {
name string `yaml:",omitempty"`
@@ -45,7 +48,7 @@ func GetLocation(name string) (Location, bool) {
return l, ok
}
func (l Location) validate(c *Config) error {
func (l Location) validate() error {
if l.From == "" {
return fmt.Errorf(`Location "%s" is missing "from" key`, l.name)
}
@@ -76,17 +79,6 @@ func (l Location) validate(c *Config) error {
return nil
}
func (l Location) getOptions(key string) []string {
var options []string
saved := l.Options[key]
for k, values := range saved {
for _, value := range values {
options = append(options, fmt.Sprintf("--%s", k), value)
}
}
return options
}
func ExecuteHooks(commands []string, options ExecuteOptions) error {
if len(commands) == 0 {
return nil
@@ -133,35 +125,50 @@ func (l Location) getPath() (string, error) {
return "", fmt.Errorf("could not get path for location \"%s\"", l.name)
}
func (l Location) Backup(cron bool) error {
func (l Location) Backup(cron bool) []error {
var errors []error
colors.PrimaryPrint(" Backing up location \"%s\" ", l.name)
t := l.getType()
options := ExecuteOptions{
Command: "bash",
Envs: map[string]string{
"AUTORESTIC_LOCATION": l.name,
},
}
if err := l.validate(); err != nil {
errors = append(errors, err)
colors.Error.Print(err)
goto after
}
if t == TypeLocal {
dir, _ := GetPathRelativeToConfig(l.From)
colors.Faint.Printf("Executing under: \"%s\"\n", dir)
options.Dir = dir
}
// Hooks
if err := ExecuteHooks(l.Hooks.Before, options); err != nil {
return err
errors = append(errors, err)
goto after
}
// Backup
for _, to := range l.To {
for i, to := range l.To {
backend, _ := GetBackend(to)
colors.Secondary.Printf("Backend: %s\n", backend.name)
env, err := backend.getEnv()
if err != nil {
return nil
errors = append(errors, err)
continue
}
flags := l.getOptions("backup")
lFlags := getOptions(l.Options, "backup")
bFlags := getOptions(backend.Options, "backup")
cmd := []string{"backup"}
cmd = append(cmd, flags...)
cmd = append(cmd, lFlags...)
cmd = append(cmd, bFlags...)
if cron {
cmd = append(cmd, "--tag", "cron")
}
@@ -181,7 +188,15 @@ func (l Location) Backup(cron bool) error {
}
if err != nil {
colors.Error.Println(out)
return err
errors = append(errors, 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
}
if VERBOSE {
colors.Faint.Println(out)
@@ -190,10 +205,22 @@ func (l Location) Backup(cron bool) error {
// After hooks
if err := ExecuteHooks(l.Hooks.After, options); err != nil {
return err
errors = append(errors, err)
}
after:
var commands []string
if len(errors) > 0 {
commands = l.Hooks.Failure
} else {
commands = l.Hooks.Success
}
if err := ExecuteHooks(commands, options); err != nil {
errors = append(errors, err)
}
colors.Success.Println("Done")
return nil
return errors
}
func (l Location) Forget(prune bool, dry bool) error {
@@ -214,7 +241,8 @@ func (l Location) Forget(prune bool, dry bool) error {
options := ExecuteOptions{
Envs: env,
}
flags := l.getOptions("forget")
lFlags := getOptions(l.Options, "forget")
bFlags := getOptions(backend.Options, "forget")
cmd := []string{"forget", "--path", path}
if prune {
cmd = append(cmd, "--prune")
@@ -222,7 +250,8 @@ func (l Location) Forget(prune bool, dry bool) error {
if dry {
cmd = append(cmd, "--dry-run")
}
cmd = append(cmd, flags...)
cmd = append(cmd, lFlags...)
cmd = append(cmd, bFlags...)
out, err := ExecuteResticCommand(options, cmd...)
if VERBOSE {
colors.Faint.Println(out)

View File

@@ -0,0 +1,22 @@
package metadata
import (
"regexp"
"strings"
)
type addedExtractor struct {
re *regexp.Regexp
}
func (e addedExtractor) Matches(line string) bool {
return e.re.MatchString(line)
}
func (e addedExtractor) Extract(metadata *BackupLogMetadata, line string) {
// Sample line: "Added to the repo: 0 B"
metadata.AddedSize = strings.TrimSpace(e.re.ReplaceAllString(line, ""))
}
func NewAddedExtractor() MetadatExtractor {
return addedExtractor{regexp.MustCompile(`(?i)^Added to the repo:`)}
}

View File

@@ -0,0 +1,57 @@
package metadata
import (
"regexp"
"strings"
)
type ChangeSetExtractor struct {
re *regexp.Regexp
cleaner *regexp.Regexp
saver changeSetSaver
}
func (e ChangeSetExtractor) Matches(line string) bool {
return e.re.MatchString(line)
}
func (e ChangeSetExtractor) Extract(metadata *BackupLogMetadata, line string) {
// Sample line: "Files: 0 new, 0 changed, 2 unmodified"
trimmed := strings.TrimSpace(e.re.ReplaceAllString(line, ""))
splitted := strings.Split(trimmed, ",")
var changeset BackupLogMetadataChangeset = BackupLogMetadataChangeset{}
changeset.Added = e.cleaner.ReplaceAllString(splitted[0], "")
changeset.Changed = e.cleaner.ReplaceAllString(splitted[1], "")
changeset.Unmodified = e.cleaner.ReplaceAllString(splitted[2], "")
e.saver.Save(metadata, changeset)
}
type changeSetSaver interface {
Save(metadata *BackupLogMetadata, changeset BackupLogMetadataChangeset)
}
type fileSaver struct{}
func (f fileSaver) Save(metadata *BackupLogMetadata, changeset BackupLogMetadataChangeset) {
metadata.Files = changeset
}
type dirsSaver struct{}
func (d dirsSaver) Save(metadata *BackupLogMetadata, changeset BackupLogMetadataChangeset) {
metadata.Dirs = changeset
}
func NewFilesExtractor() MetadatExtractor {
return ChangeSetExtractor{
re: regexp.MustCompile(`(?i)^Files:`),
cleaner: regexp.MustCompile(`[^\d]`),
saver: fileSaver{},
}
}
func NewDirsExtractor() MetadatExtractor {
return ChangeSetExtractor{
re: regexp.MustCompile(`(?i)^Dirs:`),
cleaner: regexp.MustCompile(`[^\d]`),
saver: dirsSaver{},
}
}

View File

@@ -0,0 +1,22 @@
package metadata
import (
"regexp"
"strings"
)
type parentSnapshotIDExtractor struct {
re *regexp.Regexp
}
func (e parentSnapshotIDExtractor) Matches(line string) bool {
return e.re.MatchString(line)
}
func (e parentSnapshotIDExtractor) Extract(metadata *BackupLogMetadata, line string) {
// Sample line: "using parent snapshot c65d9310"
metadata.ParentSnapshotID = strings.TrimSpace(e.re.ReplaceAllString(line, ""))
}
func NewParentSnapshotIDExtractor() MetadatExtractor {
return parentSnapshotIDExtractor{regexp.MustCompile(`(?i)^using parent snapshot`)}
}

View File

@@ -0,0 +1,32 @@
package metadata
import (
"regexp"
"strings"
)
type processedExtractor struct {
re *regexp.Regexp
cleaner *regexp.Regexp
}
func (e processedExtractor) Matches(line string) bool {
return e.re.MatchString(line)
}
func (e processedExtractor) Extract(metadata *BackupLogMetadata, line string) {
// Sample line: "processed 2 files, 24 B in 0:00"
var processed = BackupLogMetadataProcessed{}
split := strings.Split(line, "in")
processed.Duration = strings.TrimSpace(split[1])
split = strings.Split(split[0], ",")
processed.Files = e.cleaner.ReplaceAllString(split[0], "")
processed.Size = strings.TrimSpace(split[1])
metadata.Processed = processed
}
func NewProcessedExtractor() MetadatExtractor {
return processedExtractor{
regexp.MustCompile(`(?i)^processed \d* files`),
regexp.MustCompile(`(?i)[^\d]`),
}
}

View File

@@ -0,0 +1,22 @@
package metadata
import (
"regexp"
"strings"
)
type snapshotExtractor struct {
re *regexp.Regexp
}
func (e snapshotExtractor) Matches(line string) bool {
return e.re.MatchString(line)
}
func (e snapshotExtractor) Extract(metadata *BackupLogMetadata, line string) {
// Sample line: "snapshot 917c7691 saved"
metadata.SnapshotID = strings.Split(line, " ")[1]
}
func NewSnapshotExtractor() MetadatExtractor {
return snapshotExtractor{regexp.MustCompile(`(?i)^snapshot \w+ saved`)}
}

View File

@@ -0,0 +1,72 @@
package metadata
import (
"strings"
)
type BackupLogMetadataChangeset struct {
Added string
Changed string
Unmodified string
}
type BackupLogMetadataProcessed struct {
Files string
Size string
Duration string
}
type BackupLogMetadata struct {
ParentSnapshotID string
Files BackupLogMetadataChangeset
Dirs BackupLogMetadataChangeset
AddedSize string
Processed BackupLogMetadataProcessed
SnapshotID string
}
type MetadatExtractor interface {
Matches(line string) bool
Extract(metadata *BackupLogMetadata, line string)
}
var extractors = []MetadatExtractor{
NewParentSnapshotIDExtractor(),
NewFilesExtractor(),
NewDirsExtractor(),
NewAddedExtractor(),
NewProcessedExtractor(),
NewSnapshotExtractor(),
}
func ExtractMetadataFromBackupLog(log string) BackupLogMetadata {
var md BackupLogMetadata
for _, line := range strings.Split(log, "\n") {
line = strings.TrimSpace(line)
for _, extractor := range extractors {
if extractor.Matches(line) {
extractor.Extract(&md, line)
continue
}
}
}
return md
}
func MakeEnvFromMetadata(metadata *BackupLogMetadata) map[string]string {
env := make(map[string]string)
var prefix = "AUTORESTIC_"
env[prefix+"SNAPSHOT_ID"] = metadata.SnapshotID
env[prefix+"PARENT_SNAPSHOT_ID"] = metadata.ParentSnapshotID
env[prefix+"FILES_ADDED"] = metadata.Files.Added
env[prefix+"FILES_CHANGED"] = metadata.Files.Changed
env[prefix+"FILES_UNMODIFIED"] = metadata.Files.Unmodified
env[prefix+"DIRS_ADDED"] = metadata.Dirs.Added
env[prefix+"DIRS_CHANGED"] = metadata.Dirs.Changed
env[prefix+"DIRS_UNMODIFIED"] = metadata.Dirs.Unmodified
env[prefix+"ADDED_SIZE"] = metadata.AddedSize
env[prefix+"PROCESSED_FILES"] = metadata.Processed.Files
env[prefix+"PROCESSED_SIZE"] = metadata.Processed.Size
env[prefix+"PROCESSED_DURATION"] = metadata.Processed.Duration
return env
}

View File

@@ -10,13 +10,15 @@ import (
"github.com/cupcakearmy/autorestic/internal/colors"
)
var RESTIC_BIN string
func CheckIfCommandIsCallable(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
func CheckIfResticIsCallable() bool {
return CheckIfCommandIsCallable("restic")
return CheckIfCommandIsCallable(RESTIC_BIN)
}
type ExecuteOptions struct {
@@ -50,7 +52,7 @@ func ExecuteCommand(options ExecuteOptions, args ...string) (string, error) {
}
func ExecuteResticCommand(options ExecuteOptions, args ...string) (string, error) {
options.Command = "restic"
options.Command = RESTIC_BIN
return ExecuteCommand(options, args...)
}