Compare commits

..

4 Commits

Author SHA1 Message Date
Florian
1ad8935e5a Merge 8b60120342 into ced20801c1 2024-01-20 16:05:59 -07:00
rwxd
8b60120342 fix(cmd unlock): add force flag 2023-10-10 21:23:17 +02:00
rwxd
ba51d1d062 fix(unlock cmd): get user confirmation in case an instance is still running 2023-10-10 20:56:55 +02:00
rwxd
43efd1db1a fix: cli command to unlock the autorestic running value 2023-10-10 20:45:48 +02:00
37 changed files with 2076 additions and 2870 deletions

View File

@@ -1,4 +1,4 @@
name: Release name: Main
on: on:
push: push:
@@ -9,11 +9,13 @@ jobs:
docker: docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: docker/setup-qemu-action@v4 - name: Set up QEMU
- uses: docker/setup-buildx-action@v4 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Docker Labels - name: Docker Labels
id: meta id: meta
uses: docker/metadata-action@v6 uses: crazy-max/ghaction-docker-meta@v4
with: with:
images: cupcakearmy/autorestic images: cupcakearmy/autorestic
tags: | tags: |
@@ -21,12 +23,12 @@ jobs:
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v4 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v3
with: with:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
@@ -35,9 +37,10 @@ jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v3
- uses: jdx/mise-action@v3 - uses: actions/setup-go@v3
- run: mise run build with:
go-version: "^1.20"
- name: Build - name: Build
run: go run build/build.go run: go run build/build.go
- name: Release - name: Release

View File

@@ -1,29 +0,0 @@
name: CI
on:
pull_request:
push:
branches:
- main
- master
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
MISE_EXPERIMENTAL: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: jdx/mise-action@v3
- run: mise run test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: jdx/mise-action@v3
- run: mise run build

1
.gitignore vendored
View File

@@ -10,4 +10,3 @@ test
autorestic autorestic
data data
dist dist
coverage*

View File

@@ -1,4 +1,4 @@
FROM golang:1.25-alpine as builder FROM golang:1.20-alpine as builder
WORKDIR /app WORKDIR /app
COPY go.* . COPY go.* .
@@ -6,8 +6,8 @@ RUN go mod download
COPY . . COPY . .
RUN go build RUN go build
FROM restic/restic:0.17.3 FROM restic/restic:0.16.0
RUN apk add --no-cache rclone bash curl docker-cli RUN apk add --no-cache rclone bash curl
COPY --from=builder /app/autorestic /usr/bin/autorestic COPY --from=builder /app/autorestic /usr/bin/autorestic
ENTRYPOINT [] ENTRYPOINT []
CMD [ "autorestic" ] CMD [ "autorestic" ]

View File

