Compare commits

..

63 Commits

Author SHA1 Message Date
d85470459f fix home directory 2022-02-16 21:58:09 +01:00
1f69a7974a bump go version 2022-02-16 21:52:12 +01:00
db9f5dea66 1.5.4 2022-02-16 21:42:54 +01:00
75160d4d6a Merge pull request #160 from cupcakearmy/1.5.3
1.5.3
2022-02-16 21:29:10 +01:00
92feaef5bb version bump 2022-02-16 21:28:13 +01:00
2a7e233cdb only check on config if it is getting used 2022-02-16 21:22:42 +01:00
a373c07fb0 contrib 2022-02-13 16:25:51 +01:00
ec9e2aebcd 1.5.2 2022-02-13 16:25:09 +01:00
7d87160706 Merge pull request #156 from jjromannet/verbose-config-loading
Add error handling to reading of config file.
2022-02-13 16:23:05 +01:00
8e1fe6af65 Merge pull request #157 from jjromannet/fix-copy-config-file-before-overriding
Make a copy of config before overriding it
2022-02-13 16:20:46 +01:00
Jan
65ba1f6ac1 Make a copy of config before overriding it 2022-02-09 20:35:29 +01:00
Jan
6bf4953003 Add error handling to reading config file. 2022-02-09 20:26:02 +01:00
27758a03fa add help message 2021-12-22 14:45:04 +01:00
bbdae05199 changelog 2021-12-06 17:27:59 +01:00
389490c4ea Merge pull request #136 from cupcakearmy/1.5.1
1.5.1
2021-12-06 17:24:19 +01:00
21b83b4c89 docker docs 2021-12-06 17:20:06 +01:00
982f9e0b5c better error handling 2021-12-06 12:13:18 +01:00
0c37af5588 #140 Docker version pinning 2021-12-06 11:59:58 +01:00
e3c378f2a1 release day 2021-11-24 09:57:10 +01:00
3b541665ae changelog 2021-11-24 09:45:23 +01:00
e0b2c99ccd contrib 2021-11-24 09:45:19 +01:00
2b14e6b1af remove print 2021-11-24 09:35:31 +01:00
1810af8d02 Merge pull request #137 from g-a-c/issues/130
refactor downloading of binaries
2021-11-23 13:01:09 +01:00
252968e15e ensure config is loaded before lock 2021-11-23 12:32:35 +01:00
26de4385ea version bump 2021-11-23 12:32:22 +01:00
0c71bea93e use own docker image 2021-11-21 21:10:32 +01:00
3029259d82 docs 2021-11-21 21:06:25 +01:00
Gavin Chappell
389f7c25dd refactor downloading of binaries
* If `/tmp` is a `tmpfs` or somehow not the same filesystem, `os.Rename()` will fail so use `io.Copy()` instead
* don't defer cleanup of `to` as this removes the newly-created file if the operation is successful, making `install` and `upgrade` _functionally_ `uninstall`
* defer cleanup of the temporary file since it will still be in place if `os.Rename()` fails

Fixes: #130
2021-11-20 19:30:46 +00:00
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
c2f9ed9204 multiple paths 2021-10-30 13:01:31 +02:00
32 changed files with 596 additions and 189 deletions

6
.dockerignore Normal file
View File

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

View File

