package upgrader import ( "encoding/json" "fmt" _github "github.com/0xJacky/Nginx-UI/.github" "github.com/0xJacky/Nginx-UI/internal/helper" "github.com/0xJacky/Nginx-UI/settings" "github.com/jpillora/overseer" "github.com/minio/selfupdate" "github.com/pkg/errors" "github.com/uozi-tech/cosy/logger" "io" "net/http" "net/url" "os" "path/filepath" "strconv" "strings" "sync/atomic" ) type Upgrader struct { Release TRelease RuntimeInfo } func NewUpgrader(channel string) (u *Upgrader, err error) { data, err := GetRelease(channel) if err != nil { return } runtimeInfo, err := GetRuntimeInfo() if err != nil { return } u = &Upgrader{ Release: data, RuntimeInfo: runtimeInfo, } return } type ProgressWriter struct { io.Writer totalSize int64 currentSize int64 progressChan chan<- float64 } func (pw *ProgressWriter) Write(p []byte) (int, error) { n, err := pw.Writer.Write(p) pw.currentSize += int64(n) progress := float64(pw.currentSize) / float64(pw.totalSize) * 100 pw.progressChan <- progress return n, err } func downloadRelease(url string, dir string, progressChan chan float64) (tarName string, err error) { client := &http.Client{} req, err := http.NewRequest("GET", url, nil) if err != nil { return } resp, err := client.Do(req) if err != nil { return } defer resp.Body.Close() totalSize, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64) if err != nil { return } file, err := os.CreateTemp(dir, "nginx-ui-temp-*.tar.gz") if err != nil { err = errors.Wrap(err, "service.DownloadLatestRelease CreateTemp error") return } defer file.Close() progressWriter := &ProgressWriter{Writer: file, totalSize: totalSize, progressChan: progressChan} multiWriter := io.MultiWriter(progressWriter) _, err = io.Copy(multiWriter, resp.Body) close(progressChan) tarName = file.Name() return } func (u *Upgrader) DownloadLatestRelease(progressChan chan float64) (tarName string, err error) { bytes, err := _github.DistFS.ReadFile("build/build_info.json") if err != nil { err = errors.Wrap(err, "service.DownloadLatestRelease Read build_info.json error") return } type buildArch struct { Arch string `json:"arch"` Name string `json:"name"` } var buildJson map[string]map[string]buildArch _ = json.Unmarshal(bytes, &buildJson) build, ok := buildJson[u.OS] if !ok { err = errors.Wrap(err, "os not support upgrade") return } arch, ok := build[u.Arch] if !ok { err = errors.Wrap(err, "arch not support upgrade") return } assetsMap := u.Release.GetAssetsMap() // asset asset, ok := assetsMap[fmt.Sprintf("nginx-ui-%s.tar.gz", arch.Name)] if !ok { err = errors.Wrap(err, "upgrader core asset is empty") return } downloadUrl := asset.BrowserDownloadUrl if downloadUrl == "" { err = errors.New("upgrader core downloadUrl is empty") return } // digest digest, ok := assetsMap[fmt.Sprintf("nginx-ui-%s.tar.gz.digest", arch.Name)] if !ok || digest.BrowserDownloadUrl == "" { err = errors.New("upgrader core digest is empty") return } githubProxy := settings.HTTPSettings.GithubProxy if githubProxy != "" { digest.BrowserDownloadUrl, err = url.JoinPath(githubProxy, digest.BrowserDownloadUrl) if err != nil { err = errors.Wrap(err, "service.DownloadLatestRelease url.JoinPath error") return } } resp, err := http.Get(digest.BrowserDownloadUrl) if err != nil { err = errors.Wrap(err, "upgrader core download digest fail") return } defer resp.Body.Close() dir := filepath.Dir(u.ExPath) if githubProxy != "" { downloadUrl, err = url.JoinPath(githubProxy, downloadUrl) if err != nil { err = errors.Wrap(err, "service.DownloadLatestRelease url.JoinPath error") return } } tarName, err = downloadRelease(downloadUrl, dir, progressChan) if err != nil { err = errors.Wrap(err, "service.DownloadLatestRelease downloadFile error") return } // check tar digest digestFileBytes, err := io.ReadAll(resp.Body) if err != nil { err = errors.Wrap(err, "digest file content read error") return } digestFileContent := strings.TrimSpace(string(digestFileBytes)) logger.Debug("DownloadLatestRelease tar digest", helper.DigestSHA512(tarName)) logger.Debug("DownloadLatestRelease digestFileContent", digestFileContent) if digestFileContent == "" { err = errors.New("digest file content is empty") return } exeSHA512 := helper.DigestSHA512(tarName) if exeSHA512 == "" { err = errors.New("executable binary file is empty") return } if digestFileContent != exeSHA512 { err = errors.Wrap(err, "digest not equal") return } return } var updateInProgress atomic.Bool func (u *Upgrader) PerformCoreUpgrade(tarPath string) (err error) { if !updateInProgress.CompareAndSwap(false, true) { return errors.New("update already in progress") } defer updateInProgress.Store(false) opts := selfupdate.Options{} if err = opts.CheckPermissions(); err != nil { return err } tempDir, err := os.MkdirTemp("", "nginx-ui-upgrade-*") if err != nil { return err } defer os.RemoveAll(tempDir) err = helper.UnTar(tempDir, tarPath) if err != nil { err = errors.Wrap(err, "PerformCoreUpgrade unTar error") return } f, err := os.Open(filepath.Join(tempDir, "nginx-ui")) if err != nil { err = errors.Wrap(err, "PerformCoreUpgrade open error") return } defer f.Close() if err = selfupdate.PrepareAndCheckBinary(f, opts); err != nil { var pathErr *os.PathError if errors.As(err, &pathErr) { return pathErr.Err } return err } if err = selfupdate.CommitBinary(opts); err != nil { if rerr := selfupdate.RollbackError(err); rerr != nil { return rerr } var pathErr *os.PathError if errors.As(err, &pathErr) { return pathErr.Err } return err } // gracefully restart overseer.Restart() return }