@@ -34,7 +34,7 @@ Autorestic is a wrapper around the amazing [restic](https://restic.net/). While
- Backup locations to multiple backends - Backup locations to multiple backends
- Snapshot policies and pruning - Snapshot policies and pruning
- Fully encrypted - Fully encrypted
- Before/after backup hooks - Pre/After hooks
- Exclude pattern/files - Exclude pattern/files
- Cron jobs for automatic backup - Cron jobs for automatic backup
- Backup & Restore docker volume - Backup & Restore docker volume

View File

@@ -18,11 +18,9 @@ var backupCmd = &cobra.Command{
err := lock.Lock() err := lock.Lock()
CheckErr(err) CheckErr(err)
defer lock.Unlock() defer lock.Unlock()
dry, _ := cmd.Flags().GetBool("dry-run")
selected, err := internal.GetAllOrSelected(cmd, false) selected, err := internal.GetAllOrSelected(cmd, false)
CheckErr(err) CheckErr(err)
errors := 0 errors := 0
for _, name := range selected { for _, name := range selected {
var splitted = strings.Split(name, "@") var splitted = strings.Split(name, "@")
@@ -31,7 +29,7 @@ var backupCmd = &cobra.Command{
specificBackend = splitted[1] specificBackend = splitted[1]
} }
location, _ := internal.GetLocation(splitted[0]) location, _ := internal.GetLocation(splitted[0])
errs := location.Backup(false, dry, specificBackend) errs := location.Backup(false, specificBackend)
for _, err := range errs { for _, err := range errs {
colors.Error.Printf("%s\n\n", err) colors.Error.Printf("%s\n\n", err)
errors++ errors++
@@ -46,5 +44,4 @@ var backupCmd = &cobra.Command{
func init() { func init() {
rootCmd.AddCommand(backupCmd) rootCmd.AddCommand(backupCmd)
internal.AddFlagsToCommand(backupCmd, false) internal.AddFlagsToCommand(backupCmd, false)
backupCmd.Flags().Bool("dry-run", false, "do not write changes, show what would be affected")
} }

View File

@@ -3,7 +3,6 @@ package cmd
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"os"
"os/exec" "os/exec"
"strings" "strings"
@@ -62,10 +61,9 @@ func isAutoresticRunning() bool {
lines := strings.Split(out.String(), "\n") lines := strings.Split(out.String(), "\n")
autoresticProcesses := []string{} autoresticProcesses := []string{}
currentPid := fmt.Sprint(os.Getpid())
for _, line := range lines { for _, line := range lines {
if strings.Contains(line, "autorestic") && !strings.Contains(line, "grep autorestic") && !strings.Contains(line, currentPid) { if strings.Contains(line, "autorestic") && !strings.Contains(line, "grep autorestic") {
autoresticProcesses = append(autoresticProcesses, line) autoresticProcesses = append(autoresticProcesses, line)
} }
} }

View File

@@ -1 +1 @@
v24 v20.8.0

View File

@@ -1,2 +0,0 @@
[settings]
idiomatic_version_file_enable_tools = ["node", "pnpm"]

View File

@@ -1,15 +1,14 @@
{ {
"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": "^14.2.35", "next": "^13.5.3",
"nextra": "^2.13.4", "nextra": "^2.13.1",
"nextra-theme-docs": "^2.13.4", "nextra-theme-docs": "^2.13.1",
"react": "^18.3.1", "react": "^18.2.0",
"react-dom": "^18.3.1" "react-dom": "^18.2.0"
}, },
"packageManager": "pnpm@10.33.0" "packageManager": "pnpm@8.8.0"
} }

View File

@@ -16,25 +16,3 @@ 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

@@ -1,14 +1,11 @@
# Backup # Backup
```bash ```bash
autorestic backup [-l, --location] [-a, --all] [--dry-run] autorestic backup [-l, --location] [-a, --all]
``` ```
Performs a backup of all locations if the `-a` flag is passed. To only backup some locations pass one or more `-l` or `--location` flags. Performs a backup of all locations if the `-a` flag is passed. To only backup some locations pass one or more `-l` or `--location` flags.
The `--dry-run` flag will do a dry run showing what would have been deleted, but won't touch the actual data.
```bash ```bash
# All # All
autorestic backup -a autorestic backup -a

View File

@@ -4,7 +4,7 @@
autorestic forget [-l, --location] [-a, --all] [--dry-run] [--prune] 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/options/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. The `--dry-run` flag will do a dry run showing what would have been deleted, but won't touch the actual data.

View File

@@ -1,7 +1,5 @@
# 🏘 Community # 🏘 Community
## Software
A list of community driven projects. (No official affiliation) A list of community driven projects. (No official affiliation)
- SystemD Units: <https://gitlab.com/py_crash/autorestic-systemd-units> - SystemD Units: <https://gitlab.com/py_crash/autorestic-systemd-units>
@@ -11,7 +9,3 @@ A list of community driven projects. (No official affiliation)
- Ansible Role: <https://github.com/FuzzyMistborn/ansible-role-autorestic> - Ansible Role: <https://github.com/FuzzyMistborn/ansible-role-autorestic>
- Ansible Role: <https://0xacab.org/varac-projects/ansible-role-autorestic> - Ansible Role: <https://0xacab.org/varac-projects/ansible-role-autorestic>
- Ansible Role: <https://github.com/dbrennand/ansible-role-autorestic> - Ansible Role: <https://github.com/dbrennand/ansible-role-autorestic>
## Writing
- [restic: excellent resource for local and cloud backup](https://notes.nicfab.eu/en/posts/restic/)

View File

@@ -56,8 +56,6 @@ version: 2
extras: extras:
hooks: &foo hooks: &foo
prevalidate:
- echo "Wake up!"
before: before:
- echo "Hello" - echo "Hello"
after: after:

View File

@@ -13,7 +13,7 @@ Autorestic is a wrapper around the amazing [restic](https://restic.net/). While
- Backup locations to multiple backends - Backup locations to multiple backends
- Snapshot policies and pruning - Snapshot policies and pruning
- Fully encrypted - Fully encrypted
- Before/after backup hooks - Pre/After hooks
- Exclude pattern/files - Exclude pattern/files
- Cron jobs for automatic backup - Cron jobs for automatic backup
- Backup & Restore docker volumes - Backup & Restore docker volumes

View File

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

View File

@@ -6,28 +6,23 @@ They consist of a list of commands that will be executed in the same directory a
The following hooks groups are supported, none are required: The following hooks groups are supported, none are required:
- `prevalidate`
- `before` - `before`
- `after` - `after`
- `failure` - `failure`
- `success` - `success`
The difference between `prevalidate` and `before` hooks are that `prevalidate` is run before checking the backup location is valid, including checking that the `from` directories exist. This can be useful, for example, to mount the source filesystem that contains the directories listed in `from`.
```yml | .autorestic.yml ```yml | .autorestic.yml
locations: locations:
my-location: my-location:
from: /data from: /data
to: my-backend to: my-backend
hooks: hooks:
prevalidate:
- echo "Checks"
before: before:
- echo "One" - echo "One"
- echo "Two" - echo "Two"
- echo "Three" - echo "Three"
after: after:
- echo "Bye" - echo "Byte"
failure: failure:
- echo "Something went wrong" - echo "Something went wrong"
success: success:
@@ -36,15 +31,13 @@ locations:
## Flowchart ## Flowchart
1. `prevalidate` hook 1. `before` hook
2. Check backup location 2. Run backup
3. `before` hook 3. `after` hook
4. Run backup 4. - `success` hook if no errors were found
5. `after` hook
6. - `success` hook if no errors were found
- `failure` hook if at least one error was encountered - `failure` hook if at least one error was encountered
If either the `prevalidate` or `before` hook encounters errors then the backup and `after` hooks will be skipped and only the `failed` hooks will run. If the `before` hook encounters errors the backup and `after` hooks will be skipped and only the `failed` hooks will run.
## Environment variables ## Environment variables

4064
docs/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

7
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/cupcakearmy/autorestic module github.com/cupcakearmy/autorestic
go 1.26.1 go 1.20
require ( require (
github.com/blang/semver/v4 v4.0.0 github.com/blang/semver/v4 v4.0.0
@@ -11,11 +11,9 @@ 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
@@ -25,7 +23,6 @@ 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
@@ -35,5 +32,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.1 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
) )

7
go.sum
View File

@@ -182,9 +182,8 @@ 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=
@@ -488,8 +487,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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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,8 +10,6 @@ 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
@@ -19,7 +17,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"* || $NATIVE_ARCH == *"amd64"* ]]; then if [[ $NATIVE_ARCH == *"x86_64"* ]]; 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,7 +6,6 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
"regexp"
"strings" "strings"
"github.com/cupcakearmy/autorestic/internal/colors" "github.com/cupcakearmy/autorestic/internal/colors"
@@ -14,19 +13,18 @@ import (
) )
type BackendRest struct { type BackendRest struct {
User string `mapstructure:"user,omitempty" yaml:"user,omitempty"` User string `mapstructure:"user,omitempty"`
Password string `mapstructure:"password,omitempty" yaml:"password,omitempty"` Password string `mapstructure:"password,omitempty"`
} }
type Backend struct { type Backend struct {
name string name string
Type string `mapstructure:"type,omitempty" yaml:"type,omitempty"` Type string `mapstructure:"type,omitempty"`
Path string `mapstructure:"path,omitempty" yaml:"path,omitempty"` Path string `mapstructure:"path,omitempty"`
Key string `mapstructure:"key,omitempty" yaml:"key,omitempty"` Key string `mapstructure:"key,omitempty"`
RequireKey bool `mapstructure:"requireKey,omitempty" yaml:"requireKey,omitempty"` Env map[string]string `mapstructure:"env,omitempty"`
Env map[string]string `mapstructure:"env,omitempty" yaml:"env,omitempty"` Rest BackendRest `mapstructure:"rest,omitempty"`
Rest BackendRest `mapstructure:"rest,omitempty" yaml:"rest,omitempty"` Options Options `mapstructure:"options,omitempty"`
Options Options `mapstructure:"options,omitempty" yaml:"options,omitempty"`
} }
func GetBackend(name string) (Backend, bool) { func GetBackend(name string) (Backend, bool) {
@@ -59,8 +57,6 @@ 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
@@ -76,9 +72,7 @@ func (b Backend) getEnv() (map[string]string, error) {
} }
// From Envfile and passed as env // From Envfile and passed as env
nameForEnv := strings.ToUpper(b.name) var prefix = "AUTORESTIC_" + 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) {
@@ -110,9 +104,6 @@ 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
@@ -152,7 +143,6 @@ func (b Backend) Exec(args []string) error {
return err return err
} }
options := ExecuteOptions{Envs: env} options := ExecuteOptions{Envs: env}
args = append(args, combineBackendOptions("exec", b)...)
_, out, err := ExecuteResticCommand(options, args...) _, out, err := ExecuteResticCommand(options, args...)
if err != nil { if err != nil {
colors.Error.Println(out) colors.Error.Println(out)
@@ -192,7 +182,6 @@ func (b Backend) ExecDocker(l Location, args []string) (int, string, error) {
case "s3": case "s3":
case "azure": case "azure":
case "gs": case "gs":
case "rest":
// No additional setup needed // No additional setup needed
case "rclone": case "rclone":
// Read host rclone config and mount it into the container // Read host rclone config and mount it into the container

View File

@@ -6,7 +6,6 @@ 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) {
@@ -195,33 +194,6 @@ 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) {
@@ -250,16 +222,4 @@ 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

@@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"path" "path"
@@ -36,7 +37,7 @@ func dlJSON(url string) (GithubRelease, error) {
return parsed, err return parsed, err
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return parsed, err return parsed, err
@@ -72,10 +73,9 @@ func downloadAndInstallAsset(body GithubRelease, name string) error {
// Uncompress // Uncompress
bz := bzip2.NewReader(resp.Body) bz := bzip2.NewReader(resp.Body)
// Save to tmp file in the same directory as the install directory // Save to tmp
// Linux does not support overwriting the file that is currently being running // 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.
// But it can be delete the old one and a new one moved in its place. tmp, err := ioutil.TempFile(os.TempDir(), "autorestic-")
tmp, err := os.CreateTemp(INSTALL_PATH, "autorestic-")
if err != nil { if err != nil {
return err return err
} }
@@ -89,25 +89,23 @@ func downloadAndInstallAsset(body GithubRelease, name string) error {
to := path.Join(INSTALL_PATH, name) to := path.Join(INSTALL_PATH, name)
defer os.Remove(tmp.Name()) // Cleanup temporary file after thread exits defer os.Remove(tmp.Name()) // Cleanup temporary file after thread exits
if err := os.Rename(tmp.Name(), to); err != nil {
mode := os.FileMode(0755) colors.Error.Printf("os.Rename() failed (%v), retrying with io.Copy()\n", err.Error())
if originalBin, err := os.Lstat(to); err == nil { var src *os.File
mode = originalBin.Mode() var dst *os.File
err := os.Remove(to) if src, err = os.Open(tmp.Name()); err != nil {
if err != nil {
return err return err
} }
} if dst, err = os.Create(to); err != nil {
err = os.Rename(tmp.Name(), to)
if err != nil {
return err return err
} }
if _, err := io.Copy(dst, src); err != nil {
err = os.Chmod(to, mode)
if err != nil {
return err return err
} }
if err := os.Chmod(to, 0755); err != nil {
return err
}
}
colors.Success.Printf("Successfully installed '%s' under %s\n", name, INSTALL_PATH) colors.Success.Printf("Successfully installed '%s' under %s\n", name, INSTALL_PATH)
return nil return nil

View File

@@ -17,17 +17,17 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
const VERSION = "1.8.3" const VERSION = "1.7.10"
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" yaml:"version"` Version string `mapstructure:"version"`
Extras interface{} `mapstructure:"extras" yaml:"extras"` Extras interface{} `mapstructure:"extras"`
Locations map[string]Location `mapstructure:"locations" yaml:"locations"` Locations map[string]Location `mapstructure:"locations"`
Backends map[string]Backend `mapstructure:"backends" yaml:"backends"` Backends map[string]Backend `mapstructure:"backends"`
Global Options `mapstructure:"global" yaml:"global"` Global Options `mapstructure:"global"`
} }
var once sync.Once var once sync.Once
@@ -132,7 +132,6 @@ func (c *Config) Describe() {
tmp = "" tmp = ""
hooks := map[string][]string{ hooks := map[string][]string{
"PreValidate": l.Hooks.PreValidate,
"Before": l.Hooks.Before, "Before": l.Hooks.Before,
"After": l.Hooks.After, "After": l.Hooks.After,
"Failure": l.Hooks.Failure, "Failure": l.Hooks.Failure,
@@ -188,34 +187,18 @@ func CheckConfig() error {
if !CheckIfResticIsCallable() { if !CheckIfResticIsCallable() {
return fmt.Errorf(`%s was not found. Install either with "autorestic install" or manually`, flags.RESTIC_BIN) return fmt.Errorf(`%s was not found. Install either with "autorestic install" or manually`, flags.RESTIC_BIN)
} }
cwd, _ := GetPathRelativeToConfig(".")
for name, location := range c.Locations {
location.name = name
// Hooks before location validation
options := ExecuteOptions{
Command: "bash",
Dir: cwd,
Envs: map[string]string{
"AUTORESTIC_LOCATION": location.name,
},
}
if err := location.ExecuteHooks(location.Hooks.PreValidate, options); err != nil {
return err
}
if err := location.validate(); err != nil {
return err
}
}
for name, backend := range c.Backends { for name, backend := range c.Backends {
backend.name = name backend.name = name
if err := backend.validate(); err != nil { if err := backend.validate(); err != nil {
return err return err
} }
} }
for name, location := range c.Locations {
location.name = name
if err := location.validate(); err != nil {
return err
}
}
return nil return nil
} }

View File

@@ -1,15 +1,10 @@
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) {
@@ -148,48 +143,6 @@ 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,22 +1,12 @@
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 {
errs = append(errs, err) return 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,7 +1,6 @@
package internal package internal
import ( import (
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@@ -34,26 +33,25 @@ const (
) )
type Hooks struct { type Hooks struct {
Dir string `mapstructure:"dir" yaml:"dir"` Dir string `mapstructure:"dir"`
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" yaml:",omitempty"` name string `mapstructure:",omitempty"`
From []string `mapstructure:"from,omitempty" yaml:"from,omitempty"` From []string `mapstructure:"from,omitempty"`
Type string `mapstructure:"type,omitempty" yaml:"type,omitempty"` Type string `mapstructure:"type,omitempty"`
To []string `mapstructure:"to,omitempty" yaml:"to,omitempty"` To []string `mapstructure:"to,omitempty"`
Hooks Hooks `mapstructure:"hooks,omitempty" yaml:"hooks,omitempty"` Hooks Hooks `mapstructure:"hooks,omitempty"`
Cron string `mapstructure:"cron,omitempty" yaml:"cron,omitempty"` Cron string `mapstructure:"cron,omitempty"`
Options Options `mapstructure:"options,omitempty" yaml:"options,omitempty"` Options Options `mapstructure:"options,omitempty"`
ForgetOption LocationForgetOption `mapstructure:"forget,omitempty" yaml:"forget,omitempty"` ForgetOption LocationForgetOption `mapstructure:"forget,omitempty"`
CopyOption LocationCopy `mapstructure:"copy,omitempty" yaml:"copy,omitempty"` CopyOption LocationCopy `mapstructure:"copy,omitempty"`
} }
func GetLocation(name string) (Location, bool) { func GetLocation(name string) (Location, bool) {
@@ -168,7 +166,7 @@ func (l Location) getLocationTags() string {
return buildTag("location", l.name) return buildTag("location", l.name)
} }
func (l Location) Backup(cron bool, dry bool, specificBackend string) []error { func (l Location) Backup(cron bool, specificBackend string) []error {
var errors []error var errors []error
var backends []string var backends []string
colors.PrimaryPrint(" Backing up location \"%s\" ", l.name) colors.PrimaryPrint(" Backing up location \"%s\" ", l.name)
@@ -186,18 +184,12 @@ func (l Location) Backup(cron bool, dry bool, specificBackend string) []error {
}, },
} }
// Hooks before location validation
if err := l.ExecuteHooks(l.Hooks.PreValidate, options); err != nil {
errors = append(errors, err)
goto after
}
if err := l.validate(); err != nil { if err := l.validate(); err != nil {
errors = append(errors, err) errors = append(errors, err)
goto after goto after
} }
// Hooks after location validation // Hooks
if err := l.ExecuteHooks(l.Hooks.Before, options); err != nil { if err := l.ExecuteHooks(l.Hooks.Before, options); err != nil {
errors = append(errors, err) errors = append(errors, err)
goto after goto after
@@ -228,9 +220,6 @@ func (l Location) Backup(cron bool, dry bool, specificBackend string) []error {
if cron { if cron {
cmd = append(cmd, "--tag", buildTag("cron")) cmd = append(cmd, "--tag", buildTag("cron"))
} }
if dry {
cmd = append(cmd, "--dry-run")
}
cmd = append(cmd, "--tag", l.getLocationTags()) cmd = append(cmd, "--tag", l.getLocationTags())
backupOptions := ExecuteOptions{ backupOptions := ExecuteOptions{
Envs: env, Envs: env,
@@ -300,13 +289,12 @@ func (l Location) Backup(cron bool, dry bool, specificBackend string) []error {
} }
} }
// After backup hooks // After hooks
if err := l.ExecuteHooks(l.Hooks.After, options); err != nil { if err := l.ExecuteHooks(l.Hooks.After, options); err != nil {
errors = append(errors, err) errors = append(errors, err)
} }
after: after:
// Success/failure hooks
var commands []string var commands []string
var isSuccess = len(errors) == 0 var isSuccess = len(errors) == 0
if isSuccess { if isSuccess {
@@ -450,10 +438,7 @@ func (l Location) RunCron() error {
now := time.Now() now := time.Now()
if now.After(next) { if now.After(next) {
lock.SetCron(l.name, now.Unix()) lock.SetCron(l.name, now.Unix())
errs := l.Backup(true, false, "") 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,14 +1,6 @@
package internal package internal
import ( import "testing"
"os"
"path"
"sync"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
)
func TestGetType(t *testing.T) { func TestGetType(t *testing.T) {
@@ -99,46 +91,3 @@ func TestBuildRestoreCommand(t *testing.T) {
expected := []string{"restore", "--target", "to", "--tag", "ar:location:foo", "snapshot", "options"} expected := []string{"restore", "--target", "to", "--tag", "ar:location:foo", "snapshot", "options"}
assertSliceEqual(t, result, expected) assertSliceEqual(t, result, expected)
} }
func TestLocationBackupWithMock(t *testing.T) {
// Backup original
originalExecutor := DefaultExecutor
defer func() { DefaultExecutor = originalExecutor }()
// Inject mock
mock := &MockExecutor{
ExecuteResticFunc: func(options ExecuteOptions, args ...string) (int, string, error) {
assert.Equal(t, "backup", args[0])
return 0, "success", nil
},
}
DefaultExecutor = mock
// Setup dummy config
workDir := t.TempDir()
configFile := path.Join(workDir, ".autorestic.yml")
err := os.WriteFile(configFile, []byte("version: 2"), 0644)
assert.NoError(t, err)
viper.Reset()
viper.SetConfigFile(configFile)
viper.Set("version", 2)
// Register test-backend
viper.Set("backends.test-backend.type", "local")
viper.Set("backends.test-backend.path", workDir)
config = nil
once = sync.Once{}
loc := Location{
name: "test-location",
To: []string{"test-backend"},
From: []string{"/"},
Type: "local",
}
errs := loc.Backup(false, false, "")
if len(errs) != 0 {
t.Errorf("expected no error, got %v", errs)
}
}

View File

@@ -1,20 +0,0 @@
package internal
type MockExecutor struct {
ExecuteFunc func(options ExecuteOptions, args ...string) (int, string, error)
ExecuteResticFunc func(options ExecuteOptions, args ...string) (int, string, error)
}
func (m *MockExecutor) Execute(options ExecuteOptions, args ...string) (int, string, error) {
if m.ExecuteFunc != nil {
return m.ExecuteFunc(options, args...)
}
return 0, "", nil
}
func (m *MockExecutor) ExecuteRestic(options ExecuteOptions, args ...string) (int, string, error) {
if m.ExecuteResticFunc != nil {
return m.ExecuteResticFunc(options, args...)
}
return 0, "", nil
}

View File

@@ -39,14 +39,7 @@ func (w ColoredWriter) Write(p []byte) (n int, err error) {
return len(p), nil return len(p), nil
} }
type Executor interface { func ExecuteCommand(options ExecuteOptions, args ...string) (int, string, error) {
Execute(options ExecuteOptions, args ...string) (int, string, error)
ExecuteRestic(options ExecuteOptions, args ...string) (int, string, error)
}
type RealExecutor struct{}
func (e *RealExecutor) Execute(options ExecuteOptions, args ...string) (int, string, error) {
cmd := exec.Command(options.Command, args...) cmd := exec.Command(options.Command, args...)
env := os.Environ() env := os.Environ()
for k, v := range options.Envs { for k, v := range options.Envs {
@@ -83,22 +76,12 @@ func (e *RealExecutor) Execute(options ExecuteOptions, args ...string) (int, str
return 0, out.String(), nil return 0, out.String(), nil
} }
func (e *RealExecutor) ExecuteRestic(options ExecuteOptions, args ...string) (int, string, error) { func ExecuteResticCommand(options ExecuteOptions, args ...string) (int, string, error) {
options.Command = flags.RESTIC_BIN options.Command = flags.RESTIC_BIN
var c = GetConfig() var c = GetConfig()
var optionsAsString = getOptions(c.Global, []string{"all"}) var optionsAsString = getOptions(c.Global, []string{"all"})
args = append(optionsAsString, args...) args = append(optionsAsString, args...)
return e.Execute(options, args...) return ExecuteCommand(options, args...)
}
var DefaultExecutor Executor = &RealExecutor{}
func ExecuteCommand(options ExecuteOptions, args ...string) (int, string, error) {
return DefaultExecutor.Execute(options, args...)
}
func ExecuteResticCommand(options ExecuteOptions, args ...string) (int, string, error) {
return DefaultExecutor.ExecuteRestic(options, args...)
} }
func CopyFile(from, to string) error { func CopyFile(from, to string) error {

View File

@@ -1,27 +0,0 @@
package internal
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestExecuteCommandWithMock(t *testing.T) {
// Backup original
originalExecutor := DefaultExecutor
defer func() { DefaultExecutor = originalExecutor }()
// Inject mock
mock := &MockExecutor{
ExecuteFunc: func(options ExecuteOptions, args ...string) (int, string, error) {
assert.Equal(t, "docker", options.Command)
return 0, "mock output", nil
},
}
DefaultExecutor = mock
code, out, err := ExecuteCommand(ExecuteOptions{Command: "docker"}, "info")
assert.NoError(t, err)
assert.Equal(t, 0, code)
assert.Equal(t, "mock output", out)
}

View File

@@ -1,14 +0,0 @@
[tools]
go = "latest"
restic = "latest"
[tasks]
build = { description = "Build the project", run = "go build -v ." }
test = { description = "Run tests", run = "go test -v ./..." }
coverage = { description = "Generate coverage report", run = "go test -coverprofile=coverage.out ./... && go tool cover -func=coverage.out && go tool cover -html=coverage.out -o coverage.html", depends = [
"test"
] }
clean = { run = "rm -f coverage.*", description = "Clean up coverage files" }

View File

@@ -1,280 +0,0 @@
package integration_test
import (
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func runAutorestic(t *testing.T, dir string, configPath string, args ...string) string {
// Find project root dynamically
wd, err := os.Getwd()
assert.NoError(t, err)
root := filepath.Dir(wd)
mainGo := filepath.Join(root, "main.go")
// Get restic path from mise
resticPath := "/Users/cupcakearmy/.local/share/mise/installs/restic/0.18.1/restic"
// Convert configPath to absolute path
absConfigPath, err := filepath.Abs(configPath)
assert.NoError(t, err)
cmd := exec.Command("go", append([]string{"run", mainGo, "--restic-bin", resticPath, "-c", absConfigPath}, args...)...)
// Run from root to find go.mod
cmd.Dir = root
// Add mise path to environment and set dummy password
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "PATH="+os.Getenv("PATH"))
cmd.Env = append(cmd.Env, "RESTIC_PASSWORD=password")
output, err := cmd.CombinedOutput()
// NOTE: We don't assert NoError here because tests might expect failures
return string(output)
}
func initRepo(t *testing.T, repoPath string) {
resticPath := "/Users/cupcakearmy/.local/share/mise/installs/restic/0.18.1/restic"
cmd := exec.Command(resticPath, "-r", repoPath, "init")
cmd.Env = append(os.Environ(), "RESTIC_PASSWORD=password")
output, err := cmd.CombinedOutput()
assert.NoError(t, err, string(output))
}
func TestAutoresticCheck(t *testing.T) {
tempDir := t.TempDir()
configContent := `
version: 2
locations:
my-data:
from: ` + tempDir + `
to: local
backends:
local:
type: local
path: ` + filepath.Join(tempDir, "repo") + `
key: password
`
configPath := filepath.Join(tempDir, "autorestic.yml")
err := os.WriteFile(configPath, []byte(configContent), 0644)
assert.NoError(t, err)
_ = runAutorestic(t, tempDir, configPath, "check")
}
func TestBackupRestore(t *testing.T) {
tempDir := t.TempDir()
// 1. Create a source file
sourceFile := "source.txt"
err := os.WriteFile(filepath.Join(tempDir, sourceFile), []byte("hello world"), 0644)
assert.NoError(t, err)
repoPath := filepath.Join(tempDir, "repo")
initRepo(t, repoPath)
configContent := `
version: 2
locations:
my-data:
from:
- ` + sourceFile + `
to: local
backends:
local:
type: local
path: ` + repoPath + `
key: password
`
configPath := filepath.Join(tempDir, "autorestic.yml")
err = os.WriteFile(configPath, []byte(configContent), 0644)
assert.NoError(t, err)
// 2. Backup
output := runAutorestic(t, tempDir, configPath, "backup", "-l", "my-data")
assert.Contains(t, output, "Done")
// 3. Restore
restoreDir := filepath.Join(tempDir, "restore")
err = os.MkdirAll(restoreDir, 0755)
assert.NoError(t, err)
output = runAutorestic(t, tempDir, configPath, "restore", "-l", "my-data", "--to", restoreDir)
t.Logf("Restore output: %s", output)
// DEBUG: List files in restoreDir and subdirectories
err = filepath.Walk(restoreDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
t.Logf("Found: %s", path)
return nil
})
assert.NoError(t, err)
// 4. Verify
// It might be nested depending on how it was backed up
// Let's look for source.txt in any subdirectory
var restoredFile string
err = filepath.Walk(restoreDir, func(path string, info os.FileInfo, err error) error {
if !info.IsDir() && info.Name() == "source.txt" {
restoredFile = path
}
return nil
})
assert.NoError(t, err)
assert.NotEmpty(t, restoredFile, "source.txt not found")
content, err := os.ReadFile(restoredFile)
assert.NoError(t, err)
assert.Equal(t, "hello world", string(content))
}
func TestHooks(t *testing.T) {
tempDir := t.TempDir()
// Create a dummy file to back up
sourceFile := "source.txt"
err := os.WriteFile(filepath.Join(tempDir, sourceFile), []byte("data"), 0644)
assert.NoError(t, err)
repoPath := filepath.Join(tempDir, "repo")
initRepo(t, repoPath)
configContent := `
version: 2
locations:
my-data:
from:
- ` + sourceFile + `
to: local
hooks:
before:
- touch before.txt
after:
- touch after.txt
backends:
local:
type: local
path: ` + repoPath + `
key: password
`
configPath := filepath.Join(tempDir, "autorestic.yml")
err = os.WriteFile(configPath, []byte(configContent), 0644)
assert.NoError(t, err)
// Run backup
runAutorestic(t, tempDir, configPath, "backup", "-l", "my-data")
// Verify
assert.FileExists(t, filepath.Join(tempDir, "before.txt"))
assert.FileExists(t, filepath.Join(tempDir, "after.txt"))
}
func TestCopy(t *testing.T) {
tempDir := t.TempDir()
// Create a dummy file to back up
sourceFile := "source.txt"
err := os.WriteFile(filepath.Join(tempDir, sourceFile), []byte("data"), 0644)
assert.NoError(t, err)
repoPath := filepath.Join(tempDir, "repo")
initRepo(t, repoPath)
remoteRepoPath := filepath.Join(tempDir, "remote_repo")
initRepo(t, remoteRepoPath)
configContent := `
version: 2
locations:
my-data:
from:
- ` + sourceFile + `
to: local
copy:
local:
- remote
backends:
local:
type: local
path: ` + repoPath + `
key: password
remote:
type: local
path: ` + remoteRepoPath + `
key: password
`
configPath := filepath.Join(tempDir, "autorestic.yml")
err = os.WriteFile(configPath, []byte(configContent), 0644)
assert.NoError(t, err)
// Run backup
output := runAutorestic(t, tempDir, configPath, "backup", "-l", "my-data")
// Verify copy in output
assert.Contains(t, output, "Copying local → remote")
}
func TestPermissions(t *testing.T) {
tempDir := t.TempDir()
// 1. Create a source file with specific permissions (0600)
sourceFile := "source.txt"
sourcePath := filepath.Join(tempDir, sourceFile)
err := os.WriteFile(sourcePath, []byte("data"), 0600)
assert.NoError(t, err)
repoPath := filepath.Join(tempDir, "repo")
initRepo(t, repoPath)
configContent := `
version: 2
locations:
my-data:
from:
- ` + sourceFile + `
to: local
backends:
local:
type: local
path: ` + repoPath + `
key: password
`
configPath := filepath.Join(tempDir, "autorestic.yml")
err = os.WriteFile(configPath, []byte(configContent), 0644)
assert.NoError(t, err)
// 2. Backup
runAutorestic(t, tempDir, configPath, "backup", "-l", "my-data")
// 3. Restore
restoreDir := filepath.Join(tempDir, "restore")
err = os.MkdirAll(restoreDir, 0755)
assert.NoError(t, err)
runAutorestic(t, tempDir, configPath, "restore", "-l", "my-data", "--to", restoreDir)
// 4. Verify permissions
// Use walk to find the restored file
var restoredFile string
err = filepath.Walk(restoreDir, func(path string, info os.FileInfo, err error) error {
if !info.IsDir() && info.Name() == "source.txt" {
restoredFile = path
}
return nil
})
assert.NoError(t, err)
assert.NotEmpty(t, restoredFile, "source.txt not found")
info, err := os.Stat(restoredFile)
assert.NoError(t, err)
// Check permissions (masking only for permission bits)
assert.Equal(t, os.FileMode(0600), info.Mode().Perm())
}

View File

@@ -1,14 +0,0 @@
package integration_test
import (
"github.com/stretchr/testify/assert"
"os/exec"
"testing"
)
func TestVersion(t *testing.T) {
cmd := exec.Command("go", "run", "../main.go", "--version")
out, err := cmd.CombinedOutput()
assert.NoError(t, err)
assert.Contains(t, string(out), "autorestic")
}