github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/ui/versionmanager/version_downloader.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 "io" 22 "net/http" 23 "net/url" 24 "os" 25 "strconv" 26 "sync" 27 "time" 28 29 "github.com/rs/zerolog/log" 30 ) 31 32 type dlStatus string 33 34 const ( 35 inProgress dlStatus = "in_progress" 36 failed dlStatus = "failed" 37 idle dlStatus = "idle" 38 done dlStatus = "done" 39 ) 40 41 // Downloader node UI downloader 42 type Downloader struct { 43 http *http.Client 44 lock sync.Mutex 45 46 progressUpdateEvery time.Duration 47 48 status Status 49 statusLock sync.Mutex 50 } 51 52 // NewDownloader constructor for Downloader 53 func NewDownloader() *Downloader { 54 return &Downloader{ 55 http: &http.Client{ 56 Timeout: time.Minute * 5, 57 }, 58 status: Status{Status: idle}, 59 progressUpdateEvery: time.Second, 60 } 61 } 62 63 // DownloadOpts download options 64 type DownloadOpts struct { 65 URL *url.URL 66 DistFile string 67 Tag string 68 Callback func(opts DownloadOpts) error 69 } 70 71 // DownloadNodeUI download node UI 72 func (d *Downloader) DownloadNodeUI(opts DownloadOpts) { 73 d.lock.Lock() 74 defer d.lock.Unlock() 75 76 if d.status.Status == inProgress { 77 log.Warn().Msg("node UI download in progress - skipping") 78 return 79 } 80 81 d.update(Status{Status: inProgress, Tag: opts.Tag}) 82 83 go d.download(opts) 84 } 85 86 // Status download status 87 // swagger:model DownloadStatus 88 type Status struct { 89 Status dlStatus `json:"status"` 90 ProgressPct int `json:"progress_percent"` 91 Tag string `json:"tag,omitempty"` 92 Err error `json:"error,omitempty"` 93 } 94 95 func (s Status) withPct(p int) Status { 96 s.ProgressPct = p 97 return s 98 } 99 100 func (s Status) transition(status dlStatus) Status { 101 s.Status = status 102 return s 103 } 104 105 func (s Status) transitionWithErr(status dlStatus, err error) Status { 106 s.Status = status 107 s.Err = err 108 return s 109 } 110 111 // Status current download status 112 func (d *Downloader) Status() Status { 113 return d.status 114 } 115 116 func (d *Downloader) download(opts DownloadOpts) { 117 head, err := d.http.Head(opts.URL.String()) 118 if err != nil { 119 d.update(d.status.transitionWithErr(failed, err)) 120 return 121 } 122 123 totalSize, err := strconv.Atoi(head.Header.Get("Content-Length")) 124 if err != nil { 125 d.update(d.status.transitionWithErr(failed, err)) 126 return 127 } 128 129 outFile, err := os.Create(opts.DistFile) 130 if err != nil { 131 d.update(d.status.transitionWithErr(failed, err)) 132 return 133 } 134 135 doneChan := make(chan struct{}) 136 go d.progressUpdater(doneChan, outFile, totalSize) 137 138 res, err := d.http.Get(opts.URL.String()) 139 if err != nil { 140 d.update(d.status.transitionWithErr(failed, err)) 141 return 142 } 143 defer res.Body.Close() 144 145 _, err = io.Copy(outFile, res.Body) 146 if err != nil { 147 d.update(d.status.transitionWithErr(failed, err)) 148 return 149 } 150 151 close(doneChan) 152 153 if opts.Callback != nil && d.status.Status != failed { 154 log.Info().Msg("executing post download callback") 155 err := opts.Callback(opts) 156 if err != nil { 157 d.update(d.status.transitionWithErr(failed, err)) 158 return 159 } 160 } else { 161 log.Warn().Msgf("download status is %s - skipping callback", d.status.Status) 162 } 163 164 d.update(d.status.transition(done)) 165 } 166 167 func (d *Downloader) progressUpdater(done chan struct{}, file *os.File, totalSize int) { 168 for { 169 select { 170 case <-done: 171 d.update(d.status.withPct(100)) 172 return 173 case <-time.After(d.progressUpdateEvery): 174 f, err := file.Stat() 175 if err != nil { 176 log.Error().Err(err).Msgf("failed to file.Stat()") 177 return 178 } 179 180 p := float64(f.Size()) / float64(totalSize) * 100 181 d.update(d.status.withPct(int(p))) 182 } 183 } 184 } 185 186 func (d *Downloader) update(s Status) { 187 d.statusLock.Lock() 188 defer d.statusLock.Unlock() 189 d.status = s 190 191 if s.Err != nil { 192 log.Error().Err(s.Err).Msgf("node UI download transitioned to %+v", s) 193 } else { 194 log.Info().Msgf("node UI download transitioned to %+v", s) 195 } 196 }