combined updates

This commit is contained in:
Corey Butler
2024-12-28 22:23:51 -06:00
parent 096e0a20fc
commit 7de073ee16
41 changed files with 2197 additions and 1111 deletions

104
src/author/bridge.go Normal file
View File

@@ -0,0 +1,104 @@
package author
import (
"bufio"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/coreybutler/go-fsutil"
"golang.org/x/sys/windows"
)
const (
// Constants for SetWindowPos
SWP_NOMOVE = 0x0002
SWP_NOZORDER = 0x0004
SWP_SHOWWINDOW = 0x0040
SW_HIDE = 0 // Hide the window (for the main console window)
)
var (
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
moduser32 = syscall.NewLazyDLL("user32.dll")
procGetConsole = modkernel32.NewProc("GetConsoleWindow")
procShowWindow = moduser32.NewProc("ShowWindow")
)
// Hide the main console window (the one running the Go app)
func hideConsole() {
hwnd, _, _ := procGetConsole.Call()
if hwnd != 0 {
procShowWindow.Call(hwnd, uintptr(SW_HIDE)) // Hide the main console window
}
}
func Bridge(args ...string) {
exe, _ := os.Executable()
bridge := filepath.Join(filepath.Dir(exe), "author-nvm.exe")
if !fsutil.Exists(bridge) {
fmt.Println("error: author bridge not found")
os.Exit(1)
}
if len(args) < 2 {
fmt.Printf("error: invalid number of arguments passed to author bridge: %d\n", len(args))
os.Exit(1)
}
command := args[0]
args = args[1:]
// fmt.Printf("running author bridge: %s %v\n", command, args)
hideConsole()
cmd := exec.Command(bridge, append([]string{command}, args...)...)
cmd.SysProcAttr = &windows.SysProcAttr{
CreationFlags: windows.CREATE_NEW_PROCESS_GROUP | windows.DETACHED_PROCESS | windows.CREATE_NO_WINDOW,
}
// Create pipes for Stdout and Stderr
stdoutPipe, _ := cmd.StdoutPipe()
stderrPipe, _ := cmd.StderrPipe()
// Start the command
if err := cmd.Start(); err != nil {
fmt.Println("error starting bridge command:", err)
os.Exit(1)
}
// Stream Stdout
go func() {
scanner := bufio.NewScanner(stdoutPipe)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}()
// Stream Stderr
go func() {
scanner := bufio.NewScanner(stderrPipe)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}()
if command == "upgrade" {
for _, arg := range args {
if strings.Contains(arg, "--rollback") {
fmt.Println("exiting to rollback nvm.exe...")
time.Sleep(1 * time.Second)
os.Exit(0)
}
}
}
// Wait for the command to finish
if err := cmd.Wait(); err != nil {
fmt.Println("bridge command finished with error:", err)
}
}

View File

@@ -6,14 +6,20 @@ 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/dustin/go-humanize v1.0.1
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
github.com/ncruces/zenity v0.10.14
github.com/olekukonko/tablewriter v0.0.5
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
golang.org/x/sys v0.1.0
golang.org/x/sys v0.25.0
)
require (
aead.dev/minisign v0.2.0 // indirect
github.com/akavel/rsrc v0.10.2 // indirect
github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f // indirect
github.com/josephspurrier/goversioninfo v1.4.1 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 // indirect
golang.org/x/image v0.20.0 // indirect
)

View File

@@ -1,46 +1,59 @@
aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw=
github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
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=
github.com/coreybutler/go-fsutil v1.2.0/go.mod h1:B+6ufEkkRZgFwyR2sHEVG9dMzVBU3GbyGyYmCq7YkEk=
github.com/coreybutler/go-where v1.0.2 h1:Omit67KeTtKpvSJjezVxnVD4qMtvlXDlItiKpVCdcl4=
github.com/coreybutler/go-where v1.0.2/go.mod h1:IqV4saJiDXdNJURfTfVRywDHvY1IG5F+GXb2kmnmEe8=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f h1:OGqDDftRTwrvUoL6pOG7rYTmWsTCvyEWFsMjg+HcOaA=
github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f/go.mod h1:Dv9D0NUlAsaQcGQZa5kc5mqR9ua72SmA8VXi4cd+cBw=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
github.com/josephspurrier/goversioninfo v1.4.1 h1:5LvrkP+n0tg91J9yTkoVnt/QgNnrI1t4uSsWjIonrqY=
github.com/josephspurrier/goversioninfo v1.4.1/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY=
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/ncruces/zenity v0.10.14 h1:OBFl7qfXcvsdo1NUEGxTlZvAakgWMqz9nG38TuiaGLI=
github.com/ncruces/zenity v0.10.14/go.mod h1:ZBW7uVe/Di3IcRYH0Br8X59pi+O6EPnNIOU66YHpOO4=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 h1:GranzK4hv1/pqTIhMTXt2X8MmMOuH3hMeUR0o9SP5yc=
github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844/go.mod h1:T1TLSfyWVBRXVGzWd0o9BI4kfoO9InEgfQe4NV3mLz8=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/zmb3/gogetdoc v0.0.0-20190228002656-b37376c5da6a/go.mod h1:ofmGw6LrMypycsiWcyug6516EXpIxSbZ+uI9ppGypfY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
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/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
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/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

35
src/manifest.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "nvm",
"version": "1.1.12",
"description": "Node.js version manager for Windows",
"license": "SEE LICENSE IN LICENSE",
"author": "coreybutler",
"build": "nvm.go",
"output": "../bin",
"update": true,
"upx": false,
"tiny": false,
"variables": {
"main.NvmVersion": "manifest.version",
"nvm/web.nvmversion": "manifest.version"
},
"minify": true,
"default_profiles": [
"dev"
],
"profile": {
"release": {
"upx": false,
"variables": {
"main.debug": "false",
"main.mode": "prod"
}
},
"dev": {
"variables": {
"main.debug": "true",
"main.mode": "dev"
}
}
}
}

View File

