Compare commits

...

153 Commits

Author SHA1 Message Date
e055e28cfe Merge pull request #125 from cupcakearmy/1.5.0
1.5.0
2021-11-20 17:11:44 +01:00
ed3c17d678 migration docs 2021-11-20 17:09:42 +01:00
8802b74b47 changelog and docs 2021-11-20 17:03:54 +01:00
e94e7030fc docker image 2021-11-20 16:59:13 +01:00
c55e91b8ff version bump 2021-11-20 16:55:20 +01:00
170bdb81ad tags 2021-11-07 11:48:03 +01:00
4126436f7f migration docs 2021-11-07 11:42:45 +01:00
113a97c283 add config version to ensure compatibility 2021-11-06 22:00:44 +01:00
c250391f67 docs 2021-11-01 11:19:27 +01:00
d3b4915d25 deprecation notion 2021-11-01 09:09:29 +01:00
cd7a5cbc13 also enable azure and google cloud 2021-11-01 00:19:32 +01:00
3dd3956d64 support for rclone 2021-11-01 00:16:54 +01:00
59035da46a remove output 2021-10-31 23:58:08 +01:00
90914d2078 changelog 2021-10-31 23:35:52 +01:00
0ae374cd45 docs 2021-10-31 23:35:46 +01:00
3665cea62d foo 2021-10-31 23:11:57 +01:00
cf92d400c2 changelog 2021-10-31 23:11:06 +01:00
696bd14ac7 Merge pull request #124 from cupcakearmy/multiple-paths
Multiple paths
2021-10-31 23:09:23 +01:00
a68e3e426e simplify options handling 2021-10-31 23:07:12 +01:00
27e82c8529 Merge branch 'master' into multiple-paths 2021-10-31 22:54:14 +01:00
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
4fe241e6f3 support for multiple sources 2021-10-31 22:33:02 +01:00
d0b1b86fdd docker runner 2021-10-31 22:32:55 +01:00
14dd41d60d docs 2021-10-31 22:32:34 +01:00
bcfc734cd1 describe multiple sources 2021-10-31 22:32:30 +01:00
6817f494ef util to check if volume exists 2021-10-31 22:32:01 +01:00
2c46f0da0c restore arg help 2021-10-31 22:31:47 +01:00
ac756dfbde bug not showing error messages 2021-10-31 22:31:31 +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
7ad6f7ce9e Merge remote-tracking branch 'origin/master' into multiple-paths 2021-10-30 15:06:13 +02: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
c2f9ed9204 multiple paths 2021-10-30 13:01:31 +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
478e193d78 forgot version bump 2021-05-06 16:17:45 +02:00
11d4c67dce docs 2021-05-06 16:14:29 +02:00
1643309957 changelog 2021-05-06 15:57:28 +02:00
e05386b0b5 add failure and success hooks 2021-05-06 15:55:32 +02:00
aebaf0a225 Merge branch 'master' of https://github.com/cupcakearmy/autorestic 2021-05-06 15:12:37 +02:00
c090013bf5 Update README.md 2021-05-06 15:12:11 +02:00
88c6949208 custom restic binary 2021-05-06 15:04:35 +02:00
9256cdc38c changelog 2021-05-01 22:57:01 +02:00
a8c611e8ce Merge branch 'master' of https://github.com/cupcakearmy/autorestic 2021-05-01 22:54:57 +02:00
d4522c7ffe fix validation for docker volumes 2021-05-01 22:54:52 +02:00
21185d894e Update contrib.md 2021-05-01 17:25:50 +02:00
b119fc7ea5 Merge pull request #72 from themorlan/master
Correcting some typos in the Documentation
2021-05-01 17:25:20 +02:00
themorlan
73f4bcfd48 missing words in sentence 2021-05-01 15:18:05 +02:00
themorlan
3567319314 typo 2021-05-01 15:16:13 +02:00
themorlan
bf19c983d1 typo 2021-05-01 15:15:44 +02:00
themorlan
12e4de110b typo 2021-05-01 15:14:50 +02:00
c9f425ef64 docs on native flags 2021-04-30 10:47:29 +02:00
974f555dff Update contrib.md 2021-04-29 15:18:03 +02:00
1688c1f3c3 add brew docs 2021-04-29 15:16:33 +02:00
29e46d3b5c brew stuff 2021-04-29 14:45:17 +02:00
0335abb669 add sign 2021-04-28 14:51:56 +02:00
b2f9b9a54e typo 2021-04-28 13:50:11 +02:00
2b13a6e13d installtion docs 2021-04-28 13:44:20 +02:00
cc293ea256 Update installation.md 2021-04-28 13:38:20 +02:00
a4ddd5bbcb Update contrib.md 2021-04-28 13:34:58 +02:00
7cbf43b75d Update README.md 2021-04-28 11:13:16 +02:00
bae77c4673 Update README.md 2021-04-28 11:10:36 +02:00
12adeb2b06 changelog 2021-04-28 10:54:33 +02:00
37b26dfc31 consistent casing 2021-04-28 10:54:30 +02:00
c1795b2acc lean flag 2021-04-28 10:54:07 +02:00
b8d12e518c completion docs 2021-04-28 10:34:43 +02:00
50060cf539 explain verbose flag 2021-04-27 18:38:51 +02:00
c33aac42dc quickstart 2021-04-27 18:35:32 +02:00
c359053e0e Update contrib.md 2021-04-27 09:02:39 +02:00
c16340ab26 Update available.md 2021-04-27 09:01:01 +02:00
edc85c4ac3 Merge pull request #60 from david-boles/backblaze
Update Backblaze docs
2021-04-27 08:59:44 +02:00
68682777f2 Merge pull request #59 from david-boles/consistent-config-location
Recommend a consistent config location
2021-04-27 08:26:22 +02:00
David Boles
b6c7922df5 Recommend a consistent config location 2021-04-26 16:47:00 -07:00
David Boles
991b8bec22 Update backblaze docs 2021-04-26 16:38:08 -07:00
bbc32568ad parallel builds 2021-04-26 13:29:26 +02:00
f3c038c716 changelog & version bump 2021-04-26 13:18:02 +02:00
59612a97b6 Merge branch 'master' of https://github.com/cupcakearmy/autorestic 2021-04-26 13:16:01 +02:00
33319a00ef add arm for darwin 2021-04-26 13:15:58 +02:00
8eb14ea14f Update quick.md 2021-04-25 11:19:39 +02:00
70eb9e441f add rclone support 2021-04-24 16:40:53 +02:00
be25af2d76 Merge pull request #53 from TheForcer/master
Fix typo in check output
2021-04-24 13:19:34 +02:00
1c436369f0 check folder 2021-04-24 12:40:33 +02:00
6efcce07b7 show error and exit with bad code if not found 2021-04-23 23:53:57 +02:00
Felix
dc6dd2e712 Fix typo in check output 2021-04-23 19:19:03 +02:00
Felix
68628d3776 Merge pull request #1 from cupcakearmy/master
Update
2021-04-23 19:18:08 +02:00
40988ef3b4 Update contrib.md 2021-04-23 17:53:48 +02:00
fad33fcdaa Merge pull request #51 from TheForcer/master
Fix typos in docs
2021-04-23 17:53:15 +02:00
Felix
8cf8a77558 Fix typos in docs 2021-04-23 17:45:02 +02:00
36998cfd3b changelog 2021-04-23 13:47:21 +02:00
cf03562ea2 docs 2021-04-23 13:45:54 +02:00
188560395d exclude items from config when empty and rest options 2021-04-23 13:11:15 +02:00
bacbd0f806 docs 2021-04-23 12:31:24 +02:00
93bf0388a4 add force flag description 2021-04-22 10:00:52 +02:00
ec8fdbd135 updated contrib list 2021-04-22 01:14:47 +02:00
420934489c Merge pull request #46 from FuzzyMistborn/patch-1
Fix typo in install script
2021-04-22 01:07:11 +02:00
Fuzzy
2ba767c8c3 Fix typo in install script
Fix typo
2021-04-21 14:32:11 -04:00
b489c662c7 docs 2021-04-21 09:41:14 +02:00
6862529a89 move envs to new format 2021-04-21 09:36:47 +02:00
aa96a95600 typos 2021-04-21 09:34:01 +02:00
89e32c298c update the quick guide 2021-04-21 09:27:35 +02:00
873170c6d1 fix auto update 2021-04-20 23:26:57 +02:00
65 changed files with 3457 additions and 2438 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
*
!**/*.go
!build
!cmd
!internal
!go.*

View File

@@ -3,18 +3,51 @@ name: Main
on:
push:
tags:
- 'v*.*.*'
- "v*.*.*"
jobs:
build:
docker:
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Docker Labels
id: meta
uses: crazy-max/ghaction-docker-meta@v2
with:
images: cupcakearmy/autorestic
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '^1.16.3'
go-version: "^1.16.3"
- name: Build
run: go run build/build.go
- name: Sign
uses: tristan-weil/ghaction-checksum-sign-artifact@v1.0.1
with:
path: dist/*
- name: Release
uses: softprops/action-gh-release@v1
with:

View File

@@ -5,6 +5,143 @@ 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.5.0] - 2021-11-20
### Added
- Support for multiple paths
- Improved error handling
- Allow for specific snapshot to be restored
- Docker image
### Fixed
- rclone in docker volumes
### Changed
- [Breaking Change] Declaration of docker volumes. See: https://autorestic.vercel.app/migration/1.4_1.5
- [Breaking Change] Hooks default executing directory now defaults to the config file directory. See: https://autorestic.vercel.app/migration/1.4_1.5
## [1.4.1] - 2021-10-31
### Fixed
- 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
- use custom restic binary
- success & failure hooks
### Fixed
- don't skip other locations on failure
## [1.0.9] - 2021-05-01
### Fixed
- Validation for docker volumes
## [1.0.8] - 2021-04-28
### Added
- `--lean` flag to cron command for less output about skipping backups.
### Fixed
- consistent lower casing in usage descriptions.
## [1.0.7] - 2021-04-26
### Added
- Support for `darwin/arm64` aka Apple Silicon.
- Added support for `arm64` and `aarch64` in install scripts.
## [1.0.6] - 2021-04-24
### Added
- Support for rclone
## [1.0.5] - 2021-04-24
### Fixed
- Correct exit code on backup failure and better logging/output/feedback.
- Check if `from` key is an actual directory.
## [1.0.4] - 2021-04-23
### Added
- Options to add rest username and password in config
### Fixed
- Don't add empty strings when saving config
## [1.0.3] - 2021-04-20
### Fixed
- Auto upgrade script was not working on linux as linux does not support writing to the binary that is being executed
## [1.0.2] - 2021-04-20
### Added

30
DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,30 @@
# Development
## Coding
The easiest way (imo) is to run [`gowatch`](https://github.com/silenceper/gowatch) in a separate terminal and the simply run `./autorestic ...`. `gowatch` will watch the code and automatically rebuild the binary when changes are saved to disk.
## Building
```bash
go run build/build.go
```
This will build and compress binaries for multiple platforms. The output will be put in the `dist` folder.
## Releasing
Releases are automatically built by the github workflow and uploaded to the release.
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 pre-release.
5. The Github action will build the binaries, upload and mark the release as ready when done.
### Brew
1. Download the latest release.
2. Check the checksum with `shasum -a 256 autorestic-1.2.3.tar.gz`
3. Update `url` and `sha256` in the brew repo.
4. Submit PR

12
Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM golang:1.16-alpine as builder
WORKDIR /app
COPY go.* .
RUN go mod download
COPY . .
RUN go build
FROM alpine
RUN apk add --no-cache restic rclone
COPY --from=builder /app/autorestic /usr/bin/autorestic
CMD [ "autorestic" ]

View File

@@ -10,6 +10,13 @@
Config driven, easy backup cli for <a href="https://restic.net/">restic</a>.
<br>
<strong><a href="https://autorestic.vercel.app/">»»» Docs & Getting Started »»»</a></strong>
<br><br>
<a target="_blank" href="https://discord.gg/wS7RpYTYd2">
<img src="https://img.shields.io/discord/252403122348097536" alt="discord badge" />
<img src="https://img.shields.io/github/contributors/cupcakearmy/autorestic" alt="contributor badge" />
<img src="https://img.shields.io/github/downloads/cupcakearmy/autorestic/total" alt="downloads badge" />
<img src="https://img.shields.io/github/v/release/cupcakearmy/autorestic" alt="version badge" />
</a>
</p>
</p>
@@ -18,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
@@ -35,4 +42,9 @@ Autorestic is a wrapper around the amazing [restic](https://restic.net/). While
### ❓ Questions / Support
Check the [discussions page](https://github.com/cupcakearmy/autorestic/discussions)
Check the [discussions page](https://github.com/cupcakearmy/autorestic/discussions) or [join on discord](https://discord.gg/wS7RpYTYd2)
## Contributing / Developing
PRs, feature requests, etc. are welcomed :)
Have a look at [the dev docs](./DEVELOPMENT.md)

View File

@@ -1,9 +0,0 @@
# Releasing
Releases are handled by the CD server with includes checksums for each binary.
```bash
git tag 0.x
git push
git push origin --tags
```

View File

@@ -9,6 +9,7 @@ import (
"os/exec"
"path"
"path/filepath"
"sync"
"github.com/cupcakearmy/autorestic/internal"
)
@@ -16,7 +17,7 @@ import (
var DIR, _ = filepath.Abs("./dist")
var targets = map[string][]string{
"darwin": {"amd64"},
"darwin": {"amd64", "arm64"},
"freebsd": {"386", "amd64", "arm"},
"linux": {"386", "amd64", "arm", "arm64"},
"netbsd": {"386", "amd64"},
@@ -27,7 +28,7 @@ type buildOptions struct {
Target, Arch, Version string
}
func build(options buildOptions) error {
func build(options buildOptions, wg *sync.WaitGroup) {
fmt.Printf("Building %s %s\n", options.Target, options.Arch)
out := fmt.Sprintf("autorestic_%s_%s_%s", options.Version, options.Target, options.Arch)
out = path.Join(DIR, out)
@@ -46,7 +47,7 @@ func build(options buildOptions) error {
)
err := c.Run()
if err != nil {
return err
panic(err)
}
}
@@ -58,26 +59,25 @@ func build(options buildOptions) error {
c.Stderr = os.Stderr
err := c.Run()
if err != nil {
return err
panic(err)
}
}
return nil
wg.Done()
}
func main() {
os.RemoveAll(DIR)
v := internal.VERSION
var wg sync.WaitGroup
for target, archs := range targets {
for _, arch := range archs {
err := build(buildOptions{
wg.Add(1)
build(buildOptions{
Target: target,
Arch: arch,
Version: v,
})
if err != nil {
panic(err)
}
}, &wg)
}
}
wg.Wait()
}

View File

@@ -1,7 +1,11 @@
package cmd
import (
"fmt"
"strings"
"github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra"
)
@@ -10,17 +14,29 @@ var backupCmd = &cobra.Command{
Use: "backup",
Short: "Create backups for given locations",
Run: func(cmd *cobra.Command, args []string) {
internal.GetConfig()
err := lock.Lock()
CheckErr(err)
defer lock.Unlock()
CheckErr(internal.CheckConfig())
selected, err := internal.GetAllOrSelected(cmd, false)
CheckErr(err)
errors := 0
for _, name := range selected {
location, _ := internal.GetLocation(name)
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++
}
}
if errors > 0 {
CheckErr(fmt.Errorf("%d errors were found", errors))
}
},
}

View File

@@ -17,7 +17,7 @@ var checkCmd = &cobra.Command{
CheckErr(internal.CheckConfig())
colors.Success.Println("Everyting is fine.")
colors.Success.Println("Everything is fine.")
},
}

View File

@@ -11,6 +11,7 @@ var cronCmd = &cobra.Command{
Short: "Run cron job for automated backups",
Long: `Intended to be mainly triggered by an automated system like systemd or crontab. For each location checks if a cron backup is due and runs it.`,
Run: func(cmd *cobra.Command, args []string) {
internal.CRON_LEAN, _ = cmd.Flags().GetBool("lean")
err := lock.Lock()
CheckErr(err)
defer lock.Unlock()
@@ -22,4 +23,5 @@ var cronCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(cronCmd)
cronCmd.Flags().Bool("lean", false, "only output information about actual backups")
}

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")
@@ -31,6 +30,6 @@ var forgetCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(forgetCmd)
internal.AddFlagsToCommand(forgetCmd, false)
forgetCmd.Flags().Bool("prune", false, "Also prune repository")
forgetCmd.Flags().Bool("dry-run", false, "Do not write changes, show what would be affected")
forgetCmd.Flags().Bool("prune", false, "also prune repository")
forgetCmd.Flags().Bool("dry-run", false, "do not write changes, show what would be affected")
}

View File

@@ -9,8 +9,9 @@ import (
)
var restoreCmd = &cobra.Command{
Use: "restore",
Use: "restore [snapshot id]",
Short: "Restore backup for location",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := lock.Lock()
CheckErr(err)
@@ -24,7 +25,11 @@ var restoreCmd = &cobra.Command{
target, _ := cmd.Flags().GetString("to")
from, _ := cmd.Flags().GetString("from")
force, _ := cmd.Flags().GetBool("force")
err = l.Restore(target, from, force)
snapshot := ""
if len(args) > 0 {
snapshot = args[0]
}
err = l.Restore(target, from, force, snapshot)
CheckErr(err)
},
}

View File

@@ -2,6 +2,7 @@ package cmd
import (
"os"
"path/filepath"
"github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors"
@@ -37,6 +38,7 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.autorestic.yml or ./.autorestic.yml)")
rootCmd.PersistentFlags().BoolVar(&internal.CI, "ci", false, "CI mode disabled interactive mode and colors and enables verbosity")
rootCmd.PersistentFlags().BoolVarP(&internal.VERBOSE, "verbose", "v", false, "verbose mode")
rootCmd.PersistentFlags().StringVar(&internal.RESTIC_BIN, "restic-bin", "restic", "specify custom restic binary")
cobra.OnInitialize(initConfig)
}
@@ -49,13 +51,25 @@ 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()
internal.GetConfig()
}

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. Default: true")
upgradeCmd.Flags().Bool("restic", true, "also update restic")
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{
"dependencies": {
"@codedoc/core": "^0.2.15"
"@codedoc/core": "^0.2.23"
}
}

View File

@@ -1,5 +1,6 @@
[Home](/)
[Quick Start](/quick)
[Installation](/installation)
[Configuration](/config)
[Upgrade](/upgrade)
@@ -7,8 +8,13 @@
>
> [Overview](/location/overview)
> [Hooks](/location/hooks)
> [Excluding Files](/location/exclude)
> [Forget Policy](/location/forget)
>
> > :Collapse label=Options
> >
> > [Overview](/location/options)
> > [Excluding Files](/location/exclude)
> > [Forget Policy](/location/forget)
>
> [Cron](/location/cron)
> [Docker Volumes](/location/docker)
@@ -16,12 +22,15 @@
>
> [Overview](/backend/overview)
> [Available Backends](/backend/available)
> [Options](/backend/options)
> [Environment](/backend/env)
> :Collapse label=CLI
>
> [General](/cli/general)
> [Info](/cli/info)
> [Check](/cli/check)
> [Completion](/cli/completion)
> [Backup](/cli/backup)
> [Restore](/cli/restore)
> [Forget](/cli/forget)
@@ -29,10 +38,15 @@
> [Exec](/cli/exec)
> [Install](/cli/install)
> [Uninstall](/cli/uninstall)
> [Update](/cli/update)
> [Upgrade](/cli/upgrade)
> :Collapse label=Migration
>
> [0.x → 1.0](/migration/0.x_1.0)
> [1.4 → 1.5](/migration/1.4_1.5)
[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,11 +21,18 @@ backends:
backends:
name-of-backend:
type: b2
path: 'myAccount:myBucket/my/path'
B2_ACCOUNT_ID: backblaze_account_id
B2_ACCOUNT_KEY: backblaze_account_key
path: 'bucket_name'
# Or With a path
# path: 'bucket_name:/some/path'
env:
B2_ACCOUNT_ID: 'backblaze_keyID'
B2_ACCOUNT_KEY: 'backblaze_applicationKey'
```
#### 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: 'bucket_name:my/path'`).
## S3 / Minio
```yaml
@@ -33,8 +42,9 @@ backends:
path: s3.amazonaws.com/bucket_name
# Minio
# path: http://localhost:9000/bucket_name
AWS_ACCESS_KEY_ID: my_key
AWS_SECRET_ACCESS_KEY: my_secret
env:
AWS_ACCESS_KEY_ID: my_key
AWS_SECRET_ACCESS_KEY: my_secret
```
## SFTP
@@ -57,6 +67,21 @@ backends:
name-of-backend:
type: rest
path: http://localhost:8000/repo_name
# Or authenticated
path: https://user:pass@host:6969/path
```
Optionally you can set user and password separately
```yaml
backends:
rest:
type: rest
path: http://localhost:6969/path
key: ...
rest:
user: user
password: pass
```
> :ToCPrevNext

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

@@ -1,6 +1,6 @@
# 💽 Backends
Backends are the ouputs of the backup process. Each location needs at least one.
Backends are the outputs of the backup process. Each location needs at least one.
```yaml | .autorestic.yml
backends:

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

@@ -0,0 +1,17 @@
# Completion
```bash
autorestic completion [bash|zsh|fish|powershell]
```
Autorestic can generate shell completions automatically to make the experience even easier.
Supported shells are
- bash
- zsh
- fish
- powershell
To see how to install run `autorestic help completion` and follow the instructions for your specific shell
> :ToCPrevNext

View File

@@ -1,11 +1,13 @@
# Cron
```bash
autorestic cron
autorestic cron [--lean]
```
This command is mostly intended to be triggered by an automated system like systemd or crontab.
It will run cron jobs es [specified in the cron section](/locations/cron) of a specific location.
It will run cron jobs as [specified in the cron section](/location/cron) of a specific location.
The `--lean` flag will omit output like _skipping location x: not due yet_. This can be useful if you are dumping the output of the cron job to a log file and don't want to be overwhelmed by the output log.
> :ToCPrevNext

View File

@@ -4,10 +4,10 @@
autorestic exec [-b, --backend] [-a, --all] <command> -- [native options]
```
This is avery handy command which enables you to run any native restic command on desired backends. An example would be listing all the snapshots of all your backends:
This is a very handy command which enables you to run any native restic command on desired backends. Generally you will want to include the verbose flag `-v, --verbose` to see the output. An example would be listing all the snapshots of all your backends:
```bash
autorestic exec -a -- snapshots
autorestic exec -av -- snapshots
```
With `exec` you can basically run every cli command that you would be able to run with the restic cli. It only pre-fills path, key, etc.

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

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

View File

@@ -1,10 +1,12 @@
# Restore
```bash
autorestic restore [-l, --location] [--from backend] [--to <out dir>]
autorestic restore [-l, --location] [--from backend] [--to <out dir>] [-f, --force] [snapshot]
```
This will restore all the locations to the selected target. If for one location there are more than one backends specified autorestic will take the first one.
This will restore the location to the selected target. If for one location there are more than one backends specified autorestic will take the first one. If no specific snapshot is specified `autorestic` will use `latest`.
If you are sure you can pass the `-f, --force` flag and the data will be overwritten in the destination. However note that this will overwrite all the data existent in the backup, not only the 1 file that is missing e.g.
## Example

View File

@@ -1,6 +1,6 @@
# Uninstall
Installs both restic and autorestic from `/usr/local/bin`.
Uninstalls both restic and autorestic from `/usr/local/bin`.
```bash
autorestic uninstall

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

@@ -2,7 +2,16 @@
This amazing people helped the project!
- @ChanceM [Docs]
- @EliotBerriot [Docs, Pruning, S3]
- @agateblue - Docs, Pruning, S3
- @david-boles - Docs
- @SebDanielsson - Brew
- @n194 - AUR Package
- @jin-park-dev - Typos
- @sumnerboy12 - Typos
- @FuzzyMistborn - Typos
- @ChanceM - Typos
- @TheForcer - Typos
- @themorlan - Typos
- @somebox - Typos
> :ToCPrevNext

View File

@@ -3,12 +3,12 @@
## List all the snapshots for all the backends
```bash
autorestic exec -a -- snapshots
autorestic exec -av -- snapshots
```
## Unlock a locked repository
This can come in handy if a backup process crashed or if it was accidentally cancelled. Then the repository would still be locked without an actual process using it. Only do this if you know what you are sure no other process is actually reading/writing to the repository of course.
This can come in handy if a backup process crashed or if it was accidentally cancelled. Then the repository would still be locked without an actual process using it. Only do this if you know what you are doing and are sure no other process is actually reading/writing to the repository of course.
```bash
autorestic exec -b my-backend -- unlock

View File

@@ -2,10 +2,28 @@
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
### Docker
There is an official docker image over at [cupcakearmy/autorestic](https://hub.docker.com/r/cupcakearmy/autorestic).
### Manual
You can download the right binary from the release page and simply copy it to `/usr/local/bin` or whatever path you prefer. Autoupdates will still work.
### Brew
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/) (looking for maintainers).~~ - Deprecated
> :ToCPrevNext

View File

@@ -1,6 +1,6 @@
# Cron
Often it is usefull to trigger backups autmatically. For this we can specify a `cron` attribute to each location.
Often it is usefully to trigger backups automatically. For this we can specify a `cron` attribute to each location.
```yaml | .autorestic.yml
locations:
@@ -14,11 +14,11 @@ Here is a awesome website with [some examples](https://crontab.guru/examples.htm
## Installing the cron
**This has to be done only once, regadless of now many cros you have in your config file.**
**This has to be done only once, regardless of now many cron jobs you have in your config file.**
To actually enable cron jobs you need something to call `autorestic cron` on a timed shedule.
Note that the shedule has nothing to do with the `cron` attribute in each location.
My advise would be to trigger the command every 5min, but if you have a cronjob that runs only once a week, it's probably enough to shedule it once a day.
To actually enable cron jobs you need something to call `autorestic cron` on a timed schedule.
Note that the schedule has nothing to do with the `cron` attribute in each location.
My advise would be to trigger the command every 5min, but if you have a cronjob that runs only once a week, it's probably enough to schedule it once a day.
### Crontab
@@ -30,18 +30,24 @@ First, open your crontab in edit mode
crontab -e
```
Then paste this at the bottom of the file and save it. Note that in this specific example the `.autorestic.yml` is located in `/srv/`. You need to modify that part of course to fit your config file.
Then paste this at the bottom of the file and save it. Note that in this specific example the config file is located at one of the default locations (e.g. `~/.autorestic.yml`). If your config is somewhere else you'll need to specify it using the `-c` option.
```bash
# This is required, as it otherwise cannot find restic as a command.
PATH="/usr/local/bin:/usr/bin:/bin"
# Example running every 5 minutes
*/5 * * * * autorestic -c /srv/.autorestic.yml --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

