Compare commits

..

18 Commits

Author SHA1 Message Date
Boris Bera
0fdf9c77ae
feat(config): allow specifying lockfile 2024-11-10 13:57:05 -05:00
Boris Bera
8de8d0070e
chore: move lockfile code to internal module
In a future commit, I'll need the lockfile code to access the config
file. This solves an import cycle.
2024-11-10 13:37:59 -05:00
Boris Bera
ccca7c850f
Merge branch 'master' into lockfile-path-config 2024-11-10 13:24:42 -05:00
dependabot[bot]
f7d28b486c
Bump restic/restic from 0.17.1 to 0.17.2 (#407)
Bumps restic/restic from 0.17.1 to 0.17.2.

---
updated-dependencies:
- dependency-name: restic/restic
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-04 15:21:16 +01:00
Boris Bera
6895df1c83
fix(config): fix config marshaling producing unreadable config file (#402)
There are two practical changes when the config gets updated:
- The `forgetoption` and `configoption` bug is now gone
- Superfluous config keys no longer get written out
2024-11-04 15:20:42 +01:00
Wez Furlong
8a773856de
Improve error handling in install.sh (#404)
Prior to this change, running the example from the docs without root privs produces this misleading/confusing output that claims that the software was installed when it wasn't:

```console
$ wget -qO - https://raw.githubusercontent.com/cupcakearmy/autorestic/master/install.sh | bash
linux
amd64
/usr/local/bin/autorestic.bz2: Permission denied
bzip2: Can't open input file /usr/local/bin/autorestic.bz2: No such file or directory.
chmod: cannot access '/usr/local/bin/autorestic': No such file or directory
bash: line 49: autorestic: command not found
Successfully installed autorestic
```

With this change, the errors stop the script much earlier and produce this output instead:

```
linux
amd64
/usr/local/bin/autorestic.bz2: Permission denied
```
2024-10-21 16:06:49 +02:00
Boris Bera
41e4e4a5f3
fix(cron): crash when errors are encountered during a backup (#403) 2024-10-17 13:49:45 +02:00
Chao
6424c64304
Update docker.md, fix incorrect location name (#399) 2024-09-27 21:55:02 +02:00
dependabot[bot]
d39dafaef1
Bump golang from 1.22-alpine to 1.23-alpine (#391)
Bumps golang from 1.22-alpine to 1.23-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-21 22:56:08 +02:00
dependabot[bot]
5a0f7e94f4
Bump restic/restic from 0.17.0 to 0.17.1 (#393)
Bumps restic/restic from 0.17.0 to 0.17.1.

---
updated-dependencies:
- dependency-name: restic/restic
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-21 22:55:52 +02:00
Boris Bera
6a60d02759
chore(CI): add basic CI (build & test) for PRs and master pushes (#396) 2024-09-21 22:55:23 +02:00
1f7240c6a0 add start command 2024-08-29 17:09:45 +02:00
592d1093ac bump version 2024-08-28 17:32:23 +02:00
83481072bb update docs dependencies 2024-08-28 17:28:58 +02:00
Boris Bera
776638e6fe
fix(backend): treat all special chars as _ in env (#382) 2024-08-28 17:25:06 +02:00
Boris Bera
bc74d3f13e
Add option to crash autorestic when key is missing instead of generating a new key (#383)
* feat(backend): add requireKey option to backend

This option will prevent `autorestic` from generating a key and will
cause it to crash instead. This is intended for use cases where you want
to provision the key yourself and don't want `autorestic` to
accidentally generate one for you.

* doc(backend): document requireKey
2024-08-28 17:21:01 +02:00
guest20
dd01e97cf3
install.sh: FreeBSD amd64 (#385) 2024-08-28 17:19:13 +02:00
dependabot[bot]
aabb0989c6
Bump restic/restic from 0.16.4 to 0.17.0 (#386)
Bumps restic/restic from 0.16.4 to 0.17.0.

---
updated-dependencies:
- dependency-name: restic/restic
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-28 17:18:01 +02:00
27 changed files with 2298 additions and 1984 deletions

24
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: CI
on:
pull_request:
push:
branches: [master]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '^1.21'
- run: go test -v ./...
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '^1.21'
- run: go build -v .

View File

@ -1,4 +1,4 @@
FROM golang:1.22-alpine as builder FROM golang:1.23-alpine as builder
WORKDIR /app WORKDIR /app
COPY go.* . COPY go.* .
@ -6,7 +6,7 @@ RUN go mod download
COPY . . COPY . .
RUN go build RUN go build
FROM restic/restic:0.16.4 FROM restic/restic:0.17.2
RUN apk add --no-cache rclone bash curl docker-cli RUN apk add --no-cache rclone bash curl docker-cli
COPY --from=builder /app/autorestic /usr/bin/autorestic COPY --from=builder /app/autorestic /usr/bin/autorestic
ENTRYPOINT [] ENTRYPOINT []

View File

@ -6,7 +6,6 @@ import (
"github.com/cupcakearmy/autorestic/internal" "github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors" "github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -15,9 +14,9 @@ var backupCmd = &cobra.Command{
Short: "Create backups for given locations", Short: "Create backups for given locations",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
internal.GetConfig() internal.GetConfig()
err := lock.Lock() err := internal.Lock()
CheckErr(err) CheckErr(err)
defer lock.Unlock() defer internal.Unlock()
selected, err := internal.GetAllOrSelected(cmd, false) selected, err := internal.GetAllOrSelected(cmd, false)
CheckErr(err) CheckErr(err)

View File

@ -3,7 +3,6 @@ package cmd
import ( import (
"github.com/cupcakearmy/autorestic/internal" "github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors" "github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -12,9 +11,9 @@ var checkCmd = &cobra.Command{
Short: "Check if everything is setup", Short: "Check if everything is setup",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
internal.GetConfig() internal.GetConfig()
err := lock.Lock() err := internal.Lock()
CheckErr(err) CheckErr(err)
defer lock.Unlock() defer internal.Unlock()
CheckErr(internal.CheckConfig()) CheckErr(internal.CheckConfig())

View File

@ -3,7 +3,6 @@ package cmd
import ( import (
"github.com/cupcakearmy/autorestic/internal" "github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/flags" "github.com/cupcakearmy/autorestic/internal/flags"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -13,9 +12,9 @@ var cronCmd = &cobra.Command{
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.`, 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) { Run: func(cmd *cobra.Command, args []string) {
internal.GetConfig() internal.GetConfig()
err := lock.Lock() err := internal.Lock()
CheckErr(err) CheckErr(err)
defer lock.Unlock() defer internal.Unlock()
err = internal.RunCron() err = internal.RunCron()
CheckErr(err) CheckErr(err)

View File

@ -5,7 +5,6 @@ import (
"github.com/cupcakearmy/autorestic/internal" "github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors" "github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -14,9 +13,9 @@ var execCmd = &cobra.Command{
Short: "Execute arbitrary native restic commands for given backends", Short: "Execute arbitrary native restic commands for given backends",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
internal.GetConfig() internal.GetConfig()
err := lock.Lock() err := internal.Lock()
CheckErr(err) CheckErr(err)
defer lock.Unlock() defer internal.Unlock()
selected, err := internal.GetAllOrSelected(cmd, true) selected, err := internal.GetAllOrSelected(cmd, true)
CheckErr(err) CheckErr(err)

View File

@ -2,7 +2,6 @@ package cmd
import ( import (
"github.com/cupcakearmy/autorestic/internal" "github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -11,9 +10,9 @@ var forgetCmd = &cobra.Command{
Short: "Forget and optionally prune snapshots according the specified policies", Short: "Forget and optionally prune snapshots according the specified policies",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
internal.GetConfig() internal.GetConfig()
err := lock.Lock() err := internal.Lock()
CheckErr(err) CheckErr(err)
defer lock.Unlock() defer internal.Unlock()
selected, err := internal.GetAllOrSelected(cmd, false) selected, err := internal.GetAllOrSelected(cmd, false)
CheckErr(err) CheckErr(err)

View File

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"github.com/cupcakearmy/autorestic/internal" "github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -14,9 +13,9 @@ var restoreCmd = &cobra.Command{
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
internal.GetConfig() internal.GetConfig()
err := lock.Lock() err := internal.Lock()
CheckErr(err) CheckErr(err)
defer lock.Unlock() defer internal.Unlock()
location, _ := cmd.Flags().GetString("location") location, _ := cmd.Flags().GetString("location")
l, ok := internal.GetLocation(location) l, ok := internal.GetLocation(location)

View File

@ -8,7 +8,6 @@ import (
"github.com/cupcakearmy/autorestic/internal" "github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors" "github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/flags" "github.com/cupcakearmy/autorestic/internal/flags"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra" "github.com/spf13/cobra"
homedir "github.com/mitchellh/go-homedir" homedir "github.com/mitchellh/go-homedir"
@ -18,7 +17,7 @@ import (
func CheckErr(err error) { func CheckErr(err error) {
if err != nil { if err != nil {
colors.Error.Fprintln(os.Stderr, "Error:", err) colors.Error.Fprintln(os.Stderr, "Error:", err)
lock.Unlock() internal.Unlock()
os.Exit(1) os.Exit(1)
} }
} }

View File

@ -9,7 +9,6 @@ import (
"github.com/cupcakearmy/autorestic/internal" "github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors" "github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -33,7 +32,7 @@ To check you can run "ps aux | grep autorestic".`,
} }
} }
err := lock.Unlock() err := internal.Unlock()
if err != nil { if err != nil {
colors.Error.Println("Could not unlock:", err) colors.Error.Println("Could not unlock:", err)
return return

View File

@ -1 +1 @@
v20.8.0 v22.7.0

View File

@ -1,14 +1,15 @@
{ {
"scripts": { "scripts": {
"build": "NEXT_TELEMETRY_DISABLED=1 next build", "build": "NEXT_TELEMETRY_DISABLED=1 next build",
"dev": "NEXT_TELEMETRY_DISABLED=1 next" "dev": "NEXT_TELEMETRY_DISABLED=1 next",
"start": "NEXT_TELEMETRY_DISABLED=1 next start"
}, },
"dependencies": { "dependencies": {
"next": "^13.5.3", "next": "^14.2.7",
"nextra": "^2.13.1", "nextra": "^2.13.4",
"nextra-theme-docs": "^2.13.1", "nextra-theme-docs": "^2.13.4",
"react": "^18.2.0", "react": "^18.3.1",
"react-dom": "^18.2.0" "react-dom": "^18.3.1"
}, },
"packageManager": "pnpm@8.8.0" "packageManager": "pnpm@9.9.0"
} }

View File

@ -16,3 +16,25 @@ backends:
## Types ## Types
We restic supports multiple types of backends. See the [full list](/backend/available) for details. We restic supports multiple types of backends. See the [full list](/backend/available) for details.
## Avoid Generating Keys
By default, `autorestic` will generate a key for every backend if none is defined. This is done by updating your config file with the key.
In cases where you want to provide the key yourself, you can ensure that `autorestic` doesn't accidentally generate one for you by setting `requireKey: true`.
Example:
```yaml | .autorestic.yml
version: 2
backends:
foo:
type: local
path: /data/my/backups
# Alternatively, you can set the key through the `AUTORESTIC_FOO_RESTIC_PASSWORD` environment variable.
key: ... your key here ...
requireKey: true
```
With this setting, if a key is missing, `autorestic` will crash instead of generating a new key and updating your config file.

View File

@ -18,7 +18,7 @@ services:
```yaml | .autorestic.yml ```yaml | .autorestic.yml
locations: locations:
foo: hello:
from: my-data from: my-data
type: volume type: volume
# ... # ...

3916
docs/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

5
go.mod
View File

@ -11,9 +11,11 @@ require (
github.com/robfig/cron v1.2.0 github.com/robfig/cron v1.2.0
github.com/spf13/cobra v1.4.0 github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.11.0 github.com/spf13/viper v1.11.0
github.com/stretchr/testify v1.9.0
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect
@ -23,6 +25,7 @@ require (
github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/afero v1.8.2 // indirect github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.4.1 // indirect github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
@ -32,5 +35,5 @@ require (
golang.org/x/text v0.3.8 // indirect golang.org/x/text v0.3.8 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

7
go.sum
View File

@ -182,8 +182,9 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -487,8 +488,8 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
set -e -o pipefail
shopt -s nocaseglob shopt -s nocaseglob
OUT_FILE=/usr/local/bin/autorestic OUT_FILE=/usr/local/bin/autorestic
@ -10,6 +10,8 @@ if [[ $NATIVE_OS == *"linux"* ]]; then
OS=linux OS=linux
elif [[ $NATIVE_OS == *"darwin"* ]]; then elif [[ $NATIVE_OS == *"darwin"* ]]; then
OS=darwin OS=darwin
elif [[ $NATIVE_OS == *"freebsd"* ]]; then
OS=freebsd
else else
echo "Could not determine OS automatically, please check the release page manually: https://github.com/cupcakearmy/autorestic/releases" echo "Could not determine OS automatically, please check the release page manually: https://github.com/cupcakearmy/autorestic/releases"
exit 1 exit 1
@ -17,7 +19,7 @@ fi
echo $OS echo $OS
NATIVE_ARCH=$(uname -m | tr '[:upper:]' '[:lower:]') NATIVE_ARCH=$(uname -m | tr '[:upper:]' '[:lower:]')
if [[ $NATIVE_ARCH == *"x86_64"* ]]; then if [[ $NATIVE_ARCH == *"x86_64"* || $NATIVE_ARCH == *"amd64"* ]]; then
ARCH=amd64 ARCH=amd64
elif [[ $NATIVE_ARCH == *"arm64"* || $NATIVE_ARCH == *"aarch64"* ]]; then elif [[ $NATIVE_ARCH == *"arm64"* || $NATIVE_ARCH == *"aarch64"* ]]; then
ARCH=arm64 ARCH=arm64

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
"regexp"
"strings" "strings"
"github.com/cupcakearmy/autorestic/internal/colors" "github.com/cupcakearmy/autorestic/internal/colors"
@ -13,18 +14,19 @@ import (
) )
type BackendRest struct { type BackendRest struct {
User string `mapstructure:"user,omitempty"` User string `mapstructure:"user,omitempty" yaml:"user,omitempty"`
Password string `mapstructure:"password,omitempty"` Password string `mapstructure:"password,omitempty" yaml:"password,omitempty"`
} }
type Backend struct { type Backend struct {
name string name string
Type string `mapstructure:"type,omitempty"` Type string `mapstructure:"type,omitempty" yaml:"type,omitempty"`
Path string `mapstructure:"path,omitempty"` Path string `mapstructure:"path,omitempty" yaml:"path,omitempty"`
Key string `mapstructure:"key,omitempty"` Key string `mapstructure:"key,omitempty" yaml:"key,omitempty"`
Env map[string]string `mapstructure:"env,omitempty"` RequireKey bool `mapstructure:"requireKey,omitempty" yaml:"requireKey,omitempty"`
Rest BackendRest `mapstructure:"rest,omitempty"` Env map[string]string `mapstructure:"env,omitempty" yaml:"env,omitempty"`
Options Options `mapstructure:"options,omitempty"` Rest BackendRest `mapstructure:"rest,omitempty" yaml:"rest,omitempty"`
Options Options `mapstructure:"options,omitempty" yaml:"options,omitempty"`
} }
func GetBackend(name string) (Backend, bool) { func GetBackend(name string) (Backend, bool) {
@ -57,6 +59,8 @@ func (b Backend) generateRepo() (string, error) {
} }
} }
var nonAlphaRegex = regexp.MustCompile("[^A-Za-z0-9]")
func (b Backend) getEnv() (map[string]string, error) { func (b Backend) getEnv() (map[string]string, error) {
env := make(map[string]string) env := make(map[string]string)
// Key // Key
@ -72,7 +76,9 @@ func (b Backend) getEnv() (map[string]string, error) {
} }
// From Envfile and passed as env // From Envfile and passed as env
var prefix = "AUTORESTIC_" + strings.ToUpper(b.name) + "_" nameForEnv := strings.ToUpper(b.name)
nameForEnv = nonAlphaRegex.ReplaceAllString(nameForEnv, "_")
var prefix = "AUTORESTIC_" + nameForEnv + "_"
for _, variable := range os.Environ() { for _, variable := range os.Environ() {
var splitted = strings.SplitN(variable, "=", 2) var splitted = strings.SplitN(variable, "=", 2)
if strings.HasPrefix(splitted[0], prefix) { if strings.HasPrefix(splitted[0], prefix) {
@ -104,6 +110,9 @@ func (b Backend) validate() error {
// Check if key is set in environment // Check if key is set in environment
env, _ := b.getEnv() env, _ := b.getEnv()
if _, found := env["RESTIC_PASSWORD"]; !found { if _, found := env["RESTIC_PASSWORD"]; !found {
if b.RequireKey {
return fmt.Errorf("backend %s requires a key but none was provided", b.name)
}
// No key set in config file or env => generate random key and save file // No key set in config file or env => generate random key and save file
key := generateRandomKey() key := generateRandomKey()
b.Key = key b.Key = key

View File

@ -6,6 +6,7 @@ import (
"testing" "testing"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/stretchr/testify/assert"
) )
func TestGenerateRepo(t *testing.T) { func TestGenerateRepo(t *testing.T) {
@ -194,6 +195,33 @@ func TestGetEnv(t *testing.T) {
assertEqual(t, result["B2_ACCOUNT_ID"], "foo123") assertEqual(t, result["B2_ACCOUNT_ID"], "foo123")
assertEqual(t, result["B2_ACCOUNT_KEY"], "foo456") assertEqual(t, result["B2_ACCOUNT_KEY"], "foo456")
}) })
for _, char := range "@-_:/" {
t.Run(fmt.Sprintf("env var with special char (%c)", char), func(t *testing.T) {
// generate env variables
// TODO better way to teardown
defer os.Unsetenv("AUTORESTIC_FOO_BAR_RESTIC_PASSWORD")
defer os.Unsetenv("AUTORESTIC_FOO_BAR_B2_ACCOUNT_ID")
defer os.Unsetenv("AUTORESTIC_FOO_BAR_B2_ACCOUNT_KEY")
os.Setenv("AUTORESTIC_FOO_BAR_RESTIC_PASSWORD", "secret123")
os.Setenv("AUTORESTIC_FOO_BAR_B2_ACCOUNT_ID", "foo123")
os.Setenv("AUTORESTIC_FOO_BAR_B2_ACCOUNT_KEY", "foo456")
b := Backend{
name: fmt.Sprintf("foo%cbar", char),
Type: "local",
Path: "/foo/bar",
}
result, err := b.getEnv()
if err != nil {
t.Errorf("unexpected error %v", err)
}
assertEqual(t, result["RESTIC_REPOSITORY"], "/foo/bar")
assertEqual(t, result["RESTIC_PASSWORD"], "secret123")
assertEqual(t, result["B2_ACCOUNT_ID"], "foo123")
assertEqual(t, result["B2_ACCOUNT_KEY"], "foo456")
})
}
} }
func TestValidate(t *testing.T) { func TestValidate(t *testing.T) {
@ -222,4 +250,16 @@ func TestValidate(t *testing.T) {
} }
assertEqual(t, err.Error(), "Backend \"foo\" has no \"path\"") assertEqual(t, err.Error(), "Backend \"foo\" has no \"path\"")
}) })
t.Run("require key with no key", func(t *testing.T) {
b := Backend{
name: "foo",
Type: "local",
Path: "~/foo/bar",
RequireKey: true,
}
err := b.validate()
fmt.Printf("error: %v\n", err)
assert.EqualError(t, err, "backend foo requires a key but none was provided")
})
} }

View File

@ -10,24 +10,24 @@ import (
"github.com/cupcakearmy/autorestic/internal/colors" "github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/flags" "github.com/cupcakearmy/autorestic/internal/flags"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
const VERSION = "1.8.2" const VERSION = "1.8.3"
type OptionMap map[string][]interface{} type OptionMap map[string][]interface{}
type Options map[string]OptionMap type Options map[string]OptionMap
type Config struct { type Config struct {
Version string `mapstructure:"version"` Version string `mapstructure:"version" yaml:"version"`
Extras interface{} `mapstructure:"extras"` Lockfile string `mapstructure:"lockfile,omitempty" yaml:"lockfile,omitempty"`
Locations map[string]Location `mapstructure:"locations"` Extras interface{} `mapstructure:"extras" yaml:"extras"`
Backends map[string]Backend `mapstructure:"backends"` Locations map[string]Location `mapstructure:"locations" yaml:"locations"`
Global Options `mapstructure:"global"` Backends map[string]Backend `mapstructure:"backends" yaml:"backends"`
Global Options `mapstructure:"global" yaml:"global"`
} }
var once sync.Once var once sync.Once
@ -40,7 +40,7 @@ func exitConfig(err error, msg string) {
if msg != "" { if msg != "" {
colors.Error.Println(msg) colors.Error.Println(msg)
} }
lock.Unlock() Unlock()
os.Exit(1) os.Exit(1)
} }

View File

@ -1,10 +1,15 @@
package internal package internal
import ( import (
"path"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"sync"
"testing" "testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
) )
func TestOptionToString(t *testing.T) { func TestOptionToString(t *testing.T) {
@ -143,6 +148,48 @@ func TestGetOptionsMultipleKeys(t *testing.T) {
reflect.DeepEqual(result, expected) reflect.DeepEqual(result, expected)
} }
func TestSaveConfigProducesReadableConfig(t *testing.T) {
workDir := t.TempDir()
viper.SetConfigFile(path.Join(workDir, ".autorestic.yml"))
// Required to appease the config reader
viper.Set("version", 2)
c := Config{
Version: "2",
Locations: map[string]Location{
"test": {
Type: "local",
name: "test",
From: []string{"in-dir"},
To: []string{"test"},
// ForgetOption & ConfigOption have previously marshalled in a way that
// can't get read correctly
ForgetOption: "foo",
CopyOption: map[string][]string{"foo": {"bar"}},
},
},
Backends: map[string]Backend{
"test": {
name: "test",
Type: "local",
Path: "backup-target",
Key: "supersecret",
},
},
}
err := c.SaveConfig()
assert.NoError(t, err)
// Ensure we the config reading logic actually runs
config = nil
once = sync.Once{}
readConfig := GetConfig()
assert.NotNil(t, readConfig)
assert.Equal(t, c, *readConfig)
}
func assertEqual[T comparable](t testing.TB, result, expected T) { func assertEqual[T comparable](t testing.TB, result, expected T) {
t.Helper() t.Helper()

View File

@ -1,12 +1,22 @@
package internal package internal
import (
"errors"
"fmt"
)
func RunCron() error { func RunCron() error {
c := GetConfig() c := GetConfig()
var errs []error
for name, l := range c.Locations { for name, l := range c.Locations {
l.name = name l.name = name
if err := l.RunCron(); err != nil { if err := l.RunCron(); err != nil {
return err errs = append(errs, err)
} }
} }
if len(errs) > 0 {
return fmt.Errorf("Encountered errors during cron process:\n%w", errors.Join(errs...))
}
return nil return nil
} }

View File

@ -1,6 +1,7 @@
package internal package internal
import ( import (
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@ -10,7 +11,6 @@ import (
"github.com/cupcakearmy/autorestic/internal/colors" "github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/flags" "github.com/cupcakearmy/autorestic/internal/flags"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/cupcakearmy/autorestic/internal/metadata" "github.com/cupcakearmy/autorestic/internal/metadata"
"github.com/robfig/cron" "github.com/robfig/cron"
) )
@ -33,26 +33,26 @@ const (
) )
type Hooks struct { type Hooks struct {
Dir string `mapstructure:"dir"` Dir string `mapstructure:"dir" yaml:"dir"`
PreValidate HookArray `mapstructure:"prevalidate,omitempty"` PreValidate HookArray `mapstructure:"prevalidate,omitempty" yaml:"prevalidate,omitempty"`
Before HookArray `mapstructure:"before,omitempty"` Before HookArray `mapstructure:"before,omitempty" yaml:"before,omitempty"`
After HookArray `mapstructure:"after,omitempty"` After HookArray `mapstructure:"after,omitempty" yaml:"after,omitempty"`
Success HookArray `mapstructure:"success,omitempty"` Success HookArray `mapstructure:"success,omitempty" yaml:"success,omitempty"`
Failure HookArray `mapstructure:"failure,omitempty"` Failure HookArray `mapstructure:"failure,omitempty" yaml:"failure,omitempty"`
} }
type LocationCopy = map[string][]string type LocationCopy = map[string][]string
type Location struct { type Location struct {
name string `mapstructure:",omitempty"` name string `mapstructure:",omitempty" yaml:",omitempty"`
From []string `mapstructure:"from,omitempty"` From []string `mapstructure:"from,omitempty" yaml:"from,omitempty"`
Type string `mapstructure:"type,omitempty"` Type string `mapstructure:"type,omitempty" yaml:"type,omitempty"`
To []string `mapstructure:"to,omitempty"` To []string `mapstructure:"to,omitempty" yaml:"to,omitempty"`
Hooks Hooks `mapstructure:"hooks,omitempty"` Hooks Hooks `mapstructure:"hooks,omitempty" yaml:"hooks,omitempty"`
Cron string `mapstructure:"cron,omitempty"` Cron string `mapstructure:"cron,omitempty" yaml:"cron,omitempty"`
Options Options `mapstructure:"options,omitempty"` Options Options `mapstructure:"options,omitempty" yaml:"options,omitempty"`
ForgetOption LocationForgetOption `mapstructure:"forget,omitempty"` ForgetOption LocationForgetOption `mapstructure:"forget,omitempty" yaml:"forget,omitempty"`
CopyOption LocationCopy `mapstructure:"copy,omitempty"` CopyOption LocationCopy `mapstructure:"copy,omitempty" yaml:"copy,omitempty"`
} }
func GetLocation(name string) (Location, bool) { func GetLocation(name string) (Location, bool) {
@ -441,12 +441,15 @@ func (l Location) RunCron() error {
if err != nil { if err != nil {
return err return err
} }
last := time.Unix(lock.GetCron(l.name), 0) last := time.Unix(GetCron(l.name), 0)
next := schedule.Next(last) next := schedule.Next(last)
now := time.Now() now := time.Now()
if now.After(next) { if now.After(next) {
lock.SetCron(l.name, now.Unix()) SetCron(l.name, now.Unix())
l.Backup(true, "") errs := l.Backup(true, "")
if len(errs) > 0 {
return fmt.Errorf("Failed to backup location \"%s\":\n%w", l.name, errors.Join(errs...))
}
} else { } else {
if !flags.CRON_LEAN { if !flags.CRON_LEAN {
colors.Body.Printf("Skipping \"%s\", not due yet.\n", l.name) colors.Body.Printf("Skipping \"%s\", not due yet.\n", l.name)

View File

@ -1,8 +1,9 @@
package lock package internal
import ( import (
"os" "os"
"path" "path"
"path/filepath"
"sync" "sync"
"github.com/cupcakearmy/autorestic/internal/colors" "github.com/cupcakearmy/autorestic/internal/colors"
@ -12,19 +13,33 @@ import (
var lock *viper.Viper var lock *viper.Viper
var file string var file string
var once sync.Once var lockOnce sync.Once
const ( const (
RUNNING = "running" RUNNING = "running"
) )
// getLockfilePath returns the path to the lockfile. If flags.LOCKFILE_PATH is // getLockfilePath returns the path to the lockfile. The path for the lockfile
// set, its value is used, otherwise the path is generated relative to the // can be sources from multiple places If flags.LOCKFILE_PATH is set, its value
// config file. // is used; if the config has the `lockfile` option set, its value is used;
// otherwise the path is generated relative to the config file.
func getLockfilePath() string { func getLockfilePath() string {
if flags.LOCKFILE_PATH != "" { if flags.LOCKFILE_PATH != "" {
abs, err := filepath.Abs(flags.LOCKFILE_PATH)
if err != nil {
return flags.LOCKFILE_PATH return flags.LOCKFILE_PATH
} else { }
return abs
}
if lockfile := GetConfig().Lockfile; lockfile != "" {
abs, err := filepath.Abs(lockfile)
if err != nil {
return lockfile
}
return abs
}
p := viper.ConfigFileUsed() p := viper.ConfigFileUsed()
if p == "" { if p == "" {
colors.Error.Println("cannot lock before reading config location") colors.Error.Println("cannot lock before reading config location")
@ -32,11 +47,10 @@ func getLockfilePath() string {
} }
return path.Join(path.Dir(p), ".autorestic.lock.yml") return path.Join(path.Dir(p), ".autorestic.lock.yml")
} }
}
func getLock() *viper.Viper { func getLock() *viper.Viper {
if lock == nil { if lock == nil {
once.Do(func() { lockOnce.Do(func() {
lock = viper.New() lock = viper.New()
lock.SetDefault("running", false) lock.SetDefault("running", false)
file = getLockfilePath() file = getLockfilePath()

View File

@ -1,4 +1,4 @@
package lock package internal
import ( import (
"log" "log"

View File

@ -22,7 +22,7 @@ import (
"syscall" "syscall"
"github.com/cupcakearmy/autorestic/cmd" "github.com/cupcakearmy/autorestic/cmd"
"github.com/cupcakearmy/autorestic/internal/lock" "github.com/cupcakearmy/autorestic/internal"
) )
func handleCtrlC() { func handleCtrlC() {
@ -31,7 +31,7 @@ func handleCtrlC() {
go func() { go func() {
sig := <-c sig := <-c
fmt.Println("Signal:", sig) fmt.Println("Signal:", sig)
lock.Unlock() internal.Unlock()
os.Exit(0) os.Exit(0)
}() }()
} }