mirror of
https://github.com/cupcakearmy/autorestic.git
synced 2025-09-06 10:30:39 +00:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
ddc3accb30 | |||
7874512ec0 | |||
0b5f4017e4 | |||
88198c4fcb | |||
8cd759105f | |||
6137e31c3b | |||
2789502c89 | |||
b87381cd3b | |||
048a5ffed8 | |||
02a8e461d4 | |||
a1abe13a39 | |||
ddf287f6f5 | |||
56f82ae656 | |||
95b1ca3297 | |||
|
7a8830cb2f | ||
959d19cbdb | |||
9dc7763445 | |||
|
50984f6771 | ||
e80f200873 | |||
86ae70672a | |||
5c0788900f | |||
20334a7e83 | |||
7bebd04482 | |||
b9b8857bf4 | |||
dd6e618161 |
21
CHANGELOG.md
21
CHANGELOG.md
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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")
|
||||||
|
22
cmd/root.go
22
cmd/root.go
@@ -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()
|
||||||
|
@@ -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)
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
36
docs/markdown/backend/env.md
Normal file
36
docs/markdown/backend/env.md
Normal 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
|
@@ -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
|
||||||
|
11
docs/markdown/community.md
Normal file
11
docs/markdown/community.md
Normal 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
|
@@ -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
|
||||||
|
@@ -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.
|
||||||
|
@@ -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
|
||||||
|
@@ -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
878
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
1
go.mod
1
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||||
|
@@ -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()
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
22
internal/metadata/extractor_added.go
Normal file
22
internal/metadata/extractor_added.go
Normal 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:`)}
|
||||||
|
}
|
57
internal/metadata/extractor_changeset.go
Normal file
57
internal/metadata/extractor_changeset.go
Normal 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{},
|
||||||
|
}
|
||||||
|
}
|
22
internal/metadata/extractor_parent.go
Normal file
22
internal/metadata/extractor_parent.go
Normal 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`)}
|
||||||
|
}
|
32
internal/metadata/extractor_processed.go
Normal file
32
internal/metadata/extractor_processed.go
Normal 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]`),
|
||||||
|
}
|
||||||
|
}
|
22
internal/metadata/extractor_snapshot.go
Normal file
22
internal/metadata/extractor_snapshot.go
Normal 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`)}
|
||||||
|
}
|
72
internal/metadata/metadata.go
Normal file
72
internal/metadata/metadata.go
Normal 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
|
||||||
|
}
|
Reference in New Issue
Block a user