@@ -3,7 +3,7 @@
autorestic supports docker volumes directly, without needing them to be mounted to the host filesystem.
```yaml | docker-compose.yml
version: '3.7'
version: '3.8'
volumes:
data:
@@ -18,13 +18,9 @@ services:
```yaml | .autorestic.yml
locations:
- name: hello
from: volume:my-data
to:
- remote
backends:
- name: remote
foo:
from: my-data
type: volume
# ...
```

View File

@@ -1,6 +1,6 @@
# Excluding files
If you want to exclude certain files or folders it done easily by specifiyng the right flags in the location you desire to filter.
If you want to exclude certain files or folders it done easily by specifying the right flags in the location you desire to filter.
The flags are taken straight from the [restic cli exclude rules](https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files) so you can use any flag used there.

View File

@@ -14,11 +14,11 @@ locations:
options:
forget:
keep-last: 5 # always keep at least 5 snapshots
keep-hourly: 3 # keep 3 last hourly shapshots
keep-daily: 4 # keep 4 last daily shapshots
keep-weekly: 1 # keep 1 last weekly shapshots
keep-monthly: 12 # keep 12 last monthly shapshots
keep-yearly: 7 # keep 7 last yearly shapshots
keep-hourly: 3 # keep 3 last hourly snapshots
keep-daily: 4 # keep 4 last daily snapshots
keep-weekly: 1 # keep 1 last weekly snapshots
keep-monthly: 12 # keep 12 last monthly snapshots
keep-yearly: 7 # keep 7 last yearly snapshots
keep-within: '2w' # keep snapshots from the last 2 weeks
```

