From 7222830d8eba997772914e39da88b3071202addd Mon Sep 17 00:00:00 2001 From: Jannis Metrikat <120120832+jmetrikat@users.noreply.github.com> Date: Sat, 17 Jan 2026 18:05:49 +0100 Subject: [PATCH 1/3] Refactor update to minio/selfupdate --- cmd/update/update.go | 169 ++++++++++++++------------------------- core/apikey.go | 2 +- core/config/constants.go | 7 +- core/version.go | 2 +- go.mod | 2 + go.sum | 9 +++ 6 files changed, 80 insertions(+), 111 deletions(-) diff --git a/cmd/update/update.go b/cmd/update/update.go index af1e33a..aebdd92 100644 --- a/cmd/update/update.go +++ b/cmd/update/update.go @@ -9,19 +9,16 @@ import ( "io" "net/http" "os" - "os/exec" "path/filepath" "runtime" "strings" - "github.com/anyproto/anytype-cli/core" - "github.com/anyproto/anytype-cli/core/output" + "github.com/minio/selfupdate" "github.com/spf13/cobra" -) -const ( - githubOwner = "anyproto" - githubRepo = "anytype-cli" + "github.com/anyproto/anytype-cli/core" + "github.com/anyproto/anytype-cli/core/config" + "github.com/anyproto/anytype-cli/core/output" ) func NewUpdateCmd() *cobra.Command { @@ -98,22 +95,19 @@ func downloadAndInstall(version string) error { return err } - if err := extractArchive(archivePath, tempDir); err != nil { - return output.Error("Failed to extract: %w", err) - } - binaryName := "anytype" if runtime.GOOS == "windows" { binaryName = "anytype.exe" } - newBinary := filepath.Join(tempDir, binaryName) - if _, err := os.Stat(newBinary); err != nil { - return output.Error("binary not found in archive (expected %s)", binaryName) + binaryReader, err := extractBinary(archivePath, binaryName) + if err != nil { + return output.Error("Failed to extract: %w", err) } + defer binaryReader.Close() - if err := replaceBinary(newBinary); err != nil { - return output.Error("Failed to install: %w", err) + if err := selfupdate.Apply(binaryReader, selfupdate.Options{}); err != nil { + return output.Error("failed to apply update: %w", err) } return nil @@ -135,8 +129,8 @@ func downloadRelease(version, destination string) error { return downloadViaAPI(version, archiveName, destination) } - url := fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s", - githubOwner, githubRepo, version, archiveName) + url := fmt.Sprintf("%s/releases/download/%s/%s", + config.GitHubBaseURL, version, archiveName) return downloadFile(url, destination, "") } @@ -207,25 +201,25 @@ func downloadFile(url, destination, token string) error { return err } -func extractArchive(archivePath, destDir string) error { +// extractBinary extracts the binary from the archive and returns it as a ReadCloser +func extractBinary(archivePath, binaryName string) (io.ReadCloser, error) { if strings.HasSuffix(archivePath, ".zip") { - return extractZip(archivePath, destDir) + return extractBinaryFromZip(archivePath, binaryName) } - return extractTarGz(archivePath, destDir) + return extractBinaryFromTarGz(archivePath, binaryName) } -func extractTarGz(archivePath, destDir string) error { +func extractBinaryFromTarGz(archivePath, binaryName string) (io.ReadCloser, error) { file, err := os.Open(archivePath) if err != nil { - return err + return nil, err } - defer file.Close() gz, err := gzip.NewReader(file) if err != nil { - return err + file.Close() + return nil, err } - defer gz.Close() tr := tar.NewReader(gz) for { @@ -234,108 +228,67 @@ func extractTarGz(archivePath, destDir string) error { break } if err != nil { - return err + file.Close() + gz.Close() + return nil, err } - if strings.Contains(header.Name, "..") { - return fmt.Errorf("illegal file path: %s", header.Name) - } - target := filepath.Join(destDir, header.Name) - - switch header.Typeflag { - case tar.TypeDir: - if err := os.MkdirAll(target, 0755); err != nil { - return err - } - case tar.TypeReg: - if err := writeFile(target, tr, header.FileInfo().Mode()); err != nil { - return err - } + if header.Typeflag == tar.TypeReg && header.Name == binaryName { + // Return a wrapper that closes both gz and file when done + return &tarGzReader{Reader: tr, gz: gz, file: file}, nil } } - return nil -} - -func extractZip(archivePath, destDir string) error { - r, err := zip.OpenReader(archivePath) - if err != nil { - return err - } - defer r.Close() - - for _, f := range r.File { - if strings.Contains(f.Name, "..") { - return fmt.Errorf("illegal file path: %s", f.Name) - } - target := filepath.Join(destDir, f.Name) - - if f.FileInfo().IsDir() { - _ = os.MkdirAll(target, f.Mode()) - continue - } - - rc, err := f.Open() - if err != nil { - return err - } - if err := writeFile(target, rc, f.Mode()); err != nil { - rc.Close() - return err - } - rc.Close() - } - return nil + file.Close() + gz.Close() + return nil, fmt.Errorf("binary %s not found in archive", binaryName) } -func writeFile(path string, r io.Reader, mode os.FileMode) error { - if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { - return err - } - - out, err := os.Create(path) - if err != nil { - return err - } - defer out.Close() - - if _, err := io.Copy(out, r); err != nil { - return err - } - - return os.Chmod(path, mode) +type tarGzReader struct { + io.Reader + gz *gzip.Reader + file *os.File } -func replaceBinary(newBinary string) error { - if err := os.Chmod(newBinary, 0755); err != nil { - return err - } +func (r *tarGzReader) Close() error { + r.gz.Close() + return r.file.Close() +} - currentBinary, err := os.Executable() - if err != nil { - return err - } - currentBinary, err = filepath.EvalSymlinks(currentBinary) +func extractBinaryFromZip(archivePath, binaryName string) (io.ReadCloser, error) { + r, err := zip.OpenReader(archivePath) if err != nil { - return err + return nil, err } - if err := os.Rename(newBinary, currentBinary); err != nil { - if runtime.GOOS != "windows" { - cmd := exec.Command("mv", newBinary, currentBinary) - if err := cmd.Run(); err != nil { - return output.Error("failed to replace binary at %s (permission denied). To get the latest version, reinstall from repository: https://github.com/%s/%s", currentBinary, githubOwner, githubRepo) + for _, f := range r.File { + if f.Name == binaryName { + rc, err := f.Open() + if err != nil { + r.Close() + return nil, err } - } else { - return output.Error("failed to replace binary at %s. To get the latest version, reinstall from repository: https://github.com/%s/%s", currentBinary, githubOwner, githubRepo) + // Return a wrapper that closes both the file and zip reader when done + return &zipReader{ReadCloser: rc, zipReader: r}, nil } } - return nil + r.Close() + return nil, fmt.Errorf("binary %s not found in archive", binaryName) +} + +type zipReader struct { + io.ReadCloser + zipReader *zip.ReadCloser +} + +func (r *zipReader) Close() error { + r.ReadCloser.Close() + return r.zipReader.Close() } func githubAPI(method, endpoint string, body io.Reader) (*http.Response, error) { - url := fmt.Sprintf("https://api.github.com/repos/%s/%s%s", githubOwner, githubRepo, endpoint) + url := config.GitHubAPIURL + endpoint req, err := http.NewRequest(method, url, body) if err != nil { diff --git a/core/apikey.go b/core/apikey.go index c52cc2c..dfca45e 100644 --- a/core/apikey.go +++ b/core/apikey.go @@ -3,10 +3,10 @@ package core import ( "context" "fmt" - "github.com/anyproto/anytype-heart/pkg/lib/pb/model" "github.com/anyproto/anytype-heart/pb" "github.com/anyproto/anytype-heart/pb/service" + "github.com/anyproto/anytype-heart/pkg/lib/pb/model" ) // CreateAPIKey creates a new API key for local app access diff --git a/core/config/constants.go b/core/config/constants.go index 78e77ac..8ad3a1f 100644 --- a/core/config/constants.go +++ b/core/config/constants.go @@ -23,8 +23,13 @@ const ( // URLs GRPCDNSAddress = "dns:///" + DefaultGRPCAddress + // GitHub repository + GitHubOwner = "anyproto" + GitHubRepo = "anytype-cli" + // External URLs - GitHubBaseURL = "https://github.com/anyproto/anytype-cli" + GitHubBaseURL = "https://github.com/" + GitHubOwner + "/" + GitHubRepo + GitHubAPIURL = "https://api.github.com/repos/" + GitHubOwner + "/" + GitHubRepo GitHubCommitURL = GitHubBaseURL + "/commit/" GitHubReleaseURL = GitHubBaseURL + "/releases/tag/" diff --git a/core/version.go b/core/version.go index 47dbe24..826c67c 100644 --- a/core/version.go +++ b/core/version.go @@ -50,7 +50,7 @@ func GetReleaseURL() string { if Commit != "" { return config.GitHubCommitURL + Commit } - return "https://github.com/anyproto/anytype-cli" + return config.GitHubBaseURL } return config.GitHubReleaseURL + Version } diff --git a/go.mod b/go.mod index 0f1a164..baa8f24 100644 --- a/go.mod +++ b/go.mod @@ -11,12 +11,14 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/improbable-eng/grpc-web v0.15.0 github.com/kardianos/service v1.2.4 + github.com/minio/selfupdate v0.6.0 github.com/spf13/cobra v1.10.2 github.com/zalando/go-keyring v0.2.6 google.golang.org/grpc v1.78.0 ) require ( + aead.dev/minisign v0.2.0 // indirect al.essio.dev/pkg/shellescape v1.6.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/JohannesKaufmann/html-to-markdown v1.6.0 // indirect diff --git a/go.sum b/go.sum index 1aa8c87..058c85e 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk= +aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ= al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA= al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -636,6 +638,8 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= +github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU= +github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/miolini/datacounter v1.0.3 h1:tanOZPVblGXQl7/bSZWoEM8l4KK83q24qwQLMrO/HOA= @@ -986,7 +990,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= @@ -1056,6 +1062,7 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -1118,6 +1125,7 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1146,6 +1154,7 @@ golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= From 8dc2bf7494a9e06c2db4271b9d2056bb8305dd56 Mon Sep 17 00:00:00 2001 From: Jannis Metrikat <120120832+jmetrikat@users.noreply.github.com> Date: Sat, 17 Jan 2026 18:08:38 +0100 Subject: [PATCH 2/3] Cleanup --- cmd/update/update.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cmd/update/update.go b/cmd/update/update.go index aebdd92..4f546d6 100644 --- a/cmd/update/update.go +++ b/cmd/update/update.go @@ -201,7 +201,6 @@ func downloadFile(url, destination, token string) error { return err } -// extractBinary extracts the binary from the archive and returns it as a ReadCloser func extractBinary(archivePath, binaryName string) (io.ReadCloser, error) { if strings.HasSuffix(archivePath, ".zip") { return extractBinaryFromZip(archivePath, binaryName) @@ -234,7 +233,6 @@ func extractBinaryFromTarGz(archivePath, binaryName string) (io.ReadCloser, erro } if header.Typeflag == tar.TypeReg && header.Name == binaryName { - // Return a wrapper that closes both gz and file when done return &tarGzReader{Reader: tr, gz: gz, file: file}, nil } } @@ -268,7 +266,6 @@ func extractBinaryFromZip(archivePath, binaryName string) (io.ReadCloser, error) r.Close() return nil, err } - // Return a wrapper that closes both the file and zip reader when done return &zipReader{ReadCloser: rc, zipReader: r}, nil } } From ede918b781610445b9586ce27858d5d6a7a8df8f Mon Sep 17 00:00:00 2001 From: Jannis Metrikat <120120832+jmetrikat@users.noreply.github.com> Date: Sat, 17 Jan 2026 18:08:53 +0100 Subject: [PATCH 3/3] Add tests --- cmd/update/update_test.go | 182 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 cmd/update/update_test.go diff --git a/cmd/update/update_test.go b/cmd/update/update_test.go new file mode 100644 index 0000000..da774a9 --- /dev/null +++ b/cmd/update/update_test.go @@ -0,0 +1,182 @@ +package update + +import ( + "archive/tar" + "archive/zip" + "bytes" + "compress/gzip" + "io" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/minio/selfupdate" +) + +func TestExtractBinaryFromTarGz(t *testing.T) { + tempDir := t.TempDir() + archivePath := filepath.Join(tempDir, "test.tar.gz") + binaryContent := []byte("fake binary content") + binaryName := "anytype" + + f, err := os.Create(archivePath) + if err != nil { + t.Fatalf("failed to create archive: %v", err) + } + + gw := gzip.NewWriter(f) + tw := tar.NewWriter(gw) + + hdr := &tar.Header{ + Name: binaryName, + Mode: 0755, + Size: int64(len(binaryContent)), + } + if err := tw.WriteHeader(hdr); err != nil { + t.Fatalf("failed to write tar header: %v", err) + } + if _, err := tw.Write(binaryContent); err != nil { + t.Fatalf("failed to write tar content: %v", err) + } + + tw.Close() + gw.Close() + f.Close() + + reader, err := extractBinaryFromTarGz(archivePath, binaryName) + if err != nil { + t.Fatalf("extractBinaryFromTarGz failed: %v", err) + } + defer reader.Close() + + extracted, err := io.ReadAll(reader) + if err != nil { + t.Fatalf("failed to read extracted binary: %v", err) + } + + if !bytes.Equal(extracted, binaryContent) { + t.Errorf("extracted content mismatch: got %q, want %q", extracted, binaryContent) + } +} + +func TestExtractBinaryFromZip(t *testing.T) { + tempDir := t.TempDir() + archivePath := filepath.Join(tempDir, "test.zip") + binaryContent := []byte("fake binary content") + binaryName := "anytype.exe" + + f, err := os.Create(archivePath) + if err != nil { + t.Fatalf("failed to create archive: %v", err) + } + + zw := zip.NewWriter(f) + w, err := zw.Create(binaryName) + if err != nil { + t.Fatalf("failed to create zip entry: %v", err) + } + if _, err := w.Write(binaryContent); err != nil { + t.Fatalf("failed to write zip content: %v", err) + } + + zw.Close() + f.Close() + + reader, err := extractBinaryFromZip(archivePath, binaryName) + if err != nil { + t.Fatalf("extractBinaryFromZip failed: %v", err) + } + defer reader.Close() + + extracted, err := io.ReadAll(reader) + if err != nil { + t.Fatalf("failed to read extracted binary: %v", err) + } + + if !bytes.Equal(extracted, binaryContent) { + t.Errorf("extracted content mismatch: got %q, want %q", extracted, binaryContent) + } +} + +func TestExtractBinaryNotFound(t *testing.T) { + tempDir := t.TempDir() + + tarPath := filepath.Join(tempDir, "test.tar.gz") + f, _ := os.Create(tarPath) + gw := gzip.NewWriter(f) + tw := tar.NewWriter(gw) + tw.Close() + gw.Close() + f.Close() + + _, err := extractBinaryFromTarGz(tarPath, "nonexistent") + if err == nil { + t.Error("expected error for missing binary in tar.gz") + } + + zipPath := filepath.Join(tempDir, "test.zip") + f, _ = os.Create(zipPath) + zw := zip.NewWriter(f) + zw.Close() + f.Close() + + _, err = extractBinaryFromZip(zipPath, "nonexistent") + if err == nil { + t.Error("expected error for missing binary in zip") + } +} + +func TestSelfUpdateApply(t *testing.T) { + tempDir := t.TempDir() + + currentBinary := filepath.Join(tempDir, "current") + if runtime.GOOS == "windows" { + currentBinary += ".exe" + } + + oldContent := []byte("old version") + if err := os.WriteFile(currentBinary, oldContent, 0755); err != nil { + t.Fatalf("failed to write current binary: %v", err) + } + + newContent := []byte("new version") + + err := selfupdate.Apply(bytes.NewReader(newContent), selfupdate.Options{ + TargetPath: currentBinary, + }) + if err != nil { + t.Fatalf("selfupdate.Apply failed: %v", err) + } + + updatedContent, err := os.ReadFile(currentBinary) + if err != nil { + t.Fatalf("failed to read updated binary: %v", err) + } + + if !bytes.Equal(updatedContent, newContent) { + t.Errorf("update failed: got %q, want %q", updatedContent, newContent) + } + + if runtime.GOOS == "windows" { + os.Remove(currentBinary + ".old") + } +} + +func TestGetArchiveName(t *testing.T) { + version := "v1.0.0" + name := getArchiveName(version) + expectedBase := "anytype-cli-v1.0.0-" + runtime.GOOS + "-" + runtime.GOARCH + + if runtime.GOOS == "windows" { + expected := expectedBase + ".zip" + if name != expected { + t.Errorf("getArchiveName() = %q, want %q", name, expected) + } + } else { + expected := expectedBase + ".tar.gz" + if name != expected { + t.Errorf("getArchiveName() = %q, want %q", name, expected) + } + } +}