Skip to content

Commit 659d8d7

Browse files
yaneccankit-pnaooohan
authored
feat: add upgrade command to support self upgrade (version-fox#239)
* fix: mac arm64 installation * feat: add self -upgrade feature. * fix: fomat the file and used internal decompressor * Squashed commit of the following: commit 830d706 Author: ankit-pn <[email protected]> Date: Wed Jan 31 09:21:13 2024 +0530 fix: fomat the file and used internal decompressor commit fd0eb8b Author: ankit-pn <[email protected]> Date: Tue Jan 30 16:27:05 2024 +0530 feat: add self -upgrade feature. commit f478217 Author: ankit-pn <[email protected]> Date: Mon Jan 29 20:19:27 2024 +0530 fix: mac arm64 installation * Revert "Update install.sh" This reverts commit 5b9fada, reversing changes made to e47ca05. * Support self upgrade on Windows * Correct an error description * fix: permission for windows * Use VFOX_HOME/tmp to store temp assets * Put the binary file into `vfox_upgrade` directory * Remove redundant deletion * Put `defer func()` ahead * Little patch --------- Co-authored-by: ankit-pn <[email protected]> Co-authored-by: lihan <[email protected]>
1 parent bf81bf7 commit 659d8d7

File tree

4 files changed

+297
-0
lines changed

4 files changed

+297
-0
lines changed

cmd/cmd.go

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ func newCmd() *cmd {
9191
commands.Available,
9292
commands.Search,
9393
commands.Update,
94+
commands.Upgrade,
9495
commands.Remove,
9596
commands.Add,
9697
commands.Activate,

cmd/commands/upgrade.go

+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
* Copyright 2024 Han Li and contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package commands
18+
19+
import (
20+
"fmt"
21+
"io"
22+
"net/http"
23+
"os"
24+
"os/exec"
25+
"path/filepath"
26+
"regexp"
27+
"runtime"
28+
"strings"
29+
30+
"github.com/urfave/cli/v2"
31+
"github.com/version-fox/vfox/internal/util"
32+
)
33+
34+
const SelfUpgradeName = "upgrade"
35+
36+
var Upgrade = &cli.Command{
37+
Name: SelfUpgradeName,
38+
Usage: "upgrade vfox to the latest version",
39+
Action: upgradeCmd,
40+
}
41+
42+
func fetchLatestVersion() (string, error) {
43+
resp, err := http.Get("https://github.com/version-fox/vfox/tags")
44+
if err != nil {
45+
return "", err
46+
}
47+
defer resp.Body.Close()
48+
49+
body, err := io.ReadAll(resp.Body)
50+
if err != nil {
51+
return "", err
52+
}
53+
re, err := regexp.Compile(`href="/version-fox/vfox/releases/tag/(v[0-9.]+)"`)
54+
if err != nil {
55+
return "", err
56+
}
57+
matches := re.FindAllStringSubmatch(string(body), -1)
58+
59+
if len(matches) == 0 {
60+
return "", fmt.Errorf("Failed to fetch the version.")
61+
}
62+
63+
latestVersion := matches[0][1]
64+
return latestVersion, nil
65+
}
66+
67+
func constructBinaryName(tagName string) string {
68+
osType := strings.ToLower(runtime.GOOS)
69+
if osType == "darwin" {
70+
osType = "macos"
71+
}
72+
73+
archType := runtime.GOARCH
74+
if archType == "arm64" {
75+
archType = "aarch64"
76+
}
77+
if archType == "amd64" {
78+
archType = "x86_64"
79+
}
80+
81+
extName := "tar.gz"
82+
if osType == "windows" {
83+
extName = "zip"
84+
}
85+
86+
fileName := fmt.Sprintf("vfox_%s_%s_%s.%s", tagName[1:], osType, archType, extName)
87+
return fileName
88+
}
89+
90+
func generateUrls(currVersion string, tagName string) (string, string) {
91+
fileName := constructBinaryName(tagName)
92+
binURL := fmt.Sprintf("https://github.com/version-fox/vfox/releases/download/%s/%s", tagName, fileName)
93+
diffURL := fmt.Sprintf("https://github.com/version-fox/vfox/compare/%s...%s", currVersion, tagName)
94+
return binURL, diffURL
95+
}
96+
97+
func downloadFile(filepath string, url string) error {
98+
out, err := os.Create(filepath)
99+
if err != nil {
100+
return err
101+
}
102+
defer out.Close()
103+
resp, err := http.Get(url)
104+
if err != nil {
105+
return err
106+
}
107+
defer resp.Body.Close()
108+
_, err = io.Copy(out, resp.Body)
109+
return err
110+
}
111+
112+
func upgradeCmd(ctx *cli.Context) error {
113+
currVersion := fmt.Sprintf("v%s", ctx.App.Version)
114+
latestVersion, err := fetchLatestVersion()
115+
if err != nil {
116+
return cli.Exit("Failed to fetch the latest version: "+err.Error(), 1)
117+
}
118+
fmt.Println("Current version: ", currVersion)
119+
fmt.Println("Latest available:", latestVersion)
120+
if currVersion == latestVersion {
121+
return cli.Exit("vfox is already up to date.", 0)
122+
}
123+
if err = RequestPermission(); err != nil {
124+
return err
125+
}
126+
exePath, err := os.Executable()
127+
if err != nil {
128+
return cli.Exit("Failed to get executable path: "+err.Error(), 1)
129+
}
130+
exeDir, exeName := filepath.Split(exePath)
131+
binURL, diffURL := generateUrls(currVersion, latestVersion)
132+
tempFile := "vfox_latest.tar.gz"
133+
if runtime.GOOS == "windows" {
134+
tempFile = "vfox_latest.zip"
135+
}
136+
tempDir := filepath.Join(exeDir, "vfox_upgrade")
137+
tempFile = filepath.Join(tempDir, tempFile)
138+
if err := os.Mkdir(tempDir, 0755); err != nil {
139+
return cli.Exit("Failed to create directory: "+err.Error(), 1)
140+
}
141+
defer func() {
142+
if err := os.RemoveAll(tempDir); err != nil {
143+
fmt.Println("Error removing directory: ", err)
144+
}
145+
}()
146+
147+
fmt.Println("Fetching", binURL)
148+
149+
if err := downloadFile(tempFile, binURL); err != nil {
150+
return cli.Exit("Failed to download file: "+err.Error(), 1)
151+
}
152+
decompressor := util.NewDecompressor(tempFile)
153+
if err := decompressor.Decompress(tempDir); err != nil {
154+
return cli.Exit("Failed to extract file: "+err.Error(), 1)
155+
}
156+
tempExePath := filepath.Join(tempDir, exeName)
157+
if _, err := os.Stat(tempExePath); err != nil {
158+
return cli.Exit("Failed to find valid executable: "+err.Error(), 1)
159+
}
160+
161+
if runtime.GOOS == "windows" {
162+
backupExePath := filepath.Join(exeDir, "."+exeName)
163+
batchFile := filepath.Join(exeDir, ".upgrade.bat")
164+
if err := os.Rename(exePath, backupExePath); err != nil {
165+
return cli.Exit("Failed to backup: "+err.Error(), 1)
166+
}
167+
if err := os.Rename(tempExePath, exePath); err != nil {
168+
os.Rename(backupExePath, exePath)
169+
return cli.Exit("Failed to replace executable: "+err.Error(), 1)
170+
}
171+
batchContent := fmt.Sprintf(":Repeat\n"+
172+
"del \"%s\"\n"+
173+
"if exist \"%s\" goto Repeat\n"+
174+
"del \"%s\"", backupExePath, backupExePath, batchFile)
175+
if err := os.WriteFile(batchFile, []byte(batchContent), 0666); err != nil {
176+
return cli.Exit("Failed to clear: "+err.Error(), 1)
177+
}
178+
cmd := exec.Command("cmd.exe", "/C", batchFile)
179+
if err := cmd.Start(); err != nil {
180+
return cli.Exit("Failed to launch shell: "+err.Error(), 1)
181+
}
182+
} else {
183+
if err := os.Rename(tempExePath, exePath); err != nil {
184+
return cli.Exit("Failed to replace executable: "+err.Error(), 1)
185+
}
186+
if err := os.Chmod(exePath, 0755); err != nil {
187+
return cli.Exit("Failed to make executable: "+err.Error(), 1)
188+
}
189+
}
190+
191+
fmt.Printf("Updated to version: %s\nSee the diff at: %s\n", latestVersion, diffURL)
192+
193+
if runtime.GOOS == "windows" {
194+
fmt.Println("Press any key to continue...")
195+
var b = make([]byte, 1)
196+
_, _ = os.Stdin.Read(b)
197+
}
198+
return nil
199+
}

cmd/commands/upgrade_unix.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//go:build !windows
2+
3+
/*
4+
* Copyright 2024 Han Li and contributors
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package commands
20+
21+
func RequestPermission() error {
22+
return nil
23+
}

cmd/commands/upgrade_win.go

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//go:build windows
2+
3+
/*
4+
* Copyright 2024 Han Li and contributors
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package commands
20+
21+
import (
22+
"golang.org/x/sys/windows"
23+
"os"
24+
"syscall"
25+
"unsafe"
26+
)
27+
28+
func RequestPermission() error {
29+
isAdmin, err := isAdmin()
30+
if err != nil {
31+
return err
32+
}
33+
34+
if !isAdmin {
35+
if err = runAsAdmin(); err != nil {
36+
return err
37+
}
38+
}
39+
return nil
40+
}
41+
42+
func isAdmin() (bool, error) {
43+
_, err := os.Open("\\\\.\\PHYSICALDRIVE0")
44+
if err != nil {
45+
if os.IsPermission(err) {
46+
return false, nil
47+
}
48+
return false, err
49+
}
50+
51+
return true, nil
52+
}
53+
54+
func runAsAdmin() error {
55+
exePath, err := os.Executable()
56+
if err != nil {
57+
return err
58+
}
59+
60+
verb := "runas"
61+
cwd, _ := syscall.UTF16PtrFromString(".")
62+
arg, _ := syscall.UTF16PtrFromString(SelfUpgradeName)
63+
run := windows.NewLazySystemDLL("shell32.dll").NewProc("ShellExecuteW")
64+
run.Call(
65+
0,
66+
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(verb))),
67+
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(exePath))),
68+
uintptr(unsafe.Pointer(arg)),
69+
uintptr(unsafe.Pointer(cwd)),
70+
1,
71+
)
72+
os.Exit(0)
73+
return nil
74+
}

0 commit comments

Comments
 (0)