Updater update

This commit is contained in:
Александр Лазаренко 2025-05-06 10:15:04 +03:00
parent 3330006c3f
commit 26ac96bdda
Signed by: Kerblif
GPG Key ID: 5AFAD6640F4670C3
6 changed files with 339 additions and 42 deletions

View File

@ -28,6 +28,6 @@ func main() {
femaUpdater := updater.NewUpdater(cfg, binaryData) femaUpdater := updater.NewUpdater(cfg, binaryData)
// Create and show updater window // Create and show updater window
updaterWindow := ui.NewUpdaterWindow(myApp, cfg, femaUpdater.Update) updaterWindow := ui.NewUpdaterWindow(myApp, cfg, femaUpdater)
updaterWindow.ShowAndRun() updaterWindow.ShowAndRun()
} }

View File

@ -1,6 +1,7 @@
{ {
"ip": "192.168.111.111", "ip": "62.217.183.220",
"port": "22", "port": "10000",
"login": "root", "login": "root",
"password": "orangepi" "password": "orangepi",
"downloadUrl": "https://s3.ru1.storage.beget.cloud/e4b29bca179c-sparkguard/build"
} }

View File

@ -1,10 +1,14 @@
package ui package ui
import ( import (
"fmt"
"time"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"gitea.unprism.ru/KRBL/FemaInstaller/internal/updater"
"gitea.unprism.ru/KRBL/FemaInstaller/pkg/config" "gitea.unprism.ru/KRBL/FemaInstaller/pkg/config"
) )
@ -14,32 +18,107 @@ type UpdaterWindow struct {
ConfigDisplay *widget.Label ConfigDisplay *widget.Label
UpdateButton *widget.Button UpdateButton *widget.Button
StatusLabel *widget.Label StatusLabel *widget.Label
ProgressBar *widget.ProgressBar
StageLabel *widget.Label
TimeRemainingLabel *widget.Label
UpdateMethodRadio *widget.RadioGroup
Updater *updater.Updater
} }
// NewUpdaterWindow creates a new window for the updater application // NewUpdaterWindow creates a new window for the updater application
func NewUpdaterWindow(app fyne.App, config *config.UpdaterConfig, updateHandler func() error) *UpdaterWindow { func NewUpdaterWindow(app fyne.App, config *config.UpdaterConfig, femaUpdater *updater.Updater) *UpdaterWindow {
window := app.NewWindow("Обновление ПО Фема") window := app.NewWindow("Обновление ПО Фема")
// Create update method radio
updateMethodRadio := widget.NewRadioGroup(
[]string{"Использовать встроенную версию", "Загрузить с сервера по URL"},
func(selected string) {
if selected == "Использовать встроенную версию" {
femaUpdater.SetUpdateMethod(updater.UpdateMethodEmbedded)
} else {
femaUpdater.SetUpdateMethod(updater.UpdateMethodDirectDownload)
}
},
)
// Default to embedded method
updateMethodRadio.SetSelected("Использовать встроенную версию")
// Create updater window // Create updater window
updaterWindow := &UpdaterWindow{ updaterWindow := &UpdaterWindow{
Window: window, Window: window,
ConfigDisplay: widget.NewLabel(config.String()), ConfigDisplay: widget.NewLabel(config.String()),
StatusLabel: widget.NewLabel(""), StatusLabel: widget.NewLabel(""),
ProgressBar: widget.NewProgressBar(),
StageLabel: widget.NewLabel(""),
TimeRemainingLabel: widget.NewLabel(""),
UpdateMethodRadio: updateMethodRadio,
Updater: femaUpdater,
} }
// Hide progress elements initially
updaterWindow.ProgressBar.Hide()
updaterWindow.StageLabel.Hide()
updaterWindow.TimeRemainingLabel.Hide()
// Create update button // Create update button
updaterWindow.UpdateButton = widget.NewButton("Обновить ПО", func() { updaterWindow.UpdateButton = widget.NewButton("Обновить ПО", func() {
updaterWindow.StatusLabel.SetText("Начало обновления...") updaterWindow.StatusLabel.SetText("Начало обновления...")
updaterWindow.UpdateButton.Disable() updaterWindow.UpdateButton.Disable()
updaterWindow.UpdateMethodRadio.Disable()
// Show progress elements
updaterWindow.ProgressBar.Show()
updaterWindow.StageLabel.Show()
updaterWindow.TimeRemainingLabel.Show()
// Reset progress
updaterWindow.ProgressBar.SetValue(0)
updaterWindow.StageLabel.SetText("Подготовка...")
updaterWindow.TimeRemainingLabel.SetText("")
go func() { go func() {
err := updateHandler() // Create progress callback
progressCallback := func(progress updater.ProgressInfo) {
// Update UI from the main thread
window.Canvas().Refresh(updaterWindow.ProgressBar)
updaterWindow.ProgressBar.SetValue(progress.Percentage / 100)
updaterWindow.StageLabel.SetText(progress.Stage)
// Format time remaining
if progress.EstimatedTimeRemaining > 0 {
minutes := int(progress.EstimatedTimeRemaining.Minutes())
seconds := int(progress.EstimatedTimeRemaining.Seconds()) % 60
if minutes > 0 {
updaterWindow.TimeRemainingLabel.SetText(
fmt.Sprintf("Осталось примерно: %d мин %d сек", minutes, seconds))
} else {
updaterWindow.TimeRemainingLabel.SetText(
fmt.Sprintf("Осталось примерно: %d сек", seconds))
}
} else {
updaterWindow.TimeRemainingLabel.SetText("")
}
}
err := femaUpdater.Update(progressCallback)
if err != nil { if err != nil {
updaterWindow.StatusLabel.SetText("Ошибка обновления: " + err.Error()) updaterWindow.StatusLabel.SetText("Ошибка обновления: " + err.Error())
} else { } else {
updaterWindow.StatusLabel.SetText("Обновление успешно завершено!") updaterWindow.StatusLabel.SetText("Обновление успешно завершено!")
} }
// Wait a moment to show 100% completion
time.Sleep(500 * time.Millisecond)
// Hide progress elements after completion
updaterWindow.ProgressBar.Hide()
updaterWindow.StageLabel.Hide()
updaterWindow.TimeRemainingLabel.Hide()
updaterWindow.UpdateButton.Enable() updaterWindow.UpdateButton.Enable()
updaterWindow.UpdateMethodRadio.Enable()
}() }()
}) })
@ -52,6 +131,17 @@ func NewUpdaterWindow(app fyne.App, config *config.UpdaterConfig, updateHandler
configTitle := widget.NewLabel("Текущая конфигурация:") configTitle := widget.NewLabel("Текущая конфигурация:")
configTitle.TextStyle = fyne.TextStyle{Bold: true} configTitle.TextStyle = fyne.TextStyle{Bold: true}
// Create progress section
progressSection := container.NewVBox(
updaterWindow.StageLabel,
updaterWindow.ProgressBar,
updaterWindow.TimeRemainingLabel,
)
// Create update method section title
updateMethodTitle := widget.NewLabel("Метод обновления:")
updateMethodTitle.TextStyle = fyne.TextStyle{Bold: true}
// Create layout // Create layout
content := container.NewVBox( content := container.NewVBox(
title, title,
@ -59,13 +149,17 @@ func NewUpdaterWindow(app fyne.App, config *config.UpdaterConfig, updateHandler
configTitle, configTitle,
updaterWindow.ConfigDisplay, updaterWindow.ConfigDisplay,
widget.NewSeparator(), widget.NewSeparator(),
updateMethodTitle,
updaterWindow.UpdateMethodRadio,
widget.NewSeparator(),
updaterWindow.UpdateButton, updaterWindow.UpdateButton,
updaterWindow.StatusLabel, updaterWindow.StatusLabel,
progressSection,
) )
// Set window content and size // Set window content and size
window.SetContent(content) window.SetContent(content)
window.Resize(fyne.NewSize(400, 300)) window.Resize(fyne.NewSize(500, 400))
return updaterWindow return updaterWindow
} }

View File

@ -4,16 +4,39 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"time"
"gitea.unprism.ru/KRBL/FemaInstaller/internal/ssh" "gitea.unprism.ru/KRBL/FemaInstaller/internal/ssh"
"gitea.unprism.ru/KRBL/FemaInstaller/pkg/config" "gitea.unprism.ru/KRBL/FemaInstaller/pkg/config"
"gitea.unprism.ru/KRBL/FemaInstaller/pkg/fileutils" "gitea.unprism.ru/KRBL/FemaInstaller/pkg/fileutils"
gossh "golang.org/x/crypto/ssh"
)
// ProgressInfo contains information about the update progress
type ProgressInfo struct {
Stage string
Percentage float64
EstimatedTimeRemaining time.Duration
}
// ProgressCallback is a function that reports update progress
type ProgressCallback func(progress ProgressInfo)
// UpdateMethod defines the method used for updating
type UpdateMethod int
const (
// UpdateMethodEmbedded uses the binary embedded in the program
UpdateMethodEmbedded UpdateMethod = iota
// UpdateMethodDirectDownload uses wget to download directly on the device
UpdateMethodDirectDownload
) )
// Updater handles the software update process // Updater handles the software update process
type Updater struct { type Updater struct {
Config *config.UpdaterConfig Config *config.UpdaterConfig
BinaryData []byte BinaryData []byte
UpdateMethod UpdateMethod
} }
// NewUpdater creates a new updater with the provided configuration and binary data // NewUpdater creates a new updater with the provided configuration and binary data
@ -21,17 +44,28 @@ func NewUpdater(config *config.UpdaterConfig, binaryData []byte) *Updater {
return &Updater{ return &Updater{
Config: config, Config: config,
BinaryData: binaryData, BinaryData: binaryData,
UpdateMethod: UpdateMethodEmbedded, // Default to embedded method
} }
} }
// SetUpdateMethod sets the update method to use
func (u *Updater) SetUpdateMethod(method UpdateMethod) {
u.UpdateMethod = method
}
// Update performs the software update process // Update performs the software update process
func (u *Updater) Update() error { func (u *Updater) Update(progressCallback ProgressCallback) error {
// Save binary to temporary file // If no callback is provided, use a no-op callback
tempFile := "update_binary" if progressCallback == nil {
if err := os.WriteFile(tempFile, u.BinaryData, 0644); err != nil { progressCallback = func(progress ProgressInfo) {}
return fmt.Errorf("не удалось сохранить временный файл: %w", err)
} }
defer os.Remove(tempFile) // Clean up temporary file
// Report initial progress
progressCallback(ProgressInfo{
Stage: "Подготовка",
Percentage: 0,
EstimatedTimeRemaining: 0,
})
// Create SSH client configuration // Create SSH client configuration
clientConfig := ssh.NewClientConfig( clientConfig := ssh.NewClientConfig(
@ -42,12 +76,44 @@ func (u *Updater) Update() error {
) )
// Connect to SSH server // Connect to SSH server
progressCallback(ProgressInfo{
Stage: "Подключение к серверу",
Percentage: 10,
EstimatedTimeRemaining: 0,
})
sshClient, err := ssh.CreateSSHClient(clientConfig) sshClient, err := ssh.CreateSSHClient(clientConfig)
if err != nil { if err != nil {
return fmt.Errorf("ошибка подключения SSH: %w", err) return fmt.Errorf("ошибка подключения SSH: %w", err)
} }
defer sshClient.Close() defer sshClient.Close()
// Choose update method
switch u.UpdateMethod {
case UpdateMethodEmbedded:
return u.updateWithEmbeddedBinary(sshClient, clientConfig, progressCallback)
case UpdateMethodDirectDownload:
return u.updateWithDirectDownload(sshClient, progressCallback)
default:
return fmt.Errorf("неизвестный метод обновления")
}
}
// updateWithEmbeddedBinary updates using the binary embedded in the program
func (u *Updater) updateWithEmbeddedBinary(sshClient *gossh.Client, clientConfig *ssh.ClientConfig, progressCallback ProgressCallback) error {
// Save binary to temporary file
tempFile := "update_binary"
if err := os.WriteFile(tempFile, u.BinaryData, 0644); err != nil {
return fmt.Errorf("не удалось сохранить временный файл: %w", err)
}
defer os.Remove(tempFile) // Clean up temporary file
progressCallback(ProgressInfo{
Stage: "Создание SFTP-соединения",
Percentage: 20,
EstimatedTimeRemaining: 0,
})
// Create SFTP client // Create SFTP client
sftpClient, err := ssh.CreateSFTPClient(clientConfig) sftpClient, err := ssh.CreateSFTPClient(clientConfig)
if err != nil { if err != nil {
@ -55,12 +121,70 @@ func (u *Updater) Update() error {
} }
defer sftpClient.Close() defer sftpClient.Close()
// Upload binary progressCallback(ProgressInfo{
Stage: "Загрузка файла",
Percentage: 30,
EstimatedTimeRemaining: 0,
})
// Upload binary with progress reporting
remotePath := "/root/fema/build.new" remotePath := "/root/fema/build.new"
if err = fileutils.UploadFile(sftpClient, tempFile, remotePath); err != nil {
// Create a file upload progress callback
uploadProgressCallback := func(percentage float64, estimatedTimeRemaining time.Duration) {
// Map the upload progress (0-100%) to the overall progress (30-80%)
overallPercentage := 30 + (percentage * 0.5)
progressCallback(ProgressInfo{
Stage: "Загрузка файла",
Percentage: overallPercentage,
EstimatedTimeRemaining: estimatedTimeRemaining,
})
}
if err = fileutils.UploadFileWithProgress(sftpClient, tempFile, remotePath, uploadProgressCallback); err != nil {
return fmt.Errorf("ошибка загрузки файла: %w", err) return fmt.Errorf("ошибка загрузки файла: %w", err)
} }
return u.finalizeUpdate(sshClient, progressCallback)
}
// updateWithDirectDownload updates by downloading directly on the device using wget
func (u *Updater) updateWithDirectDownload(sshClient *gossh.Client, progressCallback ProgressCallback) error {
if u.Config.DownloadURL == "" {
return fmt.Errorf("URL загрузки не указан в конфигурации")
}
progressCallback(ProgressInfo{
Stage: "Загрузка файла на устройство",
Percentage: 30,
EstimatedTimeRemaining: 0,
})
// Download file directly on the device using wget
downloadCmd := fmt.Sprintf("wget -O /root/fema/build.new %s", u.Config.DownloadURL)
if err := ssh.ExecuteCommand(sshClient, downloadCmd); err != nil {
return fmt.Errorf("ошибка загрузки файла на устройство: %w", err)
}
progressCallback(ProgressInfo{
Stage: "Загрузка файла на устройство",
Percentage: 80,
EstimatedTimeRemaining: 0,
})
return u.finalizeUpdate(sshClient, progressCallback)
}
// finalizeUpdate applies the update by moving files and restarting the service
func (u *Updater) finalizeUpdate(sshClient *gossh.Client, progressCallback ProgressCallback) error {
progressCallback(ProgressInfo{
Stage: "Применение обновления",
Percentage: 80,
EstimatedTimeRemaining: 0,
})
// Execute update commands // Execute update commands
commands := []string{ commands := []string{
"mv -f /root/fema/build.new /root/fema/build", "mv -f /root/fema/build.new /root/fema/build",
@ -68,12 +192,28 @@ func (u *Updater) Update() error {
"systemctl restart fema.service", "systemctl restart fema.service",
} }
for _, cmd := range commands { for i, cmd := range commands {
// Calculate progress for each command (80-100%)
cmdProgress := 80 + float64(i+1)*20/float64(len(commands))
progressCallback(ProgressInfo{
Stage: "Применение обновления",
Percentage: cmdProgress,
EstimatedTimeRemaining: 0,
})
if err := ssh.ExecuteCommand(sshClient, cmd); err != nil { if err := ssh.ExecuteCommand(sshClient, cmd); err != nil {
return fmt.Errorf("ошибка выполнения команды '%s': %w", cmd, err) return fmt.Errorf("ошибка выполнения команды '%s': %w", cmd, err)
} }
} }
// Report completion
progressCallback(ProgressInfo{
Stage: "Завершено",
Percentage: 100,
EstimatedTimeRemaining: 0,
})
return nil return nil
} }

View File

@ -12,6 +12,7 @@ type UpdaterConfig struct {
Port string `json:"port"` Port string `json:"port"`
Login string `json:"login"` Login string `json:"login"`
Password string `json:"password"` Password string `json:"password"`
DownloadURL string `json:"downloadUrl"`
} }
// NewUpdaterConfig creates a new updater configuration with default values // NewUpdaterConfig creates a new updater configuration with default values
@ -21,6 +22,7 @@ func NewUpdaterConfig() *UpdaterConfig {
Port: "22", Port: "22",
Login: "root", Login: "root",
Password: "", Password: "",
DownloadURL: "http://example.com/fema/build",
} }
} }
@ -69,5 +71,5 @@ func SaveUpdaterConfig(config *UpdaterConfig, filePath string) error {
// String returns a string representation of the updater configuration // String returns a string representation of the updater configuration
func (c *UpdaterConfig) String() string { func (c *UpdaterConfig) String() string {
return fmt.Sprintf("IP: %s\nПорт: %s\nЛогин: %s\nПароль: %s", c.IP, c.Port, c.Login, c.Password) return fmt.Sprintf("IP: %s\nПорт: %s\nЛогин: %s\nПароль: %s\nURL загрузки: %s", c.IP, c.Port, c.Login, c.Password, c.DownloadURL)
} }

View File

@ -4,12 +4,21 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"time"
"github.com/pkg/sftp" "github.com/pkg/sftp"
) )
// ProgressCallback is a function that reports upload progress
type ProgressCallback func(percentage float64, estimatedTimeRemaining time.Duration)
// UploadFile uploads a local file to a remote server using SFTP // UploadFile uploads a local file to a remote server using SFTP
func UploadFile(client *sftp.Client, localPath, remotePath string) error { func UploadFile(client *sftp.Client, localPath, remotePath string) error {
return UploadFileWithProgress(client, localPath, remotePath, nil)
}
// UploadFileWithProgress uploads a local file to a remote server using SFTP and reports progress
func UploadFileWithProgress(client *sftp.Client, localPath, remotePath string, progressCallback ProgressCallback) error {
// Open the local file // Open the local file
localFile, err := os.Open(localPath) localFile, err := os.Open(localPath)
if err != nil { if err != nil {
@ -17,6 +26,13 @@ func UploadFile(client *sftp.Client, localPath, remotePath string) error {
} }
defer localFile.Close() defer localFile.Close()
// Get file size
fileInfo, err := localFile.Stat()
if err != nil {
return fmt.Errorf("failed to get file info: %w", err)
}
fileSize := fileInfo.Size()
// Create the remote file // Create the remote file
remoteFile, err := client.Create(remotePath) remoteFile, err := client.Create(remotePath)
if err != nil { if err != nil {
@ -24,10 +40,54 @@ func UploadFile(client *sftp.Client, localPath, remotePath string) error {
} }
defer remoteFile.Close() defer remoteFile.Close()
// Copy from the local file to the remote file // If no progress callback is provided, just copy the file
if progressCallback == nil {
if _, err = io.Copy(remoteFile, localFile); err != nil { if _, err = io.Copy(remoteFile, localFile); err != nil {
return fmt.Errorf("failed to upload file: %w", err) return fmt.Errorf("failed to upload file: %w", err)
} }
return nil
}
// Copy with progress reporting
buffer := make([]byte, 32*1024) // 32KB buffer
var totalBytesWritten int64
startTime := time.Now()
for {
bytesRead, readErr := localFile.Read(buffer)
if bytesRead > 0 {
bytesWritten, writeErr := remoteFile.Write(buffer[:bytesRead])
if writeErr != nil {
return fmt.Errorf("failed to write to remote file: %w", writeErr)
}
totalBytesWritten += int64(bytesWritten)
// Calculate progress percentage
percentage := float64(totalBytesWritten) / float64(fileSize) * 100
// Calculate estimated time remaining
elapsed := time.Since(startTime)
var estimatedTimeRemaining time.Duration
if totalBytesWritten > 0 {
bytesPerSecond := float64(totalBytesWritten) / elapsed.Seconds()
if bytesPerSecond > 0 {
remainingBytes := fileSize - totalBytesWritten
estimatedSeconds := float64(remainingBytes) / bytesPerSecond
estimatedTimeRemaining = time.Duration(estimatedSeconds * float64(time.Second))
}
}
// Report progress
progressCallback(percentage, estimatedTimeRemaining)
}
if readErr != nil {
if readErr == io.EOF {
break
}
return fmt.Errorf("failed to read from local file: %w", readErr)
}
}
return nil return nil
} }