Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 6 additions & 35 deletions .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,19 @@ on:

permissions:
contents: read
# Optional: allow read access to pull request. Use with `only-new-issues` option.
pull-requests: read

jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: '1.24'
cache: false
go-version: '1.25'
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v8
with:
# Require: The version of golangci-lint to use.
# When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version.
# When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit.
version: v1.64

# Optional: working directory, useful for monorepos
# working-directory: somedir

# Optional: golangci-lint command line arguments.
#
# Note: By default, the `.golangci.yml` file should be at the root of the repository.
# The location of the configuration file can be changed by using `--config=`
# args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0

# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true

# Optional: if set to true, then all caching functionality will be completely disabled,
# takes precedence over all other caching options.
# skip-cache: true

# Optional: if set to true, then the action won't cache or restore ~/go/pkg.
# skip-pkg-cache: true

# Optional: if set to true, then the action won't cache or restore ~/.cache/go-build.
# skip-build-cache: true

# Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'.
# install-mode: "goinstall"
version: v2.4
args: --timeout 5m
2 changes: 1 addition & 1 deletion .github/workflows/unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.24.x'
go-version: '1.25.x'
- name: Install dependencies
run: go mod download
- name: Test with the Go CLI
Expand Down
4 changes: 1 addition & 3 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
version: '2'
linters:
# Enable specific linter
# https://golangci-lint.run/usage/linters/#enabled-by-default
enable:
- staticcheck
- govet
output:
formats:
- format: colored-line-number
24 changes: 16 additions & 8 deletions cmd/backup/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,11 @@ func getCompressionWriter(file *os.File, algo string, concurrency int) (io.Write
}
}

