Compare commits

...

58 Commits

Author SHA1 Message Date
7f9251f06b Merge pull request #123 from cupcakearmy/1.4.1
allow all values from envs
2021-10-31 22:47:33 +01:00
09cfa4a98e changelog 2021-10-31 22:46:37 +01:00
05c3458a95 version bump 2021-10-31 22:45:54 +01:00
2826f9586d allow all values from envs 2021-10-31 22:45:03 +01:00
f71425be5b Merge pull request #122 from n194/patch-1
Remove credit from AUR
2021-10-31 14:47:16 +01:00
n
d78fbb6650 Remove credit from AUR 2021-10-31 19:40:53 +09:00
de663f287c Merge pull request #118 from cupcakearmy/1.4.0
1.4.0
2021-10-30 15:02:27 +02:00
439076d7ab chengelog 2021-10-30 14:54:41 +02:00
8c30134f7c error handling for upgrade and uninstall 2021-10-30 14:50:27 +02:00
11d1da7468 use wget 2021-10-30 14:38:53 +02:00
92b1577634 use wget 2021-10-30 14:36:25 +02:00
a7944aed1f don't install restic as default 2021-10-30 14:36:16 +02:00
3b99e301e9 only use wget 2021-10-30 14:30:53 +02:00
246e6fc0d8 enable generic env variables 2021-10-30 13:48:44 +02:00
4055ebf8e8 changelog 2021-10-29 18:35:24 +02:00
6be0a80b29 use built in function 2021-10-29 18:35:21 +02:00
efd4a7dfea version bump 2021-10-28 18:10:33 +02:00
440609220c support for global flags 2021-10-28 18:10:14 +02:00
fb6217d868 docs for global flags 2021-10-28 18:09:38 +02:00
1628384e1f docs 2021-10-28 17:35:51 +02:00
3e80e6d18e Merge pull request #117 from cupcakearmy/specific-location
specific location
2021-10-28 17:30:51 +02:00
83905d2993 specific location 2021-10-28 17:29:32 +02:00
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
41 changed files with 837 additions and 900 deletions

View File

@@ -5,6 +5,62 @@ 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.4.1] - 2021-10-31
### Fixes
- Numeric values from config files not being passed to env.
## [1.4.0] - 2021-10-30
### Added
- Allow specify to specify a backend for location backup
- Global restic flags
- Generic ENV support for backends
### Changed
- Install now only requires `wget`
- Env variable for the `KEY` has been renamed from `AUTORESTIC_[BACKEND NAME]_KEY` -> `AUTORESTIC_[BACKEND NAME]_RESTIC_PASSWORD`
### Fixed
- Error handling during upgrade & uninstall
## [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

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`.
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

View File

@@ -25,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

@@ -2,6 +2,7 @@ package cmd
import (
"fmt"
"strings"
"github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors"
@@ -13,18 +14,22 @@ 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()
internal.GetConfig()
selected, err := internal.GetAllOrSelected(cmd, false)
CheckErr(err)
errors := 0
for _, name := range selected {
location, _ := internal.GetLocation(name)
errs := location.Backup(false)
var splitted = strings.Split(name, "@")
var specificBackend = ""
if len(splitted) > 1 {
specificBackend = splitted[1]
}
location, _ := internal.GetLocation(splitted[0])
errs := location.Backup(false, specificBackend)
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"
@@ -50,11 +51,24 @@ func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := homedir.Dir()
CheckErr(err)
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.AutomaticEnv()

View File

@@ -9,12 +9,12 @@ var uninstallCmd = &cobra.Command{
Use: "uninstall",
Short: "Uninstall restic and autorestic",
Run: func(cmd *cobra.Command, args []string) {
noRestic, _ := cmd.Flags().GetBool("no-restic")
bins.Uninstall(!noRestic)
restic, _ := cmd.Flags().GetBool("restic")
bins.Uninstall(restic)
},
}
func init() {
rootCmd.AddCommand(uninstallCmd)
uninstallCmd.Flags().Bool("no-restic", false, "do not uninstall restic.")
uninstallCmd.Flags().Bool("restic", false, "also uninstall restic.")
}

View File

@@ -9,13 +9,13 @@ var upgradeCmd = &cobra.Command{
Use: "upgrade",
Short: "Upgrade autorestic and restic",
Run: func(cmd *cobra.Command, args []string) {
noRestic, _ := cmd.Flags().GetBool("no-restic")
err := bins.Upgrade(!noRestic)
restic, _ := cmd.Flags().GetBool("restic")
err := bins.Upgrade(restic)
CheckErr(err)
},
}
func init() {
rootCmd.AddCommand(upgradeCmd)
upgradeCmd.Flags().Bool("no-restic", false, "also update restic")
upgradeCmd.Flags().Bool("restic", true, "also update restic")
}

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

@@ -4,6 +4,8 @@ In theory [all the restic backends](https://restic.readthedocs.io/en/stable/030_
Those tested are the following:
> You can also [specify the `env` variables in a config file](/backend/env) to separate them from the config file.
## Local
```yaml
@@ -19,9 +21,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 +31,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,67 @@
# Environment
> ⚠ Available since version `v1.4.0`
Sometimes it's favorable not having the encryption keys in the config files.
For that `autorestic` allows passing the env variables to backend password as `ENV` variables, or through an env file.
You can also pass whatever `env` variable to restic by prefixing it with `AUTORESTIC_[BACKEND NAME]_`.
> Env variables and file overwrite the config file in the following order:
>
> Env Variables > Env File (`.autorestic.env`) > Config file (`.autorestic.yaml`)
## Env file
Alternatively `autorestic` can load an env file, located next to `.autorestic.yml` called `.autorestic.env`.
```
AUTORESTIC_FOO_RESTIC_PASSWORD=secret123
```
### Example with repository password
The syntax for the `ENV` variables is as follows: `AUTORESTIC_[BACKEND NAME]_RESTIC_PASSWORD`.
```yaml | autorestic.yaml
backend:
foo:
type: ...
path: ...
key: secret123 # => AUTORESTIC_FOO_RESTIC_PASSWORD=secret123
```
This means we could remove `key: secret123` from `.autorestic.yaml` and execute as follows:
```bash
AUTORESTIC_FOO_RESTIC_PASSWORD=secret123 autorestic backup ...
```
### Example with Backblaze B2
```yaml | autorestic.yaml
backends:
bb:
type: b2
path: myBucket
key: myPassword
env:
B2_ACCOUNT_ID: 123
B2_ACCOUNT_KEY: 456
```
You could create an `.autorestic.env` or pass the following `ENV` variables to autorestic:
```
AUTORESTIC_BB_RESTIC_PASSWORD=myPassword
AUTORESTIC_BB_B2_ACCOUNT_ID=123
AUTORESTIC_BB_B2_ACCOUNT_KEY=456
```
```yaml | autorestic.yaml
backends:
bb:
type: b2
path: myBucket
```
> :ToCPrevNext

