Compare commits

..

25 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
28 changed files with 585 additions and 827 deletions

View File

@@ -5,6 +5,27 @@ 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.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 ## [1.1.2] - 2021-07-11
### Fixes ### Fixes

View File

@@ -19,7 +19,7 @@ Releases are automatically built by the github workflow and uploaded to the rele
1. Bump `VERSION` in `internal/config.go`. 1. Bump `VERSION` in `internal/config.go`.
2. Update `CHANGELOG.md` 2. Update `CHANGELOG.md`
3. Commit to master 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. 5. The Github action will build the binaries, upload and mark the release as ready when done.
### Brew ### Brew

View File

@@ -25,7 +25,7 @@
### 💭 Why / What? ### 💭 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 ### 🌈 Features

View File

@@ -13,12 +13,11 @@ var backupCmd = &cobra.Command{
Use: "backup", Use: "backup",
Short: "Create backups for given locations", Short: "Create backups for given locations",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
internal.GetConfig()
err := lock.Lock() err := lock.Lock()
CheckErr(err) CheckErr(err)
defer lock.Unlock() defer lock.Unlock()
internal.GetConfig()
selected, err := internal.GetAllOrSelected(cmd, false) selected, err := internal.GetAllOrSelected(cmd, false)
CheckErr(err) CheckErr(err)
errors := 0 errors := 0

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package cmd
import ( import (
"os" "os"
"path/filepath"
"github.com/cupcakearmy/autorestic/internal" "github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors" "github.com/cupcakearmy/autorestic/internal/colors"
@@ -50,11 +51,24 @@ func initConfig() {
if cfgFile != "" { if cfgFile != "" {
viper.SetConfigFile(cfgFile) viper.SetConfigFile(cfgFile)
} else { } else {
home, err := homedir.Dir()
CheckErr(err)
viper.AddConfigPath(".") viper.AddConfigPath(".")
viper.AddConfigPath(home)
// 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.SetConfigName(".autorestic")
} }
viper.AutomaticEnv() viper.AutomaticEnv()

View File

@@ -23,6 +23,7 @@
> [Overview](/backend/overview) > [Overview](/backend/overview)
> [Available Backends](/backend/available) > [Available Backends](/backend/available)
> [Options](/backend/options) > [Options](/backend/options)
> [Environment](/backend/env)
> :Collapse label=CLI > :Collapse label=CLI
> >
@@ -40,7 +41,7 @@
> [Upgrade](/cli/upgrade) > [Upgrade](/cli/upgrade)
[Examples](/examples) [Examples](/examples)
[QA](/qa) [QA](/qa)
[Community](/community)
[Contributors](/contrib) [Contributors](/contrib)

View File

@@ -19,9 +19,9 @@ backends:
backends: backends:
name-of-backend: name-of-backend:
type: b2 type: b2
path: 'backblaze_bucketID' path: 'bucket_name'
# Or With a path # Or With a path
# path: 'backblaze_bucketID:/some/path' # path: 'bucket_name:/some/path'
env: env:
B2_ACCOUNT_ID: 'backblaze_keyID' B2_ACCOUNT_ID: 'backblaze_keyID'
B2_ACCOUNT_KEY: 'backblaze_applicationKey' B2_ACCOUNT_KEY: 'backblaze_applicationKey'
@@ -29,7 +29,7 @@ backends:
#### API Keys gotcha #### 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 ## 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

@@ -18,6 +18,8 @@ backend:
In this example, whenever `autorestic` runs `restic backup` it will append a `--tag abc --tag` to the native command. 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 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 > :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

@@ -40,4 +40,44 @@ backends:
path: /mnt/my_external_storage 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 > :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" PATH="/usr/local/bin:/usr/bin:/bin"
# Example running every 5 minutes # 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 > 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 ⏱ 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. > Also note that manually triggered backups with `autorestic backup` will not influence the cron timeline, they are willingly not linked.

View File

@@ -35,8 +35,46 @@ locations:
2. Run backup 2. Run backup
3. `after` hook 3. `after` hook
4. - `success` hook if no errors were found 4. - `success` hook if no errors were found
- `failure` hook if at least error was encountered - `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. 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 > :ToCPrevNext

View File

@@ -18,4 +18,6 @@ locations:
In this example, whenever `autorestic` runs `restic backup` it will append a `--tag abc --tag` to the native command. 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 > :ToCPrevNext

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/blang/semver/v4 v4.0.0
github.com/buger/goterm v1.0.0 github.com/buger/goterm v1.0.0
github.com/fatih/color v1.10.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/mitchellh/go-homedir v1.1.0
github.com/robfig/cron v1.2.0 github.com/robfig/cron v1.2.0
github.com/spf13/cobra v1.1.3 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/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 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 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/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 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= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=

View File

@@ -58,7 +58,15 @@ func (b Backend) generateRepo() (string, error) {
func (b Backend) getEnv() (map[string]string, error) { func (b Backend) getEnv() (map[string]string, error) {
env := make(map[string]string) env := make(map[string]string)
env["RESTIC_PASSWORD"] = b.Key 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() repo, err := b.generateRepo()
env["RESTIC_REPOSITORY"] = repo env["RESTIC_REPOSITORY"] = repo
for key, value := range b.Env { for key, value := range b.Env {
@@ -77,6 +85,17 @@ func generateRandomKey() string {
return key 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 { func (b Backend) validate() error {
if b.Type == "" { if b.Type == "" {
return fmt.Errorf(`Backend "%s" has no "type"`, b.name) return fmt.Errorf(`Backend "%s" has no "type"`, b.name)
@@ -85,14 +104,18 @@ func (b Backend) validate() error {
return fmt.Errorf(`Backend "%s" has no "path"`, b.name) return fmt.Errorf(`Backend "%s" has no "path"`, b.name)
} }
if b.Key == "" { if b.Key == "" {
key := generateRandomKey() // Check if key is set in environment
b.Key = key if _, err := b.getKey(); err != nil {
c := GetConfig() // If not generate a new one
tmp := c.Backends[b.name] key := generateRandomKey()
tmp.Key = key b.Key = key
c.Backends[b.name] = tmp c := GetConfig()
if err := c.SaveConfig(); err != nil { tmp := c.Backends[b.name]
return err tmp.Key = key
c.Backends[b.name] = tmp
if err := c.SaveConfig(); err != nil {
return err
}
} }
} }
env, err := b.getEnv() env, err := b.getEnv()

View File

@@ -2,23 +2,28 @@ package internal
import ( import (
"fmt" "fmt"
"os"
"path" "path"
"path/filepath"
"strings" "strings"
"sync" "sync"
"github.com/cupcakearmy/autorestic/internal/colors" "github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/joho/godotenv"
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
const VERSION = "1.1.2" const VERSION = "1.3.0"
var CI bool = false var CI bool = false
var VERBOSE bool = false var VERBOSE bool = false
var CRON_LEAN bool = false var CRON_LEAN bool = false
type Config struct { type Config struct {
Extras interface{} `yaml:"extras"`
Locations map[string]Location `yaml:"locations"` Locations map[string]Location `yaml:"locations"`
Backends map[string]Backend `yaml:"backends"` Backends map[string]Backend `yaml:"backends"`
} }
@@ -31,7 +36,14 @@ func GetConfig() *Config {
once.Do(func() { once.Do(func() {
if err := viper.ReadInConfig(); err == nil { if err := viper.ReadInConfig(); err == nil {
if !CRON_LEAN { 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 { } else {
return return
@@ -39,7 +51,9 @@ func GetConfig() *Config {
config = &Config{} config = &Config{}
if err := viper.UnmarshalExact(config); err != nil { if err := viper.UnmarshalExact(config); err != nil {
panic(err) colors.Error.Println("Could not parse config file!")
lock.Unlock()
os.Exit(1)
} }
}) })
} }
@@ -225,7 +239,18 @@ func getOptions(options Options, key string) []string {
var selected []string var selected []string
for k, values := range options[key] { for k, values := range options[key] {
for _, value := range values { for _, value := range values {
selected = append(selected, fmt.Sprintf("--%s", k), value) // 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 return selected

View File

@@ -10,6 +10,7 @@ import (
"github.com/cupcakearmy/autorestic/internal/colors" "github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/lock" "github.com/cupcakearmy/autorestic/internal/lock"
"github.com/cupcakearmy/autorestic/internal/metadata"
"github.com/robfig/cron" "github.com/robfig/cron"
) )
@@ -30,7 +31,7 @@ type Hooks struct {
Failure HookArray `yaml:"failure,omitempty"` Failure HookArray `yaml:"failure,omitempty"`
} }
type Options map[string]map[string][]string type Options map[string]map[string][]interface{}
type Location struct { type Location struct {
name string `yaml:",omitempty"` name string `yaml:",omitempty"`
@@ -130,10 +131,20 @@ func (l Location) Backup(cron bool) []error {
t := l.getType() t := l.getType()
options := ExecuteOptions{ options := ExecuteOptions{
Command: "bash", 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 { if t == TypeLocal {
dir, _ := GetPathRelativeToConfig(l.From) dir, _ := GetPathRelativeToConfig(l.From)
colors.Faint.Printf("Executing under: \"%s\"\n", dir)
options.Dir = dir options.Dir = dir
} }
@@ -144,7 +155,7 @@ func (l Location) Backup(cron bool) []error {
} }
// Backup // Backup
for _, to := range l.To { for i, to := range l.To {
backend, _ := GetBackend(to) backend, _ := GetBackend(to)
colors.Secondary.Printf("Backend: %s\n", backend.name) colors.Secondary.Printf("Backend: %s\n", backend.name)
env, err := backend.getEnv() env, err := backend.getEnv()
@@ -180,6 +191,13 @@ func (l Location) Backup(cron bool) []error {
errors = append(errors, err) errors = append(errors, err)
continue 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 { if VERBOSE {
colors.Faint.Println(out) 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
}