Added upgrade and reinstall commands

This commit is contained in:
Corey Butler
2024-11-18 12:10:33 -06:00
parent b7a668787a
commit 51c3b178d0
4 changed files with 941 additions and 52 deletions

View File

@@ -2,15 +2,18 @@ module nvm
go 1.18
// replace github.com/coreybutler/go-fsutil => C:\Users\corey\OneDrive\Documents\workspace\oss\coreybutler\go-fsutil
require (
github.com/blang/semver v3.5.1+incompatible
github.com/coreybutler/go-fsutil v1.2.0
github.com/coreybutler/go-where v1.0.2
github.com/minio/selfupdate v0.6.0
github.com/olekukonko/tablewriter v0.0.5
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
golang.org/x/sys v0.1.0
)
require github.com/mattn/go-runewidth v0.0.9 // indirect
require (
aead.dev/minisign v0.2.0 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b // indirect
)

View File

@@ -1,3 +1,5 @@
aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/coreybutler/go-fsutil v1.2.0 h1:kbm62NSofawglUppEOhpHC3NDf/J7ZpguBirBnsgUwU=
@@ -6,6 +8,8 @@ github.com/coreybutler/go-where v1.0.2 h1:Omit67KeTtKpvSJjezVxnVD4qMtvlXDlItiKpV
github.com/coreybutler/go-where v1.0.2/go.mod h1:IqV4saJiDXdNJURfTfVRywDHvY1IG5F+GXb2kmnmEe8=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU=
github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
@@ -13,17 +17,29 @@ github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj
github.com/zmb3/gogetdoc v0.0.0-20190228002656-b37376c5da6a/go.mod h1:ofmGw6LrMypycsiWcyug6516EXpIxSbZ+uI9ppGypfY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b h1:QAqMVf3pSa6eeTsuklijukjXBlj7Es2QQplab+/RbQ4=
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181207195948-8634b1ecd393/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190825031127-d72b05d2b1b6/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=

View File