@@ -3,16 +3,44 @@ 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
@@ -25,4 +53,4 @@ jobs:
with:
files: dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -5,30 +5,97 @@ 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.5] - 2022-02-16
### Changed
- Go version was updated from `1.16` to `1.17`
### Fixed
- Home directory was not being taken into account for loading configs.
## [1.5.4] - 2022-02-16
### Fixed
- Lean flag not omitting all output.
## [1.5.3] - 2022-02-16
### Fixed
- Error throwing not finding config even it's not being used.
## [1.5.2] - 2022-02-13
### Fixed
- Config loading @jjromannet
- Making a backup of the file @jjromannet
## [1.5.1] - 2021-12-06
### Changed
- use official docker image instead of installing rclone every time docker is used.
- docker docs
### Fixed
- lock file not always next to the config file.
- update / install bugs.
- lock docker image tag to the current autorestic version
- better error logging
## [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
- 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`
- 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
- Error handling during upgrade & uninstall.
## [1.3.0] - 2021-10-26
### Added
- Pass restic backup metadata as ENV to hooks
- 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
- Binary restic flags are now supported.
- Pass encryption keys from env variables or files.
## [1.2.0] - 2021-08-05
@@ -36,12 +103,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Community page
- Support for yaml references and aliases
- Support for yaml references and aliases.
### Fixed
- Better verbose output for hooks
- Better error message for bad formatted configs
- Better verbose output for hooks.
- Better error message for bad formatted configs.
## [1.1.2] - 2021-07-11
@@ -53,24 +120,24 @@ Don't check all backend when running `forget` or `exec` commands.
### Added
- Options for backends
- Options for backends.
## [1.1.0] - 2021-05-06
### Added
- use custom restic binary
- success & failure hooks
- use custom restic binary.
- success & failure hooks.
### Fixed
- don't skip other locations on failure
- don't skip other locations on failure.
## [1.0.9] - 2021-05-01
### Fixed
- Validation for docker volumes
- Validation for docker volumes.
## [1.0.8] - 2021-04-28
@@ -93,7 +160,7 @@ Don't check all backend when running `forget` or `exec` commands.
### Added
- Support for rclone
- Support for rclone.
## [1.0.5] - 2021-04-24
@@ -106,17 +173,17 @@ Don't check all backend when running `forget` or `exec` commands.
### Added
- Options to add rest username and password in config
- Options to add rest username and password in config.
### Fixed
- Don't add empty strings when saving config
- 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
- 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
@@ -132,7 +199,7 @@ Don't check all backend when running `forget` or `exec` commands.
### Added
- Completion command for various shells
- Completion command for various shells.
## [1.0.0] - 2021-04-17

12
Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM golang:1.17-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

@@ -30,8 +30,8 @@ var backupCmd = &cobra.Command{
}
location, _ := internal.GetLocation(splitted[0])
errs := location.Backup(false, specificBackend)
for err := range errs {
colors.Error.Println(err)
for _, err := range errs {
colors.Error.Printf("%s\n\n", err)
errors++
}
}

View File

@@ -11,6 +11,7 @@ var checkCmd = &cobra.Command{
Use: "check",
Short: "Check if everything is setup",
Run: func(cmd *cobra.Command, args []string) {
internal.GetConfig()
err := lock.Lock()
CheckErr(err)
defer lock.Unlock()

View File

@@ -2,6 +2,7 @@ package cmd
import (
"github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/flags"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra"
)
@@ -11,7 +12,8 @@ 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")
internal.GetConfig()
flags.CRON_LEAN, _ = cmd.Flags().GetBool("lean")
err := lock.Lock()
CheckErr(err)
defer lock.Unlock()

View File

@@ -9,9 +9,11 @@ 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) {
internal.GetConfig()
err := lock.Lock()
CheckErr(err)
defer lock.Unlock()
@@ -24,7 +26,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

@@ -3,9 +3,11 @@ package cmd
import (
"os"
"path/filepath"
"strings"
"github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/flags"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra"
@@ -27,7 +29,7 @@ var rootCmd = &cobra.Command{
Version: internal.VERSION,
Use: "autorestic",
Short: "CLI Wrapper for restic",
Long: "Documentation: https://autorestic.vercel.app",
Long: "Documentation:\thttps://autorestic.vercel.app\nSupport:\thttps://discord.gg/wS7RpYTYd2",
}
func Execute() {
@@ -36,8 +38,8 @@ func Execute() {
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().BoolVar(&flags.CI, "ci", false, "CI mode disabled interactive mode and colors and enables verbosity")
rootCmd.PersistentFlags().BoolVarP(&flags.VERBOSE, "verbose", "v", false, "verbose mode")
rootCmd.PersistentFlags().StringVar(&internal.RESTIC_BIN, "restic-bin", "restic", "specify custom restic binary")
cobra.OnInitialize(initConfig)
}
@@ -45,17 +47,22 @@ func init() {
func initConfig() {
if ci, _ := rootCmd.Flags().GetBool("ci"); ci {
colors.DisableColors(true)
internal.VERBOSE = true
flags.VERBOSE = true
}
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
viper.AutomaticEnv()
if viper.ConfigFileUsed() == "" {
colors.Error.Println("cannot read config file %s\n", cfgFile)
os.Exit(1)
}
} else {
viper.AddConfigPath(".")
configPaths := []string{"."}
// Home
if home, err := homedir.Dir(); err != nil {
viper.AddConfigPath(home)
if home, err := homedir.Dir(); err == nil {
configPaths = append(configPaths, home)
}
// XDG_CONFIG_HOME
@@ -66,10 +73,17 @@ func initConfig() {
prefix = filepath.Join(home, ".config")
}
}
viper.AddConfigPath(filepath.Join(prefix, "autorestic"))
xdgConfig := filepath.Join(prefix, "autorestic")
configPaths = append(configPaths, xdgConfig)
}
viper.SetConfigName(".autorestic")
for _, cfgPath := range configPaths {
viper.AddConfigPath(cfgPath)
}
if flags.VERBOSE {
colors.Faint.Printf("Using config paths: %s\n", strings.Join(configPaths, " "))
}
cfgFileName := ".autorestic"
viper.SetConfigName(cfgFileName)
viper.AutomaticEnv()
}
viper.AutomaticEnv()
}

View File

@@ -40,7 +40,13 @@
> [Uninstall](/cli/uninstall)
> [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)
[Docker](/docker)
[QA](/qa)
[Community](/community)
[Contributors](/contrib)

View File

@@ -3,6 +3,8 @@
Backends are the outputs of the backup process. Each location needs at least one.
```yaml | .autorestic.yml
version: 2
backends:
name-of-backend:
type: local

