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  }