Commit 113485ef authored by orangepi's avatar orangepi

OTA device

parents
module example.com/OTAdevice
go 1.21.5
require (
github.com/go-resty/resty/v2 v2.16.5 // indirect
golang.org/x/net v0.33.0 // indirect
)
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
package main
import (
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/go-resty/resty/v2"
"io"
"os"
"os/exec"
"path/filepath"
"time"
)
// 定义请求体的结构体
type RequestBody struct {
DeviceID string `json:"device_id"`
}
// 定义与响应 JSON 数据结构对应的结构体
type ResponseData struct {
Data struct {
Version string `json:"version"`
URL string `json:"url"`
MD5 string `json:"md5"`
Description string `json:"description"`
IsAssigned bool `json:"is_assigned"`
} `json:"data"`
ErrCode int `json:"errCode"`
ErrMsg string `json:"errMsg"`
}
// 从 conf 文件中读取版本号
func readVersionFromConf(confPath string) (string, error) {
data, err := os.ReadFile(confPath)
if err != nil {
if os.IsNotExist(err) {
return "", nil
}
return "", err
}
return string(data), nil
}
// 将版本号写入 conf 文件
func writeVersionToConf(confPath, version string) error {
return os.WriteFile(confPath, []byte(version), 0644)
}
func main() {
// 定义文件路径
filePath := "/home/orangepi/car/master/Deviceld.txt"
// 目标 URL
url := "http://47.119.190.60/api/v1/ota/latestVersion"
// 定义 conf 文件路径
confPath := "/home/orangepi/OTAdevice/version.conf"
// 定义下载和备份文件路径
downloadPath := "/home/orangepi/OTAdevice/main"
backupDir := "/home/orangepi/OTAdevice/backup"
var fileData string
// 无限循环,直到成功读取到数据
for {
// 读取文件内容
data, err := os.ReadFile(filePath)
if err != nil {
if os.IsNotExist(err) {
fmt.Printf("文件不存在,等待10s后重试...\n")
} else {
fmt.Printf("读取文件时出错: %v,等待1分钟后重试...\n", err)
}
time.Sleep(10 * time.Second)
continue
}
// 检查文件是否为空
if len(data) == 0 {
fmt.Printf("文件为空,等待10s后重试...\n")
time.Sleep(10 * time.Second)
continue
}
// 将字节切片转换为字符串
fileData = string(data)
// 成功读取到数据,退出循环
fmt.Println("成功读取文件内容:", fileData)
break
}
// 创建 resty 客户端
client := resty.New()
for {
time.Sleep(1 * time.Minute)
// 从 conf 文件中读取上次的版本号
lastVersion, err := readVersionFromConf(confPath)
if err != nil {
fmt.Printf("读取版本号配置文件出错: %v\n", err)
continue
}
// 构建请求体
requestBody := RequestBody{
DeviceID: fileData,
}
// 发送 POST 请求并解析响应
resp, err := client.R().
SetBody(requestBody).
SetResult(&ResponseData{}).
SetError(&ResponseData{}).
Post(url)
if err != nil {
fmt.Printf("发送请求失败: %v\n", err)
continue
}
if resp.IsError() {
fmt.Printf("请求出错,状态码: %d, 错误信息: %v\n", resp.StatusCode(), resp.Error())
continue
}
// 解析响应数据
result, ok := resp.Result().(*ResponseData)
if !ok {
fmt.Printf("响应数据类型转换失败\n")
continue
}
// 检查版本号是否不同
if result.Data.Version != lastVersion {
// 停止服务
err := stopCarStartService()
if err != nil {
fmt.Printf("停止 carstart.service 失败: %v\n", err)
continue
}
if _, err := os.Stat(downloadPath); err == nil {
// 文件存在,执行备份
err = backupCurrentVersion(downloadPath, backupDir, lastVersion)
if err != nil {
fmt.Printf("备份当前版本文件失败: %v\n", err)
startCarStartService()
continue
}
} else if os.IsNotExist(err) {
// 文件不存在,跳过备份
fmt.Printf("文件 %s 不存在,跳过备份\n", downloadPath)
} else {
// 其他错误
fmt.Printf("检查文件 %s 时出错: %v\n", downloadPath, err)
startCarStartService()
continue
}
// 下载文件
downloadURL := result.Data.URL
tempDownloadPath := downloadPath + ".tmp"
fmt.Println("下载成功")
err = downloadFile(downloadURL, tempDownloadPath)
if err != nil {
fmt.Printf("下载文件失败: %v\n", err)
// 恢复备份
rollbackToBackup(downloadPath, backupDir, lastVersion)
startCarStartService()
continue
}
// 计算下载文件的 MD5 值
fileMD5, err := calculateFileMD5(tempDownloadPath)
if err != nil {
fmt.Printf("计算文件 MD5 值失败: %v\n", err)
os.Remove(tempDownloadPath)
rollbackToBackup(downloadPath, backupDir, lastVersion)
startCarStartService()
continue
}
// 进行 MD5 检验
if fileMD5 == result.Data.MD5 {
fmt.Println("MD5 检验通过")
//os.Remove(tempDownloadPath) // 删除临时文件
promoteTempToFinal(tempDownloadPath, downloadPath)
err = writeVersionToConf(confPath, result.Data.Version)
if err != nil {
fmt.Printf("写入版本号配置文件出错: %v\n", err)
}
startCarStartService()
} else {
fmt.Printf("MD5 检验失败,期望的 MD5 值: %s,实际的 MD5 值: %s\n", result.Data.MD5, fileMD5)
os.Remove(tempDownloadPath)
rollbackToBackup(downloadPath, backupDir, lastVersion)
startCarStartService()
}
} else {
fmt.Println("版本号相同,无需下载")
}
//对文件进行授权
permiss()
// 轮询间隔,这里设置为 30 分钟
time.Sleep(30 * time.Minute)
}
}
// 停止 carstart.service
func stopCarStartService() error {
cmd := exec.Command("sudo", "systemctl", "stop", "carstart.service")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("停止服务失败: %v, 输出: %s", err, string(output))
}
fmt.Println("成功停止 carstart.service")
return nil
}
// 启动 carstart.service
func startCarStartService() error {
cmd := exec.Command("sudo", "systemctl", "restart", "carstart.service")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("启动服务失败: %v, 输出: %s", err, string(output))
}
fmt.Println("成功启动 carstart.service")
return nil
}
// 备份当前版本文件
func backupCurrentVersion(downloadPath, backupDir, version string) error {
if _, err := os.Stat(backupDir); os.IsNotExist(err) {
os.MkdirAll(backupDir, 0755)
}
backupPath := filepath.Join(backupDir, "main_"+version)
return os.Rename(downloadPath, backupPath)
}
// 恢复备份文件
func rollbackToBackup(downloadPath, backupDir, version string) error {
backupPath := filepath.Join(backupDir, "main_"+version)
if _, err := os.Stat(backupPath); os.IsNotExist(err) {
return fmt.Errorf("备份文件不存在: %s", backupPath)
}
return os.Rename(backupPath, downloadPath)
}
// 下载文件的函数
func downloadFile(url, filePath string) error {
client := resty.New()
resp, err := client.R().SetOutput(filePath).Get(url)
if err != nil {
return err
}
if resp.StatusCode() != 200 {
return fmt.Errorf("下载文件失败,状态码: %d", resp.StatusCode())
}
return nil
}
// 计算文件 MD5 值的函数
func calculateFileMD5(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
hash := md5.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
hashInBytes := hash.Sum(nil)[:16]
return hex.EncodeToString(hashInBytes), nil
}
// promoteTempToFinal 将临时文件变为正式文件(原子性操作)
func promoteTempToFinal(tempPath, finalPath string) error {
// 1. 确保临时文件存在且有效
if _, err := os.Stat(tempPath); os.IsNotExist(err) {
return fmt.Errorf("临时文件不存在: %s", tempPath)
}
// 2. 删除已存在的正式文件(如果存在)
if _, err := os.Stat(finalPath); err == nil {
if err := os.Remove(finalPath); err != nil {
return fmt.Errorf("删除旧文件失败: %v", err)
}
}
// 3. 原子性重命名(系统保证操作的原子性)
if err := os.Rename(tempPath, finalPath); err != nil {
return fmt.Errorf("重命名失败: %v", err)
}
fmt.Printf("已将临时文件 %s 转为正式文件 %s\n", tempPath, finalPath)
return nil
}
func permiss() error {
cmd := exec.Command("sudo", "chmod", "+x", "/home/orangepi/OTAdevice/main")
output, err := cmd.CombinedOutput()
if err != nil {
// 返回一个包含错误信息的 error 类型值
return fmt.Errorf("执行命令出错: %v,输出信息: %s", err, string(output))
}
fmt.Println("命令执行成功")
// 命令执行成功,返回 nil 表示没有错误
return nil
}
\ No newline at end of file
1.1.0
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment