Skip to content

Commit 1b280b0

Browse files
authored
refact(updater): expose a clenaer API
1 parent 232ea58 commit 1b280b0

File tree

10 files changed

+178
-47
lines changed

10 files changed

+178
-47
lines changed

cmd/releaser/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func main() {
3232
inputPath := flag.Arg(0)
3333
version := flag.Arg(1)
3434

35-
manifest, err := releaser.CreateRelease(inputPath, platform, version, outputDir)
35+
manifest, err := releaser.CreateRelease(inputPath, platform, releaser.Version(version), outputDir)
3636
if err != nil {
3737
log.Fatalf("could not create release: %v", err)
3838
}

releaser/http_client.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ func (c *Client) addHeaders(req *http.Request) {
6060
}
6161
}
6262

63-
// GetManifest fetches and decodes the manifest for the given platform.
64-
func (c *Client) GetManifest(plat Platform) (Manifest, error) {
63+
// GetLatestVersion fetches and decodes the manifest for the given platform.
64+
func (c *Client) GetLatestVersion(plat Platform) (Manifest, error) {
6565
manifestURL := c.BaseURL.JoinPath(c.CmdName, plat.String()+".json").String()
6666
req, err := http.NewRequest("GET", manifestURL, nil)
6767
if err != nil {
@@ -89,8 +89,8 @@ func (c *Client) GetManifest(plat Platform) (Manifest, error) {
8989
}
9090

9191
// FetchZip fetches the zip for the given version and platform.
92-
func (c *Client) FetchZip(version string, plat Platform) (io.ReadCloser, error) {
93-
zipURL := c.BaseURL.JoinPath(c.CmdName, version, plat.String()+".zip").String()
92+
func (c *Client) FetchZip(version Version, plat Platform) (io.ReadCloser, error) {
93+
zipURL := c.BaseURL.JoinPath(c.CmdName, version.String(), plat.String()+".zip").String()
9494
req, err := http.NewRequest("GET", zipURL, nil)
9595
if err != nil {
9696
return nil, fmt.Errorf("failed to create request: %w", err)

releaser/releaser.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,27 @@ import (
1111
)
1212

1313
type Manifest struct {
14-
Version string `json:"version"`
15-
Sha256 []byte `json:"sha256"`
14+
Version Version `json:"version"`
15+
Sha256 []byte `json:"sha256"`
1616
}
1717

18-
func CreateRelease(inputPath string, platform Platform, version, outputDir string) (Manifest, error) {
18+
type Version string
19+
20+
func (v Version) String() string {
21+
return string(v)
22+
}
23+
24+
func (v Version) Equals(other Version) bool {
25+
return v.String() == other.String()
26+
}
27+
28+
func CreateRelease(inputPath string, platform Platform, version Version, outputDir string) (Manifest, error) {
1929
if err := os.MkdirAll(outputDir, 0755); err != nil {
2030
return Manifest{}, fmt.Errorf("could not create output dir: %s. %w", outputDir, err)
2131
}
2232
// Prepare output paths
2333
jsonPath := filepath.Join(outputDir, platform.String()+".json")
24-
versionDir := filepath.Join(outputDir, version)
34+
versionDir := filepath.Join(outputDir, version.String())
2535
zipPath := filepath.Join(versionDir, platform.String()+".zip")
2636

2737
if err := os.MkdirAll(versionDir, 0755); err != nil {

releaser/releaser_test.go

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func TestCreateReleaseFile(t *testing.T) {
2222
content := []byte("hello world")
2323
require.NoError(t, os.WriteFile(dummyFile, content, 0600))
2424

25-
version := "1.2.3"
25+
version := Version("1.2.3")
2626
manifest, err := CreateRelease(dummyFile, NewPlatform("linux", "amd64"), version, outputDir)
2727
require.NoError(t, err)
2828

@@ -37,7 +37,7 @@ func TestCreateReleaseFile(t *testing.T) {
3737
// linux-amd64.zip
3838
// linux-amd64.json
3939
//
40-
zipPath := filepath.Join(outputDir, version, "linux-amd64.zip")
40+
zipPath := filepath.Join(outputDir, version.String(), "linux-amd64.zip")
4141
zf, err := zip.OpenReader(zipPath)
4242
require.NoError(t, err, "could not open zip file %s", zipPath)
4343
defer zf.Close()
@@ -100,7 +100,7 @@ func TestCreateReleaseFolder(t *testing.T) {
100100
require.NoError(t, os.WriteFile(fileA, contentA, 0600))
101101
require.NoError(t, os.WriteFile(fileB, contentB, 0600))
102102

103-
version := "2.0.0"
103+
version := Version("2.0.0")
104104
manifest, err := CreateRelease(inputDir, NewPlatform("linux", "amd64"), version, outputDir)
105105
require.NoError(t, err)
106106

@@ -109,7 +109,7 @@ func TestCreateReleaseFolder(t *testing.T) {
109109
require.Len(t, manifest.Sha256, 32)
110110

111111
// Check that the zip file exists and contains all files
112-
zipPath := filepath.Join(outputDir, version, "linux-amd64.zip")
112+
zipPath := filepath.Join(outputDir, version.String(), "linux-amd64.zip")
113113
zf, err := zip.OpenReader(zipPath)
114114
require.NoError(t, err, "could not open zip file %s", zipPath)
115115
defer zf.Close()
@@ -149,3 +149,46 @@ func TestCreateReleaseFolder(t *testing.T) {
149149
require.Equal(t, version, m.Version)
150150
require.Len(t, m.Sha256, 32)
151151
}
152+
153+
func TestVersion(t *testing.T) {
154+
tests := []struct {
155+
name string
156+
v1 Version
157+
v2 Version
158+
expected bool
159+
}{
160+
{
161+
name: "Equal versions",
162+
v1: Version("1.0.0"),
163+
v2: Version("1.0.0"),
164+
expected: true,
165+
},
166+
{
167+
name: "Different versions",
168+
v1: Version("1.0.0"),
169+
v2: Version("2.0.0"),
170+
expected: false,
171+
},
172+
{
173+
name: "Empty versions",
174+
v1: Version(""),
175+
v2: Version(""),
176+
expected: true,
177+
},
178+
{
179+
name: "One empty version",
180+
v1: Version("1.0.0"),
181+
v2: Version(""),
182+
expected: false,
183+
},
184+
}
185+
186+
for _, tt := range tests {
187+
t.Run(tt.name, func(t *testing.T) {
188+
result := tt.v1 == tt.v2
189+
if result != tt.expected {
190+
t.Errorf("Version.Equals() = %v, expected %v", result, tt.expected)
191+
}
192+
})
193+
}
194+
}

updater/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test-*

updater/updater_darwin.go renamed to updater/apply_darwin.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
"github.com/codeclysm/extract/v4"
1818
)
1919

20-
func checkForUpdates(targetPath string, current Version, client *releaser.Client) (string, error) {
20+
func apply(targetPath string, current releaser.Version, client *releaser.Client, upgradeConfirmCb UpgradeConfirmCB) (string, error) {
2121
currentAppPath := paths.New(targetPath).Parent().Parent().Parent()
2222
if currentAppPath.Ext() != ".app" {
2323
return "", fmt.Errorf("could not find app root in %s", targetPath)
@@ -31,15 +31,20 @@ func checkForUpdates(targetPath string, current Version, client *releaser.Client
3131
// Fetch information about updates
3232
plat := releaser.NewPlatform(runtime.GOOS, runtime.GOARCH)
3333
slog.Info("Checking for updates", "platform", plat)
34-
manifest, err := client.GetManifest(plat)
34+
manifest, err := client.GetLatestVersion(plat)
3535
if err != nil {
3636
return "", err
3737
}
38-
if manifest.Version == current.String() {
38+
if manifest.Version == current {
3939
// No updates available, bye bye
4040
return "", nil
4141
}
4242

43+
if upgradeConfirmCb != nil && !upgradeConfirmCb(current, manifest.Version) {
44+
slog.Info("Update not confirmed by user, exiting without applying update")
45+
return "", nil
46+
}
47+
4348
// Download the update.
4449
slog.Info("Downloading update", "version", manifest.Version, "platform", plat)
4550
download, err := client.FetchZip(manifest.Version, plat)

updater/updater_default.go renamed to updater/apply_default.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,26 @@ import (
1919
"github.com/codeclysm/extract/v4"
2020
)
2121

22-
// checkForUpdates checks for updates for the current executable.
23-
// It downloads the update, verifies it, unzips it, and replaces the current executable.
24-
// It returns the path to the updated executable or an error if something goes wrong.
25-
26-
func checkForUpdates(targetPath string, current Version, client *releaser.Client) (string, error) {
22+
func apply(targetPath string, current releaser.Version, client *releaser.Client, upgradeConfirmCb UpgradeConfirmCB) (string, error) {
2723
currentPath := paths.New(targetPath)
2824
currentDir := currentPath.Parent()
2925

3026
plat := releaser.NewPlatform(runtime.GOOS, runtime.GOARCH)
3127
slog.Info("Checking for updates", "platform", plat)
32-
manifest, err := client.GetManifest(plat)
28+
manifest, err := client.GetLatestVersion(plat)
3329
if err != nil {
3430
return "", err
3531
}
36-
if manifest.Version == current.String() {
32+
if manifest.Version == current {
3733
// No updates available, bye bye
3834
return "", nil
3935
}
4036

37+
if upgradeConfirmCb != nil && !upgradeConfirmCb(current, manifest.Version) {
38+
slog.Info("Update not confirmed by user, exiting without applying update")
39+
return "", nil
40+
}
41+
4142
// Download the update
4243
slog.Info("Downloading update", "version", manifest.Version, "platform", plat)
4344
download, err := client.FetchZip(manifest.Version, plat)

updater/updater_default_test.go renamed to updater/apply_default_test.go

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import (
1515
"github.com/arduino/go-updater/releaser"
1616
)
1717

18-
func TestPerformUpdate(t *testing.T) {
18+
func TestApply(t *testing.T) {
1919
tmpExec := CreateTmpExecutable(t, "successfulUpdate", []byte{0xDE, 0xAD, 0xBE, 0xEF})
2020
defer tmpExec.cleanup()
2121
client := CreateRelease(t, "2.0.0", []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06})
2222

23-
restartPath, err := checkForUpdates(tmpExec.targetPath, Version("1.0.0"), client)
23+
restartPath, err := apply(tmpExec.targetPath, releaser.Version("1.0.0"), client, DefaultUpgradeConfirmCb)
2424
require.NoError(t, err)
2525
require.NotEmpty(t, restartPath)
2626

@@ -29,21 +29,21 @@ func TestPerformUpdate(t *testing.T) {
2929
require.Equal(t, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, data, "Updated binary content does not match expected content")
3030
}
3131

32-
func TestNoUpdateRequired(t *testing.T) {
32+
func TestApplyWithNoUpdate(t *testing.T) {
3333
tmpExec := CreateTmpExecutable(t, "noUpdate", []byte{0xDE, 0xAD, 0xBE, 0xEF})
3434
defer tmpExec.cleanup()
3535
client := CreateRelease(t, "1.0.0", []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06})
3636

37-
result, err := checkForUpdates(tmpExec.targetPath, Version("1.0.0"), client)
37+
result, err := apply(tmpExec.targetPath, releaser.Version("1.0.0"), client, DefaultUpgradeConfirmCb)
3838
require.NoError(t, err)
3939
require.Equal(t, "", result)
4040
}
4141

42-
func TestCleanUpOldFiles(t *testing.T) {
42+
func TestApplyWillCleanUpFiles(t *testing.T) {
4343
tmpExec := CreateTmpExecutable(t, "cleanUp", []byte{0xDE, 0xAD, 0xBE, 0xEF})
4444
client := CreateRelease(t, "3.0.0", []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06})
4545

46-
result, err := checkForUpdates(tmpExec.targetPath, Version("1.0.0"), client)
46+
result, err := apply(tmpExec.targetPath, releaser.Version("1.0.0"), client, DefaultUpgradeConfirmCb)
4747
require.NoError(t, err)
4848
require.Equal(t, tmpExec.targetPath, result)
4949

@@ -66,6 +66,7 @@ type TmpExecutable struct {
6666
}
6767

6868
func CreateTmpExecutable(t *testing.T, binaryName string, content []byte) TmpExecutable {
69+
// prefix the binary name with "test-" to put the folders in the .gitignore
6970
tmpDir := filepath.Join(".", "test-"+binaryName)
7071
err := os.MkdirAll(tmpDir, 0755)
7172
require.NoError(t, err)
@@ -85,7 +86,7 @@ func CreateTmpExecutable(t *testing.T, binaryName string, content []byte) TmpExe
8586
}
8687
}
8788

88-
func CreateRelease(t *testing.T, version Version, content []byte) *releaser.Client {
89+
func CreateRelease(t *testing.T, version releaser.Version, content []byte) *releaser.Client {
8990
tmpDir := t.TempDir()
9091

9192
inputDir := filepath.Join(tmpDir, "input")
@@ -96,7 +97,7 @@ func CreateRelease(t *testing.T, version Version, content []byte) *releaser.Clie
9697

9798
outputDir := filepath.Join(tmpDir, "output")
9899

99-
_, err := releaser.CreateRelease(inputDir, releaser.NewPlatform("linux", "amd64"), version.String(), outputDir)
100+
_, err := releaser.CreateRelease(inputDir, releaser.NewPlatform("linux", "amd64"), version, outputDir)
100101
require.NoError(t, err)
101102

102103
// check zip file exists and json manifest is created
@@ -140,6 +141,20 @@ func CreateRelease(t *testing.T, version Version, content []byte) *releaser.Clie
140141
return client
141142
}
142143

144+
func CreateReleaseWithHTTPErrorResponse(t *testing.T, statusCode int) *releaser.Client {
145+
return &releaser.Client{
146+
BaseURL: &url.URL{Scheme: "http", Host: "example.com"},
147+
CmdName: "testcmd",
148+
HTTPClient: &mockHTTPClient{doFunc: func(req *http.Request) (*http.Response, error) {
149+
resp := &http.Response{
150+
StatusCode: statusCode,
151+
Body: io.NopCloser(bytes.NewBufferString("")),
152+
}
153+
return resp, nil
154+
}},
155+
}
156+
}
157+
143158
// mockHTTPClient implements releaser.HTTPDoer for testing.
144159
type mockHTTPClient struct {
145160
doFunc func(req *http.Request) (*http.Response, error)

updater/updater.go

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,28 @@ import (
77
"github.com/arduino/go-updater/releaser"
88
)
99

10-
type Version string
10+
// UpgradeConfirmCB is a function that is called when an update is ready to be applied.
11+
type UpgradeConfirmCB func(current, target releaser.Version) bool
1112

12-
func (v Version) String() string {
13-
return string(v)
14-
}
15-
16-
// Start checks if an update has to be downloaded and if so returns the path to the
17-
// binary to be executed to perform the update.
18-
// If the update is not available, it returns an empty string and no error.
19-
// If the update is available, it returns the path to the binary to be executed.
20-
// If there is an error, it returns the error.
21-
func CheckForUpdates(targetPath string, current Version, client *releaser.Client) (string, error) {
22-
return checkForUpdates(targetPath, current, client)
23-
}
13+
var DefaultUpgradeConfirmCb = func(current, target releaser.Version) bool { return true }
2414

25-
func Restart(executable string) error {
26-
err := execApp(executable)
15+
// CheckForUpdates checks for updates and applies it if available.
16+
// If the upgradeCb is not nil, it will prompt the user for confirmation before applying the update.
17+
// If an update is applied, it will restart the application with the new version.
18+
// If no update is available, it will return nil.
19+
// If an error occurs during the update process, it will return the error.
20+
func CheckForUpdates(targetPath string, current releaser.Version, client *releaser.Client, upgradeCb UpgradeConfirmCB) error {
21+
restartPath, err := apply(targetPath, current, client, upgradeCb)
2722
if err != nil {
28-
return fmt.Errorf("could not exec app: %w", err)
23+
return err
24+
}
25+
26+
if restartPath == "" {
27+
return nil // No update available
28+
}
29+
30+
if err := execApp(restartPath); err != nil {
31+
return fmt.Errorf("update applied, but failed to restart application: %w", err)
2932
}
3033
// TODO: allow to define custom "exit" code to be used in the wail app runtime.quit()
3134
os.Exit(0)

0 commit comments

Comments
 (0)