nginx-ui/internal/upgrader/upgrade.go
2023-11-26 18:59:12 +08:00

355 lines
8 KiB
Go

package upgrader
import (
"encoding/json"
"fmt"
_github "github.com/0xJacky/Nginx-UI/.github"
"github.com/0xJacky/Nginx-UI/app"
helper2 "github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/logger"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/pkg/errors"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
)
const (
GithubLatestReleaseAPI = "https://api.github.com/repos/0xJacky/nginx-ui/releases/latest"
GithubReleasesListAPI = "https://api.github.com/repos/0xJacky/nginx-ui/releases"
)
type RuntimeInfo struct {
OS string `json:"os"`
Arch string `json:"arch"`
ExPath string `json:"ex_path"`
}
func GetRuntimeInfo() (r RuntimeInfo, err error) {
ex, err := os.Executable()
if err != nil {
err = errors.Wrap(err, "service.GetRuntimeInfo os.Executable() err")
return
}
realPath, err := filepath.EvalSymlinks(ex)
if err != nil {
err = errors.Wrap(err, "service.GetRuntimeInfo filepath.EvalSymlinks() err")
return
}
r = RuntimeInfo{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
ExPath: realPath,
}
return
}
type TReleaseAsset struct {
Name string `json:"name"`
BrowserDownloadUrl string `json:"browser_download_url"`
Size uint `json:"size"`
}
type TRelease struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
PublishedAt time.Time `json:"published_at"`
Body string `json:"body"`
Prerelease bool `json:"prerelease"`
Assets []TReleaseAsset `json:"assets"`
}
func (t *TRelease) GetAssetsMap() (m map[string]TReleaseAsset) {
m = make(map[string]TReleaseAsset)
for _, v := range t.Assets {
m[v.Name] = v
}
return
}
func getLatestRelease() (data TRelease, err error) {
resp, err := http.Get(GithubLatestReleaseAPI)
if err != nil {
err = errors.Wrap(err, "service.getLatestRelease http.Get err")
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
err = errors.Wrap(err, "service.getLatestRelease io.ReadAll err")
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
err = errors.New(string(body))
return
}
err = json.Unmarshal(body, &data)
if err != nil {
err = errors.Wrap(err, "service.getLatestRelease json.Unmarshal err")
return
}
return
}
func getLatestPrerelease() (data TRelease, err error) {
resp, err := http.Get(GithubReleasesListAPI)
if err != nil {
err = errors.Wrap(err, "service.getLatestPrerelease http.Get err")
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
err = errors.Wrap(err, "service.getLatestPrerelease io.ReadAll err")
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
err = errors.New(string(body))
return
}
var releaseList []TRelease
err = json.Unmarshal(body, &releaseList)
if err != nil {
err = errors.Wrap(err, "service.getLatestPrerelease json.Unmarshal err")
return
}
latestDate := time.Time{}
for _, release := range releaseList {
if release.Prerelease && release.PublishedAt.After(latestDate) {
data = release
latestDate = release.PublishedAt
}
}
return
}
func GetRelease(channel string) (data TRelease, err error) {
switch channel {
default:
fallthrough
case "stable":
return getLatestRelease()
case "prerelease":
return getLatestPrerelease()
}
}
type CurVersion struct {
Version string `json:"version"`
BuildID int `json:"build_id"`
TotalBuild int `json:"total_build"`
}
func GetCurrentVersion() (c CurVersion, err error) {
verJson, err := app.DistFS.ReadFile("dist/version.json")
if err != nil {
err = errors.Wrap(err, "service.GetCurrentVersion ReadFile err")
return
}
err = json.Unmarshal(verJson, &c)
if err != nil {
err = errors.Wrap(err, "service.GetCurrentVersion json.Unmarshal err")
return
}
return
}
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
}
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 settings.ServerSettings.GithubProxy != "" {
downloadUrl, err = url.JoinPath(settings.ServerSettings.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, "digestFileContent read error")
return
}
digestFileContent := strings.TrimSpace(string(digestFileBytes))
logger.Debug("DownloadLatestRelease tar digest", helper2.DigestSHA512(tarName))
logger.Debug("DownloadLatestRelease digestFileContent", digestFileContent)
if digestFileContent != helper2.DigestSHA512(tarName) {
err = errors.Wrap(err, "digest not equal")
return
}
return
}
func (u *Upgrader) PerformCoreUpgrade(exPath string, tarPath string) (err error) {
dir := filepath.Dir(exPath)
err = helper2.UnTar(dir, tarPath)
if err != nil {
err = errors.Wrap(err, "PerformCoreUpgrade unTar error")
return
}
err = os.Rename(filepath.Join(dir, "nginx-ui"), exPath)
if err != nil {
err = errors.Wrap(err, "PerformCoreUpgrade rename error")
return
}
err = os.Remove(tarPath)
if err != nil {
err = errors.Wrap(err, "PerformCoreUpgrade remove tar error")
return
}
return
}