Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
6ed9086d91 Bump golang from 1.25-alpine to 1.26-alpine
Bumps golang from 1.25-alpine to 1.26-alpine.

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 21:22:45 +00:00
16 changed files with 518 additions and 948 deletions

View File

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

View File

@@ -2,28 +2,23 @@ name: CI
on:
pull_request:
push:
branches:
- main
- master
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
MISE_EXPERIMENTAL: true
branches: [master]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: jdx/mise-action@v3
- run: mise run test
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '^1.21'
- run: go test -v ./...
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: jdx/mise-action@v3
- run: mise run build
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '^1.21'
- run: go build -v .

3
.gitignore vendored
View File

@@ -9,5 +9,4 @@
test
autorestic
data
dist
coverage*
dist

View File

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

View File

@@ -1 +1 @@
v24
v22.7.0

View File

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

View File

@@ -5,11 +5,11 @@
"start": "NEXT_TELEMETRY_DISABLED=1 next start"
},
"dependencies": {
"next": "^14.2.35",
"next": "^14.2.7",
"nextra": "^2.13.4",
"nextra-theme-docs": "^2.13.4",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"packageManager": "pnpm@10.33.0"
"packageManager": "pnpm@9.9.0"
}

970
docs/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

2
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/cupcakearmy/autorestic
go 1.26.1
go 1.21
require (
github.com/blang/semver/v4 v4.0.0

View File

@@ -1,14 +1,6 @@
package internal
import (
"os"
"path"
"sync"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
)
import "testing"
func TestGetType(t *testing.T) {
@@ -99,46 +91,3 @@ func TestBuildRestoreCommand(t *testing.T) {
expected := []string{"restore", "--target", "to", "--tag", "ar:location:foo", "snapshot", "options"}
assertSliceEqual(t, result, expected)
}
func TestLocationBackupWithMock(t *testing.T) {
// Backup original
originalExecutor := DefaultExecutor
defer func() { DefaultExecutor = originalExecutor }()
// Inject mock
mock := &MockExecutor{
ExecuteResticFunc: func(options ExecuteOptions, args ...string) (int, string, error) {
assert.Equal(t, "backup", args[0])
return 0, "success", nil
},
}
DefaultExecutor = mock
// Setup dummy config
workDir := t.TempDir()
configFile := path.Join(workDir, ".autorestic.yml")
err := os.WriteFile(configFile, []byte("version: 2"), 0644)
assert.NoError(t, err)
viper.Reset()
viper.SetConfigFile(configFile)
viper.Set("version", 2)
// Register test-backend
viper.Set("backends.test-backend.type", "local")
viper.Set("backends.test-backend.path", workDir)
config = nil
once = sync.Once{}
loc := Location{
name: "test-location",
To: []string{"test-backend"},
From: []string{"/"},
Type: "local",
}
errs := loc.Backup(false, false, "")
if len(errs) != 0 {
t.Errorf("expected no error, got %v", errs)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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