Skip to content

Commit b52b271

Browse files
authored
Allow for the exclusion of files from backups (#100)
* Hoist walking of files so it can be used for features other than archive creation * Add option to ignore files from backup using glob patterns * Use Regexp instead of glob for exclusion * Ignore artifacts * Add teardown to test * Allow single Re for filtering only * Add documentation * Use MatchString on re, add bad input to message in case of error
1 parent cac5777 commit b52b271

File tree

9 files changed

+96
-14
lines changed

9 files changed

+96
-14
lines changed

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,12 @@ You can populate below template according to your requirements and use it as you
168168

169169
# BACKUP_SOURCES="/other/location"
170170

171+
# When given, all files in BACKUP_SOURCES whose full path matches the given
172+
# regular expression will be excluded from the archive. Regular Expressions
173+
# can be used as from the Go standard library https://pkg.go.dev/regexp
174+
175+
# BACKUP_EXCLUDE_REGEXP="\.log$"
176+
171177
########### BACKUP STORAGE
172178

173179
# The name of the remote bucket that should be used for storing backups. If

cmd/backup/archive.go

+3-12
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,13 @@ import (
1111
"compress/gzip"
1212
"fmt"
1313
"io"
14-
"io/fs"
1514
"os"
1615
"path"
1716
"path/filepath"
1817
"strings"
1918
)
2019

21-
func createArchive(inputFilePath, outputFilePath string) error {
20+
func createArchive(files []string, inputFilePath, outputFilePath string) error {
2221
inputFilePath = stripTrailingSlashes(inputFilePath)
2322
inputFilePath, outputFilePath, err := makeAbsolute(inputFilePath, outputFilePath)
2423
if err != nil {
@@ -28,7 +27,7 @@ func createArchive(inputFilePath, outputFilePath string) error {
2827
return fmt.Errorf("createArchive: error creating output file path: %w", err)
2928
}
3029

31-
if err := compress(inputFilePath, outputFilePath, filepath.Dir(inputFilePath)); err != nil {
30+
if err := compress(files, outputFilePath, filepath.Dir(inputFilePath)); err != nil {
3231
return fmt.Errorf("createArchive: error creating archive: %w", err)
3332
}
3433

@@ -52,7 +51,7 @@ func makeAbsolute(inputFilePath, outputFilePath string) (string, string, error)
5251
return inputFilePath, outputFilePath, err
5352
}
5453

55-
func compress(inPath, outFilePath, subPath string) error {
54+
func compress(paths []string, outFilePath, subPath string) error {
5655
file, err := os.Create(outFilePath)
5756
if err != nil {
5857
return fmt.Errorf("compress: error creating out file: %w", err)
@@ -62,14 +61,6 @@ func compress(inPath, outFilePath, subPath string) error {
6261
gzipWriter := gzip.NewWriter(file)
6362
tarWriter := tar.NewWriter(gzipWriter)
6463

65-
var paths []string
66-
if err := filepath.WalkDir(inPath, func(path string, di fs.DirEntry, err error) error {
67-
paths = append(paths, path)
68-
return err
69-
}); err != nil {
70-
return fmt.Errorf("compress: error walking filesystem tree: %w", err)
71-
}
72-
7364
for _, p := range paths {
7465
if err := writeTarGz(p, tarWriter, prefix); err != nil {
7566
return fmt.Errorf("compress error writing %s to archive: %w", p, err)

cmd/backup/config.go

+22-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33

44
package main
55

6-
import "time"
6+
import (
7+
"fmt"
8+
"regexp"
9+
"time"
10+
)
711

812
// Config holds all configuration values that are expected to be set
913
// by users.
@@ -18,6 +22,7 @@ type Config struct {
1822
BackupPruningPrefix string `split_words:"true"`
1923
BackupStopContainerLabel string `split_words:"true" default:"true"`
2024
BackupFromSnapshot bool `split_words:"true"`
25+
BackupExcludeRegexp RegexpDecoder `split_words:"true"`
2126
AwsS3BucketName string `split_words:"true"`
2227
AwsS3Path string `split_words:"true"`
2328
AwsEndpoint string `split_words:"true" default:"s3.amazonaws.com"`
@@ -44,3 +49,19 @@ type Config struct {
4449
ExecForwardOutput bool `split_words:"true"`
4550
LockTimeout time.Duration `split_words:"true" default:"60m"`
4651
}
52+
53+
type RegexpDecoder struct {
54+
Re *regexp.Regexp
55+
}
56+
57+
func (r *RegexpDecoder) Decode(v string) error {
58+
if v == "" {
59+
return nil
60+
}
61+
re, err := regexp.Compile(v)
62+
if err != nil {
63+
return fmt.Errorf("config: error compiling given regexp `%s`: %w", v, err)
64+
}
65+
*r = RegexpDecoder{Re: re}
66+
return nil
67+
}

cmd/backup/script.go

+22-1
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,28 @@ func (s *script) takeBackup() error {
398398
s.logger.Infof("Removed tar file `%s`.", tarFile)
399399
return nil
400400
})
401-
if err := createArchive(backupSources, tarFile); err != nil {
401+
402+
backupPath, err := filepath.Abs(stripTrailingSlashes(backupSources))
403+
if err != nil {
404+
return fmt.Errorf("takeBackup: error getting absolute path: %w", err)
405+
}
406+
407+
var filesEligibleForBackup []string
408+
if err := filepath.WalkDir(backupPath, func(path string, di fs.DirEntry, err error) error {
409+
if err != nil {
410+
return err
411+
}
412+
413+
if s.c.BackupExcludeRegexp.Re != nil && s.c.BackupExcludeRegexp.Re.MatchString(path) {
414+
return nil
415+
}
416+
filesEligibleForBackup = append(filesEligibleForBackup, path)
417+
return nil
418+
}); err != nil {
419+
return fmt.Errorf("compress: error walking filesystem tree: %w", err)
420+
}
421+
422+
if err := createArchive(filesEligibleForBackup, backupSources, tarFile); err != nil {
402423
return fmt.Errorf("takeBackup: error compressing backup folder: %w", err)
403424
}
404425

test/ignore/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
local

test/ignore/docker-compose.yml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
version: '3.8'
2+
3+
services:
4+
backup:
5+
image: offen/docker-volume-backup:${TEST_VERSION:-canary}
6+
deploy:
7+
restart_policy:
8+
condition: on-failure
9+
environment:
10+
BACKUP_FILENAME: test.tar.gz
11+
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
12+
BACKUP_EXCLUDE_REGEXP: '\.(me|you)$$'
13+
volumes:
14+
- ./local:/archive
15+
- ./sources:/backup/data:ro

test/ignore/run.sh

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/bin/sh
2+
3+
set -e
4+
5+
cd $(dirname $0)
6+
mkdir -p local
7+
8+
docker-compose up -d
9+
sleep 5
10+
docker-compose exec backup backup
11+
12+
docker-compose down --volumes
13+
14+
out=$(mktemp -d)
15+
sudo tar --same-owner -xvf ./local/test.tar.gz -C "$out"
16+
17+
if [ ! -f "$out/backup/data/me.txt" ]; then
18+
echo "[TEST:FAIL] Expected file was not found."
19+
exit 1
20+
fi
21+
echo "[TEST:PASS] Expected file was found."
22+
23+
if [ -f "$out/backup/data/skip.me" ]; then
24+
echo "[TEST:FAIL] Ignored file was found."
25+
exit 1
26+
fi
27+
echo "[TEST:PASS] Ignored file was not found."

test/ignore/sources/me.txt

Whitespace-only changes.

test/ignore/sources/skip.me

Whitespace-only changes.

0 commit comments

Comments
 (0)