View File

@@ -1,12 +1,12 @@
# Restore
```bash
autorestic restore [-l, --location] [--from backend] [--to <out dir>] [-f, --force]
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`.
The `--to` path has to be empty as no data will be overwritten by default. 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.
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

@@ -16,6 +16,8 @@ You can also specify a custom file with the `-c path/to/some/config.yml`
## Example configuration
```yaml | .autorestic.yml
version: 2
locations:
home:
from: /home/me
@@ -49,6 +51,8 @@ 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
version: 2
extras:
hooks: &foo
before:

View File

@@ -2,16 +2,18 @@
This amazing people helped the project!
- @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
- @agateblue - Docs, Pruning, S3.
- @g-a-c - Update/Install bugs.
- @jjromannet - Bug fixes.
- @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

28
docs/markdown/docker.md Normal file
View File

@@ -0,0 +1,28 @@
# 🐳 Docker
The docker image is build with rclone and restic already included. It's ment more as a utility image.
## Remote hosts
For remote backups (S3, B2, GCS, etc.) it's quite easy, as you only need to mount the config file and the data to backup.
```bash
docker run --rm \\
-v $(pwd):/data \\
cupcakearmy/autorestic \\
autorestic backup -va -c /data/.autorestic.yaml
```
## Rclone
For rclone you will have to also mount the rclone config file to `/root/.config/rclone/rclone.conf`.
To check where it is located you can run the following command: `rclone config file`.
**Example**
```bash
docker run \\
-v /home/user/.config/rclone/rclone.conf:/root/.config/rclone/rclone.conf:ro \\
...
```

View File

@@ -10,6 +10,12 @@ wget -qO - https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/insta
## Alternatives
### Docker
There is an official docker image over at [cupcakearmy/autorestic](https://hub.docker.com/r/cupcakearmy/autorestic).
For some examples see [here](/docker).
### 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.
@@ -20,6 +26,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).~~ - Deprecated
> :ToCPrevNext

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

@@ -7,6 +7,8 @@ This is based on [Restic's snapshots policies](https://restic.readthedocs.io/en/
> **Note** This is a full example, of course you also can specify only one of them
```yaml | .autorestic.yml
version: 2
locations:
etc:
from: /etc
@@ -22,4 +24,17 @@ locations:
keep-within: '2w' # keep snapshots from the last 2 weeks
```
## Globally
You can specify global forget policies that would be applied to all locations:
```yaml | .autorestic.yml
version: 2
global:
forget:
keep-daily: 30
keep-weekly: 52
```
> :ToCPrevNext

View File

@@ -26,7 +26,7 @@ locations:
## Example
In this example, whenever `autorestic` runs `restic backup` it will append a `--tag abc --tag` to the native command.
In this example, whenever `autorestic` runs `restic backup` it will append a `--tag foo --tag bar` to the native command.
```yaml
locations:
@@ -40,6 +40,12 @@ locations:
- 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.

