github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/ui/versionmanager/version_manager.go (about) 1 /* 2 * Copyright (C) 2021 The "MysteriumNetwork/node" Authors. 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 package versionmanager 19 20 import ( 21 "archive/tar" 22 "compress/gzip" 23 "fmt" 24 "io" 25 "os" 26 "path/filepath" 27 "time" 28 29 godvpnweb "github.com/mysteriumnetwork/go-dvpn-web/v2" 30 "github.com/mysteriumnetwork/node/requests" 31 "github.com/pkg/errors" 32 "github.com/rs/zerolog/log" 33 ) 34 35 // NodeUIServer interface with UI server 36 type NodeUIServer interface { 37 SwitchUI(path string) 38 } 39 40 // VersionManager Node UI version manager 41 type VersionManager struct { 42 uiServer NodeUIServer 43 httpClient *requests.HTTPClient 44 github *github 45 versionConfig NodeUIVersionConfig 46 47 remoteCacheExpiresAt time.Time 48 releasesCache []GitHubRelease 49 50 downloader *Downloader 51 } 52 53 // NewVersionManager VersionManager constructor 54 func NewVersionManager( 55 uiServer NodeUIServer, 56 http *requests.HTTPClient, 57 versionConfig NodeUIVersionConfig, 58 ) *VersionManager { 59 return &VersionManager{ 60 uiServer: uiServer, 61 httpClient: http, 62 versionConfig: versionConfig, 63 github: newGithub(http), 64 downloader: NewDownloader(), 65 } 66 } 67 68 // TODO check integrity of downloaded release so not to serve a broken nodeUI 69 70 // ListLocalVersions list downloaded Node UI versions 71 func (vm *VersionManager) ListLocalVersions() ([]LocalVersion, error) { 72 var versions = make([]LocalVersion, 0) 73 74 files, err := os.ReadDir(vm.versionConfig.uiDir()) 75 if err != nil { 76 if os.IsNotExist(err) { 77 return versions, nil 78 } 79 return nil, fmt.Errorf("could not read "+nodeUIPath+": %w", err) 80 } 81 82 for _, f := range files { 83 if f.IsDir() { 84 versions = append(versions, LocalVersion{ 85 Name: f.Name(), 86 }) 87 } 88 } 89 90 return versions, nil 91 } 92 93 // UsedVersion current version 94 func (vm *VersionManager) UsedVersion() (LocalVersion, error) { 95 version, err := vm.versionConfig.Version() 96 if err != nil { 97 return LocalVersion{}, err 98 } 99 return LocalVersion{ 100 Name: version, 101 }, nil 102 } 103 104 // BundledVersion bundled version 105 func (vm *VersionManager) BundledVersion() (LocalVersion, error) { 106 name, err := godvpnweb.Version() 107 if err != nil { 108 return LocalVersion{}, err 109 } 110 return LocalVersion{ 111 Name: name, 112 }, nil 113 } 114 115 // RemoteVersionRequest for paged requests 116 type RemoteVersionRequest struct { 117 PerPage int 118 Page int 119 FlushCache bool 120 } 121 122 // ListRemoteVersions list versions from github releases of NodeUI 123 func (vm *VersionManager) ListRemoteVersions(r RemoteVersionRequest) ([]RemoteVersion, error) { 124 if time.Now().Before(vm.remoteCacheExpiresAt) && vm.releasesCache != nil && !r.FlushCache { 125 return remoteVersions(vm.releasesCache), nil 126 } 127 128 releases, err := vm.github.nodeUIReleases(r.PerPage, r.Page) 129 if err != nil { 130 return nil, err 131 } 132 133 vm.releasesCache = releases 134 vm.remoteCacheExpiresAt = time.Now().Add(time.Hour) 135 136 return remoteVersions(vm.releasesCache), nil 137 } 138 139 func remoteVersions(releases []GitHubRelease) []RemoteVersion { 140 var versions = make([]RemoteVersion, 0) 141 for _, release := range releases { 142 versions = append(versions, RemoteVersion{ 143 Name: release.Name, 144 PublishedAt: release.PublishedAt, 145 CompatibilityURL: compatibilityAssetURL(release), 146 IsPreRelease: release.Prerelease, 147 ReleaseNotes: release.Body, 148 }) 149 } 150 return versions 151 } 152 153 func compatibilityAssetURL(r GitHubRelease) string { 154 for _, a := range r.Assets { 155 if a.Name == compatibilityAssetName { 156 return a.BrowserDownloadUrl 157 } 158 } 159 return "" 160 } 161 162 // TODO think about sending SSE to inform to nodeUI that a version has been downloaded 163 164 // Download and untar node UI dist 165 func (vm *VersionManager) Download(versionName string) error { 166 assetURL, err := vm.github.nodeUIDownloadURL(versionName) 167 if err != nil { 168 return err 169 } 170 171 err = os.MkdirAll(vm.versionConfig.uiDistPath(versionName), 0700) 172 if err != nil { 173 return err 174 } 175 176 vm.downloader.DownloadNodeUI(DownloadOpts{ 177 URL: assetURL, 178 Tag: versionName, 179 DistFile: vm.versionConfig.uiDistFile(versionName), 180 Callback: func(opts DownloadOpts) error { 181 return vm.untarAndExplode(versionName) 182 }, 183 }) 184 185 return nil 186 } 187 188 // SwitchTo switch to serving specific version 189 func (vm *VersionManager) SwitchTo(versionName string) error { 190 log.Info().Msgf("Switching node UI to version: %s", versionName) 191 192 if versionName == BundledVersionName { 193 if err := vm.versionConfig.write(nodeUIVersion{VersionName: BundledVersionName}); err != nil { 194 return err 195 } 196 vm.uiServer.SwitchUI(BundledVersionName) 197 return nil 198 } 199 200 local, err := vm.ListLocalVersions() 201 if err != nil { 202 return err 203 } 204 for _, lv := range local { 205 if lv.Name == versionName { 206 if err := vm.versionConfig.write(nodeUIVersion{VersionName: versionName}); err != nil { 207 return err 208 } 209 vm.uiServer.SwitchUI(vm.versionConfig.UIBuildPath(versionName)) 210 return nil 211 } 212 } 213 214 return errors.New("no local version named: " + versionName) 215 } 216 217 func (vm *VersionManager) untarAndExplode(versionName string) error { 218 file, err := os.Open(vm.versionConfig.uiDistFile(versionName)) 219 if err != nil { 220 return fmt.Errorf("failed to open file: %w", err) 221 } 222 defer file.Close() 223 224 err = untar(vm.versionConfig.uiDistPath(versionName), file) 225 if err != nil { 226 return fmt.Errorf("failed to untar nodeUI dist: %w", err) 227 } 228 229 return nil 230 } 231 232 // DownloadStatus provides download status 233 func (vm *VersionManager) DownloadStatus() Status { 234 return vm.downloader.Status() 235 } 236 237 // LocalVersion it's a local version with extra indicator if it is in use 238 type LocalVersion struct { 239 Name string `json:"name"` 240 } 241 242 // RemoteVersion it's a version 243 type RemoteVersion struct { 244 Name string `json:"name"` 245 PublishedAt time.Time `json:"released_at"` 246 CompatibilityURL string `json:"compatibility_url,omitempty"` 247 IsPreRelease bool `json:"is_pre_release"` 248 ReleaseNotes string `json:"release_notes,omitempty"` 249 } 250 251 func untar(dst string, r io.Reader) error { 252 gzr, err := gzip.NewReader(r) 253 if err != nil { 254 return err 255 } 256 defer gzr.Close() 257 258 tr := tar.NewReader(gzr) 259 260 for { 261 header, err := tr.Next() 262 263 switch { 264 case err == io.EOF: 265 return nil 266 267 case err != nil: 268 return err 269 270 case header == nil: 271 continue 272 } 273 274 target := filepath.Join(dst, header.Name) 275 276 switch header.Typeflag { 277 278 case tar.TypeDir: 279 if _, err := os.Stat(target); err != nil { 280 if err := os.MkdirAll(target, 0700); err != nil { 281 return err 282 } 283 } 284 285 case tar.TypeReg: 286 f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) 287 if err != nil { 288 return err 289 } 290 291 if _, err := io.Copy(f, tr); err != nil { 292 return err 293 } 294 295 f.Close() 296 } 297 } 298 }