@@ -34,6 +34,8 @@ func GetCurrentVersion() (string, string) {
if err == nil {
if string(str) == "x64" {
bit = "64"
} else if string(str) == "arm64" {
bit = "arm64"
} else {
bit = "32"
}

1083
src/nvm.go

File diff suppressed because it is too large Load Diff

View File

@@ -5,78 +5,78 @@
package semver
import (
"errors"
"fmt"
"strconv"
"strings"
"errors"
"fmt"
"strconv"
"strings"
)
const (
numbers string = "0123456789"
alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"
alphanum = alphas + numbers
dot = "."
hyphen = "-"
plus = "+"
numbers string = "0123456789"
alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"
alphanum = alphas + numbers
dot = "."
hyphen = "-"
plus = "+"
)
// Latest fully supported spec version
var SPEC_VERSION = Version{
Major: 2,
Minor: 0,
Patch: 0,
Major: 2,
Minor: 0,
Patch: 0,
}
type Version struct {
Major uint64
Minor uint64
Patch uint64
Pre []*PRVersion
Build []string //No Precedence
Major uint64
Minor uint64
Patch uint64
Pre []*PRVersion
Build []string //No Precedence
}
// Version to string
func (v *Version) String() string {
versionArray := []string{
strconv.FormatUint(v.Major, 10),
dot,
strconv.FormatUint(v.Minor, 10),
dot,
strconv.FormatUint(v.Patch, 10),
}
if len(v.Pre) > 0 {
versionArray = append(versionArray, hyphen)
for i, pre := range v.Pre {
if i > 0 {
versionArray = append(versionArray, dot)
}
versionArray = append(versionArray, pre.String())
}
}
if len(v.Build) > 0 {
versionArray = append(versionArray, plus, strings.Join(v.Build, dot))
}
return strings.Join(versionArray, "")
versionArray := []string{
strconv.FormatUint(v.Major, 10),
dot,
strconv.FormatUint(v.Minor, 10),
dot,
strconv.FormatUint(v.Patch, 10),
}
if len(v.Pre) > 0 {
versionArray = append(versionArray, hyphen)
for i, pre := range v.Pre {
if i > 0 {
versionArray = append(versionArray, dot)
}
versionArray = append(versionArray, pre.String())
}
}
if len(v.Build) > 0 {
versionArray = append(versionArray, plus, strings.Join(v.Build, dot))
}
return strings.Join(versionArray, "")
}
// Checks if v is greater than o.
func (v *Version) GT(o *Version) bool {
return (v.Compare(o) == 1)
return (v.Compare(o) == 1)
}
// Checks if v is greater than or equal to o.
func (v *Version) GTE(o *Version) bool {
return (v.Compare(o) >= 0)
return (v.Compare(o) >= 0)
}
// Checks if v is less than o.
func (v *Version) LT(o *Version) bool {
return (v.Compare(o) == -1)
return (v.Compare(o) == -1)
}
// Checks if v is less than or equal to o.
func (v *Version) LTE(o *Version) bool {
return (v.Compare(o) <= 0)
return (v.Compare(o) <= 0)
}
// Compares Versions v to o:
@@ -84,241 +84,242 @@ func (v *Version) LTE(o *Version) bool {
// 0 == v is equal to o
// 1 == v is greater than o
func (v *Version) Compare(o *Version) int {
if v.Major != o.Major {
if v.Major > o.Major {
return 1
} else {
return -1
}
}
if v.Minor != o.Minor {
if v.Minor > o.Minor {
return 1
} else {
return -1
}
}
if v.Patch != o.Patch {
if v.Patch > o.Patch {
return 1
} else {
return -1
}
}
if v.Major != o.Major {
if v.Major > o.Major {
return 1
} else {
return -1
}
}
if v.Minor != o.Minor {
if v.Minor > o.Minor {
return 1
} else {
return -1
}
}
if v.Patch != o.Patch {
if v.Patch > o.Patch {
return 1
} else {
return -1
}
}
// Quick comparison if a version has no prerelease versions
if len(v.Pre) == 0 && len(o.Pre) == 0 {
return 0
} else if len(v.Pre) == 0 && len(o.Pre) > 0 {
return 1
} else if len(v.Pre) > 0 && len(o.Pre) == 0 {
return -1
} else {
// Quick comparison if a version has no prerelease versions
if len(v.Pre) == 0 && len(o.Pre) == 0 {
return 0
} else if len(v.Pre) == 0 && len(o.Pre) > 0 {
return 1
} else if len(v.Pre) > 0 && len(o.Pre) == 0 {
return -1
} else {
i := 0
for ; i < len(v.Pre) && i < len(o.Pre); i++ {
if comp := v.Pre[i].Compare(o.Pre[i]); comp == 0 {
continue
} else if comp == 1 {
return 1
} else {
return -1
}
}
i := 0
for ; i < len(v.Pre) && i < len(o.Pre); i++ {
if comp := v.Pre[i].Compare(o.Pre[i]); comp == 0 {
continue
} else if comp == 1 {
return 1
} else {
return -1
}
}
// If all pr versions are the equal but one has further pr version, this one greater
if i == len(v.Pre) && i == len(o.Pre) {
return 0
} else if i == len(v.Pre) && i < len(o.Pre) {
return -1
} else {
return 1
}
// If all pr versions are the equal but one has further pr version, this one greater
if i == len(v.Pre) && i == len(o.Pre) {
return 0
} else if i == len(v.Pre) && i < len(o.Pre) {
return -1
} else {
return 1
}
}
}
}
// Validates v and returns error in case
func (v *Version) Validate() error {
// Major, Minor, Patch already validated using uint64
// Major, Minor, Patch already validated using uint64
if len(v.Pre) > 0 {
for _, pre := range v.Pre {
if !pre.IsNum { //Numeric prerelease versions already uint64
if len(pre.VersionStr) == 0 {
return fmt.Errorf("Prerelease can not be empty %q", pre.VersionStr)
}
if !containsOnly(pre.VersionStr, alphanum) {
return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr)
}
}
}
}
if len(v.Pre) > 0 {
for _, pre := range v.Pre {
if !pre.IsNum { //Numeric prerelease versions already uint64
if len(pre.VersionStr) == 0 {
return fmt.Errorf("Prerelease can not be empty %q", pre.VersionStr)
}
if !containsOnly(pre.VersionStr, alphanum) {
return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr)
}
}
}
}
if len(v.Build) > 0 {
for _, build := range v.Build {
if len(build) == 0 {
return fmt.Errorf("Build meta data can not be empty %q", build)
}
if !containsOnly(build, alphanum) {
return fmt.Errorf("Invalid character(s) found in build meta data %q", build)
}
}
}
if len(v.Build) > 0 {
for _, build := range v.Build {
if len(build) == 0 {
return fmt.Errorf("Build meta data can not be empty %q", build)
}
if !containsOnly(build, alphanum) {
return fmt.Errorf("Invalid character(s) found in build meta data %q", build)
}
}
}
return nil
return nil
}
// Alias for Parse, parses version string and returns a validated Version or error
func New(s string) (*Version, error) {
return Parse(s)
return Parse(s)
}
// Parses version string and returns a validated Version or error
func Parse(s string) (*Version, error) {
if len(s) == 0 {
return nil, errors.New("Version string empty")
}
s = strings.Replace(s, "v", "", 1)
if len(s) == 0 {
return nil, errors.New("Version string empty")
}
// Split into major.minor.(patch+pr+meta)
parts := strings.SplitN(s, ".", 3)
if len(parts) != 3 {
return nil, errors.New("No Major.Minor.Patch elements found")
}
// Split into major.minor.(patch+pr+meta)
parts := strings.SplitN(s, ".", 3)
if len(parts) != 3 {
return nil, errors.New("No Major.Minor.Patch elements found")
}
// Major
if !containsOnly(parts[0], numbers) {
return nil, fmt.Errorf("Invalid character(s) found in major number %q", parts[0])
}
if hasLeadingZeroes(parts[0]) {
return nil, fmt.Errorf("Major number must not contain leading zeroes %q", parts[0])
}
major, err := strconv.ParseUint(parts[0], 10, 64)
if err != nil {
return nil, err
}
// Major
if !containsOnly(parts[0], numbers) {
return nil, fmt.Errorf("Invalid character(s) found in major number %q", parts[0])
}
if hasLeadingZeroes(parts[0]) {
return nil, fmt.Errorf("Major number must not contain leading zeroes %q", parts[0])
}
major, err := strconv.ParseUint(parts[0], 10, 64)
if err != nil {
return nil, err
}
// Minor
if !containsOnly(parts[1], numbers) {
return nil, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1])
}
if hasLeadingZeroes(parts[1]) {
return nil, fmt.Errorf("Minor number must not contain leading zeroes %q", parts[1])
}
minor, err := strconv.ParseUint(parts[1], 10, 64)
if err != nil {
return nil, err
}
// Minor
if !containsOnly(parts[1], numbers) {
return nil, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1])
}
if hasLeadingZeroes(parts[1]) {
return nil, fmt.Errorf("Minor number must not contain leading zeroes %q", parts[1])
}
minor, err := strconv.ParseUint(parts[1], 10, 64)
if err != nil {
return nil, err
}
preIndex := strings.Index(parts[2], "-")
buildIndex := strings.Index(parts[2], "+")
preIndex := strings.Index(parts[2], "-")
buildIndex := strings.Index(parts[2], "+")
// Determine last index of patch version (first of pre or build versions)
var subVersionIndex int
if preIndex != -1 && buildIndex == -1 {
subVersionIndex = preIndex
} else if preIndex == -1 && buildIndex != -1 {
subVersionIndex = buildIndex
} else if preIndex == -1 && buildIndex == -1 {
subVersionIndex = len(parts[2])
} else {
// if there is no actual pr version but a hyphen inside the build meta data
if buildIndex < preIndex {
subVersionIndex = buildIndex
preIndex = -1 // Build meta data before preIndex found implicates there are no prerelease versions
} else {
subVersionIndex = preIndex
}
}
// Determine last index of patch version (first of pre or build versions)
var subVersionIndex int
if preIndex != -1 && buildIndex == -1 {
subVersionIndex = preIndex
} else if preIndex == -1 && buildIndex != -1 {
subVersionIndex = buildIndex
} else if preIndex == -1 && buildIndex == -1 {
subVersionIndex = len(parts[2])
} else {
// if there is no actual pr version but a hyphen inside the build meta data
if buildIndex < preIndex {
subVersionIndex = buildIndex
preIndex = -1 // Build meta data before preIndex found implicates there are no prerelease versions
} else {
subVersionIndex = preIndex
}
}
if !containsOnly(parts[2][:subVersionIndex], numbers) {
return nil, fmt.Errorf("Invalid character(s) found in patch number %q", parts[2][:subVersionIndex])
}
if hasLeadingZeroes(parts[2][:subVersionIndex]) {
return nil, fmt.Errorf("Patch number must not contain leading zeroes %q", parts[2][:subVersionIndex])
}
patch, err := strconv.ParseUint(parts[2][:subVersionIndex], 10, 64)
if err != nil {
return nil, err
}
v := &Version{}
v.Major = major
v.Minor = minor
v.Patch = patch
if !containsOnly(parts[2][:subVersionIndex], numbers) {
return nil, fmt.Errorf("Invalid character(s) found in patch number %q", parts[2][:subVersionIndex])
}
if hasLeadingZeroes(parts[2][:subVersionIndex]) {
return nil, fmt.Errorf("Patch number must not contain leading zeroes %q", parts[2][:subVersionIndex])
}
patch, err := strconv.ParseUint(parts[2][:subVersionIndex], 10, 64)
if err != nil {
return nil, err
}
v := &Version{}
v.Major = major
v.Minor = minor
v.Patch = patch
// There are PreRelease versions
if preIndex != -1 {
var preRels string
if buildIndex != -1 {
preRels = parts[2][subVersionIndex+1 : buildIndex]
} else {
preRels = parts[2][subVersionIndex+1:]
}
prparts := strings.Split(preRels, ".")
for _, prstr := range prparts {
parsedPR, err := NewPRVersion(prstr)
if err != nil {
return nil, err
}
v.Pre = append(v.Pre, parsedPR)
}
}
// There are PreRelease versions
if preIndex != -1 {
var preRels string
if buildIndex != -1 {
preRels = parts[2][subVersionIndex+1 : buildIndex]
} else {
preRels = parts[2][subVersionIndex+1:]
}
prparts := strings.Split(preRels, ".")
for _, prstr := range prparts {
parsedPR, err := NewPRVersion(prstr)
if err != nil {
return nil, err
}
v.Pre = append(v.Pre, parsedPR)
}
}
// There is build meta data
if buildIndex != -1 {
buildStr := parts[2][buildIndex+1:]
buildParts := strings.Split(buildStr, ".")
for _, str := range buildParts {
if len(str) == 0 {
return nil, errors.New("Build meta data is empty")
}
if !containsOnly(str, alphanum) {
return nil, fmt.Errorf("Invalid character(s) found in build meta data %q", str)
}
v.Build = append(v.Build, str)
}
}
// There is build meta data
if buildIndex != -1 {
buildStr := parts[2][buildIndex+1:]
buildParts := strings.Split(buildStr, ".")
for _, str := range buildParts {
if len(str) == 0 {
return nil, errors.New("Build meta data is empty")
}
if !containsOnly(str, alphanum) {
return nil, fmt.Errorf("Invalid character(s) found in build meta data %q", str)
}
v.Build = append(v.Build, str)
}
}
return v, nil
return v, nil
}
// PreRelease Version
type PRVersion struct {
VersionStr string
VersionNum uint64
IsNum bool
VersionStr string
VersionNum uint64
IsNum bool
}
// Creates a new valid prerelease version
func NewPRVersion(s string) (*PRVersion, error) {
if len(s) == 0 {
return nil, errors.New("Prerelease is empty")
}
v := &PRVersion{}
if containsOnly(s, numbers) {
if hasLeadingZeroes(s) {
return nil, fmt.Errorf("Numeric PreRelease version must not contain leading zeroes %q", s)
}
num, err := strconv.ParseUint(s, 10, 64)
if len(s) == 0 {
return nil, errors.New("Prerelease is empty")
}
v := &PRVersion{}
if containsOnly(s, numbers) {
if hasLeadingZeroes(s) {
return nil, fmt.Errorf("Numeric PreRelease version must not contain leading zeroes %q", s)
}
num, err := strconv.ParseUint(s, 10, 64)
// Might never be hit, but just in case
if err != nil {
return nil, err
}
v.VersionNum = num
v.IsNum = true
} else if containsOnly(s, alphanum) {
v.VersionStr = s
v.IsNum = false
} else {
return nil, fmt.Errorf("Invalid character(s) found in prerelease %q", s)
}
return v, nil
// Might never be hit, but just in case
if err != nil {
return nil, err
}
v.VersionNum = num
v.IsNum = true
} else if containsOnly(s, alphanum) {
v.VersionStr = s
v.IsNum = false
} else {
return nil, fmt.Errorf("Invalid character(s) found in prerelease %q", s)
}
return v, nil
}
// Is pre release version numeric?
func (v *PRVersion) IsNumeric() bool {
return v.IsNum
return v.IsNum
}
// Compares PreRelease Versions v to o:
@@ -326,54 +327,54 @@ func (v *PRVersion) IsNumeric() bool {
// 0 == v is equal to o
// 1 == v is greater than o
func (v *PRVersion) Compare(o *PRVersion) int {
if v.IsNum && !o.IsNum {
return -1
} else if !v.IsNum && o.IsNum {
return 1
} else if v.IsNum && o.IsNum {
if v.VersionNum == o.VersionNum {
return 0
} else if v.VersionNum > o.VersionNum {
return 1
} else {
return -1
}
} else { // both are Alphas
if v.VersionStr == o.VersionStr {
return 0
} else if v.VersionStr > o.VersionStr {
return 1
} else {
return -1
}
}
if v.IsNum && !o.IsNum {
return -1
} else if !v.IsNum && o.IsNum {
return 1
} else if v.IsNum && o.IsNum {
if v.VersionNum == o.VersionNum {
return 0
} else if v.VersionNum > o.VersionNum {
return 1
} else {
return -1
}
} else { // both are Alphas
if v.VersionStr == o.VersionStr {
return 0
} else if v.VersionStr > o.VersionStr {
return 1
} else {
return -1
}
}
}
// PreRelease version to string
func (v *PRVersion) String() string {
if v.IsNum {
return strconv.FormatUint(v.VersionNum, 10)
}
return v.VersionStr
if v.IsNum {
return strconv.FormatUint(v.VersionNum, 10)
}
return v.VersionStr
}
func containsOnly(s string, set string) bool {
return strings.IndexFunc(s, func(r rune) bool {
return !strings.ContainsRune(set, r)
}) == -1
return strings.IndexFunc(s, func(r rune) bool {
return !strings.ContainsRune(set, r)
}) == -1
}
func hasLeadingZeroes(s string) bool {
return len(s) > 1 && s[0] == '0'
return len(s) > 1 && s[0] == '0'
}
// Creates a new valid build version
func NewBuildVersion(s string) (string, error) {
if len(s) == 0 {
return "", errors.New("Buildversion is empty")
}
if !containsOnly(s, alphanum) {
return "", fmt.Errorf("Invalid character(s) found in build meta data %q", s)
}
return s, nil
if len(s) == 0 {
return "", errors.New("Buildversion is empty")
}
if !containsOnly(s, alphanum) {
return "", fmt.Errorf("Invalid character(s) found in build meta data %q", s)
}
return s, nil
}

198
src/upgrade/check.go Normal file
View File

@@ -0,0 +1,198 @@
package upgrade
import (
"encoding/json"
"fmt"
"nvm/node"
"nvm/semver"
"nvm/web"
"os"
"path/filepath"
"strings"
"time"
"github.com/dustin/go-humanize"
"github.com/go-toast/toast"
)
func Check(root string, nvmversion string) {
// Store the recognized version to prevent duplicates
notices := LoadNotices()
defer notices.Save()
reg := LoadRegistration(os.Args[2:]...)
// Check for Node.js updates
if reg.LTS || reg.Current {
buf, err := get(web.GetFullNodeUrl("index.json"))
abortOnError(err)
var data = make([]map[string]interface{}, 0)
abortOnError(json.Unmarshal([]byte(buf), &data))
lastLTS := notices.LastLTS()
lastCurrent := notices.LastCurrent()
installed := node.GetInstalled(root)
changes := 0
for _, value := range data {
switch value["lts"].(type) {
// LTS versions always have the lts attribute as a string
case string:
if reg.LTS {
versionDate, _ := time.Parse("2006-01-02", value["date"].(string))
if versionDate.After(lastLTS) && versionDate.After(time.Now().AddDate(0, -1, 0)) && !in(strings.Replace(value["version"].(string), "v", "", 1), installed) {
abortOnError(alertRelease(value))
changes++
}
}
// Current versions always have the lts attribute as a false boolean
case bool:
if reg.Current && !value["lts"].(bool) {
versionDate, _ := time.Parse("2006-01-02", value["date"].(string))
if versionDate.After(lastCurrent) && versionDate.After(time.Now().AddDate(0, -1, 0)) && !in(strings.Replace(value["version"].(string), "v", "", 1), installed) {
abortOnError(alertRelease(value))
changes++
}
}
}
}
if changes == 0 {
noupdate()
return
}
}
// Check for NVM for Windows updates
if reg.NVM4W {
buf, err := get("https://api.github.com/repos/coreybutler/nvm-windows/releases/latest")
abortOnError(err)
var data map[string]interface{}
abortOnError(json.Unmarshal([]byte(buf), &data))
current, err := semver.New(nvmversion)
abortOnError(err)
next, err := semver.New(data["tag_name"].(string))
abortOnError(err)
notices.NVM4W = current.String()
if !next.GT(current) {
alertNvmRelease(current, next, data)
notices.NVM4W = next.String()
}
}
now := time.Now().Format("2006-01-02")
if reg.LTS {
notices.LTS = now
}
if reg.Current {
notices.Current = now
}
}
func alertNvmRelease(current, next *semver.Version, data map[string]interface{}) {
exe, _ := os.Executable()
path := filepath.Dir(exe)
iconPath := filepath.Join(path, "nodejs.ico")
pubDate, _ := time.Parse("2006-01-02T15:04:05Z", data["published_at"].(string))
age := humanize.Time(pubDate)
notification := toast.Notification{
AppID: "NVM for Windows",
Title: "NVM for Windows Update Available",
Message: fmt.Sprintf("Version %s is was released %s.\n(currently using v%s)", next.String(), age, current.String()),
Icon: iconPath,
Actions: []toast.Action{
{"protocol", "Install", "nvm://launch?action=upgrade"},
{"protocol", "View", data["html_url"].(string)},
},
}
// Display the notification
err := notification.Push()
if err != nil {
abortOnError(err)
}
}
func in(item string, set []string) bool {
for _, i := range set {
if i == item {
return true
}
}
return false
}
func noupdate() {
fmt.Println("no new releases detected")
}
func UpgradeCompleteAlert(version string) {
exe, _ := os.Executable()
path := filepath.Dir(exe)
iconPath := filepath.Join(path, "checkmark.ico")
notification := toast.Notification{
AppID: "NVM for Windows",
Title: "Upgrade Complete",
Message: fmt.Sprintf("The upgrade to NVM for Windows v%s completed successfully.", version),
Icon: iconPath,
Actions: []toast.Action{
{"protocol", "Open PowerShell", "nvm://launch?action=open_terminal&amp;type=pwsh"},
{"protocol", "Open CMD Prompt", "nvm://launch?action=open_terminal&amp;type=cmd"},
},
}
// Display the notification
err := notification.Push()
if err != nil {
abortOnError(err)
}
}
func alertRelease(data map[string]interface{}) error {
version, err := semver.New(data["version"].(string))
if err != nil {
return err
}
exe, _ := os.Executable()
path := filepath.Dir(exe)
iconPath := filepath.Join(path, "nodejs.ico")
releaseName := ""
releaseDate, err := time.Parse("2006-01-02", data["date"].(string))
if err != nil {
return err
}
age := humanize.Time(releaseDate)
msg := fmt.Sprintf("with npm v%s & V8 v%s\nReleased %s.", data["npm"].(string), data["v8"].(string), age)
switch data["lts"].(type) {
case string:
releaseName = " (LTS " + data["lts"].(string) + ")"
}
if data["security"].(bool) {
msg += "\nThis is a security release."
}
title := fmt.Sprintf("Node.js v%s Available%s", version.String(), releaseName)
notification := toast.Notification{
AppID: "NVM for Windows",
Title: title,
Message: msg,
Icon: iconPath,
}
// Display the notification
err = notification.Push()
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,69 @@
package upgrade
import (
"encoding/json"
"os"
"path/filepath"
"time"
)
type LastNotification struct {
outpath string
LTS string `json:"lts,omitempty"`
Current string `json:"current,omitempty"`
NVM4W string `json:"nvm4w,omitempty"`
Author string `json:"author,omitempty"`
}
func LoadNotices() *LastNotification {
ln := &LastNotification{}
noticedata, err := os.ReadFile(ln.File())
if err != nil {
if !os.IsNotExist(err) {
abortOnError(err)
}
}
if noticedata != nil {
abortOnError(json.Unmarshal(noticedata, &ln))
}
return ln
}
func (ln *LastNotification) Path() string {
if ln.outpath == "" {
ln.outpath = filepath.Join(os.Getenv("APPDATA"), ".nvm")
}
return ln.outpath
}
func (ln *LastNotification) File() string {
return filepath.Join(ln.Path(), ".updates.json")
}
func (ln *LastNotification) Save() {
output, err := json.Marshal(ln)
abortOnError(err)
abortOnError(os.MkdirAll(ln.Path(), os.ModePerm))
abortOnError(os.WriteFile(ln.File(), output, os.ModePerm))
abortOnError(setHidden(ln.Path()))
}
func (ln *LastNotification) LastLTS() time.Time {
if ln.LTS == "" {
return time.Now()
}
t, _ := time.Parse("2006-01-02", ln.LTS)
return t
}
func (ln *LastNotification) LastCurrent() time.Time {
if ln.Current == "" {
return time.Now()
}
t, _ := time.Parse("2006-01-02", ln.Current)
return t
}

211
src/upgrade/register.go Normal file
View File

@@ -0,0 +1,211 @@
package upgrade
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
const (
NODE_LTS_SCHEDULE_NAME = "NVM for Windows Node.js LTS Update Check"
NODE_CURRENT_SCHEDULE_NAME = "NVM for Windows Node.js Current Update Check"
NVM4W_SCHEDULE_NAME = "NVM for Windows Update Check"
AUTHOR_SCHEDULE_NAME = "NVM for Windows Author Update Check"
)
type Registration struct {
LTS bool
Current bool
NVM4W bool
Author bool
}
func LoadRegistration(args ...string) *Registration {
reg := &Registration{
LTS: false,
Current: false,
NVM4W: false,
Author: false,
}
for _, arg := range args {
arg = strings.ToLower(strings.ReplaceAll(arg, "--", ""))
switch arg {
case "lts":
reg.LTS = true
case "current":
reg.Current = true
case "nvm4w":
reg.NVM4W = true
case "author":
reg.Author = true
}
}
return reg
}
func abortOnError(err error) {
if err != nil {
fmt.Println(err)
os.WriteFile("./error.log", []byte(err.Error()), os.ModePerm)
os.Exit(1)
}
}
func logError(err error) {
fmt.Println(err)
if err != nil {
os.WriteFile("./error.log", []byte(err.Error()), os.ModePerm)
}
}
func Register() {
reg := LoadRegistration(os.Args[2:]...)
exe, _ := os.Executable()
if reg.LTS {
abortOnError(ScheduleTask(NODE_LTS_SCHEDULE_NAME, fmt.Sprintf(`"%s" checkForUpdates lts`, exe), "HOURLY", "00:30"))
}
if reg.Current {
abortOnError(ScheduleTask(NODE_CURRENT_SCHEDULE_NAME, fmt.Sprintf(`"%s" checkForUpdates current`, exe), "HOURLY", "00:25"))
}
if reg.NVM4W {
abortOnError(ScheduleTask(NVM4W_SCHEDULE_NAME, fmt.Sprintf(`"%s" checkForUpdates nvm4w`, exe), "HOURLY", "00:15"))
}
if reg.Author {
abortOnError(ScheduleTask(AUTHOR_SCHEDULE_NAME, fmt.Sprintf(`"%s" checkForUpdates author`, exe), "HOURLY", "00:45"))
}
}
func Unregister() {
reg := LoadRegistration(os.Args[2:]...)
if reg.LTS {
abortOnError(UnscheduleTask(NODE_LTS_SCHEDULE_NAME))
}
if reg.Current {
abortOnError(UnscheduleTask(NODE_CURRENT_SCHEDULE_NAME))
}
if reg.NVM4W {
abortOnError(UnscheduleTask(NVM4W_SCHEDULE_NAME))
}
if reg.Author {
abortOnError(UnscheduleTask(AUTHOR_SCHEDULE_NAME))
}
}
// interval can be:
// MINUTE Runs the task every N minutes. Requires /MO (modifier) to specify the interval.
// HOURLY Runs the task every N hours. Requires /MO to specify the interval.
// DAILY Runs the task every N days. Requires /MO to specify the interval.
// WEEKLY Runs the task every N weeks. Requires /MO and /D (days of the week) to specify the schedule.
// MONTHLY Runs the task on specific days of the month. Requires /MO, /D, or /M for further specifics.
// ONCE Runs the task only once at the specified time. Requires /ST (start time).
// ONSTART Runs the task every time the system starts.
// ONLOGON Runs the task every time the user logs on.
// ONIDLE Runs the task when the system is idle for a specified amount of time.
// EVENT Runs the task when a specific event is triggered. Requires /EC (event log) and /ID (event ID).
func ScheduleTask(name string, command string, interval string, startTime ...string) error {
switch strings.ToUpper(interval) {
case "MINUTE":
fallthrough
case "HOURLY":
fallthrough
case "DAILY":
fallthrough
case "WEEKLY":
fallthrough
case "MONTHLY":
fallthrough
case "ONCE":
fallthrough
case "ONSTART":
fallthrough
case "ONLOGON":
fallthrough
case "ONIDLE":
fallthrough
case "EVENT":
interval = strings.ToUpper(interval)
default:
return fmt.Errorf("scheduling error: invalid interval %q", interval)
}
start := "00:00"
if len(startTime) > 0 {
start = startTime[0]
}
tmp, err := os.MkdirTemp("", "nvm4w-regitration-*")
if err != nil {
return fmt.Errorf("scheduling error: %v", err)
}
defer os.RemoveAll(tmp)
script := fmt.Sprintf(`
@echo off
set errorlog="error.log"
set output="%s\output.log"
schtasks /create /tn "%s" /tr "cmd.exe /c %s" /sc %s /st %s /F > %%output%% 2>&1
if not errorlevel 0 (
echo ERROR: Failed to create scheduled task: exit code: %%errorlevel%% >> %%errorlog%%
type %%output%% >> %%errorlog%%
exit /b %%errorlevel%%
)
`, tmp, name, escapeBackslashes(command), strings.ToLower(interval), start)
err = os.WriteFile(filepath.Join(tmp, "schedule.bat"), []byte(script), os.ModePerm)
if err != nil {
return fmt.Errorf("scheduling error: %v", err)
}
cmd := exec.Command(filepath.Join(tmp, "schedule.bat"))
// Capture standard output and standard error
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("scheduling error: %v\n%s", err, out)
}
// fmt.Sprintf(`"%s" task scheduled successfully!`, name)
return nil
}
func UnscheduleTask(name string) error {
tmp, err := os.MkdirTemp("", "nvm4w-registration-*")
if err != nil {
return fmt.Errorf("scheduling error: %v", err)
}
defer os.RemoveAll(tmp)
script := fmt.Sprintf(`
@echo off
set errorlog="error.log"
set output="%s\output.log"
schtasks /delete /tn "%s" /f > %%output%% 2>&1
if not errorlevel 0 (
echo failed to remove scheduled task: exit code: %%errorlevel%% >> %%errorlog%%
type %%output%% >> %%errorlog%%
exit /b %%errorlevel%%
)
`, tmp, name)
err = os.WriteFile(filepath.Join(tmp, "unschedule.bat"), []byte(script), os.ModePerm)
if err != nil {
return fmt.Errorf("unscheduling error: %v", err)
}
cmd := exec.Command(filepath.Join(tmp, "unschedule.bat"))
// Capture standard output and standard error
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("unscheduling error: %v\n%s", err, out)
}
return nil
}

