mirror of
https://github.com/cupcakearmy/autorestic.git
synced 2025-09-06 02:20:39 +00:00
Compare commits
56 Commits
Author | SHA1 | Date | |
---|---|---|---|
b5604b8b9f | |||
|
24220f6b62 | ||
ced20801c1 | |||
|
ce9140fa1e | ||
|
046c79fd15 | ||
|
f8603425d1 | ||
068121d722 | |||
8eea7d33f8 | |||
|
fc8b5fdbe2 | ||
|
f67bb7f73c | ||
|
530b1b646c | ||
|
3b57602fe8 | ||
|
045513234f | ||
|
78b0db50e0 | ||
|
62dd371d51 | ||
|
08766b75db | ||
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 | |||
ff2e3714d1 | |||
2e6764223d |
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"
|
23
.github/workflows/build.yml
vendored
23
.github/workflows/build.yml
vendored
@@ -3,19 +3,19 @@ name: Main
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
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.21'
|
||||
- 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:
|
||||
|
41
CHANGELOG.md
41
CHANGELOG.md
@@ -5,6 +5,47 @@ 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.
|
||||
|
||||
## [1.6.1] - 2022-04-14
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bump go version in docker file to 18.
|
||||
|
||||
## [1.6.0] - 2022-04-14
|
||||
|
||||
### Added
|
||||
|
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.17-alpine as builder
|
||||
FROM golang:1.21-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.16.4
|
||||
RUN apk add --no-cache rclone bash curl
|
||||
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)))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
43
cmd/root.go
43
cmd/root.go
@@ -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)
|
||||
}
|
||||
|
||||
@@ -54,28 +55,11 @@ func initConfig() {
|
||||
viper.SetConfigFile(cfgFile)
|
||||
viper.AutomaticEnv()
|
||||
if viper.ConfigFileUsed() == "" {
|
||||
colors.Error.Println("cannot read config file %s\n", cfgFile)
|
||||
colors.Error.Printf("cannot read config file %s\n", cfgFile)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
configPaths := []string{"."}
|
||||
|
||||
// Home
|
||||
if home, err := homedir.Dir(); err == nil {
|
||||
configPaths = append(configPaths, home)
|
||||
}
|
||||
|
||||
// XDG_CONFIG_HOME
|
||||
{
|
||||
prefix, found := os.LookupEnv("XDG_CONFIG_HOME")
|
||||
if !found {
|
||||
if home, err := homedir.Dir(); err != nil {
|
||||
prefix = filepath.Join(home, ".config")
|
||||
}
|
||||
}
|
||||
xdgConfig := filepath.Join(prefix, "autorestic")
|
||||
configPaths = append(configPaths, xdgConfig)
|
||||
}
|
||||
configPaths := getConfigPaths()
|
||||
for _, cfgPath := range configPaths {
|
||||
viper.AddConfigPath(cfgPath)
|
||||
}
|
||||
@@ -87,3 +71,22 @@ func initConfig() {
|
||||
viper.AutomaticEnv()
|
||||
}
|
||||
}
|
||||
|
||||
func getConfigPaths() []string {
|
||||
result := []string{"."}
|
||||
if home, err := homedir.Dir(); err == nil {
|
||||
result = append(result, home)
|
||||
}
|
||||
|
||||
{
|
||||
xdgConfigHome, found := os.LookupEnv("XDG_CONFIG_HOME")
|
||||
if !found {
|
||||
if home, err := homedir.Dir(); err == nil {
|
||||
xdgConfigHome = filepath.Join(home, ".config")
|
||||
}
|
||||
}
|
||||
xdgConfig := filepath.Join(xdgConfigHome, "autorestic")
|
||||
result = append(result, xdgConfig)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
36
cmd/root_test.go
Normal file
36
cmd/root_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const xdgConfigHome = "XDG_CONFIG_HOME"
|
||||
|
||||
func assertContains(t *testing.T, array []string, element string) {
|
||||
if !slices.Contains(array, element) {
|
||||
t.Errorf("Expected %s to be contained in %s", element, array)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigResolving(t *testing.T) {
|
||||
t.Run("~/.config/autorestic is used if XDG_CONFIG_HOME is not set", func(t *testing.T) {
|
||||
// Override env using testing so that env gets restored after test
|
||||
t.Setenv(xdgConfigHome, "")
|
||||
_ = os.Unsetenv("XDG_CONFIG_HOME")
|
||||
configPaths := getConfigPaths()
|
||||
homeDir, _ := homedir.Dir()
|
||||
expectedConfigPath := filepath.Join(homeDir, ".config/autorestic")
|
||||
assertContains(t, configPaths, expectedConfigPath)
|
||||
})
|
||||
|
||||
t.Run("XDG_CONFIG_HOME is respected if set", func(t *testing.T) {
|
||||
t.Setenv(xdgConfigHome, "/foo/bar")
|
||||
|
||||
configPaths := getConfigPaths()
|
||||
assertContains(t, configPaths, filepath.Join("/", "foo", "bar", "autorestic"))
|
||||
})
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
import { build } from '@codedoc/core';
|
||||
|
||||
import { config } from './config';
|
||||
import { installTheme$ } from './content/theme';
|
||||
import { content } from './content';
|
||||
|
||||
|
||||
build(config, content, installTheme$, {
|
||||
resolve: {
|
||||
modules: ['.codedoc/node_modules']
|
||||
},
|
||||
resolveLoader: {
|
||||
modules: ['.codedoc/node_modules']
|
||||
}
|
||||
});
|
@@ -1,24 +0,0 @@
|
||||
import { configuration } from '@codedoc/core'
|
||||
|
||||
export const config = configuration({
|
||||
src: {
|
||||
base: 'markdown',
|
||||
},
|
||||
dest: {
|
||||
html: './build',
|
||||
assets: './build',
|
||||
bundle: './_',
|
||||
styles: './_',
|
||||
},
|
||||
page: {
|
||||
title: {
|
||||
base: 'Autorestic',
|
||||
},
|
||||
},
|
||||
misc: {
|
||||
github: {
|
||||
user: 'cupcakearmy',
|
||||
repo: 'autorestic',
|
||||
},
|
||||
},
|
||||
})
|
@@ -1,19 +0,0 @@
|
||||
import { CodedocConfig } from '@codedoc/core';
|
||||
import { Footer as _Footer, GitterToggle$, Watermark} from '@codedoc/core/components';
|
||||
|
||||
|
||||
export function Footer(config: CodedocConfig, renderer: any) {
|
||||
let github$;
|
||||
if (config.misc?.github)
|
||||
github$ = <a href={`https://github.com/${config.misc.github.user}/${config.misc.github.repo}/`}
|
||||
target="_blank">GitHub</a>;
|
||||
|
||||
let community$;
|
||||
if (config.misc?.gitter)
|
||||
community$ = <GitterToggle$ room={config.misc.gitter.room}/>
|
||||
|
||||
if (github$ && community$) return <_Footer>{github$}<hr/>{community$}</_Footer>;
|
||||
else if (github$) return <_Footer>{github$}</_Footer>;
|
||||
else if (community$) return <_Footer>{community$}</_Footer>;
|
||||
else return <_Footer><Watermark/></_Footer>;
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
import { CodedocConfig } from '@codedoc/core';
|
||||
import { Header as _Header, GithubButton, Watermark } from '@codedoc/core/components';
|
||||
|
||||
|
||||
export function Header(config: CodedocConfig, renderer: any) {
|
||||
return (
|
||||
<_Header>{config.misc?.github ?
|
||||
<fragment>
|
||||
<GithubButton action={config.misc.github.action || 'Star'}
|
||||
repo={config.misc.github.repo}
|
||||
user={config.misc.github.user}
|
||||
large={config.misc.github.large === true}
|
||||
count={config.misc.github.count !== false}
|
||||
standardIcon={config.misc.github.standardIcon !== false}/>
|
||||
<br/><br/>
|
||||
</fragment>
|
||||
: ''}
|
||||
<Watermark/>
|
||||
</_Header>
|
||||
)
|
||||
}
|
@@ -1,57 +0,0 @@
|
||||
import { RendererLike } from '@connectv/html'
|
||||
import { File } from 'rxline/fs'
|
||||
import {
|
||||
Page,
|
||||
Meta,
|
||||
ContentNav,
|
||||
Fonts,
|
||||
ToC,
|
||||
GithubSearch$,
|
||||
} from '@codedoc/core/components'
|
||||
|
||||
import { config } from '../config'
|
||||
import { Header } from './header'
|
||||
import { Footer } from './footer'
|
||||
|
||||
export function content(
|
||||
_content: HTMLElement,
|
||||
toc: HTMLElement,
|
||||
renderer: RendererLike<any, any>,
|
||||
file: File<string>
|
||||
) {
|
||||
return (
|
||||
<Page
|
||||
title={config.page.title.extractor(_content, config, file)}
|
||||
favicon={config.page.favicon}
|
||||
meta={<Meta {...config.page.meta} />}
|
||||
fonts={<Fonts {...config.page.fonts} />}
|
||||
scripts={config.page.scripts}
|
||||
stylesheets={config.page.stylesheets}
|
||||
header={<Header {...config} />}
|
||||
footer={<Footer {...config} />}
|
||||
toc={
|
||||
<ToC
|
||||
default={'open'}
|
||||
search={
|
||||
config.misc?.github ? (
|
||||
<GithubSearch$
|
||||
repo={config.misc.github.repo}
|
||||
user={config.misc.github.user}
|
||||
root={config.src.base}
|
||||
pick={config.src.pick.source}
|
||||
drop={config.src.drop.source}
|
||||
/>
|
||||
) : (
|
||||
false
|
||||
)
|
||||
}
|
||||
>
|
||||
{toc}
|
||||
</ToC>
|
||||
}
|
||||
>
|
||||
{_content}
|
||||
<ContentNav content={_content} />
|
||||
</Page>
|
||||
)
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
import { funcTransport } from '@connectv/sdh/transport';
|
||||
import { useTheme } from '@codedoc/core/transport';
|
||||
|
||||
import { theme } from '../theme';
|
||||
|
||||
|
||||
export function installTheme() { useTheme(theme); }
|
||||
export const installTheme$ = /*#__PURE__*/funcTransport(installTheme);
|
10811
docs/.codedoc/package-lock.json
generated
10811
docs/.codedoc/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@codedoc/core": "^0.2.24"
|
||||
}
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
import { join } from 'path';
|
||||
import { serve } from '@codedoc/core';
|
||||
|
||||
import { config } from './config';
|
||||
import { content } from './content';
|
||||
import { installTheme$ } from './content/theme';
|
||||
|
||||
|
||||
const root = join(__dirname, '../');
|
||||
|
||||
serve(root, config, content, installTheme$, {
|
||||
resolve: {
|
||||
modules: ['.codedoc/node_modules']
|
||||
},
|
||||
resolveLoader: {
|
||||
modules: ['.codedoc/node_modules']
|
||||
}
|
||||
});
|
@@ -1,11 +0,0 @@
|
||||
import { createTheme } from '@codedoc/core/transport';
|
||||
|
||||
|
||||
export const theme = /*#__PURE__*/createTheme({
|
||||
light: {
|
||||
primary: '#1eb2a6'
|
||||
},
|
||||
dark: {
|
||||
primary: '#1eb2a6'
|
||||
}
|
||||
});
|
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"noImplicitAny": true,
|
||||
"declaration": false,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"jsx": "react",
|
||||
"jsxFactory": "renderer.create",
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*"
|
||||
]
|
||||
}
|
@@ -1,22 +0,0 @@
|
||||
import { exec, spawn } from 'child_process';
|
||||
import { config } from './config';
|
||||
|
||||
|
||||
const cmd = 'ts-node-dev';
|
||||
const params = `--project .codedoc/tsconfig.json`
|
||||
+ ` -T --watch ${config.src.base},.codedoc`
|
||||
+ ` --ignore-watch .codedoc/node_modules`
|
||||
+ ` .codedoc/serve`;
|
||||
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const child = exec(cmd + ' ' + params);
|
||||
|
||||
child.stdout?.pipe(process.stdout);
|
||||
child.stderr?.pipe(process.stderr);
|
||||
child.on('close', () => {});
|
||||
}
|
||||
else {
|
||||
const child = spawn(cmd, [params], { stdio: 'inherit', shell: 'bash' });
|
||||
child.on('close', () => {});
|
||||
}
|
2
docs/.gitignore
vendored
2
docs/.gitignore
vendored
@@ -1,2 +1,2 @@
|
||||
node_modules
|
||||
build
|
||||
.next
|
||||
|
1
docs/.nvmrc
Normal file
1
docs/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
v20.8.0
|
@@ -1,54 +0,0 @@
|
||||
[Home](/)
|
||||
[Quick Start](/quick)
|
||||
[Installation](/installation)
|
||||
[Configuration](/config)
|
||||
[Upgrade](/upgrade)
|
||||
|
||||
> :Collapse label=Locations
|
||||
>
|
||||
> [Overview](/location/overview)
|
||||
> [Hooks](/location/hooks)
|
||||
>
|
||||
> > :Collapse label=Options
|
||||
> >
|
||||
> > [Overview](/location/options)
|
||||
> > [Excluding Files](/location/exclude)
|
||||
> > [Forget Policy](/location/forget)
|
||||
> > [Copy](/location/copy)
|
||||
>
|
||||
> [Cron](/location/cron)
|
||||
> [Docker Volumes](/location/docker)
|
||||
|
||||
> :Collapse label=Backend
|
||||
>
|
||||
> [Overview](/backend/overview)
|
||||
> [Available Backends](/backend/available)
|
||||
> [Options](/backend/options)
|
||||
> [Environment](/backend/env)
|
||||
|
||||
> :Collapse label=CLI
|
||||
>
|
||||
> [General](/cli/general)
|
||||
> [Info](/cli/info)
|
||||
> [Check](/cli/check)
|
||||
> [Completion](/cli/completion)
|
||||
> [Backup](/cli/backup)
|
||||
> [Restore](/cli/restore)
|
||||
> [Forget](/cli/forget)
|
||||
> [Cron](/cli/cron)
|
||||
> [Exec](/cli/exec)
|
||||
> [Install](/cli/install)
|
||||
> [Uninstall](/cli/uninstall)
|
||||
> [Upgrade](/cli/upgrade)
|
||||
|
||||
> :Collapse label=Migration
|
||||
>
|
||||
> [0.x → 1.0](/migration/0.x_1.0)
|
||||
> [1.4 → 1.5](/migration/1.4_1.5)
|
||||
|
||||
[Examples](/examples)
|
||||
[Docker](/docker)
|
||||
[QA](/qa)
|
||||
[Community](/community)
|
||||
[Contributors](/contrib)
|
||||
|
@@ -1,11 +0,0 @@
|
||||
# 🏘 Community
|
||||
|
||||
A list of community driven projects. (No official affiliation)
|
||||
|
||||
- SystemD Units: https://gitlab.com/py_crash/autorestic-systemd-units
|
||||
- Docker image: https://github.com/pascaliske/docker-autorestic
|
||||
- Ansible Role: https://github.com/adsanz/ansible-restic-role
|
||||
- Ansible Role: https://github.com/ItsNotGoodName/ansible-role-autorestic
|
||||
- Ansible Role: https://github.com/FuzzyMistborn/ansible-role-autorestic
|
||||
|
||||
> :ToCPrevNext
|
6
docs/next.config.js
Normal file
6
docs/next.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const withNextra = require('nextra')({
|
||||
theme: 'nextra-theme-docs',
|
||||
themeConfig: './theme.config.jsx',
|
||||
})
|
||||
|
||||
module.exports = withNextra()
|
1149
docs/package-lock.json
generated
1149
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,14 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "codedoc install && codedoc build",
|
||||
"dev": "codedoc serve"
|
||||
"build": "NEXT_TELEMETRY_DISABLED=1 next build",
|
||||
"dev": "NEXT_TELEMETRY_DISABLED=1 next"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codedoc/cli": "^0.2.8"
|
||||
}
|
||||
"next": "^13.5.3",
|
||||
"nextra": "^2.13.1",
|
||||
"nextra-theme-docs": "^2.13.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"packageManager": "pnpm@8.8.0"
|
||||
}
|
||||
|
10
docs/pages/_meta.json
Normal file
10
docs/pages/_meta.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"index": "Home",
|
||||
"quick": "Quick Start",
|
||||
"installation": "Installation",
|
||||
"config": "Configuration",
|
||||
"location": "Locations",
|
||||
"backend": "Backend",
|
||||
"cli": "CLI",
|
||||
"migration": "Migration"
|
||||
}
|
6
docs/pages/backend/_meta.json
Normal file
6
docs/pages/backend/_meta.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"index": "Overview",
|
||||
"available": "Available backends",
|
||||
"options": "Options",
|
||||
"env": "Environment"
|
||||
}
|
@@ -83,5 +83,3 @@ backends:
|
||||
user: user
|
||||
password: pass
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
@@ -63,5 +63,3 @@ backends:
|
||||
type: b2
|
||||
path: myBucket
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
@@ -16,5 +16,3 @@ backends:
|
||||
## Types
|
||||
|
||||
We restic supports multiple types of backends. See the [full list](/backend/available) for details.
|
||||
|
||||
> :ToCPrevNext
|
@@ -15,5 +15,3 @@ backend:
|
||||
```
|
||||
|
||||
In this example, whenever `autorestic` runs `restic backup` it will append a `--tag abc --tag` to the native command.
|
||||
|
||||
> :ToCPrevNext
|
3
docs/pages/cli/_meta.json
Normal file
3
docs/pages/cli/_meta.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"general": "General"
|
||||
}
|
@@ -21,5 +21,3 @@ autorestic backup -l foo -l bar
|
||||
```bash
|
||||
autorestic backup -l location@backend
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
@@ -7,5 +7,3 @@ autorestic check
|
||||
Checks locations and backends are configured properly and initializes them if they are not already.
|
||||
|
||||
This is mostly an internal command, but useful to verify if a backend is configured correctly.
|
||||
|
||||
> :ToCPrevNext
|
@@ -13,5 +13,3 @@ Supported shells are
|
||||
- powershell
|
||||
|
||||
To see how to install run `autorestic help completion` and follow the instructions for your specific shell
|
||||
|
||||
> :ToCPrevNext
|
@@ -9,5 +9,3 @@ This command is mostly intended to be triggered by an automated system like syst
|
||||
It will run cron jobs as [specified in the cron section](/location/cron) of a specific location.
|
||||
|
||||
The `--lean` flag will omit output like _skipping location x: not due yet_. This can be useful if you are dumping the output of the cron job to a log file and don't want to be overwhelmed by the output log.
|
||||
|
||||
> :ToCPrevNext
|
@@ -11,5 +11,3 @@ autorestic exec -av -- snapshots
|
||||
```
|
||||
|
||||
With `exec` you can basically run every cli command that you would be able to run with the restic cli. It only pre-fills path, key, etc.
|
||||
|
||||
> :ToCPrevNext
|
@@ -4,10 +4,8 @@
|
||||
autorestic forget [-l, --location] [-a, --all] [--dry-run] [--prune]
|
||||
```
|
||||
|
||||
This will prune and remove old data form the backends according to the [keep policy you have specified for the location](/location/forget).
|
||||
This will prune and remove old data form the backends according to the [keep policy you have specified for the location](/location/options/forget).
|
||||
|
||||
The `--dry-run` flag will do a dry run showing what would have been deleted, but won't touch the actual data.
|
||||
|
||||
The `--prune` flag will also [prune the data](https://restic.readthedocs.io/en/latest/060_forget.html#removing-backup-snapshots). This is a costly operation that can take longer, however it will free up the actual space.
|
||||
|
||||
> :ToCPrevNext
|
@@ -34,5 +34,3 @@ With `--restic-bin` you can specify to run a specific restic binary. This can be
|
||||
```bash
|
||||
autorestic --restic-bin /some/path/to/my/custom/restic/binary
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
@@ -14,5 +14,3 @@ autorestic info
|
||||
```bash
|
||||
autorestic -c path/to/some/config.yml info
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
@@ -5,5 +5,3 @@ Installs both restic and autorestic to `/usr/local/bin`.
|
||||
```bash
|
||||
autorestic install
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
@@ -15,5 +15,3 @@ autorestic restore -l home --from hdd --to /path/where/to/restore
|
||||
```
|
||||
|
||||
This will restore the location `home` to the `/path/where/to/restore` folder and taking the data from the backend `hdd`
|
||||
|
||||
> :ToCPrevNext
|
@@ -5,5 +5,3 @@ Uninstalls both restic and autorestic from `/usr/local/bin`.
|
||||
```bash
|
||||
autorestic uninstall
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
@@ -7,5 +7,3 @@ autorestic upgrade
|
||||
```
|
||||
|
||||
Updates both restic and autorestic automagically.
|
||||
|
||||
> :ToCPrevNext
|
11
docs/pages/community.md
Normal file
11
docs/pages/community.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# 🏘 Community
|
||||
|
||||
A list of community driven projects. (No official affiliation)
|
||||
|
||||
- SystemD Units: <https://gitlab.com/py_crash/autorestic-systemd-units>
|
||||
- Docker image: <https://github.com/pascaliske/docker-autorestic>
|
||||
- Ansible Role: <https://github.com/adsanz/ansible-restic-role>
|
||||
- Ansible Role: <https://github.com/ItsNotGoodName/ansible-role-autorestic>
|
||||
- Ansible Role: <https://github.com/FuzzyMistborn/ansible-role-autorestic>
|
||||
- Ansible Role: <https://0xacab.org/varac-projects/ansible-role-autorestic>
|
||||
- Ansible Role: <https://github.com/dbrennand/ansible-role-autorestic>
|
@@ -2,10 +2,11 @@
|
||||
|
||||
## Path
|
||||
|
||||
By default autorestic searches for a `.autorestic.yml` file in the current directory and your home folder.
|
||||
By default autorestic searches for a `.autorestic.yml` file in the current directory, your home folder and your XDG config folder (`~/.config/` by default):
|
||||
|
||||
- `./.autorestic.yml`
|
||||
- `~/.autorestic.yml`
|
||||
- `~/.config/autorestic/.autorestic.yml`
|
||||
|
||||
You can also specify a custom file with the `-c path/to/some/config.yml`
|
||||
|
||||
@@ -83,5 +84,3 @@ locations:
|
||||
forget:
|
||||
<<: *bar
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
@@ -17,5 +17,3 @@ This amazing people helped the project!
|
||||
- @TheForcer - Typos.
|
||||
- @themorlan - Typos.
|
||||
- @somebox - Typos.
|
||||
|
||||
> :ToCPrevNext
|
@@ -36,5 +36,3 @@ locations:
|
||||
to:
|
||||
- somewhere-else
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
@@ -18,5 +18,3 @@ Autorestic is a wrapper around the amazing [restic](https://restic.net/). While
|
||||
- Cron jobs for automatic backup
|
||||
- Backup & Restore docker volumes
|
||||
- Generated completions for `[bash|zsh|fish|powershell]`
|
||||
|
||||
> :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
|
||||
@@ -24,8 +24,10 @@ You can download the right binary from the release page and simply copy it to `/
|
||||
|
||||
If you are on macOS you can install through brew: `brew install autorestic`.
|
||||
|
||||
### Fedora
|
||||
|
||||
Fedora users can install the [autorestic](https://src.fedoraproject.org/rpms/autorestic/) package with `dnf install autorestic`.
|
||||
|
||||
### AUR
|
||||
|
||||
~~If you are on Arch there is an [AUR Package](https://aur.archlinux.org/packages/autorestic-bin/) (looking for maintainers).~~ - Deprecated
|
||||
|
||||
> :ToCPrevNext
|
||||
If you are on Arch there is an [AUR Package](https://aur.archlinux.org/packages/autorestic-bin/)
|
7
docs/pages/location/_meta.json
Normal file
7
docs/pages/location/_meta.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"index": "Overview",
|
||||
"hooks": "Hooks",
|
||||
"options": "Options",
|
||||
"cron": "Cronjobs",
|
||||
"docker": "Docker volumes"
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
# Cron
|
||||
|
||||
Often it is usefully to trigger backups automatically. For this we can specify a `cron` attribute to each location.
|
||||
Often it is useful to trigger backups automatically. For this, we can specify a `cron` attribute to each location.
|
||||
|
||||
```yaml | .autorestic.yml
|
||||
locations:
|
||||
@@ -10,15 +10,15 @@ locations:
|
||||
cron: '0 3 * * 0' # Every Sunday at 3:00
|
||||
```
|
||||
|
||||
Here is a awesome website with [some examples](https://crontab.guru/examples.html) and an [explorer](https://crontab.guru/)
|
||||
Here is an awesome website with [some examples](https://crontab.guru/examples.html) and an [explorer](https://crontab.guru/).
|
||||
|
||||
## Installing the cron
|
||||
|
||||
**This has to be done only once, regardless of now many cron jobs you have in your config file.**
|
||||
**This has to be done only once, regardless of how many cron jobs you have in your config file.**
|
||||
|
||||
To actually enable cron jobs you need something to call `autorestic cron` on a timed schedule.
|
||||
Note that the schedule has nothing to do with the `cron` attribute in each location.
|
||||
My advise would be to trigger the command every 5min, but if you have a cronjob that runs only once a week, it's probably enough to schedule it once a day.
|
||||
My advice would be to trigger the command every 5min, but if you have a cronjob that runs only once a week, it's probably enough to schedule it once a day.
|
||||
|
||||
### Crontab
|
||||
|
||||
@@ -50,6 +50,4 @@ To debug a cron job you can use
|
||||
|
||||
Now you can add as many `cron` attributes as you wish in the config file ⏱
|
||||
|
||||
> Also note that manually triggered backups with `autorestic backup` will not influence the cron timeline, they are willingly not linked.
|
||||
|
||||
> :ToCPrevNext
|
||||
> Also note that manually triggered backups with `autorestic backup` will not influence the cron timeline, they are intentionally not linked.
|
@@ -35,5 +35,3 @@ autorestic restore -l hello
|
||||
```
|
||||
|
||||
The volume has to exists whenever backing up or restoring.
|
||||
|
||||
> :ToCPrevNext
|
@@ -76,5 +76,3 @@ AUTORESTIC_LOCATION=bar
|
||||
AUTORESTIC_FILES_ADDED_0=42
|
||||
AUTORESTIC_FILES_ADDED_FOO=42
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
@@ -4,6 +4,7 @@ Locations can be seen as the input to the backup process. Generally this is simp
|
||||
The paths can be relative from the config file. A location can have multiple backends, so that the data is secured across multiple servers.
|
||||
|
||||
Note: names of locations MUST be lower case!
|
||||
|
||||
```yaml | .autorestic.yml
|
||||
version: 2
|
||||
|
||||
@@ -30,5 +31,3 @@ Paths can be absolute or relative. If relative they are resolved relative to the
|
||||
## `to`
|
||||
|
||||
This is either a single backend or an array of backends. The backends have to be configured in the same config file.
|
||||
|
||||
> :ToCPrevNext
|
3
docs/pages/location/options/_meta.json
Normal file
3
docs/pages/location/options/_meta.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"index": "Overview"
|
||||
}
|
@@ -16,5 +16,3 @@ locations:
|
||||
- '*.abc'
|
||||
exclude-file: .gitignore
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
@@ -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
|
||||
@@ -53,5 +53,3 @@ locations:
|
||||
forget:
|
||||
keep-last: 5
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
@@ -63,5 +63,3 @@ backends:
|
||||
locations:
|
||||
# ...
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
@@ -22,5 +22,3 @@ remote:
|
||||
```
|
||||
|
||||
Other than the config file there is a new `-v, --verbose` flag which shows the output of native commands, which are now hidden by default.
|
||||
|
||||
> :ToCPrevNext
|
@@ -57,4 +57,10 @@ locations:
|
||||
type: volume
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
||||
## 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
|
||||
```
|
4
docs/pages/migration/_meta.json
Normal file
4
docs/pages/migration/_meta.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"0.x_1.0": "0.x → 1.0",
|
||||
"1.4_1.5": "1.4 → 1.5"
|
||||
}
|
@@ -6,5 +6,3 @@ This happens when autorestic needs to write to the config file: e.g. when we are
|
||||
Unfortunately during this process formatting and comments are lost because the `yaml` library used is not comment and/or format aware.
|
||||
|
||||
That is why autorestic will place a copy of your old config next to the one we are writing to.
|
||||
|
||||
> :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.
|
||||
@@ -82,5 +82,3 @@ autorestic restore -l home --from hdd --to /path/where/to/restore
|
||||
```
|
||||
|
||||
This will restore the location `home` from the backend `hdd` to the given path.
|
||||
|
||||
> :ToCPrevNext
|
3044
docs/pnpm-lock.yaml
generated
Normal file
3044
docs/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
docs/theme.config.jsx
Normal file
24
docs/theme.config.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
export default {
|
||||
logo: <span>Autorestic</span>,
|
||||
docsRepositoryBase: 'https://github.com/cupcakearmy/autorestic/tree/master/docs',
|
||||
project: {
|
||||
link: 'https://github.com/cupcakearmy/autorestic',
|
||||
},
|
||||
sidebar: {
|
||||
defaultMenuCollapseLevel: 1,
|
||||
},
|
||||
feedback: {
|
||||
content: 'Question? An error? Give feedback →',
|
||||
},
|
||||
footer: {
|
||||
text: (
|
||||
<span>
|
||||
MIT {new Date().getFullYear()} ©{' '}
|
||||
<a href="https://github.com/cupcakearmy" target="_blank">
|
||||
cupcakearmy
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
),
|
||||
},
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"cleanUrls": true
|
||||
}
|
6
go.mod
6
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/cupcakearmy/autorestic
|
||||
|
||||
go 1.18
|
||||
go 1.21
|
||||
|
||||
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.0"
|
||||
const VERSION = "1.7.11"
|
||||
|
||||
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
|
||||
cmd.Stdout = &out
|
||||
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...)
|
||||
}
|
||||
|
2
main.go
2
main.go
@@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
Reference in New Issue
Block a user