@@ -22,10 +22,12 @@ import (
"nvm/encoding"
"nvm/file"
"nvm/node"
"nvm/upgrade"
"nvm/web"
"github.com/blang/semver"
// "github.com/fatih/color"
"github.com/coreybutler/go-where"
"github.com/olekukonko/tablewriter"
"golang.org/x/sys/windows"
@@ -33,7 +35,7 @@ import (
)
const (
NvmVersion = "1.1.11"
NvmVersion = "1.2.0"
)
type Environment struct {
@@ -70,10 +72,10 @@ func main() {
detail := ""
procarch := arch.Validate(env.arch)
if !isTerminal() {
alert("NVM for Windows should be run from a terminal such as CMD or PowerShell.", "Terminal Only")
os.Exit(0)
}
// if !isTerminal() {
// alert("NVM for Windows should be run from a terminal such as CMD or PowerShell.", "Terminal Only")
// os.Exit(0)
// }
// Capture any additional arguments
if len(args) > 2 {
@@ -99,6 +101,8 @@ func main() {
install(detail, procarch)
case "uninstall":
uninstall(detail)
case "reinstall":
reinstall(detail, procarch)
case "use":
use(detail, procarch)
case "list":
@@ -167,6 +171,8 @@ func main() {
setNpmMirror(detail)
case "debug":
checkLocalEnvironment()
case "upgrade":
upgrade.Run(NvmVersion)
default:
help()
}
@@ -185,13 +191,13 @@ func setNpmMirror(uri string) {
saveSettings()
}
func isTerminal() bool {
fileInfo, err := os.Stdout.Stat()
if err != nil {
return false
}
return (fileInfo.Mode() & os.ModeCharDevice) != 0
}
// func isTerminal() bool {
// fileInfo, err := os.Stdout.Stat()
// if err != nil {
// return false
// }
// return (fileInfo.Mode() & os.ModeCharDevice) != 0
// }
// const (
// MB_YESNOCANCEL = 0x00000003
@@ -263,23 +269,23 @@ func isTerminal() bool {
// )
// }
func alert(msg string, caption ...string) {
user32 := windows.NewLazySystemDLL("user32.dll")
mbox := user32.NewProc("MessageBoxW")
getForegroundWindow := user32.NewProc("GetForegroundWindow")
var hwnd uintptr
ret, _, _ := getForegroundWindow.Call()
if ret != 0 {
hwnd = ret
}
// func alert(msg string, caption ...string) {
// user32 := windows.NewLazySystemDLL("user32.dll")
// mbox := user32.NewProc("MessageBoxW")
// getForegroundWindow := user32.NewProc("GetForegroundWindow")
// var hwnd uintptr
// ret, _, _ := getForegroundWindow.Call()
// if ret != 0 {
// hwnd = ret
// }
title := "Alert"
if len(caption) > 0 {
title = caption[0]
}
// title := "Alert"
// if len(caption) > 0 {
// title = caption[0]
// }
mbox.Call(hwnd, uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(msg))), uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(title))), uintptr(windows.MB_OK))
}
// mbox.Call(hwnd, uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(msg))), uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(title))), uintptr(windows.MB_OK))
// }
/*
func update() {
@@ -474,6 +480,7 @@ func install(version string, cpuarch string) {
npmv := getNpmVersion(version)
fmt.Println("npm v" + npmv + " installed successfully.")
fmt.Println("\n\nInstallation complete. If you want to use this version, type\n\nnvm use " + version)
// fmt.Printf("Installed to %v\n", filepath.Join(env.root, "v"+version))
return
}
@@ -557,7 +564,57 @@ func install(version string, cpuarch string) {
fmt.Println("Version " + version + " is already installed.")
return
}
}
func reinstall(version, cpuarch string) {
// Make sure a version is specified
if len(version) == 0 {
fmt.Println("Provide the version you want to uninstall.")
help()
return
}
if strings.ToLower(version) == "latest" || strings.ToLower(version) == "node" {
version = getLatest()
} else if strings.ToLower(version) == "lts" {
version = getLTS()
} else if strings.ToLower(version) == "newest" {
installed := node.GetInstalled(env.root)
if len(installed) == 0 {
fmt.Println("No versions of node.js found. Try installing the latest by typing nvm install latest.")
return
}
version = installed[0]
}
version = cleanVersion(version)
// Determine if the version exists and skip if it doesn't
if node.IsVersionInstalled(env.root, version, "32") || node.IsVersionInstalled(env.root, version, "64") {
v, _ := node.GetCurrentVersion()
fmt.Printf("Removing v%v...\n", version)
if v == version {
// _, err := runElevated(fmt.Sprintf(`"%s" cmd /C rmdir "%s"`, filepath.Join(env.root, "elevate.cmd"), filepath.Clean(env.symlink)))
_, err := elevatedRun("rmdir", filepath.Clean(env.symlink))
if err != nil {
fmt.Println(fmt.Sprint(err))
return
}
}
e := os.RemoveAll(filepath.Join(env.root, "v"+version))
if e != nil {
fmt.Printf("error: failed to remove v%v: %v\n", version, e)
os.Exit(1)
}
} else {
fmt.Printf("node v%v is not installed. Type \"nvm list\" to see what is installed.\n", version)
}
install(version, cpuarch)
}
func uninstall(version string) {
@@ -1062,11 +1119,9 @@ func checkLocalEnvironment() {
} else {
consoleTitle := syscall.UTF16ToString(title[:])
if !strings.Contains(strings.ToLower(consoleTitle), "command prompt") && !strings.Contains(strings.ToLower(consoleTitle), "powershell") {
problems = append(problems, fmt.Sprintf("\"%v\" is not an officially supported shell. Some features may not work as expected.\n", consoleTitle))
if !strings.Contains(strings.ToLower(consoleTitle), "command prompt") && !strings.Contains(strings.ToLower(consoleTitle), "powershell") && !strings.Contains(strings.ToLower(consoleTitle), "cmd.exe") && !strings.Contains(strings.ToLower(consoleTitle), "pwsh.exe") {
problems = append(problems, fmt.Sprintf("\"%v\" not recognized: the Command Prompt and Powershell are the only officially supported consoles. Some features may not work as expected.\n", consoleTitle))
}
fmt.Printf("\n%v", consoleTitle)
}
}
}
@@ -1082,7 +1137,7 @@ func checkLocalEnvironment() {
}
// fmt.Printf(" %d.%d\n", versionInfo.MajorVersion, versionInfo.MinorVersion)
maj, min, patch := windows.RtlGetNtVersionNumbers()
fmt.Printf("\nWindows Version: %d.%d (Build %d)\n", maj, min, patch)
fmt.Printf("\nWindows Version: %d.%d (Build %d)\n", maj, min, patch)
// SHELL in Linux
// TERM in Windows
@@ -1119,7 +1174,15 @@ func checkLocalEnvironment() {
v := node.GetInstalled(env.root)
nvmhome := os.Getenv("NVM_HOME")
fmt.Printf("\nNVM4W Version: %v\nNVM4W Path: %v\nNVM4W Settings: %v\nNVM_HOME: %v\nNVM_SYMLINK: %v\nNode Installations: %v\n\nTotal Node.js Versions: %v\nActive Node.js Version: %v", NvmVersion, path, home, nvmhome, symlink, env.root, len(v), out)
mirrors := "No mirrors configured"
if len(env.node_mirror) > 0 && len(env.npm_mirror) > 0 {
mirrors = env.node_mirror + " (node) and " + env.npm_mirror + " (npm)"
} else if len(env.node_mirror) > 0 {
mirrors = env.node_mirror + " (node)"
} else if len(env.npm_mirror) > 0 {
mirrors = env.npm_mirror + " (npm)"
}
fmt.Printf("\nNVM4W Version: %v\nNVM4W Path: %v\nNVM4W Settings: %v\nNVM_HOME: %v\nNVM_SYMLINK: %v\nNode Installations: %v\nDefault Architecture: %v-bit\nMirrors: %v\nHTTP Proxy: %v\n\nTotal Node.js Versions: %v\nActive Node.js Version: %v", NvmVersion, path, home, nvmhome, symlink, env.root, env.arch, mirrors, env.proxy, len(v), out)
if !nvmsymlinkfound {
problems = append(problems, "The NVM4W symlink ("+env.symlink+") was not found in the PATH environment variable.")
@@ -1138,12 +1201,8 @@ func checkLocalEnvironment() {
}
} else {
if fileInfo.Mode()&os.ModeSymlink != 0 {
targetPath, err := os.Readlink(symlink)
if err != nil {
fmt.Println(err)
}
targetFileInfo, err := os.Lstat(targetPath)
targetPath, _ := os.Readlink(symlink)
targetFileInfo, _ := os.Lstat(targetPath)
if !targetFileInfo.Mode().IsDir() {
problems = append(problems, "NVM_SYMLINK is a symlink linking to a file instead of a directory.")
@@ -1153,11 +1212,15 @@ func checkLocalEnvironment() {
}
}
if strings.Contains(symlink, home) {
problems = append(problems, "Storing the NVM_SYMLINK ("+symlink+") within the NVM_HOME directory ("+home+") has been known to cause problems in many Windows environments. Change NVM_SYMLINK to a different directory that does not already exist.")
}
ipv6, err := web.IsLocalIPv6()
if err != nil {
problems = append(problems, "Connection type cannot be determined: "+err.Error())
} else if !ipv6 {
fmt.Println("\nIPv6 is enabled. This can slow downloads significantly.")
fmt.Println("\nIPv6 is enabled. This has been known to slow downloads significantly.")
}
nodelist := web.Ping(web.GetFullNodeUrl("index.json"))
@@ -1179,6 +1242,7 @@ func checkLocalEnvironment() {
if _, err = os.Stat(filepath.Join(env.root, v[i], "node.exe")); err != nil {
invalid = append(invalid, v[i])
} else if _, err = os.Stat(filepath.Join(env.root, v[i], "npm.cmd")); err != nil {
fmt.Println(err)
invalidnpm = append(invalid, v[i])
}
}
@@ -1222,6 +1286,36 @@ func checkLocalEnvironment() {
}
}
// Check for updates
colorize := true
if err := upgrade.EnableVirtualTerminalProcessing(); err != nil {
colorize = false
}
update, checkerr := upgrade.Get()
if checkerr == nil {
if len(update.Warnings) > 0 {
fmt.Println("")
}
for _, warning := range update.Warnings {
upgrade.Warn(warning, colorize)
}
if len(update.Warnings) > 0 {
fmt.Println("")
}
}
if checkerr != nil {
fmt.Println("error checking for updates: " + checkerr.Error())
} else {
newVersion, available, err := update.Available(NvmVersion)
if err != nil {
fmt.Println("Error checking for updates: " + err.Error())
} else if available {
upgrade.Warn(fmt.Sprintf("An upgrade is available: v%s", newVersion), colorize)
fmt.Println(" run \"nvm upgrade\" to update.\n")
}
}
fmt.Println("\n" + "Find help at https://github.com/coreybutler/nvm-windows/wiki/Common-Issues")
}
@@ -1244,10 +1338,11 @@ func help() {
fmt.Println(" nvm node_mirror [url] : Set the node mirror. Defaults to https://nodejs.org/dist/. Leave [url] blank to use default url.")
fmt.Println(" nvm npm_mirror [url] : Set the npm mirror. Defaults to https://github.com/npm/cli/archive/. Leave [url] blank to default url.")
fmt.Println(" nvm uninstall <version> : The version must be a specific version.")
// fmt.Println(" nvm update : Automatically update nvm to the latest version.")
fmt.Println(" nvm upgrade [restore] : Update nvm to the latest version. Use \"restore\" to revert to the previously installed version.")
fmt.Println(" nvm use [version] [arch] : Switch to use the specified version. Optionally use \"latest\", \"lts\", or \"newest\".")
fmt.Println(" \"newest\" is the latest installed version. Optionally specify 32/64bit architecture.")
fmt.Println(" nvm use <arch> will continue using the selected version, but switch to 32/64 bit mode.")
fmt.Println(" nvm reinstall <version> : A shortcut method to clean and reinstall a specific version.")
fmt.Println(" nvm root [path] : Set the directory where nvm should store different versions of node.js.")
fmt.Println(" If <path> is not set, the current root will be displayed.")
fmt.Println(" nvm [--]version : Displays the current running version of nvm for Windows. Aliased as v.")
@@ -1304,6 +1399,10 @@ func cleanVersion(version string) string {
// Given a node.js version, returns the associated npm version
func getNpmVersion(nodeversion string) string {
_, _, _, _, _, npm := node.GetAvailable()
if len(npm) == 0 {
fmt.Println("Error looking up versions: Remote host returned no results. This usually indicates a problem with with Node.js web server. Please try again in a few minutes.")
os.Exit(0)
}
return npm[nodeversion]
}
@@ -1316,14 +1415,20 @@ func getLatest() string {
}
func getLTS() string {
all, ltsList, current, stable, unstable, npm := node.GetAvailable()
fmt.Println(all)
fmt.Println(ltsList)
fmt.Println(current)
fmt.Println(stable)
fmt.Println(unstable)
fmt.Println(npm)
// _, ltsList, _, _, _, _ := node.GetAvailable()
// all, ltsList, current, stable, unstable, npm := node.GetAvailable()
// fmt.Println(all)
// fmt.Println(ltsList)
// fmt.Println(current)
// fmt.Println(stable)
// fmt.Println(unstable)
// fmt.Println(npm)
_, ltsList, _, _, _, _ := node.GetAvailable()
if len(ltsList) == 0 {
fmt.Println("Error looking up LTS version: Remote host returned no results. This usually indicates a problem with with Node.js web server. Please try again in a few minutes.")
os.Exit(0)
}
// ltsList has already been numerically sorted
return ltsList[0]
}

765
src/upgrade/upgrade.go Normal file
View File

@@ -0,0 +1,765 @@
package upgrade
import (
"archive/zip"
"crypto/md5"
"encoding/json"
"fmt"
"io"
"net/http"
"nvm/semver"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
"unsafe"
"github.com/coreybutler/go-fsutil"
"golang.org/x/sys/windows"
)
const (
UPDATE_URL = "https://gist.githubusercontent.com/coreybutler/a12af0f17956a0f25b60369b5f8a661a/raw/nvm4w.json"
// Color codes
yellow = "\033[33m"
reset = "\033[0m"
// Windows console modes
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
FILE_ATTRIBUTE_HIDDEN = 0x2
CREATE_NEW_CONSOLE = 0x00000010 // Create a new console for the child process
DETACHED_PROCESS = 0x00000008 // Detach the child process from the parent
warningIcon = "⚠️"
// exclamationIcon = "❗"
)
type Update struct {
Version string `json:"version"`
Assets []string `json:"assets"`
Warnings []string `json:"notices"`
VersionWarnings []string `json:"versionNotices"`
SourceURL string `json:"sourceTpl"`
}
func (u *Update) Available(sinceVersion string) (string, bool, error) {
currentVersion, err := semver.New(sinceVersion)
if err != nil {
return "", false, err
}
updateVersion, err := semver.New(u.Version)
if err != nil {
return "", false, err
}
if currentVersion.LT(updateVersion) {
return u.Version, true, nil
}
return "", false, nil
}
func Warn(msg string, colorized ...bool) {
if len(colorized) > 0 && colorized[0] {
fmt.Println(warningIcon + " " + highlight(msg))
} else {
fmt.Println(strings.ToUpper(msg))
}
}
func Run(version string) error {
args := os.Args[2:]
colorize := true
if err := EnableVirtualTerminalProcessing(); err != nil {
colorize = false
}
// Retrieve remote metadata
update, err := checkForUpdate(UPDATE_URL)
if err != nil {
return fmt.Errorf("error: failed to obtain update data: %v\n", err)
}
for _, warning := range update.Warnings {
Warn(warning, colorize)
}
verbose := false
rollback := false
for _, arg := range args {
switch strings.ToLower(arg) {
case "--verbose":
verbose = true
case "rollback":
rollback = true
}
}
// Check for a backup
if rollback {
if fsutil.Exists(filepath.Join(".", ".update", "nvm4w-backup.zip")) {
fmt.Println("restoring NVM4W backup...")
rbtmp, err := os.MkdirTemp("", "nvm-rollback-*")
if err != nil {
fmt.Printf("error: failed to create rollback directory: %v\n", err)
os.Exit(1)
}
defer os.RemoveAll(rbtmp)
err = unzip(filepath.Join(".", ".update", "nvm4w-backup.zip"), rbtmp)
if err != nil {
fmt.Printf("error: failed to extract backup: %v\n", err)
os.Exit(1)
}
// Copy the backup files to the current directory
err = copyDirContents(rbtmp, ".")
if err != nil {
fmt.Printf("error: failed to restore backup files: %v\n", err)
os.Exit(1)
}
// Remove the restoration directory
os.RemoveAll(filepath.Join(".", ".update"))
fmt.Println("rollback complete")
rbcmd := exec.Command("nvm.exe", "version")
o, err := rbcmd.Output()
if err != nil {
fmt.Println("error running nvm.exe:", err)
os.Exit(1)
}
exec.Command("schtasks", "/delete", "/tn", "\"RemoveNVM4WBackup\"", "/f").Run()
fmt.Printf("rollback to v%s complete\n", string(o))
os.Exit(0)
} else {
fmt.Println("no backup available: backups are only available for 7 days after upgrading")
os.Exit(0)
}
}
currentVersion, err := semver.New(version)
if err != nil {
return err
}
updateVersion, err := semver.New(update.Version)
if err != nil {
return err
}
if currentVersion.LT(updateVersion) {
if len(update.VersionWarnings) > 0 {
if len(update.Warnings) > 0 || len(update.VersionWarnings) > 0 {
fmt.Println("")
}
for _, warning := range update.VersionWarnings {
Warn(warning, colorize)
}
fmt.Println("")
}
fmt.Printf("upgrading from v%s-->%s\n\ndownloading...\n", version, highlight(update.Version))
} else {
fmt.Println("nvm is up to date")
return nil
}
// Make temp directory
tmp, err := os.MkdirTemp("", "nvm-upgrade-*")
if err != nil {
return fmt.Errorf("error: failed to create temporary directory: %v\n", err)
}
defer os.RemoveAll(tmp)
// Download the new app
// TODO: Replace version with update.Version
// source := fmt.Sprintf(update.SourceURL, update.Version)
source := fmt.Sprintf(update.SourceURL, "1.1.11")
body, err := get(source)
if err != nil {
return fmt.Errorf("error: failed to download new version: %v\n", err)
}
os.WriteFile(filepath.Join(tmp, "assets.zip"), body, os.ModePerm)
os.Mkdir(filepath.Join(tmp, "assets"), os.ModePerm)
source = source + ".checksum.txt"
body, err = get(source)
if err != nil {
return fmt.Errorf("error: failed to download checksum: %v\n", err)
}
os.WriteFile(filepath.Join(tmp, "assets.zip.checksum.txt"), body, os.ModePerm)
filePath := filepath.Join(tmp, "assets.zip") // path to the file you want to validate
checksumFile := filepath.Join(tmp, "assets.zip.checksum.txt") // path to the checksum file
// Step 1: Compute the MD5 checksum of the file
fmt.Println("verifying checksum...")
computedChecksum, err := computeMD5Checksum(filePath)
if err != nil {
return fmt.Errorf("Error computing checksum: %v", err)
}
// Step 2: Read the checksum from the .checksum.txt file
storedChecksum, err := readChecksumFromFile(checksumFile)
if err != nil {
return fmt.Errorf("Error readirng checksum from file: %v", err)
}
// Step 3: Compare the computed checksum with the stored checksum
if strings.ToLower(computedChecksum) != strings.ToLower(storedChecksum) {
return fmt.Errorf("cannot validate update file (checksum mismatch)")
}
fmt.Println("extracting update...")
if err := unzip(filepath.Join(tmp, "assets.zip"), filepath.Join(tmp, "assets")); err != nil {
return err
}
// Get any additional assets
if len(update.Assets) > 0 {
fmt.Printf("downloading %d additional assets...\n", len(update.Assets))
for _, asset := range update.Assets {
var assetURL string
if !strings.HasPrefix(asset, "http") {
assetURL = fmt.Sprintf(update.SourceURL, asset)
} else {
assetURL = asset
}
assetBody, err := get(assetURL)
if err != nil {
return fmt.Errorf("error: failed to download asset: %v\n", err)
}
assetPath := filepath.Join(tmp, "assets", asset)
os.WriteFile(assetPath, assetBody, os.ModePerm)
}
}
// Debugging
if verbose {
tree(tmp, "downloaded files (extracted):")
nvmtestcmd := exec.Command(filepath.Join(tmp, "assets", "nvm.exe"), "version")
nvmtestcmd.Stdout = os.Stdout
nvmtestcmd.Stderr = os.Stderr
err = nvmtestcmd.Run()
if err != nil {
fmt.Println("error running nvm.exe:", err)
}
}
// Backup current version to zip
fmt.Println("applying update...")
currentExe, _ := os.Executable()
currentPath := filepath.Dir(currentExe)
bkp, err := os.MkdirTemp("", "nvm-backup-*")
if err != nil {
return fmt.Errorf("error: failed to create backup directory: %v\n", err)
}
defer os.RemoveAll(bkp)
err = zipDirectory(currentPath, filepath.Join(bkp, "backup.zip"))
if err != nil {
return fmt.Errorf("error: failed to create backup: %v\n", err)
}
os.MkdirAll(filepath.Join(currentPath, ".update"), os.ModePerm)
copyFile(filepath.Join(bkp, "backup.zip"), filepath.Join(currentPath, ".update", "nvm4w-backup.zip"))
// Copy the new files to the current directory
// copyFile(currentExe, fmt.Sprintf("%s.%s.bak", currentExe, version))
copyDirContents(filepath.Join(tmp, "assets"), currentPath)
copyFile(filepath.Join(tmp, "assets", "nvm.exe"), filepath.Join(currentPath, ".update/nvm.exe"))
if verbose {
nvmtestcmd := exec.Command(filepath.Join(currentPath, ".update/nvm.exe"), "version")
nvmtestcmd.Stdout = os.Stdout
nvmtestcmd.Stderr = os.Stderr
err = nvmtestcmd.Run()
if err != nil {
fmt.Println("error running nvm.exe:", err)
}
}
// TODO: schedule removal of .backup folder for 30 days from now
// TODO: warn user that the restore function is available for 30 days
// Debugging
if verbose {
tree(currentPath, "final directory contents:")
}
// Hide the update directory
setHidden(filepath.Join(currentPath, ".update"))
// The upgrade process should be able to roll back if there is a failure.
// TODO: Upgrade the registry data to reflect the new version
// Potentially provide a desktop notification when the upgrade is complete.
// If an "update.exe" exists, run it
if fsutil.IsExecutable(filepath.Join(tmp, "assets", "update.exe")) {
err = copyFile(filepath.Join(tmp, "assets", "update.exe"), filepath.Join(currentPath, ".update", "update.exe"))
if err != nil {
fmt.Println(fmt.Errorf("error: failed to copy update.exe: %v\n", err))
os.Exit(1)
}
}
autoupdate()
return nil
}
func Get() (*Update, error) {
return checkForUpdate(UPDATE_URL)
}
func autoupdate() {
currentPath, err := os.Executable()
if err != nil {
fmt.Println("error getting updater path:", err)
os.Exit(1)
}
// Create temporary directory for the updater script
tempDir := filepath.Dir(currentPath) // Use the same temp dir as the new executable
scriptPath := filepath.Join(tempDir, "updater.bat")
// Temporary batch file that deletes the directory and the scheduled task
tmp, err := os.MkdirTemp("", "nvm4w-remove-*")
if err != nil {
fmt.Printf("error creating temporary directory: %v", err)
os.Exit(1)
}
tempBatchFile := filepath.Join(tmp, "remove_backup.bat")
now := time.Now()
futureDate := now.AddDate(0, 0, 7)
formattedDate := futureDate.Format("01/02/2006")
batchContent := fmt.Sprintf(`
@echo off
schtasks /delete /tn "RemoveNVM4WBackup" /f
rmdir /s /q "%s"
`, escapeBackslashes(filepath.Join(filepath.Dir(currentPath), ".update")))
// Write the batch file to a temporary location
err = os.WriteFile(tempBatchFile, []byte(batchContent), os.ModePerm)
if err != nil {
fmt.Printf("error creating temporary batch file: %v", err)
os.Exit(1)
}
updaterScript := fmt.Sprintf(`@echo off
setlocal enabledelayedexpansion
echo ========= Update Script Started ========= >> error.log
echo Started updater script with PID %%1 at %%TIME%% >> error.log
echo Source: %%~2 >> error.log
echo Target: %%~3 >> error.log
:wait
timeout /t 1 /nobreak >nul
tasklist /fi "PID eq %%1" 2>nul | find "%%1" >nul
if not errorlevel 1 (
echo Waiting for PID %%1 to exit at %%TIME%%... >> error.log
goto :wait
)
echo ========= Starting Copy Operation ========= >> error.log
echo Checking if source (%%~2) exists... >> error.log
if not exist "%%~2" (
echo ERROR: Source file does not exist: %%~2 >> error.log
exit /b 1
)
echo Source file exists >> error.log
del "%%~3" >> error.log
echo Checking if target location is writable... >> error.log
echo Test > "%%~dp3test.txt" 2>>error.log
if errorlevel 1 (
echo ERROR: Target location is not writable: %%~dp3 >> error.log
exit /b 1
)
del "%%~dp3test.txt"
echo Target location is writable >> error.log
echo Attempting copy at %%TIME%%... >> error.log
echo Running: copy /y "%%~2" "%%~3" >> error.log
copy /y "%%~2" "%%~3" >> error.log 2>&1
if errorlevel 1 (
echo ERROR: Copy failed with error level %%errorlevel%% >> error.log
exit /b %%errorlevel%%
)
echo Verifying copy... >> error.log
if not exist "%%~3" (
echo ERROR: Target file does not exist after copy: %%~3 >> error.log
exit /b 1
)
del "%%~2" >> error.log
if exist "%%~2" (
echo ERROR: Source file still exists after deletion: %%~2 >> error.log
exit /b 1
)
:: Schedule the task to delete the directory
echo schtasks /create /tn "RemoveNVM4WBackup" /tr "cmd.exe /c %s" /sc once /sd %s /st 12:00 /f >> error.log
schtasks /create /tn "RemoveNVM4WBackup" /tr "cmd.exe /c %s" /sc once /sd %s /st 12:00 /f
if not errorlevel 0 (
echo ERROR: Failed to create scheduled task: exit code: %%errorlevel%% >> error.log
exit /b %%errorlevel%%
)
echo Update complete >> error.log
del error.log
del "%%~f0"
exit /b 0
`, escapeBackslashes(tempBatchFile), formattedDate, escapeBackslashes(tempBatchFile), formattedDate)
err = os.WriteFile(scriptPath, []byte(updaterScript), os.ModePerm) // Use standard Windows file permissions
if err != nil {
fmt.Printf("error creating updater script: %v", err)
os.Exit(1)
}
// Start the updater script
cmd := exec.Command(scriptPath, fmt.Sprintf("%d", os.Getpid()), filepath.Join(tempDir, ".update", "nvm.exe"), currentPath)
err = cmd.Start()
if err != nil {
fmt.Printf("error starting updater script: %v", err)
os.Exit(1)
}
// Exit the current process (delay for cleanup)
time.Sleep(300 * time.Millisecond)
os.Exit(0)
}
func escapeBackslashes(path string) string {
return strings.Replace(path, "\\", "\\\\", -1)
}
func tree(dir string, title ...string) {
if len(title) > 0 {
fmt.Println("\n" + highlight(title[0]))
}
cmd := exec.Command("tree", dir, "/F")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Println("Error executing command:", err)
}
}
func get(url string, verbose ...bool) ([]byte, error) {
if len(verbose) == 0 || verbose[0] {
fmt.Printf(" GET %s\n", url)
}
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return []byte{}, fmt.Errorf("error: received status code %d\n", resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
func checkForUpdate(url string) (*Update, error) {
u := Update{}
// Make the HTTP GET request
body, err := get(url, false)
if err != nil {
return &u, fmt.Errorf("error: reading response body: %v", err)
}
// Parse JSON into the struct
err = json.Unmarshal(body, &u)
if err != nil {
return &u, fmt.Errorf("error: parsing update: %v", err)
}
return &u, nil
}
func EnableVirtualTerminalProcessing() error {
// Get the handle to the standard output
handle := windows.Stdout
// Retrieve the current console mode
var mode uint32
if err := windows.GetConsoleMode(handle, &mode); err != nil {
return err
}
// Enable the virtual terminal processing mode
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING
if err := windows.SetConsoleMode(handle, mode); err != nil {
return err
}
return nil
}
func highlight(message string) string {
return fmt.Sprintf("%s%s%s", yellow, message, reset)
}
// Unzip function extracts a zip file to a specified directory
func unzip(src string, dest string) error {
// Open the zip archive for reading
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()
// Iterate over each file in the zip archive
for _, f := range r.File {
// Build the path for each file in the destination directory
fpath := filepath.Join(dest, f.Name)
// Check if the file is a directory
if f.FileInfo().IsDir() {
// Create directory if it doesn't exist
if err := os.MkdirAll(fpath, os.ModePerm); err != nil {
return err
}
continue
}
// Create directories leading to the file if they don't exist
if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return err
}
// Open the file in the zip archive
rc, err := f.Open()
if err != nil {
return err
}
defer rc.Close()
// Create the destination file
outFile, err := os.Create(fpath)
if err != nil {
return err
}
defer outFile.Close()
// Copy the file contents from the archive to the destination file
_, err = io.Copy(outFile, rc)
if err != nil {
return err
}
}
return nil
}
// function to compute the MD5 checksum of a file
func computeMD5Checksum(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
hasher := md5.New()
_, err = io.Copy(hasher, file)
if err != nil {
return "", err
}
// Return the hex string representation of the MD5 hash
return fmt.Sprintf("%x", hasher.Sum(nil)), nil
}
// function to read the checksum from the .checksum.txt file
func readChecksumFromFile(checksumFile string) (string, error) {
file, err := os.Open(checksumFile)
if err != nil {
return "", err
}
defer file.Close()
var checksum string
_, err = fmt.Fscan(file, &checksum)
if err != nil {
return "", err
}
return checksum, nil
}
func copyFile(src, dst string) error {
// Open the source file
sourceFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("failed to open source file: %v", err)
}
defer sourceFile.Close()
// Create the destination file
destinationFile, err := os.Create(dst)
if err != nil {
return fmt.Errorf("failed to create destination file: %v", err)
}
defer destinationFile.Close()
// Copy contents from the source file to the destination file
_, err = io.Copy(destinationFile, sourceFile)
if err != nil {
return fmt.Errorf("failed to copy file: %v", err)
}
// Optionally, copy file permissions (this can be skipped if not needed)
sourceInfo, err := os.Stat(src)
if err != nil {
return fmt.Errorf("failed to get source file info: %v", err)
}
err = os.Chmod(dst, sourceInfo.Mode())
if err != nil {
return fmt.Errorf("failed to set file permissions: %v", err)
}
return nil
}
// copyDirContents copies all the contents (files and subdirectories) of a source directory to a destination directory.
func copyDirContents(srcDir, dstDir string) error {
// Ensure destination directory exists
err := os.MkdirAll(dstDir, 0755)
if err != nil {
return fmt.Errorf("failed to create destination directory %s: %v", dstDir, err)
}
// Walk through the source directory recursively
err = filepath.Walk(srcDir, func(srcPath string, info os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("error accessing %s: %v", srcPath, err)
}
// Construct the corresponding path in the destination directory
relPath, err := filepath.Rel(srcDir, srcPath)
if err != nil {
return fmt.Errorf("failed to get relative path for %s: %v", srcPath, err)
}
dstPath := filepath.Join(dstDir, relPath)
// If it's a directory, ensure it's created in the destination
if info.IsDir() {
return os.MkdirAll(dstPath, info.Mode())
}
// If it's a file, copy it
return copyFile(srcPath, dstPath)
})
return err
}
// zipDirectory zips the contents of a directory.
func zipDirectory(sourceDir, outputZip string) error {
// Create the zip file.
zipFile, err := os.Create(outputZip)
if err != nil {
return err
}
defer zipFile.Close()
// Create a new zip writer.
zipWriter := zip.NewWriter(zipFile)
defer zipWriter.Close()
// Walk through the directory.
return filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Get the relative path.
relPath, err := filepath.Rel(sourceDir, path)
if err != nil {
return err
}
// Skip the directory itself but include subdirectories.
if info.IsDir() {
if relPath == "." {
return nil
}
// Add a trailing slash for directories in the zip archive.
relPath += "/"
}
// Create a zip header.
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = relPath
if info.IsDir() {
header.Method = zip.Store
} else {
header.Method = zip.Deflate
}
// Create a writer for the file in the zip archive.
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
// If the file is not a directory, copy its contents into the archive.
if !info.IsDir() {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(writer, file)
if err != nil {
return err
}
}
return nil
})
}
func setHidden(path string) error {
// Convert the path to a UTF-16 encoded string
lpFileName, err := syscall.UTF16PtrFromString(path)
if err != nil {
return fmt.Errorf("failed to encode path: %w", err)
}
// Call the Windows API function
ret, _, err := syscall.NewLazyDLL("kernel32.dll").
NewProc("SetFileAttributesW").
Call(
uintptr(unsafe.Pointer(lpFileName)),
uintptr(FILE_ATTRIBUTE_HIDDEN),
)
// Check the result
if ret == 0 {
return fmt.Errorf("failed to set hidden attribute: %w", err)
}
return nil
}