Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow updating gopass.exe on Windows #2345

Merged
merged 1 commit into from
Sep 20, 2022
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
7 changes: 0 additions & 7 deletions internal/action/update.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package action

import (
"fmt"
"runtime"

"github.com/gopasspw/gopass/internal/action/exit"
"github.com/gopasspw/gopass/internal/out"
"github.com/gopasspw/gopass/internal/updater"
Expand All @@ -23,10 +20,6 @@ func (s *Action) Update(c *cli.Context) error {
return nil
}

if runtime.GOOS == "windows" {
return fmt.Errorf("gopass update is not supported on windows (#1722)")
}

out.Printf(ctx, "⚒ Checking for available updates ...")
if err := updater.Update(ctx, s.version); err != nil {
return exit.Error(exit.Unknown, err, "Failed to update gopass: %s", err)
Expand Down
5 changes: 5 additions & 0 deletions internal/updater/access_others.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ import "golang.org/x/sys/unix"
func canWrite(path string) error {
return unix.Access(path, unix.W_OK) //nolint:wrapcheck
}

func removeOldBinary(dir, dest string) error {
// no need, os.Rename will replace the destination
return nil
}
27 changes: 27 additions & 0 deletions internal/updater/access_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,33 @@

package updater

import (
"fmt"
"os"
"path/filepath"
)

func canWrite(path string) error {
return nil
}

// Windows won't allow us to remove the binary that's currently being executed.
// So rename the binary and then the updater should be able to write it's
// update to the correct location.
//
// See https://stackoverflow.com/a/459860
func removeOldBinary(dir, dest string) error {
bakFile := filepath.Join(dir, filepath.Base(dest)+".bak")
// check if the bakup file already exists
if _, err := os.Stat(bakFile); err == nil {
// ... then remove it
_ = os.Remove(bakFile)
}
// we can't remove the currently running binary, but should be able to
// rename it.
if err := os.Rename(dest, bakFile); err != nil {
return fmt.Errorf("unable to rename %s to %s: %w", dest, bakFile, err)
}

return nil
}
61 changes: 38 additions & 23 deletions internal/updater/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"

Expand All @@ -17,26 +18,40 @@ import (

func extractFile(buf []byte, filename, dest string) error {
mode := os.FileMode(0o755)
dir := filepath.Dir(dest)

// if overwriting an existing binary retain it's mode flags
fi, err := os.Lstat(dest)
if err == nil {
mode = fi.Mode()
}

if err := os.Remove(dest); err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("unable to remove destination file: %w", err)
}
tfn, err := extractToTempFile(buf, filename, dest)
if err != nil {
return fmt.Errorf("failed to extract update to %s: %w", dest, err)
}

if err := removeOldBinary(dir, dest); err != nil {
return fmt.Errorf("failed to remove old binary %s: %w", dest, err)
}

// open the destination file for writing
dfh, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode)
if err := os.Rename(tfn, dest); err != nil {
return fmt.Errorf("failed to rename tempfile %s to %s: %w", tfn, dest, err)
}

return os.Chmod(dest, mode)
}

func extractToTempFile(buf []byte, filename, dest string) (string, error) {
// open a temp file for writing
dir := filepath.Dir(dest)
dfh, err := ioutil.TempFile(dir, "gopass")
if err != nil {
return fmt.Errorf("failed to open file %q: %w", dest, err)
return "", fmt.Errorf("failed to create temp file in %s: %w", dir, err)
}

defer func() {
_ = dfh.Sync()
_ = dfh.Close()
}()

Expand All @@ -46,23 +61,23 @@ func extractFile(buf []byte, filename, dest string) error {
case ".gz":
gzr, err := gzip.NewReader(rd)
if err != nil {
return fmt.Errorf("failed to open gzip file: %w", err)
return "", fmt.Errorf("failed to open gzip file: %w", err)
}

return extractTar(gzr, dfh, dest)
return extractTar(gzr, dfh, dfh.Name())
case ".bz2":
return extractTar(bzip2.NewReader(rd), dfh, dest)
return extractTar(bzip2.NewReader(rd), dfh, dfh.Name())
case ".zip":
return extractZip(buf, dfh, dest)
return extractZip(buf, dfh, dfh.Name())
default:
return fmt.Errorf("unsupported file extension: %q", filepath.Ext(filename))
return "", fmt.Errorf("unsupported file extension: %q", filepath.Ext(filename))
}
}

func extractZip(buf []byte, dfh io.WriteCloser, dest string) error {
func extractZip(buf []byte, dfh io.WriteCloser, dest string) (string, error) {
zrd, err := zip.NewReader(bytes.NewReader(buf), int64(len(buf)))
if err != nil {
return fmt.Errorf("failed to open zip file: %w", err)
return "", fmt.Errorf("failed to open zip file: %w", err)
}

for i := 0; i < len(zrd.File); i++ {
Expand All @@ -72,26 +87,26 @@ func extractZip(buf []byte, dfh io.WriteCloser, dest string) error {

file, err := zrd.File[i].Open()
if err != nil {
return fmt.Errorf("failed to read from zip file: %w", err)
return "", fmt.Errorf("failed to read from zip file: %w", err)
}

n, err := io.Copy(dfh, file)
if err != nil {
_ = dfh.Close()
_ = os.Remove(dest)

return fmt.Errorf("failed to read gopass.exe from zip file: %w", err)
return "", fmt.Errorf("failed to read gopass.exe from zip file: %w", err)
}
// success
debug.Log("wrote %d bytes to %v", n, dest)

return nil
return dest, nil
}

return fmt.Errorf("file not found in archive")
return "", fmt.Errorf("file not found in archive")
}

func extractTar(rd io.Reader, dfh io.WriteCloser, dest string) error {
func extractTar(rd io.Reader, dfh io.WriteCloser, dest string) (string, error) {
tarReader := tar.NewReader(rd)

for {
Expand All @@ -101,7 +116,7 @@ func extractTar(rd io.Reader, dfh io.WriteCloser, dest string) error {
}

if err != nil {
return fmt.Errorf("failed to read from tar file: %w", err)
return "", fmt.Errorf("failed to read from tar file: %w", err)
}

name := filepath.Base(header.Name)
Expand All @@ -119,13 +134,13 @@ func extractTar(rd io.Reader, dfh io.WriteCloser, dest string) error {
_ = dfh.Close()
_ = os.Remove(dest)

return fmt.Errorf("failed to read gopass from tar file: %w", err)
return "", fmt.Errorf("failed to read gopass from tar file: %w", err)
}
// success
debug.Log("wrote %d bytes to %v", n, dest)

return nil
return dest, nil
}

return fmt.Errorf("file not found in archive")
return "", fmt.Errorf("file not found in archive")
}