mirror of
https://github.com/gowvp/gb28181.git
synced 2026-04-22 15:07:10 +08:00
Implement OTA functionality for version checking
This commit is contained in:
+2
-1
@@ -42,4 +42,5 @@ cover/
|
|||||||
*.jpg
|
*.jpg
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pt
|
*.pt
|
||||||
*.onnx
|
*.onnx
|
||||||
|
*.remember/
|
||||||
+1
-1
@@ -59,7 +59,7 @@ def setup_logging(level_str: str = "INFO", retention_days: int = 3):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 设置后缀格式,例如 app.log.2023-12-31
|
# 设置后缀格式,例如 app.log.2023-12-31
|
||||||
file_handler.suffix = "%Y-%m-%d"
|
file_handler.suffix = "%Y-%m-%d.log"
|
||||||
file_handler.setFormatter(formatter)
|
file_handler.setFormatter(formatter)
|
||||||
root_logger.addHandler(file_handler)
|
root_logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
|||||||
+15
-5
@@ -2,10 +2,18 @@
|
|||||||
Debug = false
|
Debug = false
|
||||||
# rtmp 推流秘钥
|
# rtmp 推流秘钥
|
||||||
RTMPSecret = '123'
|
RTMPSecret = '123'
|
||||||
|
# 登录用户名
|
||||||
Username = 'admin'
|
Username = 'admin'
|
||||||
|
# 登录密码
|
||||||
Password = 'admin'
|
Password = 'admin'
|
||||||
|
|
||||||
|
# ai 分析服务
|
||||||
|
[Server.AI]
|
||||||
|
# 是否禁用 ai 分析服务
|
||||||
|
Disabled = false
|
||||||
|
# 保留天数
|
||||||
|
RetainDays = 0
|
||||||
|
|
||||||
# 对外提供的服务,建议由 nginx 代理
|
# 对外提供的服务,建议由 nginx 代理
|
||||||
[Server.HTTP]
|
[Server.HTTP]
|
||||||
# http 端口
|
# http 端口
|
||||||
@@ -13,7 +21,7 @@
|
|||||||
# 请求超时时间
|
# 请求超时时间
|
||||||
Timeout = '1m0s'
|
Timeout = '1m0s'
|
||||||
# jwt 秘钥,空串时,每次启动程序将随机赋值
|
# jwt 秘钥,空串时,每次启动程序将随机赋值
|
||||||
JwtSecret = ''
|
JwtSecret = '6caOiETMs8SPWNHgEKA1Jhmn9wxpjAj9'
|
||||||
|
|
||||||
[Server.HTTP.PProf]
|
[Server.HTTP.PProf]
|
||||||
# 是否启用 pprof, 建议设置为 true
|
# 是否启用 pprof, 建议设置为 true
|
||||||
@@ -22,7 +30,7 @@
|
|||||||
AccessIps = ['::1', '127.0.0.1']
|
AccessIps = ['::1', '127.0.0.1']
|
||||||
|
|
||||||
[Data]
|
[Data]
|
||||||
# 数据库支持 sqlite/postgres/mysql ,使用 sqlite 时 dsn 应当填写文件存储路径
|
# 数据库支持 sqlite/postgres/mysql, 使用 sqlite 时 dsn 应当填写文件存储路径
|
||||||
# postgres://postgres:123456@127.0.0.1:5432/gb28181?sslmode=disable
|
# postgres://postgres:123456@127.0.0.1:5432/gb28181?sslmode=disable
|
||||||
# mysql://root:123456@127.0.0.1:5432/gb28181?sslmode=disable
|
# mysql://root:123456@127.0.0.1:5432/gb28181?sslmode=disable
|
||||||
[Data.Database]
|
[Data.Database]
|
||||||
@@ -61,9 +69,11 @@
|
|||||||
HTTPPort = 8080
|
HTTPPort = 8080
|
||||||
# 媒体服务器密钥
|
# 媒体服务器密钥
|
||||||
Secret = 'jvRqCAzEg7AszBi4gm1cfhwXpmnVmJMG'
|
Secret = 'jvRqCAzEg7AszBi4gm1cfhwXpmnVmJMG'
|
||||||
|
# 媒体服务器类型 zlm/lalmax
|
||||||
|
Type = 'zlm'
|
||||||
# 用于流媒体 webhook 回调
|
# 用于流媒体 webhook 回调
|
||||||
WebHookIP = '192.168.10.10'
|
WebHookIP = '192.168.1.3'
|
||||||
# 媒体服务器 RTP 端口范围
|
# 媒体服务器 RTP 端口范围
|
||||||
RTPPortRange = '20000-20100'
|
RTPPortRange = '20000-20100'
|
||||||
# 媒体服务器 SDP IP
|
# 媒体服务器 SDP IP
|
||||||
SDPIP = '192.168.10.10'
|
SDPIP = '192.168.1.3'
|
||||||
+1
-1
@@ -3,7 +3,7 @@ services:
|
|||||||
# 如果拉不到 docker hub 镜像,也可以尝试
|
# 如果拉不到 docker hub 镜像,也可以尝试
|
||||||
# registry.cn-shanghai.aliyuncs.com/ixugo/homenvr:latest
|
# registry.cn-shanghai.aliyuncs.com/ixugo/homenvr:latest
|
||||||
image: gospace/gowvp:latest
|
image: gospace/gowvp:latest
|
||||||
restart: unless-stopped
|
restart: always
|
||||||
# linux 解开下行注释,并将 ports 全部注释
|
# linux 解开下行注释,并将 ports 全部注释
|
||||||
# network_mode: host
|
# network_mode: host
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/gin-contrib/gzip"
|
"github.com/gin-contrib/gzip"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gowvp/owl/internal/core/sms"
|
"github.com/gowvp/owl/internal/core/sms"
|
||||||
|
"github.com/gowvp/owl/pkg/ota"
|
||||||
"github.com/gowvp/owl/plugin/stat"
|
"github.com/gowvp/owl/plugin/stat"
|
||||||
"github.com/gowvp/owl/plugin/stat/statapi"
|
"github.com/gowvp/owl/plugin/stat/statapi"
|
||||||
"github.com/ixugo/goddd/domain/version/versionapi"
|
"github.com/ixugo/goddd/domain/version/versionapi"
|
||||||
@@ -94,6 +95,8 @@ func setupRouter(r *gin.Engine, uc *Usecase) {
|
|||||||
auth := web.AuthMiddleware(uc.Conf.Server.HTTP.JwtSecret)
|
auth := web.AuthMiddleware(uc.Conf.Server.HTTP.JwtSecret)
|
||||||
r.GET("/health", web.WrapH(uc.getHealth))
|
r.GET("/health", web.WrapH(uc.getHealth))
|
||||||
r.GET("/app/metrics/api", web.WrapH(uc.getMetricsAPI))
|
r.GET("/app/metrics/api", web.WrapH(uc.getMetricsAPI))
|
||||||
|
r.GET("/app/version/check", web.WrapH(uc.checkVersion))
|
||||||
|
r.POST("/app/upgrade", auth, uc.upgradeApp)
|
||||||
|
|
||||||
versionapi.Register(r, uc.Version, auth)
|
versionapi.Register(r, uc.Version, auth)
|
||||||
statapi.Register(r)
|
statapi.Register(r)
|
||||||
@@ -202,6 +205,109 @@ func sortExpvarMap(data *expvar.Map, top int) []KV {
|
|||||||
return kvs[:idx]
|
return kvs[:idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const repoName = "gowvp/owl"
|
||||||
|
|
||||||
|
type checkVersionOutput struct {
|
||||||
|
HasNewVersion bool `json:"has_new_version"`
|
||||||
|
CurrentVersion string `json:"current_version"`
|
||||||
|
NewVersion string `json:"new_version"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkVersion 检查是否有新版本
|
||||||
|
// 通过 GitHub API 获取最新 release 信息,与当前版本比较
|
||||||
|
func (uc *Usecase) checkVersion(_ *gin.Context, _ *struct{}) (checkVersionOutput, error) {
|
||||||
|
currentVersion := uc.Conf.BuildVersion
|
||||||
|
newVersion, body, err := ota.GetLastVersion(repoName)
|
||||||
|
if err != nil {
|
||||||
|
return checkVersionOutput{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNew := compareVersion(currentVersion, newVersion) < 0
|
||||||
|
|
||||||
|
return checkVersionOutput{
|
||||||
|
HasNewVersion: hasNew,
|
||||||
|
CurrentVersion: currentVersion,
|
||||||
|
NewVersion: newVersion,
|
||||||
|
Description: body,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// compareVersion 比较两个版本号
|
||||||
|
// 返回值: -1 表示 v1 < v2, 0 表示相等, 1 表示 v1 > v2
|
||||||
|
func compareVersion(v1, v2 string) int {
|
||||||
|
v1 = strings.TrimPrefix(v1, "v")
|
||||||
|
v2 = strings.TrimPrefix(v2, "v")
|
||||||
|
|
||||||
|
parts1 := strings.Split(v1, ".")
|
||||||
|
parts2 := strings.Split(v2, ".")
|
||||||
|
|
||||||
|
maxLen := len(parts1)
|
||||||
|
if len(parts2) > maxLen {
|
||||||
|
maxLen = len(parts2)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < maxLen; i++ {
|
||||||
|
var n1, n2 int
|
||||||
|
if i < len(parts1) {
|
||||||
|
fmt.Sscanf(parts1[i], "%d", &n1)
|
||||||
|
}
|
||||||
|
if i < len(parts2) {
|
||||||
|
fmt.Sscanf(parts2[i], "%d", &n2)
|
||||||
|
}
|
||||||
|
if n1 < n2 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if n1 > n2 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// upgradeApp 执行应用升级
|
||||||
|
// 通过 SSE 返回下载进度,下载完成后由回调决定如何升级
|
||||||
|
func (uc *Usecase) upgradeApp(c *gin.Context) {
|
||||||
|
c.Header("Content-Type", "text/event-stream")
|
||||||
|
c.Header("Cache-Control", "no-cache")
|
||||||
|
c.Header("Connection", "keep-alive")
|
||||||
|
c.Header("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
flusher, ok := c.Writer.(http.Flusher)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"msg": "不支持 SSE"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent := func(event, data string) {
|
||||||
|
fmt.Fprintf(c.Writer, "event: %s\ndata: %s\n\n", event, data)
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent("start", `{"msg":"开始下载升级包"}`)
|
||||||
|
|
||||||
|
filename := "linux_amd64"
|
||||||
|
if runtime.GOARCH == "arm64" {
|
||||||
|
filename = "linux_arm64"
|
||||||
|
}
|
||||||
|
|
||||||
|
o := ota.NewOTA(repoName, filename)
|
||||||
|
o.SetProgressCallback(func(current, total int64) {
|
||||||
|
percent := 0
|
||||||
|
if total > 0 {
|
||||||
|
percent = int(current * 100 / total)
|
||||||
|
}
|
||||||
|
sendEvent("progress", fmt.Sprintf(`{"current":%d,"total":%d,"percent":%d}`, current, total, percent))
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := o.Download().Error(); err != nil {
|
||||||
|
sendEvent("error", fmt.Sprintf(`{"msg":"%s"}`, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent("complete", `{"msg":"下载完成,请手动重启服务"}`)
|
||||||
|
}
|
||||||
|
|
||||||
func (uc *Usecase) proxySMS(c *gin.Context) {
|
func (uc *Usecase) proxySMS(c *gin.Context) {
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = recover()
|
_ = recover()
|
||||||
|
|||||||
+306
@@ -0,0 +1,306 @@
|
|||||||
|
package ota
|
||||||
|
|
||||||
|
const linuxTarPath = "upgrade.tar.gz"
|
||||||
|
|
||||||
|
// var _ Upgrader = &LinuxOTA{}
|
||||||
|
|
||||||
|
// type LinuxOTA struct {
|
||||||
|
// err error
|
||||||
|
// OnProgress func(current, total int64)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Download implements Upgrader.
|
||||||
|
// func (l *LinuxOTA) Download(link string) Upgrader {
|
||||||
|
// if l.err != nil {
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
// resp, err := http.Get(linuxPackage)
|
||||||
|
// if err != nil {
|
||||||
|
// l.err = err
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
// defer resp.Body.Close()
|
||||||
|
|
||||||
|
// _ = os.RemoveAll(filepath.Join(system.Getwd(), linuxTarPath))
|
||||||
|
|
||||||
|
// f, err := os.OpenFile(filepath.Join(system.Getwd(), linuxTarPath), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755)
|
||||||
|
// if err != nil {
|
||||||
|
// l.err = err
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
// defer f.Close()
|
||||||
|
|
||||||
|
// p := NewProgressReader(resp.ContentLength, resp.Body, l.OnProgress)
|
||||||
|
// defer p.Close()
|
||||||
|
|
||||||
|
// _, err = io.Copy(f, p)
|
||||||
|
// if err != nil {
|
||||||
|
// l.err = err
|
||||||
|
// }
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Unzip implements Upgrader.
|
||||||
|
// func (l *LinuxOTA) Unzip() Upgrader {
|
||||||
|
// if l.err != nil {
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 清理旧的升级目录
|
||||||
|
// upgradeDir := filepath.Join(system.Getwd(), "upgrade")
|
||||||
|
// _ = os.RemoveAll(upgradeDir)
|
||||||
|
|
||||||
|
// // 打开 tar.gz 文件
|
||||||
|
// file, err := os.Open(filepath.Join(system.Getwd(), linuxTarPath))
|
||||||
|
// if err != nil {
|
||||||
|
// l.err = err
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
// defer file.Close()
|
||||||
|
|
||||||
|
// // 创建 gzip reader
|
||||||
|
// gzr, err := gzip.NewReader(file)
|
||||||
|
// if err != nil {
|
||||||
|
// l.err = err
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
// defer gzr.Close()
|
||||||
|
|
||||||
|
// // 创建 tar reader
|
||||||
|
// tr := tar.NewReader(gzr)
|
||||||
|
|
||||||
|
// // 找到顶层目录名称
|
||||||
|
// var topLevelDir string
|
||||||
|
// for {
|
||||||
|
// header, err := tr.Next()
|
||||||
|
// if err == io.EOF {
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// if err != nil {
|
||||||
|
// l.err = err
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 获取第一级目录名
|
||||||
|
// parts := strings.Split(header.Name, "/")
|
||||||
|
// if len(parts) > 0 && parts[0] != "" {
|
||||||
|
// topLevelDir = parts[0]
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 重新打开文件进行解压
|
||||||
|
// file.Close()
|
||||||
|
// file, err = os.Open(filepath.Join(system.Getwd(), linuxTarPath))
|
||||||
|
// if err != nil {
|
||||||
|
// l.err = err
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
// defer file.Close()
|
||||||
|
|
||||||
|
// gzr, err = gzip.NewReader(file)
|
||||||
|
// if err != nil {
|
||||||
|
// l.err = err
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
// defer gzr.Close()
|
||||||
|
|
||||||
|
// tr = tar.NewReader(gzr)
|
||||||
|
|
||||||
|
// // 解压所有文件
|
||||||
|
// for {
|
||||||
|
// header, err := tr.Next()
|
||||||
|
// if err == io.EOF {
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// if err != nil {
|
||||||
|
// l.err = err
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if err := l.extractFile(tr, header, upgradeDir, topLevelDir); err != nil {
|
||||||
|
// l.err = err
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Backup implements Upgrader.
|
||||||
|
// func (l *LinuxOTA) Backup() Upgrader {
|
||||||
|
// if l.err != nil {
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
|
||||||
|
// execName := os.Args[0]
|
||||||
|
// backupName := execName + ".bak"
|
||||||
|
// if err := os.RemoveAll(backupName); err != nil {
|
||||||
|
// l.err = err
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
// if err := os.Rename(execName, backupName); err != nil {
|
||||||
|
// l.err = err
|
||||||
|
// }
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Replace implements Upgrader.
|
||||||
|
// func (l *LinuxOTA) Replace() Upgrader {
|
||||||
|
// if l.err != nil {
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
|
||||||
|
// upgradeDir := filepath.Join(system.Getwd(), "upgrade")
|
||||||
|
// currentDir := system.Getwd()
|
||||||
|
|
||||||
|
// // 获取当前可执行文件名
|
||||||
|
// execName := filepath.Base(os.Args[0])
|
||||||
|
|
||||||
|
// // 替换可执行文件
|
||||||
|
// newExecPath := filepath.Join(upgradeDir, execName)
|
||||||
|
// currentExecPath := filepath.Join(currentDir, execName)
|
||||||
|
|
||||||
|
// if _, err := os.Stat(newExecPath); err == nil {
|
||||||
|
// if err := l.copyFile(newExecPath, currentExecPath); err != nil {
|
||||||
|
// l.err = fmt.Errorf("替换可执行文件失败: %w", err)
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
// // 设置可执行权限
|
||||||
|
// if err := os.Chmod(currentExecPath, 0o755); err != nil {
|
||||||
|
// l.err = fmt.Errorf("设置可执行权限失败: %w", err)
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 替换 www 目录
|
||||||
|
// newWwwPath := filepath.Join(upgradeDir, "www")
|
||||||
|
// currentWwwPath := filepath.Join(currentDir, "www")
|
||||||
|
|
||||||
|
// if _, err := os.Stat(newWwwPath); err == nil {
|
||||||
|
// // 备份现有 www 目录
|
||||||
|
// backupWwwPath := filepath.Join(currentDir, "www.bak")
|
||||||
|
// _ = os.RemoveAll(backupWwwPath)
|
||||||
|
// if _, err := os.Stat(currentWwwPath); err == nil {
|
||||||
|
// if err := os.Rename(currentWwwPath, backupWwwPath); err != nil {
|
||||||
|
// l.err = fmt.Errorf("备份 www 目录失败: %w", err)
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 复制新的 www 目录
|
||||||
|
// if err := l.copyDir(newWwwPath, currentWwwPath); err != nil {
|
||||||
|
// // 恢复备份
|
||||||
|
// _ = os.Rename(backupWwwPath, currentWwwPath)
|
||||||
|
// l.err = fmt.Errorf("替换 www 目录失败: %w", err)
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 删除备份
|
||||||
|
// _ = os.RemoveAll(backupWwwPath)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 保留升级目录,下次升级的时候删除
|
||||||
|
// // _ = os.RemoveAll(upgradeDir)
|
||||||
|
// // 清理升级临时文件
|
||||||
|
// _ = os.RemoveAll(filepath.Join(currentDir, linuxTarPath))
|
||||||
|
|
||||||
|
// return l
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Error implements Upgrader.
|
||||||
|
// func (l *LinuxOTA) Error() error {
|
||||||
|
// return l.err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // extractFile 解压单个文件,跳过顶层目录
|
||||||
|
// func (l *LinuxOTA) extractFile(tr *tar.Reader, header *tar.Header, destDir, topLevelDir string) error {
|
||||||
|
// // 跳过顶层目录
|
||||||
|
// relativePath := header.Name
|
||||||
|
// if topLevelDir != "" && strings.HasPrefix(relativePath, topLevelDir+"/") {
|
||||||
|
// relativePath = strings.TrimPrefix(relativePath, topLevelDir+"/")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 如果是顶层目录本身,跳过
|
||||||
|
// if relativePath == "" || relativePath == topLevelDir {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// target := filepath.Join(destDir, relativePath)
|
||||||
|
|
||||||
|
// switch header.Typeflag {
|
||||||
|
// case tar.TypeDir:
|
||||||
|
// if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// case tar.TypeReg:
|
||||||
|
// if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// defer f.Close()
|
||||||
|
|
||||||
|
// _, err = io.Copy(f, tr)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // copyFile 复制文件
|
||||||
|
// func (l *LinuxOTA) copyFile(src, dst string) error {
|
||||||
|
// sourceFile, err := os.Open(src)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// defer sourceFile.Close()
|
||||||
|
|
||||||
|
// destFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// defer destFile.Close()
|
||||||
|
|
||||||
|
// _, err = io.Copy(destFile, sourceFile)
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // copyDir 递归复制目录
|
||||||
|
// func (l *LinuxOTA) copyDir(src, dst string) error {
|
||||||
|
// srcInfo, err := os.Stat(src)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// entries, err := os.ReadDir(src)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for _, entry := range entries {
|
||||||
|
// srcPath := filepath.Join(src, entry.Name())
|
||||||
|
// dstPath := filepath.Join(dst, entry.Name())
|
||||||
|
|
||||||
|
// if entry.IsDir() {
|
||||||
|
// if err := l.copyDir(srcPath, dstPath); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// if err := l.copyFile(srcPath, dstPath); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
package ota
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
linuxPackage = `/releases/latest/download/`
|
||||||
|
LastVersionURL = `https://api.github.com/repos/%s/releases/latest`
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReleaseInfo GitHub Release 信息
|
||||||
|
type ReleaseInfo struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OTA 提供版本检查和下载功能的结构体
|
||||||
|
// OTA 只负责下载,不关心后续的解压、备份、替换等操作
|
||||||
|
type OTA struct {
|
||||||
|
repoName string
|
||||||
|
filename string
|
||||||
|
err error
|
||||||
|
onProgress func(current, total int64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOTA 创建 OTA 实例
|
||||||
|
// repoName: GitHub 仓库名,如 "gowvp/owl",也支持 "github.com/gowvp/owl" 格式
|
||||||
|
// filename: 下载的文件名
|
||||||
|
func NewOTA(repoName, filename string) *OTA {
|
||||||
|
return &OTA{
|
||||||
|
repoName: cleanRepoName(repoName),
|
||||||
|
filename: filename,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetProgressCallback 设置下载进度回调
|
||||||
|
func (o *OTA) SetProgressCallback(callback func(current, total int64)) *OTA {
|
||||||
|
o.onProgress = callback
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastVersion 从 GitHub API 获取最新版本信息
|
||||||
|
// 返回 tag_name, body(release notes), error
|
||||||
|
func (o *OTA) GetLastVersion() (string, string, error) {
|
||||||
|
return GetLastVersion(o.repoName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download 下载升级包到指定路径
|
||||||
|
func (o *OTA) Download() *OTA {
|
||||||
|
if o.err != nil {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
// link := o.getDownloadLink()
|
||||||
|
// linuxOTA := &LinuxOTA{OnProgress: o.onProgress}
|
||||||
|
// linuxOTA.Download(link)
|
||||||
|
// o.err = linuxOTA.Error()
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error 返回错误
|
||||||
|
func (o *OTA) Error() error {
|
||||||
|
return o.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDownloadLink 获取下载链接
|
||||||
|
func (o *OTA) getDownloadLink() string {
|
||||||
|
repoLink := "https://github.com/" + o.repoName
|
||||||
|
link, _ := url.JoinPath(repoLink, linuxPackage, o.filename)
|
||||||
|
return link
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanRepoName 清理仓库名称,移除前缀
|
||||||
|
// 支持 "gowvp/owl"、"github.com/gowvp/owl" 等格式
|
||||||
|
func cleanRepoName(repoName string) string {
|
||||||
|
repoName = strings.TrimPrefix(repoName, "https://")
|
||||||
|
repoName = strings.TrimPrefix(repoName, "http://")
|
||||||
|
repoName = strings.TrimPrefix(repoName, "github.com/")
|
||||||
|
repoName = strings.TrimPrefix(repoName, "api.github.com/repos/")
|
||||||
|
return repoName
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastVersion 从 GitHub API 获取最新版本信息
|
||||||
|
// repoName: GitHub 仓库名,如 "gowvp/owl"
|
||||||
|
// 返回 tag_name, body(release notes), error
|
||||||
|
func GetLastVersion(repoName string) (string, string, error) {
|
||||||
|
repoName = cleanRepoName(repoName)
|
||||||
|
apiURL := fmt.Sprintf(LastVersionURL, repoName)
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("创建请求失败: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/vnd.github+json")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("请求失败: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", "", fmt.Errorf("请求失败,状态码: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var release ReleaseInfo
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||||
|
return "", "", fmt.Errorf("解析响应失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return release.TagName, release.Body, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package ota
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestGetLastVersion(t *testing.T) {
|
||||||
|
version, desc, err := GetLastVersion("gowvp/owl")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetLastVersion() error = %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("version = %s", version)
|
||||||
|
t.Logf("desc = %s", desc)
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package ota
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProgressReader struct {
|
||||||
|
Total int64
|
||||||
|
Current atomic.Int64
|
||||||
|
io.Reader
|
||||||
|
OnProgress func(current, total int64)
|
||||||
|
quit chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProgressReader(total int64, reader io.Reader, onProgress func(current, total int64)) *ProgressReader {
|
||||||
|
p := ProgressReader{
|
||||||
|
Total: total,
|
||||||
|
Reader: reader,
|
||||||
|
OnProgress: onProgress,
|
||||||
|
quit: make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
if onProgress != nil {
|
||||||
|
go p.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProgressReader) Close() {
|
||||||
|
close(p.quit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProgressReader) Start() {
|
||||||
|
ticker := time.NewTicker(100 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
p.OnProgress(p.Current.Load(), p.Total)
|
||||||
|
case <-p.quit:
|
||||||
|
p.OnProgress(p.Current.Load(), p.Total)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProgressReader) Read(b []byte) (int, error) {
|
||||||
|
n, err := p.Reader.Read(b)
|
||||||
|
p.Current.Add(int64(n))
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user