Compare commits

..

4 Commits

Author SHA1 Message Date
dependabot[bot]
9cf919b42b Bump golang from 1.24-alpine to 1.25-alpine (#454)
Bumps golang from 1.24-alpine to 1.25-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.25-alpine
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-31 13:48:49 +02:00
Duru Can Celasun
bb29a98614 feat: Run PreValidate hooks for check cmd (#437)
PreValidate can be used to mount remote directories (e.g. via NFS) so
they must executed first before running any restic commands.

This was done for the backup command in 13aa560, but not for check. This
commit fixes that.
2025-03-22 19:12:36 +01:00
Duru Can Celasun
39f4f87ce3 feat: Add --dry-run to backup command (#438)
Restic supports --dry-run for backups since 0.13.0 [1] and this adds
support for that.

[1] bc97a3d1f9
2025-03-22 19:10:51 +01:00
dependabot[bot]
bd36bbe429 Bump golang from 1.23-alpine to 1.24-alpine (#429)
Bumps golang from 1.23-alpine to 1.24-alpine.

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-25 13:24:12 +01:00
19 changed files with 91 additions and 164 deletions

View File

@@ -1,4 +1,4 @@
FROM golang:1.23-alpine as builder
FROM golang:1.25-alpine as builder
WORKDIR /app
COPY go.* .

View File

@@ -6,6 +6,7 @@ import (
"github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra"
)
@@ -14,12 +15,14 @@ var backupCmd = &cobra.Command{
Short: "Create backups for given locations",
Run: func(cmd *cobra.Command, args []string) {
internal.GetConfig()
err := internal.Lock()
err := lock.Lock()
CheckErr(err)
defer internal.Unlock()
defer lock.Unlock()
dry, _ := cmd.Flags().GetBool("dry-run")
selected, err := internal.GetAllOrSelected(cmd, false)
CheckErr(err)
errors := 0
for _, name := range selected {
var splitted = strings.Split(name, "@")
@@ -28,7 +31,7 @@ var backupCmd = &cobra.Command{
specificBackend = splitted[1]
}
location, _ := internal.GetLocation(splitted[0])
errs := location.Backup(false, specificBackend)
errs := location.Backup(false, dry, specificBackend)
for _, err := range errs {
colors.Error.Printf("%s\n\n", err)
errors++
@@ -43,4 +46,5 @@ var backupCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(backupCmd)
internal.AddFlagsToCommand(backupCmd, false)
backupCmd.Flags().Bool("dry-run", false, "do not write changes, show what would be affected")
}

View File

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

View File

@@ -3,6 +3,7 @@ package cmd
import (
"github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/flags"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra"
)
@@ -12,9 +13,9 @@ var cronCmd = &cobra.Command{
Long: `Intended to be mainly triggered by an automated system like systemd or crontab. For each location checks if a cron backup is due and runs it.`,
Run: func(cmd *cobra.Command, args []string) {
internal.GetConfig()
err := internal.Lock()
err := lock.Lock()
CheckErr(err)
defer internal.Unlock()
defer lock.Unlock()
err = internal.RunCron()
CheckErr(err)

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ import (
"github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/flags"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra"
homedir "github.com/mitchellh/go-homedir"
@@ -17,7 +18,7 @@ import (
func CheckErr(err error) {
if err != nil {
colors.Error.Fprintln(os.Stderr, "Error:", err)
internal.Unlock()
lock.Unlock()
os.Exit(1)
}
}
@@ -41,7 +42,6 @@ func init() {
rootCmd.PersistentFlags().BoolVarP(&flags.VERBOSE, "verbose", "v", false, "verbose mode")
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")
rootCmd.PersistentFlags().StringVar(&flags.LOCKFILE, "lockfile", "", "specify a custom path for the lockfile (defaults to .autorestic.lock.yml next to the loaded autorestic config file)")
cobra.OnInitialize(initConfig)
}

View File

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

View File

@@ -3,7 +3,6 @@
"quick": "Quick Start",
"installation": "Installation",
"config": "Configuration",
"lockfile": "Lockfile",
"location": "Locations",
"backend": "Backend",
"cli": "CLI",

View File

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

View File

@@ -34,11 +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
```
## `--lockfile`
Specify the path for the [lockfile](../lockfile.md) used by `autorestic`. If omitted, this will default to `.autorestic.lock.yml` next to the loaded config file.
```bash
autorestic --lockfile /path/to/my/.autorestic.lock.yml
```

View File

@@ -1,14 +0,0 @@
# Lockfile
Under the hood, `autorestic` uses a lockfile to ensure that only one instance is running and to keep track of when [cronjobs](./location/cron.md) were last run.
By default, the lockfile is stored next to your [configuration file](./config.md) as `.autorestic.lock.yml`. In other words, if your config file is located at `/some/path/.autorestic.yml`, then the lockfile will be located at `/some/path/.autorestic.lock.yml`.
## Customization
The path to the lockfile can be customized if need be. This can be done is a few ways:
1. Using the `--lockfile ...` command line flag
1. Setting `lockfile: ...` in the configuration file
Note that `autorestic` will check for a customized lockfile path in the order listed above. This means that if you specify a lockfile path in multiple places, the method that's higher in the list will win.

View File

@@ -10,6 +10,7 @@ import (
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/flags"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/joho/godotenv"
"github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
@@ -23,7 +24,6 @@ type Options map[string]OptionMap
type Config struct {
Version string `mapstructure:"version" yaml:"version"`
Lockfile string `mapstructure:"lockfile,omitempty" yaml:"lockfile,omitempty"`
Extras interface{} `mapstructure:"extras" yaml:"extras"`
Locations map[string]Location `mapstructure:"locations" yaml:"locations"`
Backends map[string]Backend `mapstructure:"backends" yaml:"backends"`
@@ -40,7 +40,7 @@ func exitConfig(err error, msg string) {
if msg != "" {
colors.Error.Println(msg)
}
Unlock()
lock.Unlock()
os.Exit(1)
}
@@ -188,15 +188,31 @@ func CheckConfig() error {
if !CheckIfResticIsCallable() {
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
if err := backend.validate(); err != nil {
cwd, _ := GetPathRelativeToConfig(".")
for name, location := range c.Locations {
location.name = name
// Hooks before location validation
options := ExecuteOptions{
Command: "bash",
Dir: cwd,
Envs: map[string]string{
"AUTORESTIC_LOCATION": location.name,
},
}
if err := location.ExecuteHooks(location.Hooks.PreValidate, options); err != nil {
return err
}
if err := location.validate(); err != nil {
return err
}
}
for name, location := range c.Locations {
location.name = name
if err := location.validate(); err != nil {
for name, backend := range c.Backends {
backend.name = name
if err := backend.validate(); err != nil {
return err
}
}

View File

@@ -1,10 +1,9 @@
package flags
var (
CI bool = false
VERBOSE bool = false
CRON_LEAN bool = false
RESTIC_BIN string
CI bool = false
VERBOSE bool = false
CRON_LEAN bool = false
RESTIC_BIN string
DOCKER_IMAGE string
LOCKFILE string
)

View File

@@ -11,6 +11,7 @@ import (
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/flags"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/cupcakearmy/autorestic/internal/metadata"
"github.com/robfig/cron"
)
@@ -167,7 +168,7 @@ func (l Location) getLocationTags() string {
return buildTag("location", l.name)
}
func (l Location) Backup(cron bool, specificBackend string) []error {
func (l Location) Backup(cron bool, dry bool, specificBackend string) []error {
var errors []error
var backends []string
colors.PrimaryPrint(" Backing up location \"%s\" ", l.name)
@@ -227,6 +228,9 @@ func (l Location) Backup(cron bool, specificBackend string) []error {
if cron {
cmd = append(cmd, "--tag", buildTag("cron"))
}
if dry {
cmd = append(cmd, "--dry-run")
}
cmd = append(cmd, "--tag", l.getLocationTags())
backupOptions := ExecuteOptions{
Envs: env,
@@ -441,12 +445,12 @@ func (l Location) RunCron() error {
if err != nil {
return err
}
last := time.Unix(GetCron(l.name), 0)
last := time.Unix(lock.GetCron(l.name), 0)
next := schedule.Next(last)
now := time.Now()
if now.After(next) {
SetCron(l.name, now.Unix())
errs := l.Backup(true, "")
lock.SetCron(l.name, now.Unix())
errs := l.Backup(true, false, "")
if len(errs) > 0 {
return fmt.Errorf("Failed to backup location \"%s\":\n%w", l.name, errors.Join(errs...))
}

View File

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

View File

@@ -1,88 +1,30 @@
package internal
package lock
import (
"log"
"os"
"os/exec"
"path"
"strconv"
"sync"
"testing"
"github.com/cupcakearmy/autorestic/internal/flags"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
)
var testDirectory = "autorestic_test_tmp"
// All tests must share the same lock file as it is only initialized once
func setup(t *testing.T) {
t.Helper()
cleanup := func() {
flags.LOCKFILE = ""
config = nil
once = sync.Once{}
viper.Reset()
d, err := os.MkdirTemp("", testDirectory)
if err != nil {
log.Fatalf("error creating temp dir: %v", err)
return
}
cleanup()
d := t.TempDir()
// set config file location
viper.SetConfigFile(d + "/.autorestic.yml")
viper.Set("version", 2)
viper.WriteConfig()
t.Cleanup(cleanup)
}
func TestGetLockfilePath(t *testing.T) {
t.Run("user specified", func(t *testing.T) {
testCases := []struct {
name string
flag string
config string
expected string
}{
{
name: "flag and config",
flag: "/flag.lock.yml",
config: "/config.lock.yml",
expected: "/flag.lock.yml",
},
{
name: "flag only",
flag: "/flag.lock.yml",
config: "",
expected: "/flag.lock.yml",
},
{
name: "config only",
flag: "",
config: "/config.lock.yml",
expected: "/config.lock.yml",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
setup(t)
flags.LOCKFILE = testCase.flag
if testCase.config != "" {
viper.Set("lockfile", testCase.config)
err := viper.WriteConfig()
assert.NoError(t, err)
}
p := getLockfilePath()
assert.Equal(t, testCase.expected, p)
})
}
})
t.Run("default", func(t *testing.T) {
setup(t)
configPath := viper.ConfigFileUsed()
expectedLockfile := path.Join(path.Dir(configPath), ".autorestic.lock.yml")
p := getLockfilePath()
assert.Equal(t, expectedLockfile, p)
t.Cleanup(func() {
os.RemoveAll(d)
viper.Reset()
})
}

View File

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