View File

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

View File

@@ -0,0 +1,67 @@
# Options
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 foo --tag bar` to the native command.
```yaml
locations:
foo:
path: ...
to: ...
options:
backup:
tag:
- foo
- bar
```
## Priority
Options can be set globally, on the backends or on the locations.
The priority is as follows: `location > backend > global`.
## 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

@@ -22,6 +22,6 @@ Paths can be absolute or relative. If relative they are resolved relative to the
## `to`
This is einther a single backend or an array of backends. The backends have to be configured in the same config file.
This is either a single backend or an array of backends. The backends have to be configured in the same config file.
> :ToCPrevNext

View File

@@ -1,6 +1,4 @@
# Update
## From `0.x` to `1.0`
# From `0.x` to `1.0`
Most of the config file is remained compatible, however to clean up the backends custom environment variables were moved from the root object to an `env` object.

View File

@@ -0,0 +1,60 @@
# Migration from `1.4` to `1.5`
## ⚠️ Important notes
The way snapshots are referenced in the `restore` and `prune` commands has been changed. Before they were referenced by the path. Now every backup is tagged and those tags are then referenced in the cli. This means that when running restore and forget commands old backups are not taken into account anymore.
## Config files
- The config file now required to have a version number. This has to be added with `version: 2` at the root.
- Hooks now optionally support `dir: /some/dir` in the [options object](https://pkg.go.dev/github.com/cupcakearmy/autorestic/internal#Hooks).
- Docker volumes don't get prefixed with `volume:` anymore, rather you have to set the `type: volume` in the [location config](https://pkg.go.dev/github.com/cupcakearmy/autorestic/internal#Hooks).
See detailed instructions below.
## Config Version
```yaml
version: 2 # Added
backends:
# ...
```
## Hooks
Since `1.5` multiple sources for a location are possible.
For this reason, while before hooks where executed in the folder of the source, now they are executed in the directory of the config `.autorestic.yaml`.
You can overwrite this behavior with the new `dir` option in the hook section of the config.
```yaml
locations:
l1:
# ...
from: /foo/bar
hooks:
dir: /foo/bar
before: pwd
```
## Docker volumes
The syntax with docker volumes has changed and needs to be adjusted.
```yaml
# Before
locations:
foo:
from: volume:my-data
```
```yaml
# After
locations:
foo:
from: my-data
type: volume
```
> :ToCPrevNext

View File

@@ -0,0 +1,4 @@
# Migration
- [From 0.x to 1.0](/migration/0.x_1.0)
- [From 1.4 to 1.5](/migration/1.4_1.5)

View File

@@ -3,19 +3,21 @@
## 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.
## Write a simple config file
```bash
vim .autorestic.yml
vim ~/.autorestic.yml
```
For a quick overview:
- `locations` can be seen as the inputs and `backends` the output where the data is stored and backed up.
- One `location` can have one or multiple `backends` for redudancy.
- One `location` can have one or multiple `backends` for redundancy.
- One `backend` can also be the target for multiple `locations`.
> **⚠️ WARNING ⚠️**
@@ -24,25 +26,26 @@ For a quick overview:
```yaml | .autorestic.yml
locations:
- name: home
home:
from: /home/me
to: remote
- name: important
important:
from: /path/to/important/stuff
to:
- remote
- hdd
backends:
- name: remote
remote:
type: s3
path: 's3.amazonaws.com/bucket_name'
key: some-random-password-198rc79r8y1029c8yfewj8f1u0ef87yh198uoieufy
AWS_ACCESS_KEY_ID: account_id
AWS_SECRET_ACCESS_KEY: account_key
env:
AWS_ACCESS_KEY_ID: account_id
AWS_SECRET_ACCESS_KEY: account_key
- name: hdd
hdd:
type: local
path: /mnt/my_external_storage
key: 'if not key is set it will be generated for you'
@@ -51,7 +54,7 @@ backends:
## Check
```bash
autorestic check -a
autorestic check
```
This checks if the config file has any issues. If this is the first time this can take longer as autorestic will setup the backends.

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