View File

@@ -4,9 +4,15 @@ Locations can be seen as the input to the backup process. Generally this is simp
The paths can be relative from the config file. A location can have multiple backends, so that the data is secured across multiple servers.
```yaml | .autorestic.yml
version: 2
locations:
my-location-name:
from: path/to/backup
# Or multiple
# from:
# - /a
# - /b
to:
- name-of-backend
- also-backup-to-this-backend
@@ -14,7 +20,7 @@ locations:
## `from`
This is the source of the location.
This is the source of the location. Can be an `array` for multiple sources.
#### How are paths resolved?

View File

@@ -1,6 +1,4 @@
# Upgrade
## 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

@@ -25,9 +25,15 @@ For a quick overview:
> Note that the data is automatically encrypted on the server. The key will be generated and added to your config file. Every backend will have a separate key. **You should keep a copy of the keys or config file somewhere in case your server dies**. Otherwise DATA IS LOST!
```yaml | .autorestic.yml
version: 2
locations:
home:
from: /home/me
from: /home
# Or multiple
# from:
# - /foo
# - /bar
to: remote
important:

22
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/cupcakearmy/autorestic
go 1.16
go 1.17
require (
github.com/blang/semver/v4 v4.0.0
@@ -12,3 +12,23 @@ require (
github.com/spf13/cobra v1.1.3
github.com/spf13/viper v1.7.1
)
require (
github.com/fsnotify/fsnotify v1.4.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/magiconair/properties v1.8.1 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/pelletier/go-toml v1.2.0 // indirect
github.com/spf13/afero v1.1.2 // indirect
github.com/spf13/cast v1.3.0 // indirect
github.com/spf13/jwalterweatherman v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 // indirect
golang.org/x/text v0.3.2 // indirect
gopkg.in/ini.v1 v1.51.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

View File

@@ -9,6 +9,7 @@ import (
"strings"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/flags"
)
type BackendRest struct {
@@ -128,7 +129,7 @@ func (b Backend) validate() error {
// If not initialize
colors.Body.Printf("Initializing backend \"%s\"...\n", b.name)
out, err := ExecuteResticCommand(options, "init")
if VERBOSE {
if flags.VERBOSE {
colors.Faint.Println(out)
}
return err
@@ -146,7 +147,7 @@ func (b Backend) Exec(args []string) error {
colors.Error.Println(out)
return err
}
if VERBOSE {
if flags.VERBOSE {
colors.Faint.Println(out)
}
return nil
@@ -157,30 +158,50 @@ func (b Backend) ExecDocker(l Location, args []string) (string, error) {
if err != nil {
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")
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, "cupcakearmy/autorestic:"+VERSION, "-c", strings.Join(args, " "))
out, err := ExecuteCommand(options, docker...)
return out, err
}

View File

@@ -87,9 +87,23 @@ func downloadAndInstallAsset(body GithubRelease, name string) error {
}
to := path.Join(INSTALL_PATH, name)
defer os.Remove(to) // Delete if current, ignore error if file does not exits.
defer os.Remove(tmp.Name()) // Cleanup temporary file after thread exits
if err := os.Rename(tmp.Name(), to); err != nil {
return nil
colors.Error.Printf("os.Rename() failed (%v), retrying with io.Copy()\n", err.Error())
var src *os.File
var dst *os.File
if src, err = os.Open(tmp.Name()); err != nil {
return err
}
if dst, err = os.Create(to); err != nil {
return err
}
if _, err := io.Copy(dst, src); err != nil {
return err
}
if err := os.Chmod(to, 0755); err != nil {
return err
}
}
colors.Success.Printf("Successfully installed '%s' under %s\n", name, INSTALL_PATH)

View File

@@ -9,6 +9,7 @@ import (
"sync"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/flags"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/joho/godotenv"
"github.com/mitchellh/go-homedir"
@@ -16,16 +17,13 @@ import (
"github.com/spf13/viper"
)
const VERSION = "1.4.0"
var CI bool = false
var VERBOSE bool = false
var CRON_LEAN bool = false
const VERSION = "1.5.5"
type OptionMap map[string][]interface{}
type Options map[string]OptionMap
type Config struct {
Version string `yaml:"version"`
Extras interface{} `yaml:"extras"`
Locations map[string]Location `yaml:"locations"`
Backends map[string]Backend `yaml:"backends"`
@@ -35,29 +33,59 @@ type Config struct {
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 {
if !CRON_LEAN {
absConfig, _ := filepath.Abs(viper.ConfigFileUsed())
absConfig, _ := filepath.Abs(viper.ConfigFileUsed())
if !flags.CRON_LEAN {
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)
}
}
// 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
cfgFileName := ".autorestic"
colors.Error.Println(
fmt.Sprintf(
"cannot find configuration file '%s.yml' or '%s.yaml'.",
cfgFileName, cfgFileName))
os.Exit(1)
}
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 {
colors.Error.Println("Could not parse config file!")
lock.Unlock()
os.Exit(1)
exitConfig(err, "Could not parse config file!")
}
})
}
@@ -81,7 +109,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 {
@@ -229,7 +261,7 @@ func (c *Config) SaveConfig() error {
if err := CopyFile(file, file+".old"); err != nil {
return err
}
colors.Secondary.Println("Saved a backup copy of your file next the the original.")
colors.Secondary.Println("Saved a backup copy of your file next to the original.")
viper.Set("backends", c.Backends)
viper.Set("locations", c.Locations)
@@ -253,12 +285,7 @@ func appendOptionsToSlice(str *[]string, options OptionMap) {
*str = append(*str, optionToString(key))
continue
}
// String
asString, ok := value.(string)
if ok {
*str = append(*str, optionToString(key), asString)
continue
}
*str = append(*str, optionToString(key), fmt.Sprint(value))
}
}
}
@@ -274,3 +301,15 @@ func getOptions(options Options, key string) []string {
}
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
}

5
internal/flags/flags.go Normal file
View File

@@ -0,0 +1,5 @@
package flags
var CI bool = false
var VERBOSE bool = false
var CRON_LEAN bool = false

View File

@@ -9,6 +9,7 @@ import (
"time"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/flags"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/cupcakearmy/autorestic/internal/metadata"
"github.com/robfig/cron"
@@ -17,14 +18,14 @@ import (
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 {
Dir string `yaml:"dir"`
Before HookArray `yaml:"before,omitempty"`
After HookArray `yaml:"after,omitempty"`
Success HookArray `yaml:"success,omitempty"`
@@ -33,7 +34,8 @@ type Hooks struct {
type Location struct {
name string `yaml:",omitempty"`
From string `yaml:"from,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"`
@@ -47,21 +49,32 @@ func GetLocation(name string) (Location, bool) {
}
func (l Location) validate() error {
if l.From == "" {
if len(l.From) == 0 {
return fmt.Errorf(`Location "%s" is missing "from" key`, l.name)
}
if l.getType() == TypeLocal {
if from, err := GetPathRelativeToConfig(l.From); err != nil {
return err
} else {
if stat, err := os.Stat(from); err != nil {
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.IsDir() {
return fmt.Errorf("\"%s\" is not valid directory for location \"%s\"", from, l.name)
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 {
@@ -77,10 +90,17 @@ func (l Location) validate() error {
return nil
}
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)
@@ -89,7 +109,7 @@ func ExecuteHooks(commands []string, options ExecuteOptions) error {
colors.Error.Println(out)
return err
}
if VERBOSE {
if flags.VERBOSE {
colors.Faint.Println(out)
}
}
@@ -97,39 +117,38 @@ func ExecuteHooks(commands []string, options ExecuteOptions) error {
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, 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,
},
@@ -137,18 +156,11 @@ func (l Location) Backup(cron bool, specificBackend string) []error {
if err := l.validate(); err != nil {
errors = append(errors, err)
colors.Error.Print(err)
goto after
}
if t == TypeLocal {
dir, _ := GetPathRelativeToConfig(l.From)
colors.Faint.Printf("Executing under: \"%s\"\n", dir)
options.Dir = dir
}
// Hooks
if err := ExecuteHooks(l.Hooks.Before, options); err != nil {
if err := l.ExecuteHooks(l.Hooks.Before, options); err != nil {
errors = append(errors, err)
goto after
}
@@ -173,31 +185,40 @@ func (l Location) Backup(cron bool, specificBackend string) []error {
continue
}
lFlags := getOptions(l.Options, "backup")
bFlags := getOptions(backend.Options, "backup")
cmd := []string{"backup"}
cmd = append(cmd, lFlags...)
cmd = append(cmd, bFlags...)
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:
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:
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 {
colors.Error.Println(out)
errors = append(errors, err)
errors = append(errors, fmt.Errorf("%s@%s:\n%s%s", l.name, backend.name, out, err))
continue
}
@@ -207,13 +228,13 @@ func (l Location) Backup(cron bool, specificBackend string) []error {
options.Envs[k+"_"+fmt.Sprint(i)] = v
options.Envs[k+"_"+strings.ToUpper(backend.name)] = v
}
if VERBOSE {
if flags.VERBOSE {
colors.Faint.Println(out)
}
}
// After hooks
if err := ExecuteHooks(l.Hooks.After, options); err != nil {
if err := l.ExecuteHooks(l.Hooks.After, options); err != nil {
errors = append(errors, err)
}
@@ -224,22 +245,19 @@ after:
} else {
commands = l.Hooks.Success
}
if err := ExecuteHooks(commands, options); err != nil {
if err := l.ExecuteHooks(commands, options); err != nil {
errors = append(errors, err)
}
colors.Success.Println("Done")
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)
@@ -250,19 +268,16 @@ func (l Location) Forget(prune bool, dry bool) error {
options := ExecuteOptions{
Envs: env,
}
lFlags := getOptions(l.Options, "forget")
bFlags := getOptions(backend.Options, "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, lFlags...)
cmd = append(cmd, bFlags...)
cmd = append(cmd, combineOptions("forget", l, backend)...)
out, err := ExecuteResticCommand(options, cmd...)
if VERBOSE {
if flags.VERBOSE {
colors.Faint.Println(out)
}
if err != nil {
@@ -282,7 +297,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) {
@@ -293,16 +308,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 {
@@ -322,9 +341,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
@@ -349,7 +368,7 @@ func (l Location) RunCron() error {
lock.SetCron(l.name, now.Unix())
l.Backup(true, "")
} else {
if !CRON_LEAN {
if !flags.CRON_LEAN {
colors.Body.Printf("Skipping \"%s\", not due yet.\n", l.name)
}
}

View File

@@ -6,6 +6,7 @@ import (
"sync"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/flags"
"github.com/spf13/viper"
)
@@ -19,8 +20,15 @@ func getLock() *viper.Viper {
once.Do(func() {
lock = viper.New()
lock.SetDefault("running", false)
p := path.Dir(viper.ConfigFileUsed())
file = path.Join(p, ".autorestic.lock.yml")
p := viper.ConfigFileUsed()
if p == "" {
colors.Error.Println("cannot lock before reading config location")
os.Exit(1)
}
file = path.Join(path.Dir(p), ".autorestic.lock.yml")
if !flags.CRON_LEAN {
colors.Faint.Println("Using lock:\t", file)
}
lock.SetConfigFile(file)
lock.SetConfigType("yml")
lock.ReadInConfig()

View File

@@ -8,6 +8,7 @@ import (
"os/exec"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/flags"
)
var RESTIC_BIN string
@@ -36,7 +37,7 @@ func ExecuteCommand(options ExecuteOptions, args ...string) (string, error) {
cmd.Env = env
cmd.Dir = options.Dir
if VERBOSE {
if flags.VERBOSE {
colors.Faint.Printf("> Executing: %s\n", cmd)
}
@@ -60,13 +61,13 @@ func ExecuteResticCommand(options ExecuteOptions, args ...string) (string, error
}
func CopyFile(from, to string) error {
original, err := os.Open("original.txt")
original, err := os.Open(from)
if err != nil {
return nil
}
defer original.Close()
new, err := os.Create("new.txt")
new, err := os.Create(to)
if err != nil {
return nil
}
@@ -77,3 +78,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
}