diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 85301c7..fdc78e5 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -7,7 +7,6 @@ on: permissions: contents: read - # Optional: allow read access to pull request. Use with `only-new-issues` option. pull-requests: read jobs: @@ -15,40 +14,12 @@ jobs: 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 diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 1b1d18a..22833dc 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -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 diff --git a/.golangci.yml b/.golangci.yml index dc89d3f..652aecc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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 diff --git a/cmd/backup/archive.go b/cmd/backup/archive.go index 61f6829..abc6129 100644 --- a/cmd/backup/archive.go +++ b/cmd/backup/archive.go @@ -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 { @@ -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() { @@ -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 diff --git a/cmd/backup/config_provider.go b/cmd/backup/config_provider.go index 3a6d8fe..aea853c 100644 --- a/cmd/backup/config_provider.go +++ b/cmd/backup/config_provider.go @@ -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 diff --git a/cmd/backup/config_provider_test.go b/cmd/backup/config_provider_test.go index 5e7fc07..b82c48c 100644 --- a/cmd/backup/config_provider_test.go +++ b/cmd/backup/config_provider_test.go @@ -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) { diff --git a/cmd/backup/exec.go b/cmd/backup/exec.go index b329e57..03fbfd1 100644 --- a/cmd/backup/exec.go +++ b/cmd/backup/exec.go @@ -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") diff --git a/cmd/backup/stop_restart.go b/cmd/backup/stop_restart.go index 5b95416..cbca129 100644 --- a/cmd/backup/stop_restart.go +++ b/cmd/backup/stop_restart.go @@ -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" @@ -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), @@ -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, @@ -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 { diff --git a/go.mod b/go.mod index 4d6f09c..2a7f03d 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/internal/storage/dropbox/dropbox.go b/internal/storage/dropbox/dropbox.go index 42a39d9..b658531 100644 --- a/internal/storage/dropbox/dropbox.go +++ b/internal/storage/dropbox/dropbox.go @@ -87,7 +87,7 @@ 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) @@ -95,19 +95,24 @@ func (b *dropboxStorage) Copy(file string) error { 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) @@ -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 } @@ -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) diff --git a/internal/storage/googledrive/googledrive.go b/internal/storage/googledrive/googledrive.go index 89635be..2e8d917 100644 --- a/internal/storage/googledrive/googledrive.go +++ b/internal/storage/googledrive/googledrive.go @@ -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 { @@ -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 != "" { @@ -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) diff --git a/internal/storage/local/local.go b/internal/storage/local/local.go index 174eefd..0f80ece 100644 --- a/internal/storage/local/local.go +++ b/internal/storage/local/local.go @@ -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") @@ -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() } diff --git a/internal/storage/ssh/ssh.go b/internal/storage/ssh/ssh.go index 10cac89..299052c 100644 --- a/internal/storage/ssh/ssh.go +++ b/internal/storage/ssh/ssh.go @@ -106,19 +106,25 @@ func (b *sshStorage) Name() string { } // Copy copies the given file to the SSH storage backend. -func (b *sshStorage) Copy(file string) error { +func (b *sshStorage) Copy(file string) (returnErr error) { source, err := os.Open(file) _, name := path.Split(file) if err != nil { - return errwrap.Wrap(err, " error reading the file to be uploaded") + returnErr = errwrap.Wrap(err, " error reading the file to be uploaded") + return } - defer source.Close() + defer func() { + returnErr = source.Close() + }() destination, err := b.sftpClient.Create(path.Join(b.DestinationPath, name)) if err != nil { - return errwrap.Wrap(err, "error creating file") + returnErr = errwrap.Wrap(err, "error creating file") + return } - defer destination.Close() + defer func() { + returnErr = destination.Close() + }() chunk := make([]byte, 1e9) for { @@ -126,27 +132,32 @@ func (b *sshStorage) Copy(file string) error { if err == io.EOF { tot, err := destination.Write(chunk[:num]) if err != nil { - return errwrap.Wrap(err, "error uploading the file") + returnErr = errwrap.Wrap(err, "error uploading the file") + return } if tot != len(chunk[:num]) { - return errwrap.Wrap(nil, "failed to write stream") + returnErr = errwrap.Wrap(nil, "failed to write stream") + return } break } if err != nil { - return errwrap.Wrap(err, "error uploading the file") + returnErr = errwrap.Wrap(err, "error uploading the file") + return } tot, err := destination.Write(chunk[:num]) if err != nil { - return errwrap.Wrap(err, "error uploading the file") + returnErr = errwrap.Wrap(err, "error uploading the file") + return } if tot != len(chunk[:num]) { - return errwrap.Wrap(nil, "failed to write stream") + returnErr = errwrap.Wrap(nil, "failed to write stream") + return } }