mirror of
https://github.com/TeaOSLab/EdgeAdmin.git
synced 2025-11-29 16:10:26 +08:00
增加edge-admin upgrade命令
This commit is contained in:
292
internal/utils/upgrade_manager.go
Normal file
292
internal/utils/upgrade_manager.go
Normal file
@@ -0,0 +1,292 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
stringutil "github.com/iwind/TeaGo/utils/string"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UpgradeFileWriter struct {
|
||||
rawWriter io.Writer
|
||||
written int64
|
||||
}
|
||||
|
||||
func NewUpgradeFileWriter(rawWriter io.Writer) *UpgradeFileWriter {
|
||||
return &UpgradeFileWriter{rawWriter: rawWriter}
|
||||
}
|
||||
|
||||
func (this *UpgradeFileWriter) Write(p []byte) (n int, err error) {
|
||||
n, err = this.rawWriter.Write(p)
|
||||
this.written += int64(n)
|
||||
return
|
||||
}
|
||||
|
||||
func (this *UpgradeFileWriter) TotalWritten() int64 {
|
||||
return this.written
|
||||
}
|
||||
|
||||
type UpgradeManager struct {
|
||||
client *http.Client
|
||||
|
||||
component string
|
||||
|
||||
newVersion string
|
||||
contentLength int64
|
||||
isDownloading bool
|
||||
writer *UpgradeFileWriter
|
||||
body io.ReadCloser
|
||||
isCancelled bool
|
||||
}
|
||||
|
||||
func NewUpgradeManager(component string) *UpgradeManager {
|
||||
return &UpgradeManager{
|
||||
component: component,
|
||||
client: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
CheckRedirect: nil,
|
||||
Jar: nil,
|
||||
Timeout: 30 * time.Minute,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (this *UpgradeManager) Start() error {
|
||||
if this.isDownloading {
|
||||
return errors.New("another process is running")
|
||||
}
|
||||
|
||||
this.isDownloading = true
|
||||
|
||||
defer func() {
|
||||
this.client.CloseIdleConnections()
|
||||
this.isDownloading = false
|
||||
}()
|
||||
|
||||
// 检查unzip
|
||||
unzipExe, _ := exec.LookPath("unzip")
|
||||
if len(unzipExe) == 0 {
|
||||
// TODO install unzip automatically or pack with a static 'unzip' file
|
||||
return errors.New("can not find 'unzip' command")
|
||||
}
|
||||
|
||||
// 检查cp
|
||||
cpExe, _ := exec.LookPath("cp")
|
||||
if len(cpExe) == 0 {
|
||||
return errors.New("can not find 'cp' command")
|
||||
}
|
||||
|
||||
// 检查新版本
|
||||
var downloadURL = ""
|
||||
{
|
||||
var url = teaconst.UpdatesURL
|
||||
var osName = runtime.GOOS
|
||||
if Tea.IsTesting() && osName == "darwin" {
|
||||
osName = "linux"
|
||||
}
|
||||
url = strings.ReplaceAll(url, "${os}", osName)
|
||||
url = strings.ReplaceAll(url, "${arch}", runtime.GOARCH)
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return errors.New("create url request failed: " + err.Error())
|
||||
}
|
||||
|
||||
resp, err := this.client.Do(req)
|
||||
if err != nil {
|
||||
return errors.New("read latest version failed: " + err.Error())
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New("read latest version failed: invalid response code '" + types.String(resp.StatusCode) + "'")
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errors.New("read latest version failed: " + err.Error())
|
||||
}
|
||||
|
||||
var m = maps.Map{}
|
||||
err = json.Unmarshal(data, &m)
|
||||
if err != nil {
|
||||
return errors.New("invalid response data: " + err.Error() + ", origin data: " + string(data))
|
||||
}
|
||||
|
||||
var code = m.GetInt("code")
|
||||
if code != 200 {
|
||||
return errors.New(m.GetString("message"))
|
||||
}
|
||||
|
||||
var dataMap = m.GetMap("data")
|
||||
var downloadHost = dataMap.GetString("host")
|
||||
var versions = dataMap.GetSlice("versions")
|
||||
var downloadPath = ""
|
||||
for _, component := range versions {
|
||||
var componentMap = maps.NewMap(component)
|
||||
if componentMap.Has("version") {
|
||||
if componentMap.GetString("code") == this.component {
|
||||
var version = componentMap.GetString("version")
|
||||
if stringutil.VersionCompare(version, teaconst.Version) > 0 {
|
||||
this.newVersion = version
|
||||
downloadPath = componentMap.GetString("url")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(downloadPath) == 0 {
|
||||
return errors.New("no latest version to download")
|
||||
}
|
||||
|
||||
downloadURL = downloadHost + downloadPath
|
||||
}
|
||||
|
||||
{
|
||||
req, err := http.NewRequest(http.MethodGet, downloadURL, nil)
|
||||
if err != nil {
|
||||
return errors.New("create download request failed: " + err.Error())
|
||||
}
|
||||
|
||||
resp, err := this.client.Do(req)
|
||||
if err != nil {
|
||||
return errors.New("download failed: " + downloadURL + ": " + err.Error())
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New("download failed: " + downloadURL + ": invalid response code '" + types.String(resp.StatusCode) + "'")
|
||||
}
|
||||
|
||||
this.contentLength = resp.ContentLength
|
||||
this.body = resp.Body
|
||||
|
||||
// download to tmp
|
||||
var tmpDir = os.TempDir()
|
||||
var filename = filepath.Base(downloadURL)
|
||||
|
||||
var destFile = tmpDir + "/" + filename
|
||||
_ = os.Remove(destFile)
|
||||
|
||||
fp, err := os.Create(destFile)
|
||||
if err != nil {
|
||||
return errors.New("create file failed: " + err.Error())
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// 删除安装文件
|
||||
_ = os.Remove(destFile)
|
||||
}()
|
||||
|
||||
this.writer = NewUpgradeFileWriter(fp)
|
||||
|
||||
_, err = io.Copy(this.writer, resp.Body)
|
||||
if err != nil {
|
||||
_ = fp.Close()
|
||||
if this.isCancelled {
|
||||
return nil
|
||||
}
|
||||
return errors.New("download failed: " + err.Error())
|
||||
}
|
||||
|
||||
_ = fp.Close()
|
||||
|
||||
// unzip
|
||||
var unzipDir = tmpDir + "/edge-" + this.component + "-tmp"
|
||||
stat, err := os.Stat(unzipDir)
|
||||
if err == nil && stat.IsDir() {
|
||||
err = os.RemoveAll(unzipDir)
|
||||
if err != nil {
|
||||
return errors.New("remove old dir '" + unzipDir + "' failed: " + err.Error())
|
||||
}
|
||||
}
|
||||
var unzipCmd = exec.Command(unzipExe, "-q", "-o", destFile, "-d", unzipDir)
|
||||
var unzipStderr = &bytes.Buffer{}
|
||||
unzipCmd.Stderr = unzipStderr
|
||||
err = unzipCmd.Run()
|
||||
if err != nil {
|
||||
return errors.New("unzip installation file failed: " + err.Error() + ": " + unzipStderr.String())
|
||||
}
|
||||
|
||||
installationFiles, err := filepath.Glob(unzipDir + "/edge-" + this.component + "/*")
|
||||
if err != nil {
|
||||
return errors.New("lookup installation files failed: " + err.Error())
|
||||
}
|
||||
|
||||
// cp to target dir
|
||||
currentExe, err := os.Executable()
|
||||
if err != nil {
|
||||
return errors.New("reveal current executable file path failed: " + err.Error())
|
||||
}
|
||||
var targetDir = filepath.Dir(filepath.Dir(currentExe))
|
||||
if !Tea.IsTesting() {
|
||||
for _, installationFile := range installationFiles {
|
||||
var cpCmd = exec.Command(cpExe, "-R", "-f", installationFile, targetDir)
|
||||
var cpStderr = &bytes.Buffer{}
|
||||
cpCmd.Stderr = cpStderr
|
||||
err = cpCmd.Run()
|
||||
if err != nil {
|
||||
return errors.New("overwrite installation files failed: '" + cpCmd.String() + "': " + cpStderr.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove tmp
|
||||
_ = os.RemoveAll(unzipDir)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *UpgradeManager) IsDownloading() bool {
|
||||
return this.isDownloading
|
||||
}
|
||||
|
||||
func (this *UpgradeManager) Progress() float32 {
|
||||
if this.contentLength <= 0 {
|
||||
return -1
|
||||
}
|
||||
if this.writer == nil {
|
||||
return -1
|
||||
}
|
||||
return float32(this.writer.TotalWritten()) / float32(this.contentLength)
|
||||
}
|
||||
|
||||
func (this *UpgradeManager) NewVersion() string {
|
||||
return this.newVersion
|
||||
}
|
||||
|
||||
func (this *UpgradeManager) Cancel() error {
|
||||
this.isCancelled = true
|
||||
this.isDownloading = false
|
||||
|
||||
if this.body != nil {
|
||||
_ = this.body.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user