View File

@@ -7,16 +7,20 @@ import (
"fmt"
"io"
"net/http"
"nvm/author"
"nvm/semver"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"sync"
"syscall"
"time"
"unsafe"
"github.com/coreybutler/go-fsutil"
"github.com/ncruces/zenity"
"golang.org/x/sys/windows"
)
@@ -36,6 +40,28 @@ const (
// exclamationIcon = "❗"
)
type Notification struct {
AppID string `json:"app_id"`
Title string `json:"title"`
Message string `json:"message"`
Icon string `json:"icon"`
Actions []Action `json:"actions"`
Duration string `json:"duration"`
Link string `json:"link"`
}
type Action struct {
Type string `json:"type"`
Label string `json:"label"`
URI string `json:"uri"`
}
func display(data Notification) {
data.AppID = "NVM for Windows"
content, _ := json.Marshal(data)
go author.Bridge("notify", string(content))
}
type Update struct {
Version string `json:"version"`
Assets []string `json:"assets"`
@@ -44,6 +70,442 @@ type Update struct {
SourceURL string `json:"sourceTpl"`
}
func Run(version string) error {
show_progress := false
for _, arg := range os.Args[2:] {
if strings.ToLower(arg) == "--show-progress-ui" {
show_progress = true
break
}
}
status := make(chan Status)
if !show_progress {
// Setup signal handling first
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(signalChan)
// Add signal handler
go func() {
<-signalChan
fmt.Println("Installation canceled by user")
os.Exit(0)
}()
return run(version, status)
}
wg := &sync.WaitGroup{}
wg.Add(1)
var dlg zenity.ProgressDialog
var exitCode = 0
var u *Update
// Display visual progress UI
go func() {
defer wg.Done()
for {
select {
case s := <-status:
if s.Cancel {
display(Notification{
Title: "Installation Canceled",
Message: fmt.Sprintf("Installation of NVM for Windows v%s was canceled by the user.", u.Version),
Icon: "error",
Actions: []Action{
{Type: "protocol", Label: "Install Again", URI: fmt.Sprintf("nvm://launch?action=upgrade&version=%s", u.Version)},
},
})
return
}
if s.Err != nil {
exitCode = 1
display(Notification{
Title: "Installation Error",
Message: s.Err.Error(),
Icon: "error",
})
dlg.Text(fmt.Sprintf("error: %v", s.Err))
dlg.Close()
// err := restore(status, verbose)
// if err != nil {
// if show_progress {
// display(notify.Notification{
// Title: "Rollback Error",
// Message: err.Error(),
// Icon: "error",
// })
// } else {
// fmt.Printf("rollback error: %v\n", err)
// }
// }
return
}
if s.Done {
display(Notification{
Title: "Upgrade Complete",
Message: fmt.Sprintf("Now running version %s.", u.Version),
Icon: "success",
})
dlg.Text("Upgrade complete")
time.Sleep(1 * time.Second)
return
}
if s.Warn != "" {
display(Notification{
Message: s.Warn,
Icon: "nvm",
})
}
var empty string
if s.Text != empty && len(strings.TrimSpace(s.Text)) > 0 {
dlg.Text(s.Text)
}
}
}
}()
// Wait for the prior subroutine to initialize before starting the next dependent thread
time.Sleep(300 * time.Millisecond)
// Run the update
go func() {
exe, _ := os.Executable()
winIco := filepath.Join(filepath.Dir(exe), "nvm.ico")
ico := filepath.Join(filepath.Dir(exe), "download.ico")
var err error
u, err = checkForUpdate(UPDATE_URL)
if err != nil {
display(Notification{
Title: "Update Error",
Message: err.Error(),
Icon: "error",
})
status <- Status{Err: fmt.Errorf("error: failed to obtain update data: %v\n", err)}
}
var perr error
dlg, perr = zenity.Progress(
zenity.Title(fmt.Sprintf("Installing NVM for Windows v%s", u.Version)),
zenity.Icon(ico),
zenity.WindowIcon(winIco),
zenity.AutoClose(),
zenity.NoCancel(),
zenity.Pulsate())
if perr != nil {
fmt.Println("Failed to create progress dialog")
}
go func() {
for {
select {
case <-dlg.Done():
if err := dlg.Complete(); err == zenity.ErrCanceled {
status <- Status{Cancel: true}
}
}
}
}()
status <- Status{Text: "Validating version..."}
run(version, status, u)
}()
wg.Wait()
os.Exit(exitCode)
return nil
}
func run(version string, status chan Status, updateMetadata ...*Update) error {
args := os.Args[2:]
colorize := true
if err := EnableVirtualTerminalProcessing(); err != nil {
colorize = false
}
// Retrieve remote metadata
var update *Update
if len(updateMetadata) > 0 {
update = updateMetadata[0]
} else {
var err error
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 {
status <- Status{Warn: warning}
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 {
status <- Status{Warn: warning}
Warn(warning, colorize)
}
fmt.Println("")
}
status <- Status{Text: "downloading..."}
fmt.Printf("upgrading from v%s-->%s\n\ndownloading...\n", version, highlight(update.Version))
} else {
status <- Status{Text: "nvm is up to date", Done: true}
fmt.Println("nvm is up to date")
return nil
}
// Make temp directory
tmp, err := os.MkdirTemp("", "nvm-upgrade-*")
if err != nil {
status <- Status{Err: err}
return fmt.Errorf("error: failed to create temporary directory: %v\n", err)
}
defer os.RemoveAll(tmp)
// Download the new app
source := fmt.Sprintf(update.SourceURL, update.Version)
// source := fmt.Sprintf(update.SourceURL, "1.1.11") // testing
body, err := get(source)
if err != nil {
status <- Status{Err: err}
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...")
status <- Status{Text: "verifying checksum..."}
computedChecksum, err := computeMD5Checksum(filePath)
if err != nil {
status <- Status{Err: err}
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 {
status <- Status{Err: err}
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) {
status <- Status{Err: fmt.Errorf("cannot validate update file (checksum mismatch)")}
return fmt.Errorf("cannot validate update file (checksum mismatch)")
}
fmt.Println("extracting update...")
status <- Status{Text: "extracting update..."}
if err := unzip(filepath.Join(tmp, "assets.zip"), filepath.Join(tmp, "assets")); err != nil {
status <- Status{Err: err}
return err
}
// Get any additional assets
if len(update.Assets) > 0 {
status <- Status{Text: fmt.Sprintf("downloading %d additional assets...", len(update.Assets))}
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 {
status <- Status{Err: err}
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...")
status <- Status{Text: "applying update..."}
currentExe, _ := os.Executable()
currentPath := filepath.Dir(currentExe)
bkp, err := os.MkdirTemp("", "nvm-backup-*")
if err != nil {
status <- Status{Err: fmt.Errorf("error: failed to create backup directory: %v\n", err)}
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 {
status <- Status{Err: fmt.Errorf("error: failed to create backup: %v\n", err)}
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 {
status <- Status{Err: err}
fmt.Println("error running nvm.exe:", err)
}
}
// Debugging
if verbose {
tree(currentPath, "final directory contents:")
}
// Hide the update directory
setHidden(filepath.Join(currentPath, ".update"))
// 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 {
status <- Status{Err: err}
fmt.Println(fmt.Errorf("error: failed to copy update.exe: %v\n", err))
os.Exit(1)
}
}
autoupdate(status)
return nil
}
type Status struct {
Text string
Err error
Done bool
Help bool
Cancel bool
Warn string
}
func (u *Update) Available(sinceVersion string) (string, bool, error) {
currentVersion, err := semver.New(sinceVersion)
if err != nil {
@@ -70,259 +532,14 @@ func Warn(msg string, colorized ...bool) {
}
}
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() {
func autoupdate(status chan Status) {
currentPath, err := os.Executable()
if err != nil {
status <- Status{Err: err}
fmt.Println("error getting updater path:", err)
os.Exit(1)
}
@@ -334,9 +551,12 @@ func autoupdate() {
// Temporary batch file that deletes the directory and the scheduled task
tmp, err := os.MkdirTemp("", "nvm4w-remove-*")
if err != nil {
status <- Status{Err: err}
fmt.Printf("error creating temporary directory: %v", err)
os.Exit(1)
}
// schedule removal of restoration folder for 30 days from now
tempBatchFile := filepath.Join(tmp, "remove_backup.bat")
now := time.Now()
futureDate := now.AddDate(0, 0, 7)
@@ -350,6 +570,7 @@ rmdir /s /q "%s"
// Write the batch file to a temporary location
err = os.WriteFile(tempBatchFile, []byte(batchContent), os.ModePerm)
if err != nil {
status <- Status{Err: err}
fmt.Printf("error creating temporary batch file: %v", err)
os.Exit(1)
}
@@ -422,11 +643,13 @@ echo Update complete >> error.log
del error.log
del "%%~f0"
start "nvm://launch?action=upgrade_notify"
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 {
status <- Status{Err: err}
fmt.Printf("error creating updater script: %v", err)
os.Exit(1)
}
@@ -435,12 +658,15 @@ exit /b 0
cmd := exec.Command(scriptPath, fmt.Sprintf("%d", os.Getpid()), filepath.Join(tempDir, ".update", "nvm.exe"), currentPath)
err = cmd.Start()
if err != nil {
status <- Status{Err: err}
fmt.Printf("error starting updater script: %v", err)
os.Exit(1)
}
// Exit the current process (delay for cleanup)
time.Sleep(300 * time.Millisecond)
status <- Status{Text: "Restarting app...", Done: true}
time.Sleep(2 * time.Second)
os.Exit(0)
}

View File

@@ -23,6 +23,7 @@ import (
fs "github.com/coreybutler/go-fsutil"
)
var nvmversion = ""
var client = &http.Client{}
var nodeBaseAddress = "https://nodejs.org/dist/"
var npmBaseAddress = "https://github.com/npm/cli/archive/"
@@ -130,8 +131,7 @@ func Download(url string, target string, version string) bool {
return false
}
// TODO: Add version to user agent
req.Header.Set("User-Agent", fmt.Sprintf("NVM for Windows %s", version))
req.Header.Set("User-Agent", fmt.Sprintf("NVM for Windows %s", nvmversion))
response, err := client.Do(req)
if err != nil {
@@ -166,7 +166,7 @@ func Download(url string, target string, version string) bool {
} else {
_, err = io.Copy(output, response.Body)
if err != nil {
fmt.Println("Error while downloading", url, "-", err)
fmt.Printf("Error while downloading %s: %v\n", url, err)
}
}