@@ -16,18 +16,22 @@ else
fi
echo $OS
NATIVE_ARCH=$(uname -m)
NATIVE_ARCH=$(uname -m | tr '[:upper:]' '[:lower:]')
if [[ $NATIVE_ARCH == *"x86_64"* ]]; then
ARCH=amd64
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 \" \
@@ -36,4 +40,4 @@ bzip2 -fd "${OUT_FILE}.bz2"
chmod +x ${OUT_FILE}
autorestic install
echo "Succefsully installed autorestic"
echo "Successfully installed autorestic"

View File

@@ -4,19 +4,26 @@ import (
"crypto/rand"
"encoding/base64"
"fmt"
"net/url"
"os"
"strings"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/spf13/viper"
)
type BackendRest struct {
User string `yaml:"user,omitempty"`
Password string `yaml:"password,omitempty"`
}
type Backend struct {
name string
Type string `mapstructure:"type,omitempty"`
Path string `mapstructure:"path,omitempty"`
Key string `mapstructure:"key,omitempty"`
Env map[string]string `mapstructure:"env,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) {
@@ -29,7 +36,20 @@ func (b Backend) generateRepo() (string, error) {
switch b.Type {
case "local":
return GetPathRelativeToConfig(b.Path)
case "b2", "azure", "gs", "s3", "sftp", "rest":
case "rest":
parsed, err := url.Parse(b.Path)
if err != nil {
return "", err
}
if b.Rest.User != "" {
if b.Rest.Password == "" {
parsed.User = url.User(b.Rest.User)
} else {
parsed.User = url.UserPassword(b.Rest.User, b.Rest.Password)
}
}
return fmt.Sprintf("%s:%s", b.Type, parsed.String()), nil
case "b2", "azure", "gs", "s3", "sftp", "rclone":
return fmt.Sprintf("%s:%s", b.Type, b.Path), nil
default:
return "", fmt.Errorf("backend type \"%s\" is invalid", b.Type)
@@ -38,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
}
@@ -65,20 +100,20 @@ 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
tmp.name = ""
c.Backends[b.name] = tmp
file := viper.ConfigFileUsed()
if err := CopyFile(file, file+".old"); 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
}
}
colors.Secondary.Println("Saved a backup copy of your file next the the original.")
viper.Set("backends", c.Backends)
viper.WriteConfig()
}
env, err := b.getEnv()
if err != nil {
@@ -107,44 +142,67 @@ func (b Backend) Exec(args []string) error {
}
options := ExecuteOptions{Envs: env}
out, err := ExecuteResticCommand(options, args...)
if err != nil {
colors.Error.Println(out)
return err
}
if VERBOSE {
colors.Faint.Println(out)
}
return err
return nil
}
func (b Backend) ExecDocker(l Location, args []string) error {
func (b Backend) ExecDocker(l Location, args []string) (string, error) {
env, err := b.getEnv()
if err != nil {
return err
return "", err
}
volume := l.getVolumeName()
path, _ := l.getPath()
volume := l.From[0]
options := ExecuteOptions{
Command: "docker",
Envs: env,
}
dir := "/data"
args = append([]string{"restic"}, args...)
docker := []string{
"run", "--rm",
"--pull", "always",
"--entrypoint", "ash",
"--workdir", path,
"--volume", volume + ":" + path,
"--workdir", dir,
"--volume", volume + ":" + dir,
}
// Use of docker host, not the container host
if hostname, err := os.Hostname(); err == nil {
docker = append(docker, "--hostname", hostname)
}
if b.Type == "local" {
switch b.Type {
case "local":
actual := env["RESTIC_REPOSITORY"]
docker = append(docker, "--volume", actual+":"+"/repo")
env["RESTIC_REPOSITORY"] = "/repo"
case "b2":
case "s3":
case "azure":
case "gs":
// No additional setup needed
case "rclone":
// Read host rclone config and mount it into the container
configFile, err := ExecuteCommand(ExecuteOptions{Command: "rclone"}, "config", "file")
if err != nil {
return "", err
}
splitted := strings.Split(strings.TrimSpace(configFile), "\n")
configFilePath := splitted[len(splitted)-1]
docker = append(docker, "--volume", configFilePath+":"+"/root/.config/rclone/rclone.conf:ro")
// Install rclone in the container
args = append([]string{"apk", "add", "rclone", "&&"}, args...)
default:
return "", fmt.Errorf("Backend type \"%s\" is not supported as volume endpoint", b.Type)
}
for key, value := range env {
docker = append(docker, "--env", key+"="+value)
}
docker = append(docker, "restic/restic", "-c", "restic "+strings.Join(args, " "))
docker = append(docker, "restic/restic", "-c", strings.Join(args, " "))
out, err := ExecuteCommand(options, docker...)
if VERBOSE {
colors.Faint.Println(out)
}
return err
return out, err
}

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
@@ -72,14 +72,25 @@ func downloadAndInstallAsset(body GithubRelease, name string) error {
// Uncompress
bz := bzip2.NewReader(resp.Body)
// Save binary
file, err := os.Create(path.Join(INSTALL_PATH, name))
// Save to tmp
// Linux does not support overwriting the file that is currently being overwritten, but it can be deleted and a new one moved in its place.
tmp, err := ioutil.TempFile(os.TempDir(), "autorestic-")
if err != nil {
return err
}
file.Chmod(0755)
defer file.Close()
io.Copy(file, bz)
defer tmp.Close()
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)
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
}
colors.Success.Printf("Successfully installed '%s' under %s\n", name, INSTALL_PATH)
return nil
@@ -113,8 +124,12 @@ func upgradeRestic() error {
func Upgrade(restic bool) error {
// Upgrade restic
if restic {
InstallRestic()
upgradeRestic()
if err := InstallRestic(); err != nil {
return err
}
if err := upgradeRestic(); err != nil {
return err
}
}
// Upgrade self
@@ -131,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,41 +2,88 @@ package internal
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"sync"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/joho/godotenv"
"github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
const VERSION = "1.0.2"
const VERSION = "1.5.0"
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 {
Locations map[string]Location `mapstructure:"locations"`
Backends map[string]Backend `mapstructure:"backends"`
Version string `yaml:"version"`
Extras interface{} `yaml:"extras"`
Locations map[string]Location `yaml:"locations"`
Backends map[string]Backend `yaml:"backends"`
Global Options `yaml:"global"`
}
var once sync.Once
var config *Config
func exitConfig(err error, msg string) {
if err != nil {
colors.Error.Println(err)
}
if msg != "" {
colors.Error.Println(msg)
}
lock.Unlock()
os.Exit(1)
}
func GetConfig() *Config {
if config == nil {
once.Do(func() {
if err := viper.ReadInConfig(); err == nil {
colors.Faint.Println("Using config file:", viper.ConfigFileUsed())
if !CRON_LEAN {
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
}
var versionConfig interface{}
viper.UnmarshalKey("version", &versionConfig)
if versionConfig == nil {
exitConfig(nil, "no version specified in config file. please see docs on how to migrate")
}
version, ok := versionConfig.(int)
if !ok {
exitConfig(nil, "version specified in config file is not an int")
} else {
// Check for version
if version != 2 {
exitConfig(nil, "unsupported config version number. please check the docs for migration\nhttps://autorestic.vercel.app/migration/")
}
}
config = &Config{}
if err := viper.UnmarshalExact(config); err != nil {
panic(err)
exitConfig(err, "Could not parse config file!")
}
})
}
@@ -60,7 +107,11 @@ func (c *Config) Describe() {
var tmp string
colors.PrimaryPrint(`Location: "%s"`, name)
colors.PrintDescription("From", l.From)
tmp = ""
for _, path := range l.From {
tmp += fmt.Sprintf("\t%s %s\n", colors.Success.Sprint("←"), path)
}
colors.PrintDescription("From", tmp)
tmp = ""
for _, to := range l.To {
@@ -72,21 +123,22 @@ func (c *Config) Describe() {
colors.PrintDescription("Cron", l.Cron)
}
after, before := len(l.Hooks.After), len(l.Hooks.Before)
if after+before > 0 {
tmp = ""
if before > 0 {
tmp += "\tBefore"
for _, cmd := range l.Hooks.Before {
tmp += colors.Faint.Sprintf("\n\t ▶ %s", cmd)
}
}
if after > 0 {
tmp += "\n\tAfter"
for _, cmd := range l.Hooks.After {
tmp = ""
hooks := map[string][]string{
"Before": l.Hooks.Before,
"After": l.Hooks.After,
"Failure": l.Hooks.Failure,
"Success": l.Hooks.Success,
}
for hook, commands := range hooks {
if len(commands) > 0 {
tmp += "\n\t" + hook
for _, cmd := range commands {
tmp += colors.Faint.Sprintf("\n\t ▶ %s", cmd)
}
}
}
if tmp != "" {
colors.PrintDescription("Hooks", tmp)
}
@@ -126,7 +178,7 @@ func CheckConfig() error {
return fmt.Errorf("config could not be loaded/found")
}
if !CheckIfResticIsCallable() {
return fmt.Errorf(`restic was not found. Install either with "autorestic install" or manually`)
return fmt.Errorf(`%s was not found. Install either with "autorestic install" or manually`, RESTIC_BIN)
}
for name, backend := range c.Backends {
backend.name = name
@@ -136,7 +188,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
}
}
@@ -167,20 +219,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 {
@@ -190,10 +240,74 @@ func GetAllOrSelected(cmd *cobra.Command, backends bool) ([]string, error) {
}
func AddFlagsToCommand(cmd *cobra.Command, backend bool) {
cmd.PersistentFlags().BoolP("all", "a", false, "Backup all locations")
var usage string
if backend {
cmd.PersistentFlags().StringSliceP("backend", "b", []string{}, "backends")
usage = "all backends"
} else {
cmd.PersistentFlags().StringSliceP("location", "l", []string{}, "Locations")
usage = "all locations"
}
cmd.PersistentFlags().BoolP("all", "a", false, usage)
if backend {
cmd.PersistentFlags().StringSliceP("backend", "b", []string{}, "select backends")
} else {
cmd.PersistentFlags().StringSliceP("location", "l", []string{}, "select locations")
}
}
func (c *Config) SaveConfig() error {
file := viper.ConfigFileUsed()
if err := CopyFile(file, file+".old"); err != nil {
return err
}
colors.Secondary.Println("Saved a backup copy of your file next the the original.")
viper.Set("backends", c.Backends)
viper.Set("locations", c.Locations)
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
}
func combineOptions(key string, l Location, b Backend) []string {
// Priority: location > backend > global
var options []string
gFlags := getOptions(GetConfig().Global, key)
bFlags := getOptions(b.Options, key)
lFlags := getOptions(l.Options, key)
options = append(options, gFlags...)
options = append(options, bFlags...)
options = append(options, lFlags...)
return options
}

View File

@@ -10,33 +10,35 @@ import (
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/cupcakearmy/autorestic/internal/metadata"
"github.com/robfig/cron"
)
type LocationType string
const (
TypeLocal LocationType = "local"
TypeVolume LocationType = "volume"
VolumePrefix string = "volume:"
TypeLocal LocationType = "local"
TypeVolume LocationType = "volume"
)
type HookArray = []string
type Hooks struct {
Before HookArray `mapstructure:"before"`
After HookArray `mapstructure:"after"`
Dir string `yaml:"dir"`
Before HookArray `yaml:"before,omitempty"`
After HookArray `yaml:"after,omitempty"`
Success HookArray `yaml:"success,omitempty"`
Failure HookArray `yaml:"failure,omitempty"`
}
type Options map[string]map[string][]string
type Location struct {
name string `mapstructure:",omitempty"`
From string `mapstructure:"from,omitempty"`
To []string `mapstructure:"to,omitempty"`
Hooks Hooks `mapstructure:"hooks,omitempty"`
Cron string `mapstructure:"cron,omitempty"`
Options Options `mapstructure:"options,omitempty"`
name string `yaml:",omitempty"`
From []string `yaml:"from,omitempty"`
Type string `yaml:"type,omitempty"`
To []string `yaml:"to,omitempty"`
Hooks Hooks `yaml:"hooks,omitempty"`
Cron string `yaml:"cron,omitempty"`
Options Options `yaml:"options,omitempty"`
}
func GetLocation(name string) (Location, bool) {
@@ -45,10 +47,35 @@ func GetLocation(name string) (Location, bool) {
return l, ok
}
func (l Location) validate(c *Config) error {
if l.From == "" {
func (l Location) validate() error {
if len(l.From) == 0 {
return fmt.Errorf(`Location "%s" is missing "from" key`, l.name)
}
t, err := l.getType()
if err != nil {
return err
}
switch t {
case TypeLocal:
for _, path := range l.From {
if from, err := GetPathRelativeToConfig(path); err != nil {
return err
} else {
if stat, err := os.Stat(from); err != nil {
return err
} else {
if !stat.IsDir() {
return fmt.Errorf("\"%s\" is not valid directory for location \"%s\"", from, l.name)
}
}
}
}
case TypeVolume:
if len(l.From) > 1 {
return fmt.Errorf(`location "%s" has more than one docker volume`, l.name)
}
}
if len(l.To) == 0 {
return fmt.Errorf(`Location "%s" has no "to" targets`, l.name)
}
@@ -62,133 +89,173 @@ 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 {
func (l Location) ExecuteHooks(commands []string, options ExecuteOptions) error {
if len(commands) == 0 {
return nil
}
if l.Hooks.Dir != "" {
if dir, err := GetPathRelativeToConfig(l.Hooks.Dir); err != nil {
return err
} else {
options.Dir = dir
}
}
colors.Secondary.Println("\nRunning hooks")
for _, command := range commands {
colors.Body.Println("> " + command)
out, err := ExecuteCommand(options, "-c", command)
if err != nil {
colors.Error.Println(out)
return err
}
if VERBOSE {
colors.Faint.Println(out)
}
if err != nil {
return err
}
}
colors.Body.Println("")
return nil
}
func (l Location) getType() LocationType {
if strings.HasPrefix(l.From, VolumePrefix) {
return TypeVolume
func (l Location) getType() (LocationType, error) {
t := strings.ToLower(l.Type)
if t == "" || t == "local" {
return TypeLocal, nil
} else if t == "volume" {
return TypeVolume, nil
}
return TypeLocal
return "", fmt.Errorf("invalid location type \"%s\"", l.Type)
}
func (l Location) getVolumeName() string {
return strings.TrimPrefix(l.From, VolumePrefix)
func buildTag(parts ...string) string {
parts = append([]string{"ar"}, parts...)
return strings.Join(parts, ":")
}
func (l Location) getPath() (string, error) {
t := l.getType()
switch t {
case TypeLocal:
if path, err := GetPathRelativeToConfig(l.From); err != nil {
return "", err
} else {
return path, nil
}
case TypeVolume:
return "/volume/" + l.name + "/" + l.getVolumeName(), nil
}
return "", fmt.Errorf("could not get path for location \"%s\"", l.name)
func (l Location) getLocationTags() string {
return buildTag("location", 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()
t, err := l.getType()
if err != nil {
errors = append(errors, err)
return errors
}
cwd, _ := GetPathRelativeToConfig(".")
options := ExecuteOptions{
Command: "bash",
Dir: cwd,
Envs: map[string]string{
"AUTORESTIC_LOCATION": l.name,
},
}
if t == TypeLocal {
dir, _ := GetPathRelativeToConfig(l.From)
options.Dir = dir
if err := l.validate(); err != nil {
errors = append(errors, err)
goto after
}
// Hooks
if err := ExecuteHooks(l.Hooks.Before, options); err != nil {
return err
if err := l.ExecuteHooks(l.Hooks.Before, options); err != nil {
errors = append(errors, err)
goto after
}
// 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()
if err != nil {
return nil
errors = append(errors, err)
continue
}
flags := l.getOptions("backup")
cmd := []string{"backup"}
cmd = append(cmd, flags...)
cmd = append(cmd, combineOptions("backup", l, backend)...)
if cron {
cmd = append(cmd, "--tag", "cron")
cmd = append(cmd, "--tag", buildTag("cron"))
}
cmd = append(cmd, ".")
cmd = append(cmd, "--tag", l.getLocationTags())
backupOptions := ExecuteOptions{
Dir: options.Dir,
Envs: env,
}
var out string
switch t {
case TypeLocal:
out, err = ExecuteResticCommand(backupOptions, cmd...)
if VERBOSE {
colors.Faint.Println(out)
for _, from := range l.From {
path, err := GetPathRelativeToConfig(from)
if err != nil {
errors = append(errors, err)
goto after
}
cmd = append(cmd, path)
}
out, err = ExecuteResticCommand(backupOptions, cmd...)
case TypeVolume:
err = backend.ExecDocker(l, cmd)
ok := CheckIfVolumeExists(l.From[0])
if !ok {
errors = append(errors, fmt.Errorf("volume \"%s\" does not exist", l.From[0]))
continue
}
cmd = append(cmd, "/data")
out, err = backend.ExecDocker(l, cmd)
}
if err != nil {
errors = append(errors, err)
continue
}
if err != nil {
return err
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)
}
}
// After hooks
if err := ExecuteHooks(l.Hooks.After, options); err != nil {
return err
if err := l.ExecuteHooks(l.Hooks.After, options); err != nil {
errors = append(errors, err)
}
colors.Success.Println("Done")
return nil
after:
var commands []string
if len(errors) > 0 {
commands = l.Hooks.Failure
} else {
commands = l.Hooks.Success
}
if err := l.ExecuteHooks(commands, options); err != nil {
errors = append(errors, err)
}
if len(errors) == 0 {
colors.Success.Println("Done")
}
return errors
}
func (l Location) Forget(prune bool, dry bool) error {
colors.PrimaryPrint("Forgetting for location \"%s\"", l.name)
path, err := l.getPath()
if err != nil {
return err
}
for _, to := range l.To {
backend, _ := GetBackend(to)
colors.Secondary.Printf("For backend \"%s\"\n", backend.name)
@@ -199,15 +266,14 @@ func (l Location) Forget(prune bool, dry bool) error {
options := ExecuteOptions{
Envs: env,
}
flags := l.getOptions("forget")
cmd := []string{"forget", "--path", path}
cmd := []string{"forget", "--tag", l.getLocationTags()}
if prune {
cmd = append(cmd, "--prune")
}
if dry {
cmd = append(cmd, "--dry-run")
}
cmd = append(cmd, flags...)
cmd = append(cmd, combineOptions("forget", l, backend)...)
out, err := ExecuteResticCommand(options, cmd...)
if VERBOSE {
colors.Faint.Println(out)
@@ -229,7 +295,7 @@ func (l Location) hasBackend(backend string) bool {
return false
}
func (l Location) Restore(to, from string, force bool) error {
func (l Location) Restore(to, from string, force bool, snapshot string) error {
if from == "" {
from = l.To[0]
} else if !l.hasBackend(from) {
@@ -240,16 +306,20 @@ func (l Location) Restore(to, from string, force bool) error {
if err != nil {
return err
}
colors.PrimaryPrint("Restoring location \"%s\"", l.name)
backend, _ := GetBackend(from)
path, err := l.getPath()
if err != nil {
return nil
if snapshot == "" {
snapshot = "latest"
}
colors.Secondary.Println("Restoring lastest snapshot")
colors.Body.Printf("%s → %s.\n", from, path)
switch l.getType() {
colors.PrimaryPrint("Restoring location \"%s\"", l.name)
backend, _ := GetBackend(from)
colors.Secondary.Printf("Restoring %s@%s → %s\n", snapshot, backend.name, to)
t, err := l.getType()
if err != nil {
return err
}
switch t {
case TypeLocal:
// Check if target is empty
if !force {
@@ -269,9 +339,9 @@ func (l Location) Restore(to, from string, force bool) error {
}
}
}
err = backend.Exec([]string{"restore", "--target", to, "--path", path, "latest"})
err = backend.Exec([]string{"restore", "--target", to, "--tag", l.getLocationTags(), snapshot})
case TypeVolume:
err = backend.ExecDocker(l, []string{"restore", "--target", ".", "--path", path, "latest"})
_, err = backend.ExecDocker(l, []string{"restore", "--target", "/", "--tag", l.getLocationTags(), snapshot})
}
if err != nil {
return err
@@ -294,9 +364,11 @@ 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 {
colors.Body.Printf("Skipping \"%s\", not due yet.\n", l.name)
if !CRON_LEAN {
colors.Body.Printf("Skipping \"%s\", not due yet.\n", l.name)
}
}
return nil
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,13 +10,15 @@ import (
"github.com/cupcakearmy/autorestic/internal/colors"
)
var RESTIC_BIN string
func CheckIfCommandIsCallable(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
func CheckIfResticIsCallable() bool {
return CheckIfCommandIsCallable("restic")
return CheckIfCommandIsCallable(RESTIC_BIN)
}
type ExecuteOptions struct {
@@ -50,7 +52,10 @@ func ExecuteCommand(options ExecuteOptions, args ...string) (string, error) {
}
func ExecuteResticCommand(options ExecuteOptions, args ...string) (string, error) {
options.Command = "restic"
options.Command = RESTIC_BIN
var c = GetConfig()
var optionsAsString = getOptions(c.Global, "")
args = append(optionsAsString, args...)
return ExecuteCommand(options, args...)
}
@@ -72,3 +77,8 @@ func CopyFile(from, to string) error {
}
return nil
}
func CheckIfVolumeExists(volume string) bool {
_, err := ExecuteCommand(ExecuteOptions{Command: "docker"}, "volume", "inspect", volume)
return err == nil
}