mirror of
https://github.com/cupcakearmy/autorestic.git
synced 2025-09-06 18:40:40 +00:00
Compare commits
38 Commits
v1.6.2
...
b3b7c8df95
Author | SHA1 | Date | |
---|---|---|---|
b3b7c8df95 | |||
2c5266c9a0 | |||
087b293c39 | |||
|
112a69d743 | ||
|
37d55c691f | ||
|
715b6f791c | ||
|
38b38c6805 | ||
c82db56069 | |||
27821dc3ef | |||
866975d32d | |||
|
955ac0e323 | ||
|
1512db5b55 | ||
|
046331748c | ||
72a40eaaa2 | |||
ec61effe22 | |||
d6acab94a5 | |||
|
d0d2fcf0f2 | ||
58fd41fafb | |||
|
d0c4a32879 | ||
74979e9a2a | |||
|
3732dcf6ff | ||
|
874ed52e3b | ||
4d9a2b828e | |||
|
b830667264 | ||
|
83eeb847ac | ||
|
a89ba5a40a | ||
|
6990bf6adc | ||
|
2f407cf211 | ||
489f3078fe | |||
|
e07dd0d991 | ||
|
2b9dc9f17c | ||
|
465bc037c2 | ||
|
37a043afff | ||
|
e91b632181 | ||
|
2b30998b9a | ||
|
49b37a0a9a | ||
3bc091f826 | |||
5bcf5c9217 |
8
.github/dependabot.yml
vendored
Normal file
8
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
21
.github/workflows/build.yml
vendored
21
.github/workflows/build.yml
vendored
@@ -10,12 +10,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Docker Labels
|
||||
id: meta
|
||||
uses: crazy-max/ghaction-docker-meta@v2
|
||||
uses: crazy-max/ghaction-docker-meta@v4
|
||||
with:
|
||||
images: cupcakearmy/autorestic
|
||||
tags: |
|
||||
@@ -23,12 +23,12 @@ jobs:
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
@@ -37,17 +37,12 @@ jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: "^1.16.3"
|
||||
go-version: "^1.20"
|
||||
- name: Build
|
||||
run: go run build/build.go
|
||||
|
||||
- name: Sign
|
||||
uses: tristan-weil/ghaction-checksum-sign-artifact@v1.0.1
|
||||
with:
|
||||
path: dist/*
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
|
31
CHANGELOG.md
31
CHANGELOG.md
@@ -5,11 +5,40 @@ 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.7.4] - 2023-01-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- Transformer for extracting information. @11mariom
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump docker restic version.
|
||||
- Docs dependencies updated.
|
||||
|
||||
## [1.7.1] - 2022-04-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- #178 Lean flag not working properly.
|
||||
|
||||
## [1.7.0] - 2022-04-27
|
||||
|
||||
### Changed
|
||||
|
||||
- #147 Stream output instead of buffering.
|
||||
|
||||
### Fixed
|
||||
|
||||
- #184 duplicate global options.
|
||||
- #154 add docs for migration.
|
||||
- #182 fix bug with upgrading custom restic with custom path.
|
||||
|
||||
## [1.6.2] - 2022-04-14
|
||||
|
||||
### Fixed
|
||||
|
||||
- Version bump in code
|
||||
- Version bump in code.
|
||||
|
||||
## [1.6.1] - 2022-04-14
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.18-alpine as builder
|
||||
FROM golang:1.20-alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY go.* .
|
||||
@@ -6,7 +6,8 @@ RUN go mod download
|
||||
COPY . .
|
||||
RUN go build
|
||||
|
||||
FROM alpine
|
||||
RUN apk add --no-cache restic rclone bash openssh
|
||||
FROM restic/restic:0.15.1
|
||||
RUN apk add --no-cache rclone bash
|
||||
COPY --from=builder /app/autorestic /usr/bin/autorestic
|
||||
ENTRYPOINT []
|
||||
CMD [ "autorestic" ]
|
||||
|
@@ -4,7 +4,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@@ -17,23 +21,51 @@ import (
|
||||
var DIR, _ = filepath.Abs("./dist")
|
||||
|
||||
var targets = map[string][]string{
|
||||
// "aix": {"ppc64"}, // Not supported by fsnotify
|
||||
"darwin": {"amd64", "arm64"},
|
||||
"freebsd": {"386", "amd64", "arm"},
|
||||
"linux": {"386", "amd64", "arm", "arm64"},
|
||||
"linux": {"386", "amd64", "arm", "arm64", "ppc64le", "mips", "mipsle", "mips64", "mips64le", "s390x"},
|
||||
"netbsd": {"386", "amd64"},
|
||||
"openbsd": {"386", "amd64"},
|
||||
// "windows": {"386", "amd64"}, // Not supported by autorestic
|
||||
"solaris": {"amd64"},
|
||||
}
|
||||
|
||||
type buildOptions struct {
|
||||
Target, Arch, Version string
|
||||
}
|
||||
|
||||
func build(options buildOptions, wg *sync.WaitGroup) {
|
||||
fmt.Printf("Building %s %s\n", options.Target, options.Arch)
|
||||
const (
|
||||
CHECKSUM_MD5 = "MD5SUMS"
|
||||
CHECKSUM_SHA_1 = "SHA1SUMS"
|
||||
CHECKSUM_SHA_256 = "SHA256SUMS"
|
||||
)
|
||||
|
||||
type Checksums struct {
|
||||
filename, md5, sha1, sha256 string
|
||||
}
|
||||
|
||||
func writeChecksums(checksums *[]Checksums) {
|
||||
FILE_MD5, _ := os.OpenFile(path.Join(DIR, CHECKSUM_MD5), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
defer FILE_MD5.Close()
|
||||
FILE_SHA1, _ := os.OpenFile(path.Join(DIR, CHECKSUM_SHA_1), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
defer FILE_SHA1.Close()
|
||||
FILE_SHA256, _ := os.OpenFile(path.Join(DIR, CHECKSUM_SHA_256), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
defer FILE_SHA256.Close()
|
||||
|
||||
for _, checksum := range *checksums {
|
||||
fmt.Fprintf(FILE_MD5, "%s %s\n", checksum.md5, checksum.filename)
|
||||
fmt.Fprintf(FILE_SHA1, "%s %s\n", checksum.sha1, checksum.filename)
|
||||
fmt.Fprintf(FILE_SHA256, "%s %s\n", checksum.sha256, checksum.filename)
|
||||
}
|
||||
}
|
||||
|
||||
func build(options buildOptions, wg *sync.WaitGroup, checksums *[]Checksums) {
|
||||
defer wg.Done()
|
||||
|
||||
fmt.Printf("Building: %s %s\n", options.Target, options.Arch)
|
||||
out := fmt.Sprintf("autorestic_%s_%s_%s", options.Version, options.Target, options.Arch)
|
||||
out = path.Join(DIR, out)
|
||||
out, _ = filepath.Abs(out)
|
||||
fmt.Println(out)
|
||||
|
||||
// Build
|
||||
{
|
||||
@@ -62,22 +94,39 @@ func build(options buildOptions, wg *sync.WaitGroup) {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
|
||||
// Checksum
|
||||
{
|
||||
file := out + ".bz2"
|
||||
content, _ := ioutil.ReadFile(file)
|
||||
*checksums = append(*checksums, Checksums{
|
||||
filename: path.Base(file),
|
||||
md5: fmt.Sprintf("%x", md5.Sum(content)),
|
||||
sha1: fmt.Sprintf("%x", sha1.Sum(content)),
|
||||
sha256: fmt.Sprintf("%x", sha256.Sum256(content)),
|
||||
})
|
||||
}
|
||||
|
||||
fmt.Printf("Built: %s\n", path.Base(out))
|
||||
}
|
||||
|
||||
func main() {
|
||||
os.RemoveAll(DIR)
|
||||
v := internal.VERSION
|
||||
checksums := []Checksums{}
|
||||
|
||||
// Build async
|
||||
var wg sync.WaitGroup
|
||||
for target, archs := range targets {
|
||||
for _, arch := range archs {
|
||||
wg.Add(1)
|
||||
build(buildOptions{
|
||||
go build(buildOptions{
|
||||
Target: target,
|
||||
Arch: arch,
|
||||
Version: v,
|
||||
}, &wg)
|
||||
}, &wg, &checksums)
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
writeChecksums(&checksums)
|
||||
}
|
||||
|
@@ -13,7 +13,6 @@ 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.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
internal.GetConfig()
|
||||
flags.CRON_LEAN, _ = cmd.Flags().GetBool("lean")
|
||||
err := lock.Lock()
|
||||
CheckErr(err)
|
||||
defer lock.Unlock()
|
||||
@@ -25,5 +24,5 @@ var cronCmd = &cobra.Command{
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(cronCmd)
|
||||
cronCmd.Flags().Bool("lean", false, "only output information about actual backups")
|
||||
cronCmd.Flags().BoolVar(&flags.CRON_LEAN, "lean", false, "only output information about actual backups")
|
||||
}
|
||||
|
17
cmd/exec.go
17
cmd/exec.go
@@ -1,6 +1,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cupcakearmy/autorestic/internal"
|
||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||
"github.com/cupcakearmy/autorestic/internal/lock"
|
||||
@@ -18,10 +20,23 @@ var execCmd = &cobra.Command{
|
||||
|
||||
selected, err := internal.GetAllOrSelected(cmd, true)
|
||||
CheckErr(err)
|
||||
|
||||
var errors []error
|
||||
for _, name := range selected {
|
||||
colors.PrimaryPrint(" Executing on \"%s\" ", name)
|
||||
backend, _ := internal.GetBackend(name)
|
||||
backend.Exec(args)
|
||||
err := backend.Exec(args)
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
for _, err := range errors {
|
||||
colors.Error.Printf("%s\n\n", err)
|
||||
}
|
||||
|
||||
CheckErr(fmt.Errorf("%d errors were found", len(errors)))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@@ -40,7 +40,8 @@ func init() {
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.autorestic.yml or ./.autorestic.yml)")
|
||||
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")
|
||||
rootCmd.PersistentFlags().StringVar(&flags.RESTIC_BIN, "restic-bin", "restic", "specify custom restic binary")
|
||||
rootCmd.PersistentFlags().StringVar(&flags.DOCKER_IMAGE, "docker-image", "cupcakearmy/autorestic:"+internal.VERSION, "specify a custom docker image")
|
||||
cobra.OnInitialize(initConfig)
|
||||
}
|
||||
|
||||
|
9713
docs/.codedoc/package-lock.json
generated
9713
docs/.codedoc/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@codedoc/core": "^0.2.24"
|
||||
"@codedoc/core": "^0.3.2"
|
||||
}
|
||||
}
|
||||
|
@@ -2,10 +2,12 @@
|
||||
|
||||
A list of community driven projects. (No official affiliation)
|
||||
|
||||
- SystemD Units: https://gitlab.com/py_crash/autorestic-systemd-units
|
||||
- Docker image: https://github.com/pascaliske/docker-autorestic
|
||||
- Ansible Role: https://github.com/adsanz/ansible-restic-role
|
||||
- Ansible Role: https://github.com/ItsNotGoodName/ansible-role-autorestic
|
||||
- Ansible Role: https://github.com/FuzzyMistborn/ansible-role-autorestic
|
||||
- SystemD Units: <https://gitlab.com/py_crash/autorestic-systemd-units>
|
||||
- Docker image: <https://github.com/pascaliske/docker-autorestic>
|
||||
- Ansible Role: <https://github.com/adsanz/ansible-restic-role>
|
||||
- Ansible Role: <https://github.com/ItsNotGoodName/ansible-role-autorestic>
|
||||
- Ansible Role: <https://github.com/FuzzyMistborn/ansible-role-autorestic>
|
||||
- Ansible Role: <https://0xacab.org/varac-projects/ansible-role-autorestic>
|
||||
- Ansible Role: <https://github.com/dbrennand/ansible-role-autorestic>
|
||||
|
||||
> :ToCPrevNext
|
||||
|
@@ -5,7 +5,7 @@ Linux & macOS. Windows is not supported. If you have problems installing please
|
||||
Autorestic requires `bash`, `wget` and `bzip2` to be installed. For most systems these should be already installed.
|
||||
|
||||
```bash
|
||||
wget -qO - https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash
|
||||
wget -qO - https://raw.githubusercontent.com/cupcakearmy/autorestic/master/install.sh | bash
|
||||
```
|
||||
|
||||
## Alternatives
|
||||
|
@@ -21,7 +21,7 @@ locations:
|
||||
keep-weekly: 1 # keep 1 last weekly snapshots
|
||||
keep-monthly: 12 # keep 12 last monthly snapshots
|
||||
keep-yearly: 7 # keep 7 last yearly snapshots
|
||||
keep-within: '2w' # keep snapshots from the last 2 weeks
|
||||
keep-within: '14d' # keep snapshots from the last 14 days
|
||||
```
|
||||
|
||||
## Globally
|
||||
|
@@ -57,4 +57,12 @@ locations:
|
||||
type: volume
|
||||
```
|
||||
|
||||
## Tagging
|
||||
|
||||
Autorestic changed the way backups are referenced. Before we took the paths as the identifying information. Now autorestic uses native restic tags to reference them. This means that old backups are not referenced. You can the old snapshots manually. An example can be shown below.
|
||||
|
||||
```bash
|
||||
autorestic exec -va -- tag --add ar:location:LOCATION_NAME # Only if you have only one location
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
||||
|
@@ -3,7 +3,7 @@
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
wget -qO - https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash
|
||||
wget -qO - https://raw.githubusercontent.com/cupcakearmy/autorestic/master/install.sh | bash
|
||||
```
|
||||
|
||||
See [installation](/installation) for alternative options.
|
||||
|
1872
docs/package-lock.json
generated
1872
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,6 @@
|
||||
"dev": "codedoc serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codedoc/cli": "^0.2.8"
|
||||
"@codedoc/cli": "^0.3.0"
|
||||
}
|
||||
}
|
||||
|
6
go.mod
6
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/cupcakearmy/autorestic
|
||||
|
||||
go 1.18
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/blang/semver/v4 v4.0.0
|
||||
@@ -28,8 +28,8 @@ require (
|
||||
github.com/spf13/jwalterweatherman v1.1.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-20220412211240-33da011f77ad // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
|
||||
golang.org/x/text v0.3.8 // indirect
|
||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
|
8
go.sum
8
go.sum
@@ -324,8 +324,8 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -333,8 +333,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
@@ -31,6 +31,11 @@ else
|
||||
fi
|
||||
echo $ARCH
|
||||
|
||||
if ! command -v bzip2 &>/dev/null; then
|
||||
echo "Missing bzip2 command. Please install the bzip2 package for your system."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wget -qO - https://api.github.com/repos/cupcakearmy/autorestic/releases/latest \
|
||||
| grep "browser_download_url.*_${OS}_${ARCH}" \
|
||||
| cut -d : -f 2,3 \
|
||||
|
@@ -38,7 +38,7 @@ func (b Backend) generateRepo() (string, error) {
|
||||
case "local":
|
||||
return GetPathRelativeToConfig(b.Path)
|
||||
case "rest":
|
||||
parsed, err := url.Parse(b.Path)
|
||||
parsed, err := url.Parse(os.ExpandEnv(b.Path))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -120,18 +120,19 @@ func (b Backend) validate() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options := ExecuteOptions{Envs: env}
|
||||
options := ExecuteOptions{Envs: env, Silent: true}
|
||||
// Check if already initialized
|
||||
_, _, err = ExecuteResticCommand(options, "snapshots")
|
||||
cmd := []string{"check"}
|
||||
cmd = append(cmd, combineBackendOptions("check", b)...)
|
||||
_, _, err = ExecuteResticCommand(options, cmd...)
|
||||
if err == nil {
|
||||
return nil
|
||||
} else {
|
||||
// If not initialize
|
||||
colors.Body.Printf("Initializing backend \"%s\"...\n", b.name)
|
||||
_, out, err := ExecuteResticCommand(options, "init")
|
||||
if flags.VERBOSE {
|
||||
colors.Faint.Println(out)
|
||||
}
|
||||
cmd := []string{"init"}
|
||||
cmd = append(cmd, combineBackendOptions("init", b)...)
|
||||
_, _, err := ExecuteResticCommand(options, cmd...)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -147,9 +148,6 @@ func (b Backend) Exec(args []string) error {
|
||||
colors.Error.Println(out)
|
||||
return err
|
||||
}
|
||||
if flags.VERBOSE {
|
||||
colors.Faint.Println(out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -167,7 +165,6 @@ func (b Backend) ExecDocker(l Location, args []string) (int, string, error) {
|
||||
args = append([]string{"restic"}, args...)
|
||||
docker := []string{
|
||||
"run", "--rm",
|
||||
"--pull", "always",
|
||||
"--entrypoint", "ash",
|
||||
"--workdir", dir,
|
||||
"--volume", volume + ":" + dir,
|
||||
@@ -201,6 +198,7 @@ func (b Backend) ExecDocker(l Location, args []string) (int, string, error) {
|
||||
for key, value := range env {
|
||||
docker = append(docker, "--env", key+"="+value)
|
||||
}
|
||||
docker = append(docker, "cupcakearmy/autorestic:"+VERSION, "-c", strings.Join(args, " "))
|
||||
|
||||
docker = append(docker, flags.DOCKER_IMAGE, "-c", strings.Join(args, " "))
|
||||
return ExecuteCommand(options, docker...)
|
||||
}
|
||||
|
225
internal/backend_test.go
Normal file
225
internal/backend_test.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func TestGenerateRepo(t *testing.T) {
|
||||
|
||||
t.Run("empty backend", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "empty backend",
|
||||
Type: "",
|
||||
}
|
||||
_, err := b.generateRepo()
|
||||
if err == nil {
|
||||
t.Errorf("Error expected for empty backend type")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("local backend", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "local backend",
|
||||
Type: "local",
|
||||
Path: "/foo/bar",
|
||||
}
|
||||
result, err := b.generateRepo()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
assertEqual(t, result, "/foo/bar")
|
||||
})
|
||||
|
||||
t.Run("local backend with homedir prefix", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "local backend",
|
||||
Type: "local",
|
||||
Path: "~/foo/bar",
|
||||
}
|
||||
result, err := b.generateRepo()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
assertEqual(t, result, fmt.Sprintf("%s/foo/bar", os.Getenv("HOME")))
|
||||
})
|
||||
|
||||
t.Run("local backend with config file", func(t *testing.T) {
|
||||
// config file path should always be present from initConfig
|
||||
viper.SetConfigFile("/tmp/.autorestic.yml")
|
||||
defer viper.Reset()
|
||||
|
||||
b := Backend{
|
||||
name: "local backend",
|
||||
Type: "local",
|
||||
}
|
||||
result, err := b.generateRepo()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
assertEqual(t, result, "/tmp")
|
||||
})
|
||||
|
||||
t.Run("rest backend with valid path", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "rest backend",
|
||||
Type: "rest",
|
||||
Path: "http://localhost:8000/foo",
|
||||
}
|
||||
result, err := b.generateRepo()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
assertEqual(t, result, "rest:http://localhost:8000/foo")
|
||||
})
|
||||
|
||||
t.Run("rest backend with user", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "rest backend",
|
||||
Type: "rest",
|
||||
Path: "http://localhost:8000/foo",
|
||||
Rest: BackendRest{
|
||||
User: "user",
|
||||
Password: "",
|
||||
},
|
||||
}
|
||||
result, err := b.generateRepo()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
assertEqual(t, result, "rest:http://user@localhost:8000/foo")
|
||||
})
|
||||
|
||||
t.Run("rest backend with user and password", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "rest backend",
|
||||
Type: "rest",
|
||||
Path: "http://localhost:8000/foo",
|
||||
Rest: BackendRest{
|
||||
User: "user",
|
||||
Password: "pass",
|
||||
},
|
||||
}
|
||||
result, err := b.generateRepo()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
assertEqual(t, result, "rest:http://user:pass@localhost:8000/foo")
|
||||
})
|
||||
|
||||
backendTests := []struct {
|
||||
name string
|
||||
backend Backend
|
||||
want string
|
||||
}{
|
||||
{name: "b2 backend", backend: Backend{name: "b2", Type: "b2", Path: "foo"}, want: "b2:foo"},
|
||||
{name: "azure backend", backend: Backend{name: "azure", Type: "azure", Path: "foo"}, want: "azure:foo"},
|
||||
{name: "gs backend", backend: Backend{name: "gs", Type: "gs", Path: "foo"}, want: "gs:foo"},
|
||||
{name: "s3 backend", backend: Backend{name: "s3", Type: "s3", Path: "foo"}, want: "s3:foo"},
|
||||
{name: "sftp backend", backend: Backend{name: "sftp", Type: "sftp", Path: "foo"}, want: "sftp:foo"},
|
||||
{name: "rclone backend", backend: Backend{name: "rclone", Type: "rclone", Path: "foo"}, want: "rclone:foo"},
|
||||
}
|
||||
|
||||
for _, tt := range backendTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.backend.generateRepo()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
assertEqual(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEnv(t *testing.T) {
|
||||
t.Run("env in key field", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "",
|
||||
Type: "local",
|
||||
Path: "/foo/bar",
|
||||
Key: "secret123",
|
||||
}
|
||||
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")
|
||||
})
|
||||
|
||||
t.Run("env in config file", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "",
|
||||
Type: "local",
|
||||
Path: "/foo/bar",
|
||||
Env: map[string]string{
|
||||
"B2_ACCOUNT_ID": "foo123",
|
||||
"B2_ACCOUNT_KEY": "foo456",
|
||||
},
|
||||
}
|
||||
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"], "")
|
||||
assertEqual(t, result["B2_ACCOUNT_ID"], "foo123")
|
||||
assertEqual(t, result["B2_ACCOUNT_KEY"], "foo456")
|
||||
})
|
||||
|
||||
t.Run("env in Envfile or env vars", func(t *testing.T) {
|
||||
// generate env variables
|
||||
// TODO better way to teardown
|
||||
defer os.Unsetenv("AUTORESTIC_FOO_RESTIC_PASSWORD")
|
||||
defer os.Unsetenv("AUTORESTIC_FOO_B2_ACCOUNT_ID")
|
||||
defer os.Unsetenv("AUTORESTIC_FOO_B2_ACCOUNT_KEY")
|
||||
os.Setenv("AUTORESTIC_FOO_RESTIC_PASSWORD", "secret123")
|
||||
os.Setenv("AUTORESTIC_FOO_B2_ACCOUNT_ID", "foo123")
|
||||
os.Setenv("AUTORESTIC_FOO_B2_ACCOUNT_KEY", "foo456")
|
||||
|
||||
b := Backend{
|
||||
name: "foo",
|
||||
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) {
|
||||
t.Run("no type given", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "foo",
|
||||
Type: "",
|
||||
Path: "/foo/bar",
|
||||
}
|
||||
err := b.validate()
|
||||
if err == nil {
|
||||
t.Error("expected to get error")
|
||||
}
|
||||
assertEqual(t, err.Error(), "Backend \"foo\" has no \"type\"")
|
||||
})
|
||||
|
||||
t.Run("no path given", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "foo",
|
||||
Type: "local",
|
||||
Path: "",
|
||||
}
|
||||
err := b.validate()
|
||||
if err == nil {
|
||||
t.Error("expected to get error")
|
||||
}
|
||||
assertEqual(t, err.Error(), "Backend \"foo\" has no \"path\"")
|
||||
})
|
||||
}
|
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/cupcakearmy/autorestic/internal"
|
||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||
"github.com/cupcakearmy/autorestic/internal/flags"
|
||||
)
|
||||
|
||||
const INSTALL_PATH = "/usr/local/bin"
|
||||
@@ -128,10 +129,9 @@ func InstallRestic() error {
|
||||
}
|
||||
|
||||
func upgradeRestic() error {
|
||||
_, out, err := internal.ExecuteCommand(internal.ExecuteOptions{
|
||||
Command: "restic",
|
||||
_, _, err := internal.ExecuteCommand(internal.ExecuteOptions{
|
||||
Command: flags.RESTIC_BIN,
|
||||
}, "self-update")
|
||||
colors.Faint.Println(out)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const VERSION = "1.6.2"
|
||||
const VERSION = "1.7.7"
|
||||
|
||||
type OptionMap map[string][]interface{}
|
||||
type Options map[string]OptionMap
|
||||
@@ -185,7 +185,7 @@ func CheckConfig() error {
|
||||
return fmt.Errorf("config could not be loaded/found")
|
||||
}
|
||||
if !CheckIfResticIsCallable() {
|
||||
return fmt.Errorf(`%s was not found. Install either with "autorestic install" or manually`, RESTIC_BIN)
|
||||
return fmt.Errorf(`%s was not found. Install either with "autorestic install" or manually`, flags.RESTIC_BIN)
|
||||
}
|
||||
for name, backend := range c.Backends {
|
||||
backend.name = name
|
||||
@@ -295,24 +295,30 @@ func appendOptionsToSlice(str *[]string, options OptionMap) {
|
||||
}
|
||||
}
|
||||
|
||||
func getOptions(options Options, key string) []string {
|
||||
func getOptions(options Options, keys []string) []string {
|
||||
var selected []string
|
||||
var keys = []string{"all"}
|
||||
if key != "" {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
for _, key := range keys {
|
||||
appendOptionsToSlice(&selected, options[key])
|
||||
}
|
||||
return selected
|
||||
}
|
||||
|
||||
func combineOptions(key string, l Location, b Backend) []string {
|
||||
func combineBackendOptions(key string, b Backend) []string {
|
||||
// Priority: backend > global
|
||||
var options []string
|
||||
gFlags := getOptions(GetConfig().Global, []string{key})
|
||||
bFlags := getOptions(b.Options, []string{"all", key})
|
||||
options = append(options, gFlags...)
|
||||
options = append(options, bFlags...)
|
||||
return options
|
||||
}
|
||||
|
||||
func combineAllOptions(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)
|
||||
gFlags := getOptions(GetConfig().Global, []string{key})
|
||||
bFlags := getOptions(b.Options, []string{"all", key})
|
||||
lFlags := getOptions(l.Options, []string{"all", key})
|
||||
options = append(options, gFlags...)
|
||||
options = append(options, bFlags...)
|
||||
options = append(options, lFlags...)
|
||||
|
164
internal/config_test.go
Normal file
164
internal/config_test.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOptionToString(t *testing.T) {
|
||||
t.Run("no prefix", func(t *testing.T) {
|
||||
opt := "test"
|
||||
result := optionToString(opt)
|
||||
assertEqual(t, result, "--test")
|
||||
})
|
||||
|
||||
t.Run("single prefix", func(t *testing.T) {
|
||||
opt := "-test"
|
||||
result := optionToString(opt)
|
||||
assertEqual(t, result, "-test")
|
||||
})
|
||||
|
||||
t.Run("double prefix", func(t *testing.T) {
|
||||
opt := "--test"
|
||||
result := optionToString(opt)
|
||||
assertEqual(t, result, "--test")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppendOneOptionToSlice(t *testing.T) {
|
||||
t.Run("string flag", func(t *testing.T) {
|
||||
result := []string{}
|
||||
optionMap := OptionMap{"string-flag": []interface{}{"/root"}}
|
||||
|
||||
appendOptionsToSlice(&result, optionMap)
|
||||
expected := []string{
|
||||
"--string-flag", "/root",
|
||||
}
|
||||
assertSliceEqual(t, result, expected)
|
||||
})
|
||||
|
||||
t.Run("bool flag", func(t *testing.T) {
|
||||
result := []string{}
|
||||
optionMap := OptionMap{"boolean-flag": []interface{}{true}}
|
||||
|
||||
appendOptionsToSlice(&result, optionMap)
|
||||
expected := []string{
|
||||
"--boolean-flag",
|
||||
}
|
||||
assertSliceEqual(t, result, expected)
|
||||
})
|
||||
|
||||
t.Run("int flag", func(t *testing.T) {
|
||||
result := []string{}
|
||||
optionMap := OptionMap{"int-flag": []interface{}{123}}
|
||||
|
||||
appendOptionsToSlice(&result, optionMap)
|
||||
expected := []string{
|
||||
"--int-flag", "123",
|
||||
}
|
||||
assertSliceEqual(t, result, expected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppendMultipleOptionsToSlice(t *testing.T) {
|
||||
result := []string{}
|
||||
optionMap := OptionMap{
|
||||
"string-flag": []interface{}{"/root"},
|
||||
"int-flag": []interface{}{123},
|
||||
}
|
||||
|
||||
appendOptionsToSlice(&result, optionMap)
|
||||
expected := []string{
|
||||
"--string-flag", "/root",
|
||||
"--int-flag", "123",
|
||||
}
|
||||
if len(result) != len(expected) {
|
||||
t.Errorf("got length %d, want length %d", len(result), len(expected))
|
||||
}
|
||||
|
||||
// checks that expected option comes after flag, regardless of key order in map
|
||||
for i, v := range expected {
|
||||
v = strings.TrimPrefix(v, "--")
|
||||
|
||||
if value, ok := optionMap[v]; ok {
|
||||
if val, ok := value[0].(int); ok {
|
||||
if expected[i+1] != strconv.Itoa(val) {
|
||||
t.Errorf("Flags and options order are mismatched. got %v, want %v", result, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendOptionWithMultipleValuesToSlice(t *testing.T) {
|
||||
result := []string{}
|
||||
optionMap := OptionMap{
|
||||
"string-flag": []interface{}{"/root", "/bin"},
|
||||
}
|
||||
|
||||
appendOptionsToSlice(&result, optionMap)
|
||||
expected := []string{
|
||||
"--string-flag", "/root",
|
||||
"--string-flag", "/bin",
|
||||
}
|
||||
assertSliceEqual(t, result, expected)
|
||||
}
|
||||
|
||||
func TestGetOptionsOneKey(t *testing.T) {
|
||||
optionMap := OptionMap{
|
||||
"string-flag": []interface{}{"/root"},
|
||||
}
|
||||
options := Options{"backend": optionMap}
|
||||
keys := []string{"backend"}
|
||||
|
||||
result := getOptions(options, keys)
|
||||
expected := []string{
|
||||
"--string-flag", "/root",
|
||||
}
|
||||
assertSliceEqual(t, result, expected)
|
||||
}
|
||||
|
||||
func TestGetOptionsMultipleKeys(t *testing.T) {
|
||||
firstOptionMap := OptionMap{
|
||||
"string-flag": []interface{}{"/root"},
|
||||
}
|
||||
secondOptionMap := OptionMap{
|
||||
"boolean-flag": []interface{}{true},
|
||||
"int-flag": []interface{}{123},
|
||||
}
|
||||
options := Options{
|
||||
"all": firstOptionMap,
|
||||
"forget": secondOptionMap,
|
||||
}
|
||||
keys := []string{"all", "forget"}
|
||||
|
||||
result := getOptions(options, keys)
|
||||
expected := []string{
|
||||
"--string-flag", "/root",
|
||||
"--boolean-flag",
|
||||
"--int-flag", "123",
|
||||
}
|
||||
reflect.DeepEqual(result, expected)
|
||||
}
|
||||
|
||||
func assertEqual[T comparable](t testing.TB, result, expected T) {
|
||||
t.Helper()
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("got %v, want %v", result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func assertSliceEqual(t testing.TB, result, expected []string) {
|
||||
t.Helper()
|
||||
|
||||
if len(result) != len(expected) {
|
||||
t.Errorf("got length %d, want length %d", len(result), len(expected))
|
||||
}
|
||||
|
||||
for i := range result {
|
||||
assertEqual(t, result[i], expected[i])
|
||||
}
|
||||
}
|
@@ -1,5 +1,9 @@
|
||||
package flags
|
||||
|
||||
var CI bool = false
|
||||
var VERBOSE bool = false
|
||||
var CRON_LEAN bool = false
|
||||
var (
|
||||
CI bool = false
|
||||
VERBOSE bool = false
|
||||
CRON_LEAN bool = false
|
||||
RESTIC_BIN string
|
||||
DOCKER_IMAGE string
|
||||
)
|
||||
|
@@ -74,12 +74,8 @@ func (l Location) validate() error {
|
||||
if from, err := GetPathRelativeToConfig(path); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if stat, err := os.Stat(from); err != nil {
|
||||
if _, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,9 +142,6 @@ func (l Location) ExecuteHooks(commands []string, options ExecuteOptions) error
|
||||
colors.Error.Println(out)
|
||||
return err
|
||||
}
|
||||
if flags.VERBOSE {
|
||||
colors.Faint.Println(out)
|
||||
}
|
||||
}
|
||||
colors.Body.Println("")
|
||||
return nil
|
||||
@@ -223,7 +216,7 @@ func (l Location) Backup(cron bool, specificBackend string) []error {
|
||||
}
|
||||
|
||||
cmd := []string{"backup"}
|
||||
cmd = append(cmd, combineOptions("backup", l, backend)...)
|
||||
cmd = append(cmd, combineAllOptions("backup", l, backend)...)
|
||||
if cron {
|
||||
cmd = append(cmd, "--tag", buildTag("cron"))
|
||||
}
|
||||
@@ -284,24 +277,16 @@ func (l Location) Backup(cron bool, specificBackend string) []error {
|
||||
for k, v := range env2 {
|
||||
env[k+"2"] = v
|
||||
}
|
||||
_, out, err := ExecuteResticCommand(ExecuteOptions{
|
||||
_, _, err := ExecuteResticCommand(ExecuteOptions{
|
||||
Envs: env,
|
||||
}, "copy", md.SnapshotID)
|
||||
|
||||
if flags.VERBOSE {
|
||||
colors.Faint.Println(out)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if flags.VERBOSE {
|
||||
colors.Faint.Println(out)
|
||||
}
|
||||
}
|
||||
|
||||
// After hooks
|
||||
@@ -323,7 +308,10 @@ after:
|
||||
|
||||
// Forget and optionally prune
|
||||
if isSuccess && l.ForgetOption != "" && l.ForgetOption != LocationForgetNo {
|
||||
l.Forget(l.ForgetOption == LocationForgetPrune, false)
|
||||
err := l.Forget(l.ForgetOption == LocationForgetPrune, false)
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) == 0 {
|
||||
@@ -335,7 +323,12 @@ after:
|
||||
func (l Location) Forget(prune bool, dry bool) error {
|
||||
colors.PrimaryPrint("Forgetting for location \"%s\"", l.name)
|
||||
|
||||
for _, to := range l.To {
|
||||
backendsToForget := l.To
|
||||
for _, copyBackends := range l.CopyOption {
|
||||
backendsToForget = append(backendsToForget, copyBackends...)
|
||||
}
|
||||
|
||||
for _, to := range backendsToForget {
|
||||
backend, _ := GetBackend(to)
|
||||
colors.Secondary.Printf("For backend \"%s\"\n", backend.name)
|
||||
env, err := backend.getEnv()
|
||||
@@ -352,11 +345,8 @@ func (l Location) Forget(prune bool, dry bool) error {
|
||||
if dry {
|
||||
cmd = append(cmd, "--dry-run")
|
||||
}
|
||||
cmd = append(cmd, combineOptions("forget", l, backend)...)
|
||||
_, out, err := ExecuteResticCommand(options, cmd...)
|
||||
if flags.VERBOSE {
|
||||
colors.Faint.Println(out)
|
||||
}
|
||||
cmd = append(cmd, combineAllOptions("forget", l, backend)...)
|
||||
_, _, err = ExecuteResticCommand(options, cmd...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
93
internal/location_test.go
Normal file
93
internal/location_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package internal
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGetType(t *testing.T) {
|
||||
|
||||
t.Run("TypeLocal", func(t *testing.T) {
|
||||
l := Location{
|
||||
Type: "local",
|
||||
}
|
||||
result, err := l.getType()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
assertEqual(t, result, TypeLocal)
|
||||
})
|
||||
|
||||
t.Run("TypeVolume", func(t *testing.T) {
|
||||
l := Location{
|
||||
Type: "volume",
|
||||
}
|
||||
result, err := l.getType()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
assertEqual(t, result, TypeVolume)
|
||||
})
|
||||
|
||||
t.Run("Empty type", func(t *testing.T) {
|
||||
l := Location{
|
||||
Type: "",
|
||||
}
|
||||
result, err := l.getType()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
assertEqual(t, result, TypeLocal)
|
||||
})
|
||||
|
||||
t.Run("Invalid type", func(t *testing.T) {
|
||||
l := Location{
|
||||
Type: "foo",
|
||||
}
|
||||
_, err := l.getType()
|
||||
if err == nil {
|
||||
t.Error("expected error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildTag(t *testing.T) {
|
||||
result := buildTag("foo", "bar")
|
||||
expected := "ar:foo:bar"
|
||||
assertEqual(t, result, expected)
|
||||
}
|
||||
|
||||
func TestGetLocationTags(t *testing.T) {
|
||||
l := Location{
|
||||
name: "foo",
|
||||
}
|
||||
result := l.getLocationTags()
|
||||
expected := "ar:location:foo"
|
||||
assertEqual(t, result, expected)
|
||||
}
|
||||
|
||||
func TestHasBackend(t *testing.T) {
|
||||
t.Run("backend present", func(t *testing.T) {
|
||||
l := Location{
|
||||
name: "foo",
|
||||
To: []string{"foo", "bar"},
|
||||
}
|
||||
result := l.hasBackend("foo")
|
||||
assertEqual(t, result, true)
|
||||
})
|
||||
|
||||
t.Run("backend absent", func(t *testing.T) {
|
||||
l := Location{
|
||||
name: "foo",
|
||||
To: []string{"bar", "baz"},
|
||||
}
|
||||
result := l.hasBackend("foo")
|
||||
assertEqual(t, result, false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildRestoreCommand(t *testing.T) {
|
||||
l := Location{
|
||||
name: "foo",
|
||||
}
|
||||
result := buildRestoreCommand(l, "to", "snapshot", []string{"options"})
|
||||
expected := []string{"restore", "--target", "to", "--tag", "ar:location:foo", "snapshot", "options"}
|
||||
assertSliceEqual(t, result, expected)
|
||||
}
|
@@ -14,6 +14,10 @@ var lock *viper.Viper
|
||||
var file string
|
||||
var once sync.Once
|
||||
|
||||
const (
|
||||
RUNNING = "running"
|
||||
)
|
||||
|
||||
func getLock() *viper.Viper {
|
||||
if lock == nil {
|
||||
|
||||
@@ -37,36 +41,38 @@ func getLock() *viper.Viper {
|
||||
return lock
|
||||
}
|
||||
|
||||
func setLock(locked bool) error {
|
||||
func setLockValue(key string, value interface{}) (*viper.Viper, error) {
|
||||
lock := getLock()
|
||||
if locked {
|
||||
running := lock.GetBool("running")
|
||||
if running {
|
||||
|
||||
if key == RUNNING {
|
||||
value := value.(bool)
|
||||
if value && lock.GetBool(key) {
|
||||
colors.Error.Println("an instance is already running. exiting")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
lock.Set("running", locked)
|
||||
|
||||
lock.Set(key, value)
|
||||
if err := lock.WriteConfigAs(file); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
return nil
|
||||
return lock, nil
|
||||
}
|
||||
|
||||
func GetCron(location string) int64 {
|
||||
lock := getLock()
|
||||
return lock.GetInt64("cron." + location)
|
||||
return getLock().GetInt64("cron." + location)
|
||||
}
|
||||
|
||||
func SetCron(location string, value int64) {
|
||||
lock.Set("cron."+location, value)
|
||||
lock.WriteConfigAs(file)
|
||||
setLockValue("cron."+location, value)
|
||||
}
|
||||
|
||||
func Lock() error {
|
||||
return setLock(true)
|
||||
_, err := setLockValue(RUNNING, true)
|
||||
return err
|
||||
}
|
||||
|
||||
func Unlock() error {
|
||||
return setLock(false)
|
||||
_, err := setLockValue(RUNNING, false)
|
||||
return err
|
||||
}
|
||||
|
114
internal/lock/lock_test.go
Normal file
114
internal/lock/lock_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package lock
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var testDirectory = "autorestic_test_tmp"
|
||||
|
||||
// All tests must share the same lock file as it is only initialized once
|
||||
func setup(t *testing.T) {
|
||||
d, err := os.MkdirTemp("", testDirectory)
|
||||
if err != nil {
|
||||
log.Fatalf("error creating temp dir: %v", err)
|
||||
return
|
||||
}
|
||||
// set config file location
|
||||
viper.SetConfigFile(d + "/.autorestic.yml")
|
||||
|
||||
t.Cleanup(func() {
|
||||
os.RemoveAll(d)
|
||||
viper.Reset()
|
||||
})
|
||||
}
|
||||
|
||||
func TestLock(t *testing.T) {
|
||||
setup(t)
|
||||
|
||||
t.Run("getLock", func(t *testing.T) {
|
||||
result := getLock().GetBool(RUNNING)
|
||||
|
||||
if result {
|
||||
t.Errorf("got %v, want %v", result, false)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("lock", func(t *testing.T) {
|
||||
lock, err := setLockValue(RUNNING, true)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
result := lock.GetBool(RUNNING)
|
||||
if !result {
|
||||
t.Errorf("got %v, want %v", result, true)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unlock", func(t *testing.T) {
|
||||
lock, err := setLockValue(RUNNING, false)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
result := lock.GetBool(RUNNING)
|
||||
if result {
|
||||
t.Errorf("got %v, want %v", result, false)
|
||||
}
|
||||
})
|
||||
|
||||
// locking a locked instance exits the instance
|
||||
// this trick to capture os.Exit(1) is discussed here:
|
||||
// https://talks.golang.org/2014/testing.slide#23
|
||||
t.Run("lock twice", func(t *testing.T) {
|
||||
if os.Getenv("CRASH") == "1" {
|
||||
err := Lock()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
// should fail
|
||||
Lock()
|
||||
}
|
||||
|
||||
cmd := exec.Command(os.Args[0], "-test.run=TestLock/lock_twice")
|
||||
cmd.Env = append(os.Environ(), "CRASH=1")
|
||||
err := cmd.Run()
|
||||
|
||||
err, ok := err.(*exec.ExitError)
|
||||
if !ok {
|
||||
t.Error("unexpected error")
|
||||
}
|
||||
expected := "exit status 1"
|
||||
if err.Error() != expected {
|
||||
t.Errorf("got %q, want %q", err.Error(), expected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("set cron", func(t *testing.T) {
|
||||
expected := int64(5)
|
||||
SetCron("foo", expected)
|
||||
|
||||
result, err := strconv.ParseInt(getLock().GetString("cron.foo"), 10, 64)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if result != expected {
|
||||
t.Errorf("got %d, want %d", result, expected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get cron", func(t *testing.T) {
|
||||
expected := int64(5)
|
||||
result := GetCron("foo")
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("got %d, want %d", result, expected)
|
||||
}
|
||||
})
|
||||
}
|
@@ -14,9 +14,9 @@ func (e addedExtractor) Matches(line string) bool {
|
||||
}
|
||||
func (e addedExtractor) Extract(metadata *BackupLogMetadata, line string) {
|
||||
// Sample line: "Added to the repo: 0 B"
|
||||
metadata.AddedSize = strings.TrimSpace(e.re.ReplaceAllString(line, ""))
|
||||
metadata.AddedSize = strings.TrimSpace(e.re.ReplaceAllString(line, "$2"))
|
||||
}
|
||||
|
||||
func NewAddedExtractor() MetadatExtractor {
|
||||
return addedExtractor{regexp.MustCompile(`(?i)^Added to the repo:`)}
|
||||
return addedExtractor{regexp.MustCompile(`(?i)^Added to the repo(sitory)?: ([\d\.]+ \w+)( \([\d\.]+[\w\s]+\))?`)}
|
||||
}
|
||||
|
@@ -9,23 +9,34 @@ import (
|
||||
|
||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||
"github.com/cupcakearmy/autorestic/internal/flags"
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
var RESTIC_BIN string
|
||||
|
||||
func CheckIfCommandIsCallable(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func CheckIfResticIsCallable() bool {
|
||||
return CheckIfCommandIsCallable(RESTIC_BIN)
|
||||
return CheckIfCommandIsCallable(flags.RESTIC_BIN)
|
||||
}
|
||||
|
||||
type ExecuteOptions struct {
|
||||
Command string
|
||||
Envs map[string]string
|
||||
Dir string
|
||||
Silent bool
|
||||
}
|
||||
|
||||
type ColoredWriter struct {
|
||||
target io.Writer
|
||||
color *color.Color
|
||||
}
|
||||
|
||||
func (w ColoredWriter) Write(p []byte) (n int, err error) {
|
||||
colored := []byte(w.color.Sprint(string(p)))
|
||||
w.target.Write(colored)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func ExecuteCommand(options ExecuteOptions, args ...string) (int, string, error) {
|
||||
@@ -43,23 +54,32 @@ func ExecuteCommand(options ExecuteOptions, args ...string) (int, string, error)
|
||||
|
||||
var out bytes.Buffer
|
||||
var error bytes.Buffer
|
||||
if flags.VERBOSE && !options.Silent {
|
||||
var colored ColoredWriter = ColoredWriter{
|
||||
target: os.Stdout,
|
||||
color: colors.Faint,
|
||||
}
|
||||
mw := io.MultiWriter(colored, &out)
|
||||
cmd.Stdout = mw
|
||||
} else {
|
||||
cmd.Stdout = &out
|
||||
}
|
||||
cmd.Stderr = &error
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
code := -1
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
return exitError.ExitCode(), error.String(), err
|
||||
} else {
|
||||
return -1, error.String(), err
|
||||
code = exitError.ExitCode()
|
||||
}
|
||||
return code, error.String(), err
|
||||
}
|
||||
return 0, out.String(), nil
|
||||
}
|
||||
|
||||
func ExecuteResticCommand(options ExecuteOptions, args ...string) (int, string, error) {
|
||||
options.Command = RESTIC_BIN
|
||||
options.Command = flags.RESTIC_BIN
|
||||
var c = GetConfig()
|
||||
var optionsAsString = getOptions(c.Global, "")
|
||||
var optionsAsString = getOptions(c.Global, []string{"all"})
|
||||
args = append(optionsAsString, args...)
|
||||
return ExecuteCommand(options, args...)
|
||||
}
|
||||
|
Reference in New Issue
Block a user