View File

@@ -0,0 +1,19 @@
# Options
> For more detail see the [location docs](/location/options) for options, as they are the same.
```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.
> :ToCPrevNext

View File

@@ -14,4 +14,12 @@ autorestic backup -a
autorestic backup -l foo -l bar
```
## Specific location
`autorestic` also allows selecting specific backends for a location with the `location@backend` syntax.
```bash
autorestic backup -l location@backend
```
> :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

@@ -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,12 +31,53 @@ backends:
remote:
type: b2
path: 'myBucket:backup/home'
B2_ACCOUNT_ID: account_id
B2_ACCOUNT_KEY: account_key
env:
B2_ACCOUNT_ID: account_id
B2_ACCOUNT_KEY: account_key
hdd:
type: local
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

@@ -2,10 +2,10 @@
Linux & macOS. Windows is not supported. If you have problems installing please open an issue :)
Autorestic requires `curl`, `wget` and `bzip2` to be installed. For most systems these should be already installed.
Autorestic requires `bash`, `wget` and `bzip2` to be installed. For most systems these should be already installed.
```bash
curl -s https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash
wget -qO - https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash
```
## Alternatives
@@ -20,6 +20,6 @@ If you are on macOS you can install through brew: `brew install autorestic`.
### AUR
If you are on Arch there is an [AUR Package](https://aur.archlinux.org/packages/autorestic-bin/) by @n194.
If you are on Arch there is an [AUR Package](https://aur.archlinux.org/packages/autorestic-bin/) (looking for maintainers).
> :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

@@ -35,8 +35,46 @@ locations:
2. Run backup
3. `after` hook
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.
## 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

@@ -1,6 +1,32 @@
# Options
For the `backup` and `forget` commands you can pass any native flags to `restic`.
For the `backup` and `forget` commands you can pass any native flags to `restic`. In addition you can specify flags for every command with `all`.
If flags don't start with `-` they will get prefixed with `--`.
Flags without arguments can be set to `true`. They will be handled accordingly.
> It is also possible to set options for an [entire backend](/backend/options) or globally (see below).
```yaml
locations:
foo:
# ...
options:
all:
some-flag: 123
# Equivalent to
--some-flag: 123
backup:
boolean-flag: true
tag:
- foo
- bar
```
## Example
In this example, whenever `autorestic` runs `restic backup` it will append a `--tag abc --tag` to the native command.
```yaml
locations:
@@ -14,6 +40,22 @@ locations:
- bar
```
In this example, whenever `autorestic` runs `restic backup` it will append a `--tag abc --tag` to the native command.
## Global Options
It is possible to specify global flags that will be run every time restic is invoked. To do so specify them under `global` in your config file.
```yaml
global:
all:
cache-dir: ~/restic
backup:
tag:
- foo
backends:
# ...
locations:
# ...
```
> :ToCPrevNext

View File

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

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,13 +23,15 @@ 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
fi
echo $ARCH
curl -s https://api.github.com/repos/cupcakearmy/autorestic/releases/latest \
wget -qO - https://api.github.com/repos/cupcakearmy/autorestic/releases/latest \
| grep "browser_download_url.*_${OS}_${ARCH}" \
| cut -d : -f 2,3 \
| tr -d \" \

View File

@@ -17,12 +17,13 @@ type BackendRest struct {
}
type Backend struct {
name string
Type string `yaml:"type,omitempty"`
Path string `yaml:"path,omitempty"`
Key string `yaml:"key,omitempty"`
Env map[string]string `yaml:"env,omitempty"`
Rest BackendRest `yaml:"rest,omitempty"`
name string
Type string `yaml:"type,omitempty"`
Path string `yaml:"path,omitempty"`
Key string `yaml:"key,omitempty"`
Env map[string]string `yaml:"env,omitempty"`
Rest BackendRest `yaml:"rest,omitempty"`
Options Options `yaml:"options,omitempty"`
}
func GetBackend(name string) (Backend, bool) {
@@ -57,12 +58,27 @@ func (b Backend) generateRepo() (string, error) {
func (b Backend) getEnv() (map[string]string, error) {
env := make(map[string]string)
env["RESTIC_PASSWORD"] = b.Key
// Key
if b.Key != "" {
env["RESTIC_PASSWORD"] = b.Key
}
// From config file
repo, err := b.generateRepo()
env["RESTIC_REPOSITORY"] = repo
for key, value := range b.Env {
env[strings.ToUpper(key)] = value
}
// From Envfile and passed as env
var prefix = "AUTORESTIC_" + strings.ToUpper(b.name) + "_"
for _, variable := range os.Environ() {
var splitted = strings.SplitN(variable, "=", 2)
if strings.HasPrefix(splitted[0], prefix) {
env[strings.TrimPrefix(splitted[0], prefix)] = splitted[1]
}
}
return env, err
}
@@ -84,14 +100,19 @@ func (b Backend) validate() error {
return fmt.Errorf(`Backend "%s" has no "path"`, b.name)
}
if b.Key == "" {
key := generateRandomKey()
b.Key = key
c := GetConfig()
tmp := c.Backends[b.name]
tmp.Key = key
c.Backends[b.name] = tmp
if err := c.SaveConfig(); err != nil {
return err
// Check if key is set in environment
env, _ := b.getEnv()
if _, found := env["RESTIC_PASSWORD"]; !found {
// No key set in config file or env => generate random key and save file
key := generateRandomKey()
b.Key = key
c := GetConfig()
tmp := c.Backends[b.name]
tmp.Key = key
c.Backends[b.name] = tmp
if err := c.SaveConfig(); err != nil {
return err
}
}
}
env, err := b.getEnv()

View File

@@ -47,11 +47,11 @@ func dlJSON(url string) (GithubRelease, error) {
func Uninstall(restic bool) error {
if err := os.Remove(path.Join(INSTALL_PATH, "autorestic")); err != nil {
colors.Error.Println(err)
return err
}
if restic {
if err := os.Remove(path.Join(INSTALL_PATH, "restic")); err != nil {
colors.Error.Println(err)
return err
}
}
return nil
@@ -79,11 +79,15 @@ func downloadAndInstallAsset(body GithubRelease, name string) error {
return err
}
defer tmp.Close()
tmp.Chmod(0755)
io.Copy(tmp, bz)
if err := tmp.Chmod(0755); err != nil {
return err
}
if _, err := io.Copy(tmp, bz); err != nil {
return err
}
to := path.Join(INSTALL_PATH, name)
os.Remove(to) // Delete if current, ignore error if file does not exits.
defer os.Remove(to) // Delete if current, ignore error if file does not exits.
if err := os.Rename(tmp.Name(), to); err != nil {
return nil
}
@@ -121,9 +125,11 @@ func Upgrade(restic bool) error {
// Upgrade restic
if restic {
if err := InstallRestic(); err != nil {
colors.Error.Println(err)
return err
}
if err := upgradeRestic(); err != nil {
return err
}
upgradeRestic()
}
// Upgrade self
@@ -140,7 +146,9 @@ func Upgrade(restic bool) error {
return err
}
if current.LT(latest) {
downloadAndInstallAsset(body, "autorestic")
if err := downloadAndInstallAsset(body, "autorestic"); err != nil {
return err
}
colors.Success.Println("Updated autorestic")
} else {
colors.Body.Println("Already up to date")

View File

@@ -2,25 +2,34 @@ 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.1.0"
const VERSION = "1.4.1"
var CI bool = false
var VERBOSE bool = false
var CRON_LEAN bool = false
type OptionMap map[string][]interface{}
type Options map[string]OptionMap
type Config struct {
Extras interface{} `yaml:"extras"`
Locations map[string]Location `yaml:"locations"`
Backends map[string]Backend `yaml:"backends"`
Global Options `yaml:"global"`
}
var once sync.Once
@@ -31,7 +40,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 +55,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)
}
})
}
@@ -140,7 +158,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
}
}
@@ -171,20 +189,18 @@ func GetAllOrSelected(cmd *cobra.Command, backends bool) ([]string, error) {
selected, _ = cmd.Flags().GetStringSlice("location")
}
for _, s := range selected {
found := false
var splitted = strings.Split(s, "@")
for _, l := range list {
if l == s {
found = true
break
if l == splitted[0] {
goto found
}
}
if !found {
if backends {
return nil, fmt.Errorf("invalid backend \"%s\"", s)
} else {
return nil, fmt.Errorf("invalid location \"%s\"", s)
}
if backends {
return nil, fmt.Errorf("invalid backend \"%s\"", s)
} else {
return nil, fmt.Errorf("invalid location \"%s\"", s)
}
found:
}
if len(selected) == 0 {
@@ -220,3 +236,36 @@ func (c *Config) SaveConfig() error {
return viper.WriteConfig()
}
func optionToString(option string) string {
if !strings.HasPrefix(option, "-") {
return "--" + option
}
return option
}
func appendOptionsToSlice(str *[]string, options OptionMap) {
for key, values := range options {
for _, value := range values {
// Bool
asBool, ok := value.(bool)
if ok && asBool {
*str = append(*str, optionToString(key))
continue
}
*str = append(*str, optionToString(key), fmt.Sprint(value))
}
}
}
func getOptions(options Options, key string) []string {
var selected []string
var keys = []string{"all"}
if key != "" {
keys = append(keys, key)
}
for _, key := range keys {
appendOptionsToSlice(&selected, options[key])
}
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"
)
@@ -30,8 +31,6 @@ type Hooks struct {
Failure HookArray `yaml:"failure,omitempty"`
}
type Options map[string]map[string][]string
type Location struct {
name string `yaml:",omitempty"`
From string `yaml:"from,omitempty"`
@@ -47,7 +46,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)
}
@@ -78,17 +77,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
@@ -135,16 +123,27 @@ 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, specificBackend string) []error {
var errors []error
var backends []string
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
}
@@ -155,7 +154,17 @@ func (l Location) Backup(cron bool) []error {
}
// Backup
for _, to := range l.To {
if specificBackend == "" {
backends = l.To
} else {
if l.hasBackend(specificBackend) {
backends = []string{specificBackend}
} else {
errors = append(errors, fmt.Errorf("backup location \"%s\" has no backend \"%s\"", l.name, specificBackend))
return errors
}
}
for i, to := range backends {
backend, _ := GetBackend(to)
colors.Secondary.Printf("Backend: %s\n", backend.name)
env, err := backend.getEnv()
@@ -164,9 +173,11 @@ func (l Location) Backup(cron bool) []error {
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")
}
@@ -189,6 +200,13 @@ func (l Location) Backup(cron bool) []error {
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)
}
@@ -232,7 +250,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")
@@ -240,7 +259,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)
@@ -327,7 +347,7 @@ func (l Location) RunCron() error {
now := time.Now()
if now.After(next) {
lock.SetCron(l.name, now.Unix())
l.Backup(true)
l.Backup(true, "")
} else {
if !CRON_LEAN {
colors.Body.Printf("Skipping \"%s\", not due yet.\n", l.name)

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

@@ -53,6 +53,9 @@ func ExecuteCommand(options ExecuteOptions, args ...string) (string, error) {
func ExecuteResticCommand(options ExecuteOptions, args ...string) (string, error) {
options.Command = RESTIC_BIN
var c = GetConfig()
var optionsAsString = getOptions(c.Global, "")
args = append(optionsAsString, args...)
return ExecuteCommand(options, args...)
}