func writeTarball(path string, tarWriter *tar.Writer, prefix string) error {
func writeTarball(path string, tarWriter *tar.Writer, prefix string) (returnErr error) {
fileInfo, err := os.Lstat(path)
if err != nil {
return errwrap.Wrap(err, fmt.Sprintf("error getting file info for %s", path))
returnErr = errwrap.Wrap(err, fmt.Sprintf("error getting file info for %s", path))
return
}

if fileInfo.Mode()&os.ModeSocket == os.ModeSocket {
Expand All @@ -135,19 +136,22 @@ func writeTarball(path string, tarWriter *tar.Writer, prefix string) error {
if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
var err error
if link, err = os.Readlink(path); err != nil {
return errwrap.Wrap(err, fmt.Sprintf("error resolving symlink %s", path))
returnErr = errwrap.Wrap(err, fmt.Sprintf("error resolving symlink %s", path))
return
}
}

header, err := tar.FileInfoHeader(fileInfo, link)
if err != nil {
return errwrap.Wrap(err, "error getting file info header")
returnErr = errwrap.Wrap(err, "error getting file info header")
return
}
header.Name = strings.TrimPrefix(path, prefix)

err = tarWriter.WriteHeader(header)
if err != nil {
return errwrap.Wrap(err, "error writing file info header")
returnErr = errwrap.Wrap(err, "error writing file info header")
return
}

if !fileInfo.Mode().IsRegular() {
Expand All @@ -156,13 +160,17 @@ func writeTarball(path string, tarWriter *tar.Writer, prefix string) error {

file, err := os.Open(path)
if err != nil {
return errwrap.Wrap(err, fmt.Sprintf("error opening %s", path))
returnErr = errwrap.Wrap(err, fmt.Sprintf("error opening %s", path))
return
}
defer file.Close()
defer func() {
returnErr = file.Close()
}()

_, err = io.Copy(tarWriter, file)
if err != nil {
return errwrap.Wrap(err, fmt.Sprintf("error copying %s to tar writer", path))
returnErr = errwrap.Wrap(err, fmt.Sprintf("error copying %s to tar writer", path))
return
}

return nil
Expand Down
6 changes: 3 additions & 3 deletions cmd/backup/config_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,13 @@ func source(path string) (map[string]string, error) {
currentValue, currentOk := os.LookupEnv(key)
defer func() {
if currentOk {
os.Setenv(key, currentValue)
_ = os.Setenv(key, currentValue)
return
}
os.Unsetenv(key)
_ = os.Unsetenv(key)
}()
result[key] = value
os.Setenv(key, value)
_ = os.Setenv(key, value)
}
}
return result, nil
Expand Down
6 changes: 4 additions & 2 deletions cmd/backup/config_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@ func TestSource(t *testing.T) {
},
}

os.Setenv("QUX", "yyy")
defer os.Unsetenv("QUX")
_ = os.Setenv("QUX", "yyy")
defer func() {
_ = os.Unsetenv("QUX")
}()

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
Expand Down
8 changes: 6 additions & 2 deletions cmd/backup/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,12 @@ func (s *script) runLabeledCommands(label string) error {
s.logger.Info(fmt.Sprintf("Running %s command %s for container %s", label, cmd, strings.TrimPrefix(c.Names[0], "/")))
stdout, stderr, err := s.exec(c.ID, cmd, user)
if s.c.ExecForwardOutput {
os.Stderr.Write(stderr)
os.Stdout.Write(stdout)
if _, err := os.Stderr.Write(stderr); err != nil {
return errwrap.Wrap(err, "error writing to stderr")
}
if _, err := os.Stdout.Write(stdout); err != nil {
return errwrap.Wrap(err, "error writing to stdout")
}
}
if err != nil {
return errwrap.Wrap(err, "error executing command")
Expand Down
9 changes: 4 additions & 5 deletions cmd/backup/stop_restart.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"time"

"github.com/docker/cli/cli/command/service/progress"
"github.com/docker/docker/api/types/container"
ctr "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
Expand Down Expand Up @@ -66,7 +65,7 @@ func awaitContainerCountForService(cli *client.Client, serviceID string, count i
),
)
case <-poll.C:
containers, err := cli.ContainerList(context.Background(), container.ListOptions{
containers, err := cli.ContainerList(context.Background(), ctr.ListOptions{
Filters: filters.NewArgs(filters.KeyValuePair{
Key: "label",
Value: fmt.Sprintf("com.docker.swarm.service.id=%s", serviceID),
Expand Down Expand Up @@ -124,11 +123,11 @@ func (s *script) stopContainersAndServices() (func() error, error) {
labelValue,
)

allContainers, err := s.cli.ContainerList(context.Background(), container.ListOptions{})
allContainers, err := s.cli.ContainerList(context.Background(), ctr.ListOptions{})
if err != nil {
return noop, errwrap.Wrap(err, "error querying for containers")
}
containersToStop, err := s.cli.ContainerList(context.Background(), container.ListOptions{
containersToStop, err := s.cli.ContainerList(context.Background(), ctr.ListOptions{
Filters: filters.NewArgs(filters.KeyValuePair{
Key: "label",
Value: filterMatchLabel,
Expand Down Expand Up @@ -215,7 +214,7 @@ func (s *script) stopContainersAndServices() (func() error, error) {
)
}

var stoppedContainers []container.Summary
var stoppedContainers []ctr.Summary
var stopErrors []error
for _, container := range containersToStop {
if err := s.cli.ContainerStop(context.Background(), container.ID, ctr.StopOptions{}); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/offen/docker-volume-backup

go 1.24.0
go 1.25

require (
filippo.io/age v1.2.1
Expand Down
21 changes: 14 additions & 7 deletions internal/storage/dropbox/dropbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,27 +87,32 @@ func (b *dropboxStorage) Name() string {
}

// Copy copies the given file to the WebDav storage backend.
func (b *dropboxStorage) Copy(file string) error {
func (b *dropboxStorage) Copy(file string) (returnErr error) {
_, name := path.Split(file)

folderArg := files.NewCreateFolderArg(b.DestinationPath)
if _, err := b.client.CreateFolderV2(folderArg); err != nil {
switch err := err.(type) {
case files.CreateFolderV2APIError:
if err.EndpointError.Path.Tag != files.WriteErrorConflict {
return errwrap.Wrap(err, fmt.Sprintf("error creating directory '%s'", b.DestinationPath))
returnErr = errwrap.Wrap(err, fmt.Sprintf("error creating directory '%s'", b.DestinationPath))
return
}
b.Log(storage.LogLevelInfo, b.Name(), "Destination path '%s' already exists, no new directory required.", b.DestinationPath)
default:
return errwrap.Wrap(err, fmt.Sprintf("error creating directory '%s'", b.DestinationPath))
returnErr = errwrap.Wrap(err, fmt.Sprintf("error creating directory '%s'", b.DestinationPath))
return
}
}

r, err := os.Open(file)
if err != nil {
return errwrap.Wrap(err, "error opening the file to be uploaded")
returnErr = errwrap.Wrap(err, "error opening the file to be uploaded")
return
}
defer r.Close()
defer func() {
returnErr = r.Close()
}()

// Start new upload session and get session id
b.Log(storage.LogLevelInfo, b.Name(), "Starting upload session for backup '%s' at path '%s'.", file, b.DestinationPath)
Expand All @@ -116,7 +121,8 @@ func (b *dropboxStorage) Copy(file string) error {
uploadSessionStartArg := files.NewUploadSessionStartArg()
uploadSessionStartArg.SessionType = &files.UploadSessionType{Tagged: dropbox.Tagged{Tag: files.UploadSessionTypeConcurrent}}
if res, err := b.client.UploadSessionStart(uploadSessionStartArg, nil); err != nil {
return errwrap.Wrap(err, "error starting the upload session")
returnErr = errwrap.Wrap(err, "error starting the upload session")
return
} else {
sessionId = res.SessionId
}
Expand Down Expand Up @@ -197,7 +203,8 @@ loop:
files.NewCommitInfo(path.Join(b.DestinationPath, name)),
), nil)
if err != nil {
return errwrap.Wrap(err, "error finishing the upload session")
returnErr = errwrap.Wrap(err, "error finishing the upload session")
return
}

b.Log(storage.LogLevelInfo, b.Name(), "Uploaded a copy of backup '%s' at path '%s'.", file, b.DestinationPath)
Expand Down
16 changes: 10 additions & 6 deletions internal/storage/googledrive/googledrive.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import (
"strings"
"time"

"crypto/tls"
"github.com/offen/docker-volume-backup/internal/errwrap"
"github.com/offen/docker-volume-backup/internal/storage"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/drive/v3"
"google.golang.org/api/option"
"golang.org/x/oauth2"
"net/http"
"crypto/tls"
)

type googleDriveStorage struct {
Expand Down Expand Up @@ -84,15 +84,18 @@ func (b *googleDriveStorage) Name() string {
}

// Copy copies the given file to the Google Drive storage backend.
func (b *googleDriveStorage) Copy(file string) error {
func (b *googleDriveStorage) Copy(file string) (returnErr error) {
_, name := filepath.Split(file)
b.Log(storage.LogLevelInfo, b.Name(), "Starting upload for backup '%s'.", name)

f, err := os.Open(file)
if err != nil {
return errwrap.Wrap(err, fmt.Sprintf("failed to open file %s", file))
returnErr = errwrap.Wrap(err, fmt.Sprintf("failed to open file %s", file))
return
}
defer f.Close()
defer func() {
returnErr = f.Close()
}()

driveFile := &drive.File{Name: name}
if b.DestinationPath != "" {
Expand All @@ -104,7 +107,8 @@ func (b *googleDriveStorage) Copy(file string) error {
createCall := b.client.Files.Create(driveFile).SupportsAllDrives(true).Fields("id")
created, err := createCall.Media(f).Do()
if err != nil {
return errwrap.Wrap(err, fmt.Sprintf("failed to upload %s", name))
returnErr = errwrap.Wrap(err, fmt.Sprintf("failed to upload %s", name))
return
}

b.Log(storage.LogLevelInfo, b.Name(), "Finished upload for %s. File ID: %s", name, created.Id)
Expand Down
19 changes: 12 additions & 7 deletions internal/storage/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ func (b *localStorage) Copy(file string) error {
if b.latestSymlink != "" {
symlink := path.Join(b.DestinationPath, b.latestSymlink)
if _, err := os.Lstat(symlink); err == nil {
os.Remove(symlink)
if err := os.Remove(symlink); err != nil {
return errwrap.Wrap(err, "error removing existing symlink")
}
}
if err := os.Symlink(name, symlink); err != nil {
return errwrap.Wrap(err, "error creating latest symlink")
Expand Down Expand Up @@ -146,22 +148,25 @@ func (b *localStorage) Prune(deadline time.Time, pruningPrefix string) (*storage
}

// copy creates a copy of the file located at `dst` at `src`.
func copyFile(src, dst string) error {
func copyFile(src, dst string) (returnErr error) {
in, err := os.Open(src)
if err != nil {
return err
returnErr = err
return
}
defer in.Close()
defer func() {
returnErr = in.Close()
}()

out, err := os.Create(dst)
if err != nil {
return err
returnErr = err
return
}

_, err = io.Copy(out, in)
if err != nil {
out.Close()
return err
return errors.Join(err, out.Close())
}
return out.Close()
}
Loading