Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
73bf0acc2c
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,3 +1,7 @@
|
||||
*build*
|
||||
*\dict
|
||||
.idea
|
||||
.idea
|
||||
deployment_package
|
||||
tmp
|
||||
deployer_config.json
|
||||
deployment.tar.gz
|
||||
|
2
Makefile
Normal file
2
Makefile
Normal file
@ -0,0 +1,2 @@
|
||||
compress:
|
||||
tar czf deployment.tar.gz deployment_package
|
15
cmd/deployer/main.go
Normal file
15
cmd/deployer/main.go
Normal file
@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2/app"
|
||||
"gitea.unprism.ru/KRBL/FemaDeployer/internal/ui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Создаем Fyne приложение с уникальным ID
|
||||
myApp := app.NewWithID("ru.unprism.gitea.KRBL.FemaDeployer")
|
||||
|
||||
// Создаем и показываем главное окно
|
||||
mainWindow := ui.NewAppUI(myApp)
|
||||
mainWindow.ShowAndRun()
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,24 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fyne.io/fyne/v2/app"
|
||||
|
||||
"gitea.unprism.ru/KRBL/FemaInstaller/internal/installer"
|
||||
"gitea.unprism.ru/KRBL/FemaInstaller/internal/ui"
|
||||
)
|
||||
|
||||
//go:embed defaultSettings.json
|
||||
var defaultSettings string
|
||||
|
||||
func main() {
|
||||
// Create application
|
||||
myApp := app.New()
|
||||
|
||||
// Create installer
|
||||
femaInstaller := installer.NewInstaller(defaultSettings)
|
||||
|
||||
// Create and show main window
|
||||
mainWindow := ui.NewMainWindow(myApp, femaInstaller.Install)
|
||||
mainWindow.ShowAndRun()
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fyne.io/fyne/v2/app"
|
||||
"log"
|
||||
|
||||
"gitea.unprism.ru/KRBL/FemaInstaller/internal/ui"
|
||||
"gitea.unprism.ru/KRBL/FemaInstaller/internal/updater"
|
||||
"gitea.unprism.ru/KRBL/FemaInstaller/pkg/config"
|
||||
)
|
||||
|
||||
//go:embed build
|
||||
var binaryData []byte
|
||||
|
||||
func main() {
|
||||
// Load configuration
|
||||
configPath := updater.GetConfigFilePath()
|
||||
cfg, err := config.LoadUpdaterConfig(configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Ошибка загрузки конфигурации: %v", err)
|
||||
}
|
||||
|
||||
// Create application
|
||||
myApp := app.New()
|
||||
|
||||
// Create updater
|
||||
femaUpdater := updater.NewUpdater(cfg, binaryData)
|
||||
|
||||
// Create and show updater window
|
||||
updaterWindow := ui.NewUpdaterWindow(myApp, cfg, femaUpdater)
|
||||
updaterWindow.ShowAndRun()
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"ip": "62.217.183.220",
|
||||
"port": "10000",
|
||||
"login": "root",
|
||||
"password": "orangepi",
|
||||
"downloadUrl": "https://s3.ru1.storage.beget.cloud/e4b29bca179c-sparkguard/build"
|
||||
}
|
25
go.mod
25
go.mod
@ -1,11 +1,12 @@
|
||||
module gitea.unprism.ru/KRBL/FemaInstaller
|
||||
module gitea.unprism.ru/KRBL/FemaDeployer
|
||||
|
||||
go 1.24
|
||||
go 1.24.5
|
||||
|
||||
require (
|
||||
fyne.io/fyne/v2 v2.6.0
|
||||
fyne.io/fyne/v2 v2.6.1
|
||||
gitea.unprism.ru/KRBL/FemaInstaller v0.0.0-20250506071504-26ac96bdda1b
|
||||
github.com/pkg/sftp v1.13.9
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/crypto v0.40.0
|
||||
)
|
||||
|
||||
require (
|
||||
@ -14,7 +15,7 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fredbi/uri v1.1.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fyne-io/gl-js v0.1.0 // indirect
|
||||
github.com/fyne-io/gl-js v0.2.0 // indirect
|
||||
github.com/fyne-io/glfw-js v0.2.0 // indirect
|
||||
github.com/fyne-io/image v0.1.1 // indirect
|
||||
github.com/fyne-io/oksvg v0.1.0 // indirect
|
||||
@ -25,21 +26,21 @@ require (
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
|
||||
github.com/hack-pad/safejs v0.1.1 // indirect
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250421151639-a9d6ed1b3d45 // indirect
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rymdport/portal v0.4.1 // indirect
|
||||
github.com/rymdport/portal v0.4.2 // indirect
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/yuin/goldmark v1.7.10 // indirect
|
||||
golang.org/x/image v0.26.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
github.com/yuin/goldmark v1.7.12 // indirect
|
||||
golang.org/x/image v0.29.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
52
go.sum
52
go.sum
@ -1,7 +1,9 @@
|
||||
fyne.io/fyne/v2 v2.6.0 h1:Rywo9yKYN4qvNuvkRuLF+zxhJYWbIFM+m4N4KV4p1pQ=
|
||||
fyne.io/fyne/v2 v2.6.0/go.mod h1:YZt7SksjvrSNJCwbWFV32WON3mE1Sr7L41D29qMZ/lU=
|
||||
fyne.io/fyne/v2 v2.6.1 h1:kjPJD4/rBS9m2nHJp+npPSuaK79yj6ObMTuzR6VQ1Is=
|
||||
fyne.io/fyne/v2 v2.6.1/go.mod h1:YZt7SksjvrSNJCwbWFV32WON3mE1Sr7L41D29qMZ/lU=
|
||||
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
|
||||
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
gitea.unprism.ru/KRBL/FemaInstaller v0.0.0-20250506071504-26ac96bdda1b h1:DD6K0lsf2EE/shAfLHyC1CPzXQixcMGes05HXA2mSYA=
|
||||
gitea.unprism.ru/KRBL/FemaInstaller v0.0.0-20250506071504-26ac96bdda1b/go.mod h1:GGSc0PWEso9vvK2bmAnYDYIbRcWhSXOFYFte5/ptIyo=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
@ -16,6 +18,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fyne-io/gl-js v0.1.0 h1:8luJzNs0ntEAJo+8x8kfUOXujUlP8gB3QMOxO2mUdpM=
|
||||
github.com/fyne-io/gl-js v0.1.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
|
||||
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
|
||||
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
|
||||
github.com/fyne-io/glfw-js v0.2.0 h1:8GUZtN2aCoTPNqgRDxK5+kn9OURINhBEBc7M4O1KrmM=
|
||||
github.com/fyne-io/glfw-js v0.2.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
|
||||
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
|
||||
@ -41,8 +45,8 @@ github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQb
|
||||
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
|
||||
github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8=
|
||||
github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250421151639-a9d6ed1b3d45 h1:vFdvrlsVU+p/KFBWTq0lTG4fvWvG88sawGlCzM+RUEU=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250421151639-a9d6ed1b3d45/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
@ -63,6 +67,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA=
|
||||
github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
||||
github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
|
||||
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
|
||||
@ -74,18 +80,22 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.10 h1:S+LrtBjRmqMac2UdtB6yyCEJm+UILZ2fefI4p7o0QpI=
|
||||
github.com/yuin/goldmark v1.7.10/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY=
|
||||
github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
|
||||
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
|
||||
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
|
||||
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
|
||||
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
@ -99,8 +109,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -119,8 +131,10 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@ -130,8 +144,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
@ -141,8 +155,10 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
|
207
internal/deployer/deployer.go
Normal file
207
internal/deployer/deployer.go
Normal file
@ -0,0 +1,207 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.unprism.ru/KRBL/FemaDeployer/internal/ssh"
|
||||
"gitea.unprism.ru/KRBL/FemaDeployer/pkg/config"
|
||||
"gitea.unprism.ru/KRBL/FemaDeployer/pkg/fileutils"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// ... структура ServiceStatus и LogFunc без изменений ...
|
||||
|
||||
type ServiceStatus struct {
|
||||
Name string
|
||||
Version string
|
||||
Status string
|
||||
Raw string
|
||||
}
|
||||
|
||||
type LogFunc func(message string)
|
||||
|
||||
// ... структура Deployer и функция NewDeployer без изменений ...
|
||||
|
||||
type Deployer struct {
|
||||
ConnConfig *config.ConnectionConfig
|
||||
Log LogFunc
|
||||
}
|
||||
|
||||
func NewDeployer(cfg *config.ConnectionConfig, logFunc LogFunc) *Deployer {
|
||||
return &Deployer{
|
||||
ConnConfig: cfg,
|
||||
Log: logFunc,
|
||||
}
|
||||
}
|
||||
|
||||
// ... функция GetServicesStatus без изменений ...
|
||||
func (d *Deployer) GetServicesStatus(components []config.Component) ([]ServiceStatus, error) {
|
||||
d.Log("Подключение для проверки статуса...")
|
||||
client, err := ssh.CreateSSHClient(ssh.NewClientConfig(d.ConnConfig.IP, d.ConnConfig.Port, d.ConnConfig.Login, d.ConnConfig.Password))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ошибка SSH: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
var statuses []ServiceStatus
|
||||
for _, comp := range components {
|
||||
d.Log(fmt.Sprintf("Проверка статуса для %s...", comp.Name))
|
||||
status := ServiceStatus{Name: comp.Name, Version: comp.Version}
|
||||
|
||||
cmd := fmt.Sprintf("systemctl status %s.service", comp.Name)
|
||||
|
||||
output, err := ssh.ExecuteCommandWithOutput(client, cmd)
|
||||
|
||||
status.Raw = output
|
||||
if err != nil {
|
||||
if strings.Contains(status.Raw, "Loaded: not-found") {
|
||||
status.Status = "Не найден"
|
||||
} else if strings.Contains(status.Raw, "inactive (dead)") {
|
||||
status.Status = "Не активен"
|
||||
} else {
|
||||
status.Status = "Ошибка"
|
||||
}
|
||||
} else {
|
||||
status.Status = "Активен"
|
||||
}
|
||||
|
||||
statuses = append(statuses, status)
|
||||
}
|
||||
d.Log("Проверка статусов завершена.")
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
func (d *Deployer) FullDeploy() error {
|
||||
client, err := d.connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if err := d.prepareRemotePackage(client); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.Log("Запуск скрипта полной установки...")
|
||||
installScriptPath := "/tmp/fema_deployment/deployment_package/scripts/install.sh"
|
||||
|
||||
// ИСПОЛЬЗУЕМ ExecuteCommandWithOutput для отладки
|
||||
output, err := ssh.ExecuteCommandWithOutput(client, "sudo "+installScriptPath)
|
||||
if err != nil {
|
||||
d.Log("!!! ОШИБКА ВЫПОЛНЕНИЯ СКРИПТА !!!")
|
||||
d.Log("ВЫВОД СКРИПТА:\n" + output)
|
||||
return fmt.Errorf("ошибка выполнения install.sh: %w", err)
|
||||
}
|
||||
|
||||
d.Log("ВЫВОД СКРИПТА:\n" + output)
|
||||
d.Log("Полная установка успешно завершена!")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Deployer) SelectiveUpdate(components []string) error {
|
||||
if len(components) == 0 {
|
||||
return fmt.Errorf("не выбраны компоненты для обновления")
|
||||
}
|
||||
|
||||
client, err := d.connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if err := d.prepareRemotePackage(client); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.Log(fmt.Sprintf("Запуск скрипта выборочного обновления для: %s", strings.Join(components, ", ")))
|
||||
updateScriptPath := "/tmp/fema_deployment/deployment_package/scripts/update.sh"
|
||||
cmd := fmt.Sprintf("sudo %s %s", updateScriptPath, strings.Join(components, " "))
|
||||
|
||||
// ИСПОЛЬЗУЕМ ExecuteCommandWithOutput для отладки
|
||||
output, err := ssh.ExecuteCommandWithOutput(client, cmd)
|
||||
if err != nil {
|
||||
d.Log("!!! ОШИБКА ВЫПОЛНЕНИЯ СКРИПТА !!!")
|
||||
d.Log("ВЫВОД СКРИПТА:\n" + output)
|
||||
return fmt.Errorf("ошибка выполнения update.sh: %w", err)
|
||||
}
|
||||
|
||||
d.Log("ВЫВОД СКРИПТА:\n" + output)
|
||||
d.Log("Выборочное обновление успешно завершено!")
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- НОВАЯ ВСПОМОГАТЕЛЬНАЯ ФУНКЦИЯ ---
|
||||
// prepareRemotePackage загружает, распаковывает и подготавливает пакет на удаленной машине
|
||||
func (d *Deployer) prepareRemotePackage(client *gossh.Client) error {
|
||||
remotePath := "/tmp/" + filepath.Base(d.ConnConfig.DeploymentPackagePath)
|
||||
if err := d.uploadPackage(remotePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.Log("Распаковка архива на устройстве...")
|
||||
unpackDir := "/tmp/fema_deployment"
|
||||
unpackCmds := []string{
|
||||
fmt.Sprintf("rm -rf %s", unpackDir),
|
||||
fmt.Sprintf("mkdir -p %s", unpackDir),
|
||||
fmt.Sprintf("tar -xzf %s -C %s", remotePath, unpackDir),
|
||||
}
|
||||
for _, cmd := range unpackCmds {
|
||||
if err := ssh.ExecuteCommand(client, cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// ГАРАНТИРУЕМ, ЧТО СКРИПТЫ МОЖНО ЗАПУСКАТЬ
|
||||
d.Log("Установка прав на исполнение для скриптов...")
|
||||
scriptsPath := filepath.Join(unpackDir, "deployment_package", "scripts")
|
||||
installScriptPath := filepath.Join(scriptsPath, "install.sh")
|
||||
updateScriptPath := filepath.Join(scriptsPath, "update.sh")
|
||||
chmodCmd := fmt.Sprintf("chmod +x %s %s", installScriptPath, updateScriptPath)
|
||||
if err := ssh.ExecuteCommand(client, chmodCmd); err != nil {
|
||||
return fmt.Errorf("не удалось установить права на скрипты: %w", err)
|
||||
}
|
||||
|
||||
if err := ssh.ExecuteCommand(client, "cd "+scriptsPath); err != nil {
|
||||
return fmt.Errorf("не удалось перейти в директорию с скриптами: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ... connect и uploadPackage без изменений ...
|
||||
func (d *Deployer) connect() (*gossh.Client, error) {
|
||||
d.Log("Подключение к устройству...")
|
||||
clientConfig := ssh.NewClientConfig(d.ConnConfig.IP, d.ConnConfig.Port, d.ConnConfig.Login, d.ConnConfig.Password)
|
||||
client, err := ssh.CreateSSHClient(clientConfig)
|
||||
if err != nil {
|
||||
d.Log(fmt.Sprintf("Ошибка подключения: %v", err))
|
||||
return nil, fmt.Errorf("ошибка подключения SSH: %w", err)
|
||||
}
|
||||
d.Log("Подключение успешно.")
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (d *Deployer) uploadPackage(remotePath string) error {
|
||||
d.Log("Загрузка пакета развертывания...")
|
||||
sftpConfig := ssh.NewClientConfig(d.ConnConfig.IP, d.ConnConfig.Port, d.ConnConfig.Login, d.ConnConfig.Password)
|
||||
sftpClient, err := ssh.CreateSFTPClient(sftpConfig)
|
||||
if err != nil {
|
||||
d.Log(fmt.Sprintf("Ошибка SFTP: %v", err))
|
||||
return fmt.Errorf("ошибка подключения SFTP: %w", err)
|
||||
}
|
||||
defer sftpClient.Close()
|
||||
|
||||
progressCallback := func(percentage float64, etr time.Duration) {
|
||||
d.Log(fmt.Sprintf("Загрузка: %.2f%% (осталось ~%v)", percentage, etr.Round(time.Second)))
|
||||
}
|
||||
|
||||
if err := fileutils.UploadFileWithProgress(sftpClient, d.ConnConfig.DeploymentPackagePath, remotePath, progressCallback); err != nil {
|
||||
d.Log(fmt.Sprintf("Ошибка загрузки файла: %v", err))
|
||||
return fmt.Errorf("ошибка загрузки файла: %w", err)
|
||||
}
|
||||
d.Log("Пакет успешно загружен.")
|
||||
return nil
|
||||
}
|
69
internal/deployer/package.go
Normal file
69
internal/deployer/package.go
Normal file
@ -0,0 +1,69 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gitea.unprism.ru/KRBL/FemaDeployer/pkg/config"
|
||||
)
|
||||
|
||||
// DeploymentPackage представляет собой загруженный пакет
|
||||
type DeploymentPackage struct {
|
||||
Path string
|
||||
Manifest *config.Manifest
|
||||
}
|
||||
|
||||
// LoadDeploymentPackage загружает и валидирует пакет развертывания
|
||||
func LoadDeploymentPackage(path string) (*DeploymentPackage, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось открыть файл пакета: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
gzr, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось создать gzip reader: %w", err)
|
||||
}
|
||||
defer gzr.Close()
|
||||
|
||||
tr := tar.NewReader(gzr)
|
||||
|
||||
var manifest *config.Manifest
|
||||
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break // Конец архива
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ошибка чтения tar-архива: %w", err)
|
||||
}
|
||||
|
||||
// Ищем manifest.json в корне архива
|
||||
if header.Typeflag == tar.TypeReg && (filepath.Clean(header.Name) == "manifest.json" || filepath.Clean(header.Name) == "deployment_package/manifest.json") {
|
||||
manifestData, err := io.ReadAll(tr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось прочитать manifest.json: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(manifestData, &manifest); err != nil {
|
||||
return nil, fmt.Errorf("ошибка парсинга manifest.json: %w", err)
|
||||
}
|
||||
break // Манифест найден, выходим из цикла
|
||||
}
|
||||
}
|
||||
|
||||
if manifest == nil {
|
||||
return nil, fmt.Errorf("файл manifest.json не найден в архиве")
|
||||
}
|
||||
|
||||
return &DeploymentPackage{
|
||||
Path: path,
|
||||
Manifest: manifest,
|
||||
}, nil
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gitea.unprism.ru/KRBL/FemaInstaller/internal/ssh"
|
||||
"gitea.unprism.ru/KRBL/FemaInstaller/pkg/config"
|
||||
"gitea.unprism.ru/KRBL/FemaInstaller/pkg/fileutils"
|
||||
)
|
||||
|
||||
// Installer handles the installation process
|
||||
type Installer struct {
|
||||
DefaultSettings string
|
||||
}
|
||||
|
||||
// NewInstaller creates a new installer with the provided default settings
|
||||
func NewInstaller(defaultSettings string) *Installer {
|
||||
return &Installer{
|
||||
DefaultSettings: defaultSettings,
|
||||
}
|
||||
}
|
||||
|
||||
// Install performs the installation process
|
||||
func (i *Installer) Install(sshConfig *config.SSHConfig) error {
|
||||
// Validate serial number
|
||||
if len([]rune(sshConfig.Serial)) != 16 {
|
||||
return fmt.Errorf("serial number must be 16 characters")
|
||||
}
|
||||
|
||||
// Update settings with user configuration
|
||||
updatedSettings, err := config.UpdateSettingsJSON(i.DefaultSettings, sshConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update settings: %w", err)
|
||||
}
|
||||
|
||||
// Save updated settings to file
|
||||
err = config.SaveSettingsJSON(updatedSettings, "settings.json")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save settings: %w", err)
|
||||
}
|
||||
|
||||
// Create SSH client configuration
|
||||
clientConfig := ssh.NewClientConfig(
|
||||
sshConfig.IP,
|
||||
sshConfig.Port,
|
||||
sshConfig.Login,
|
||||
sshConfig.Password,
|
||||
)
|
||||
|
||||
// Connect to SSH server
|
||||
sshClient, err := ssh.CreateSSHClient(clientConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SSH connection failed: %w", err)
|
||||
}
|
||||
defer sshClient.Close()
|
||||
|
||||
// Create SFTP client
|
||||
sftpClient, err := ssh.CreateSFTPClient(clientConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SFTP connection failed: %w", err)
|
||||
}
|
||||
defer sftpClient.Close()
|
||||
|
||||
// Upload archive
|
||||
remotePath := "/root/dict.tar"
|
||||
if err = fileutils.UploadFile(sftpClient, sshConfig.ArchivePath, remotePath); err != nil {
|
||||
return fmt.Errorf("file upload failed: %w", err)
|
||||
}
|
||||
|
||||
// Upload settings
|
||||
settingsPath := "/root/settings.json"
|
||||
if err = fileutils.UploadFile(sftpClient, "settings.json", settingsPath); err != nil {
|
||||
return fmt.Errorf("settings upload failed: %w", err)
|
||||
}
|
||||
|
||||
// Execute installation commands
|
||||
commands := []string{
|
||||
"tar -xf /root/dict.tar -C /root/",
|
||||
"mkdir -p /root/fema/storage",
|
||||
"mv -f ~/settings.json /root/fema/storage",
|
||||
"chmod +x /root/dict/*",
|
||||
fmt.Sprintf("sudo /root/dict/install.sh -s %s", sshConfig.Serial),
|
||||
}
|
||||
|
||||
for _, cmd := range commands {
|
||||
if err := ssh.ExecuteCommand(sshClient, cmd); err != nil {
|
||||
return fmt.Errorf("command execution failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up temporary files
|
||||
os.Remove("settings.json")
|
||||
|
||||
return nil
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@ -73,7 +74,7 @@ func CreateSFTPClient(config *ClientConfig) (*sftp.Client, error) {
|
||||
return sftpClient, nil
|
||||
}
|
||||
|
||||
// ExecuteCommand executes a command on the remote server
|
||||
// ExecuteCommand executes a command on the remote server without returning output
|
||||
func ExecuteCommand(client *ssh.Client, command string) error {
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
@ -87,3 +88,19 @@ func ExecuteCommand(client *ssh.Client, command string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecuteCommandWithOutput executes a command and returns its combined stdout/stderr
|
||||
func ExecuteCommandWithOutput(client *ssh.Client, command string) (string, error) {
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("session creation failed: %w", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
var stdoutBuf bytes.Buffer
|
||||
session.Stdout = &stdoutBuf
|
||||
session.Stderr = &stdoutBuf // Combine stderr and stdout
|
||||
|
||||
err = session.Run(command)
|
||||
return stdoutBuf.String(), err
|
||||
}
|
||||
|
@ -1,49 +0,0 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
// CustomFileSelector displays a custom file selection dialog
|
||||
func CustomFileSelector(window fyne.Window, setText func(string)) {
|
||||
// Get list of files in current directory
|
||||
files, _ := filepath.Glob("*")
|
||||
|
||||
// Create list widget with files
|
||||
fileList := widget.NewList(
|
||||
func() int {
|
||||
return len(files)
|
||||
},
|
||||
func() fyne.CanvasObject {
|
||||
return widget.NewLabel("File")
|
||||
},
|
||||
func(id widget.ListItemID, obj fyne.CanvasObject) {
|
||||
obj.(*widget.Label).SetText(files[id])
|
||||
},
|
||||
)
|
||||
|
||||
// Set selection handler
|
||||
fileList.OnSelected = func(id widget.ListItemID) {
|
||||
setText(files[id]) // Set the selected path in the text field
|
||||
}
|
||||
|
||||
// Create dialog title
|
||||
title := widget.NewLabel("Files in current directory:")
|
||||
title.Alignment = fyne.TextAlignCenter
|
||||
|
||||
// Create layout with title and file list
|
||||
content := container.NewBorder(
|
||||
title, nil, nil, nil, // Title at the top
|
||||
container.NewMax(fileList), // List takes maximum space
|
||||
)
|
||||
|
||||
// Create and show dialog
|
||||
fileDialog := dialog.NewCustom("Select a file", "Close", content, window)
|
||||
fileDialog.Resize(fyne.NewSize(400, 500)) // Set dialog size
|
||||
fileDialog.Show()
|
||||
}
|
@ -1,165 +1,378 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sync"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/data/binding" // <-- ИМПОРТИРУЕМ BINDING
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
|
||||
"gitea.unprism.ru/KRBL/FemaInstaller/pkg/config"
|
||||
"gitea.unprism.ru/KRBL/FemaDeployer/internal/deployer"
|
||||
"gitea.unprism.ru/KRBL/FemaDeployer/pkg/config"
|
||||
)
|
||||
|
||||
var mu = sync.Mutex{}
|
||||
const configFileName = "deployer_config.json"
|
||||
|
||||
// MainWindow represents the main application window
|
||||
type MainWindow struct {
|
||||
Window fyne.Window
|
||||
IPEntry *widget.Entry
|
||||
PortEntry *widget.Entry
|
||||
LoginEntry *widget.Entry
|
||||
PasswordEntry *widget.Entry
|
||||
SerialEntry *widget.Entry
|
||||
TailNumberEntry *widget.Entry
|
||||
DefaultHostEntry *widget.Entry
|
||||
ArchivePathEntry *widget.Entry
|
||||
StatusLabel *widget.Label
|
||||
InstallButton *widget.Button
|
||||
// AppUI представляет главный UI приложения
|
||||
type AppUI struct {
|
||||
app fyne.App
|
||||
window fyne.Window
|
||||
|
||||
// Общие данные
|
||||
connConfig *config.ConnectionConfig
|
||||
deployer *deployer.Deployer
|
||||
manifest *config.Manifest
|
||||
statuses []deployer.ServiceStatus // Статусы пока оставляем как slice, они обновляются редко
|
||||
|
||||
// Виджеты
|
||||
ipEntry *widget.Entry
|
||||
portEntry *widget.Entry
|
||||
loginEntry *widget.Entry
|
||||
passwordEntry *widget.Entry
|
||||
packagePathLabel *widget.Label
|
||||
statusList *widget.List
|
||||
componentList *widget.List
|
||||
latestLogLabel *widget.Label // Метка для отображения последнего лога
|
||||
logData binding.StringList // Хранилище всех логов
|
||||
|
||||
// Состояние для выбора компонентов
|
||||
selectedComponents map[string]bool
|
||||
}
|
||||
|
||||
func loadConfig() *config.SSHConfig {
|
||||
fileName := "config.json"
|
||||
if _, err := os.Stat(fileName); os.IsNotExist(err) {
|
||||
return nil
|
||||
// NewAppUI создает новый UI
|
||||
func NewAppUI(app fyne.App) *AppUI {
|
||||
w := app.NewWindow("Fema Deployer")
|
||||
ui := &AppUI{
|
||||
app: app,
|
||||
window: w,
|
||||
selectedComponents: make(map[string]bool),
|
||||
logData: binding.NewStringList(), // <-- ИНИЦИАЛИЗИРУЕМ BINDING
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
// Загружаем конфиг
|
||||
ui.connConfig, _ = config.Load(configFileName)
|
||||
|
||||
var cfg config.SSHConfig
|
||||
err = json.Unmarshal(data, &cfg)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
// Инициализируем Deployer с функцией логирования
|
||||
ui.deployer = deployer.NewDeployer(ui.connConfig, ui.addLog)
|
||||
|
||||
return &cfg
|
||||
}
|
||||
// Больше не нужен таймер для прокрутки логов, так как мы показываем только последний лог
|
||||
|
||||
// NewMainWindow creates a new main window for the application
|
||||
func NewMainWindow(app fyne.App, installHandler func(*config.SSHConfig) error) *MainWindow {
|
||||
window := app.NewWindow("Fema Installer")
|
||||
|
||||
// Create form fields
|
||||
mainWindow := &MainWindow{
|
||||
Window: window,
|
||||
IPEntry: widget.NewEntry(),
|
||||
PortEntry: widget.NewEntry(),
|
||||
LoginEntry: widget.NewEntry(),
|
||||
PasswordEntry: widget.NewPasswordEntry(),
|
||||
SerialEntry: widget.NewEntry(),
|
||||
TailNumberEntry: widget.NewEntry(),
|
||||
DefaultHostEntry: widget.NewEntry(),
|
||||
ArchivePathEntry: widget.NewEntry(),
|
||||
StatusLabel: widget.NewLabel(""),
|
||||
}
|
||||
|
||||
cfg := loadConfig()
|
||||
|
||||
if cfg != nil {
|
||||
mainWindow.IPEntry.SetText(cfg.IP)
|
||||
mainWindow.PortEntry.SetText(cfg.Port)
|
||||
mainWindow.LoginEntry.SetText(cfg.Login)
|
||||
mainWindow.PasswordEntry.SetText(cfg.Password)
|
||||
mainWindow.SerialEntry.SetText(cfg.Serial)
|
||||
mainWindow.TailNumberEntry.SetText(cfg.TailNumber)
|
||||
mainWindow.DefaultHostEntry.SetText(cfg.DefaultHost)
|
||||
mainWindow.ArchivePathEntry.SetText(cfg.ArchivePath)
|
||||
}
|
||||
|
||||
// Disable archive path entry (will be set by file selector)
|
||||
mainWindow.ArchivePathEntry.Disable()
|
||||
|
||||
// Create archive selection button
|
||||
selectArchiveBtn := widget.NewButton("Select Archive", func() {
|
||||
CustomFileSelector(window, func(path string) {
|
||||
mainWindow.ArchivePathEntry.SetText(path)
|
||||
})
|
||||
})
|
||||
|
||||
// Create install button
|
||||
mainWindow.InstallButton = widget.NewButton("Install", func() {
|
||||
config := &config.SSHConfig{
|
||||
IP: mainWindow.IPEntry.Text,
|
||||
Port: mainWindow.PortEntry.Text,
|
||||
Login: mainWindow.LoginEntry.Text,
|
||||
Password: mainWindow.PasswordEntry.Text,
|
||||
Serial: mainWindow.SerialEntry.Text,
|
||||
TailNumber: mainWindow.TailNumberEntry.Text,
|
||||
DefaultHost: mainWindow.DefaultHostEntry.Text,
|
||||
ArchivePath: mainWindow.ArchivePathEntry.Text,
|
||||
}
|
||||
|
||||
mainWindow.StatusLabel.SetText("Starting installation...")
|
||||
go func() {
|
||||
// Validate serial number
|
||||
if len([]rune(config.Serial)) != 16 {
|
||||
mainWindow.StatusLabel.SetText("Serial number must be 16 characters")
|
||||
dialog.ShowInformation("Error", "Serial number must be 16 characters", window)
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
jsonData, _ := json.MarshalIndent(config, "", " ")
|
||||
|
||||
// Записываем JSON в файл
|
||||
fileName := "config.json"
|
||||
os.WriteFile(fileName, jsonData, 0644)
|
||||
mu.Unlock()
|
||||
// Perform installation
|
||||
err := installHandler(config)
|
||||
if err != nil {
|
||||
dialog.ShowError(err, window)
|
||||
mainWindow.StatusLabel.SetText("Installation failed")
|
||||
} else {
|
||||
mainWindow.StatusLabel.SetText("Installation completed successfully!")
|
||||
dialog.ShowInformation("Success", "Installation completed successfully!", window)
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
// Create form layout
|
||||
form := container.NewVBox(
|
||||
widget.NewForm(
|
||||
widget.NewFormItem("IP", mainWindow.IPEntry),
|
||||
widget.NewFormItem("Port", mainWindow.PortEntry),
|
||||
widget.NewFormItem("Login", mainWindow.LoginEntry),
|
||||
widget.NewFormItem("Password", mainWindow.PasswordEntry),
|
||||
widget.NewFormItem("Serial Number", mainWindow.SerialEntry),
|
||||
widget.NewFormItem("Tail Number", mainWindow.TailNumberEntry),
|
||||
widget.NewFormItem("Default Server", mainWindow.DefaultHostEntry),
|
||||
widget.NewFormItem("Archive", container.NewHBox(mainWindow.ArchivePathEntry, selectArchiveBtn)),
|
||||
),
|
||||
mainWindow.InstallButton,
|
||||
mainWindow.StatusLabel,
|
||||
// Создаем вкладки
|
||||
tabs := container.NewAppTabs(
|
||||
container.NewTabItem("Статус", ui.createStatusTab()),
|
||||
container.NewTabItem("Развертывание", ui.createDeployTab()),
|
||||
)
|
||||
|
||||
// Set window content and size
|
||||
window.SetContent(form)
|
||||
window.Resize(fyne.NewSize(400, 300))
|
||||
// Создаем лог-панель
|
||||
logPanel := ui.createLogPanel()
|
||||
|
||||
return mainWindow
|
||||
// Собираем главный контейнер
|
||||
mainContent := container.NewBorder(tabs, logPanel, nil, nil, nil)
|
||||
|
||||
w.SetContent(mainContent)
|
||||
w.Resize(fyne.NewSize(800, 600))
|
||||
return ui
|
||||
}
|
||||
|
||||
// Show displays the main window
|
||||
func (w *MainWindow) Show() {
|
||||
w.Window.Show()
|
||||
func (ui *AppUI) createStatusTab() fyne.CanvasObject {
|
||||
// ... код полей для подключения без изменений ...
|
||||
ui.ipEntry = widget.NewEntry()
|
||||
ui.ipEntry.SetText(ui.connConfig.IP)
|
||||
ui.portEntry = widget.NewEntry()
|
||||
ui.portEntry.SetText(ui.connConfig.Port)
|
||||
ui.loginEntry = widget.NewEntry()
|
||||
ui.loginEntry.SetText(ui.connConfig.Login)
|
||||
ui.passwordEntry = widget.NewPasswordEntry()
|
||||
ui.passwordEntry.SetText(ui.connConfig.Password)
|
||||
|
||||
connectForm := widget.NewForm(
|
||||
widget.NewFormItem("IP", ui.ipEntry),
|
||||
widget.NewFormItem("Port", ui.portEntry),
|
||||
widget.NewFormItem("Login", ui.loginEntry),
|
||||
widget.NewFormItem("Password", ui.passwordEntry),
|
||||
)
|
||||
|
||||
// Список статусов
|
||||
ui.statusList = widget.NewList(
|
||||
func() int {
|
||||
return len(ui.statuses)
|
||||
},
|
||||
func() fyne.CanvasObject {
|
||||
return container.NewGridWithColumns(3, widget.NewLabel(""), widget.NewLabel(""), widget.NewLabel(""))
|
||||
},
|
||||
func(id widget.ListItemID, obj fyne.CanvasObject) {
|
||||
if id >= len(ui.statuses) {
|
||||
return
|
||||
}
|
||||
s := ui.statuses[id]
|
||||
c := obj.(*fyne.Container)
|
||||
c.Objects[0].(*widget.Label).SetText(s.Name)
|
||||
c.Objects[1].(*widget.Label).SetText(s.Version)
|
||||
c.Objects[2].(*widget.Label).SetText(s.Status)
|
||||
},
|
||||
)
|
||||
|
||||
// Кнопка обновления статуса
|
||||
refreshBtn := widget.NewButton("Проверить статус", ui.onRefreshStatus)
|
||||
|
||||
return container.NewBorder(
|
||||
container.NewVBox(connectForm, refreshBtn),
|
||||
nil, nil, nil,
|
||||
container.NewMax(ui.statusList),
|
||||
)
|
||||
}
|
||||
|
||||
// ShowAndRun displays the main window and starts the application
|
||||
func (w *MainWindow) ShowAndRun() {
|
||||
w.Window.ShowAndRun()
|
||||
func (ui *AppUI) createDeployTab() fyne.CanvasObject {
|
||||
ui.packagePathLabel = widget.NewLabel("Пакет развертывания не выбран")
|
||||
if ui.connConfig.DeploymentPackagePath != "" {
|
||||
ui.packagePathLabel.SetText(filepath.Base(ui.connConfig.DeploymentPackagePath))
|
||||
go ui.loadManifest() // Загружаем манифест при старте, если путь уже есть
|
||||
}
|
||||
|
||||
selectPackageBtn := widget.NewButton("Выбрать пакет (.tar.gz)", func() {
|
||||
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
||||
if err != nil || reader == nil {
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
path := reader.URI().Path()
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Эти операции быстрые, их оставляем в основном потоке
|
||||
ui.connConfig.DeploymentPackagePath = path
|
||||
ui.packagePathLabel.SetText(filepath.Base(path))
|
||||
|
||||
// А вот долгую операцию запускаем в отдельной горутине
|
||||
go ui.loadManifest()
|
||||
|
||||
}, ui.window)
|
||||
})
|
||||
|
||||
// ... остальной код функции createDeployTab без изменений ...
|
||||
ui.componentList = widget.NewList(
|
||||
func() int {
|
||||
if ui.manifest == nil {
|
||||
return 0
|
||||
}
|
||||
return len(ui.manifest.Components)
|
||||
},
|
||||
func() fyne.CanvasObject {
|
||||
return container.NewHBox(widget.NewCheck("", func(b bool) {}), widget.NewLabel(""))
|
||||
},
|
||||
func(id widget.ListItemID, obj fyne.CanvasObject) {
|
||||
comp := ui.manifest.Components[id]
|
||||
c := obj.(*fyne.Container)
|
||||
check := c.Objects[0].(*widget.Check)
|
||||
label := c.Objects[1].(*widget.Label)
|
||||
label.SetText(fmt.Sprintf("%s (v%s)", comp.Name, comp.Version))
|
||||
check.SetText("")
|
||||
check.Checked = ui.selectedComponents[comp.Name]
|
||||
check.OnChanged = func(b bool) {
|
||||
ui.selectedComponents[comp.Name] = b
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
fullInstallBtn := widget.NewButton("Полная установка", ui.onFullDeploy)
|
||||
selectiveUpdateBtn := widget.NewButton("Обновить выбранное", ui.onSelectiveUpdate)
|
||||
|
||||
return container.NewVBox(
|
||||
container.NewHBox(selectPackageBtn, ui.packagePathLabel),
|
||||
widget.NewSeparator(),
|
||||
container.NewMax(ui.componentList),
|
||||
widget.NewSeparator(),
|
||||
container.NewGridWithColumns(2, fullInstallBtn, selectiveUpdateBtn),
|
||||
)
|
||||
}
|
||||
|
||||
func (ui *AppUI) createLogPanel() fyne.CanvasObject {
|
||||
// Создаем метку для отображения последнего лога
|
||||
ui.latestLogLabel = widget.NewLabel("Готов к работе")
|
||||
|
||||
// Кнопка для просмотра всех логов
|
||||
viewLogsBtn := widget.NewButton("Просмотр всех логов", func() {
|
||||
ui.showLogsWindow()
|
||||
})
|
||||
|
||||
// Размещаем метку и кнопку в контейнере
|
||||
return container.NewBorder(nil, nil, nil, viewLogsBtn, ui.latestLogLabel)
|
||||
}
|
||||
|
||||
// showLogsWindow открывает новое окно со всеми логами
|
||||
func (ui *AppUI) showLogsWindow() {
|
||||
logWindow := ui.app.NewWindow("Журнал операций")
|
||||
|
||||
// Создаем многострочное текстовое поле для отображения всех логов
|
||||
logEntry := widget.NewEntry()
|
||||
logEntry.MultiLine = true
|
||||
logEntry.Wrapping = fyne.TextWrapWord
|
||||
logEntry.Disable() // Делаем поле только для чтения, но с возможностью копирования
|
||||
|
||||
// Получаем все логи и объединяем их в одну строку
|
||||
count := ui.logData.Length()
|
||||
var allLogs strings.Builder
|
||||
for i := 0; i < count; i++ {
|
||||
item, err := ui.logData.GetItem(i)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s, err := item.(binding.String).Get()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
allLogs.WriteString(s)
|
||||
allLogs.WriteString("\n")
|
||||
}
|
||||
|
||||
// Устанавливаем текст в поле
|
||||
logEntry.SetText(allLogs.String())
|
||||
|
||||
// Кнопка закрытия
|
||||
closeBtn := widget.NewButton("Закрыть", func() {
|
||||
logWindow.Close()
|
||||
})
|
||||
|
||||
// Размещаем текстовое поле и кнопку в контейнере
|
||||
content := container.NewBorder(nil, closeBtn, nil, nil, container.NewScroll(logEntry))
|
||||
|
||||
logWindow.SetContent(content)
|
||||
logWindow.Resize(fyne.NewSize(600, 400))
|
||||
logWindow.Show()
|
||||
}
|
||||
|
||||
// --- Обработчики событий ---
|
||||
|
||||
func (ui *AppUI) onRefreshStatus() {
|
||||
ui.updateAndSaveConfig()
|
||||
if ui.manifest == nil {
|
||||
ui.addLog("Ошибка: сначала выберите пакет развертывания на вкладке 'Развертывание'.")
|
||||
dialog.ShowInformation("Ошибка", "Сначала выберите пакет развертывания", ui.window)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
statuses, err := ui.deployer.GetServicesStatus(ui.manifest.Components)
|
||||
if err != nil {
|
||||
ui.addLog(fmt.Sprintf("Ошибка проверки статуса: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Безопасно обновляем данные и виджет
|
||||
ui.statuses = statuses
|
||||
ui.statusList.Refresh() // Fyne спроектирован так, что Refresh можно вызывать из других горутин
|
||||
}()
|
||||
}
|
||||
|
||||
// ... функции onFullDeploy и onSelectiveUpdate без изменений ...
|
||||
func (ui *AppUI) onFullDeploy() {
|
||||
ui.updateAndSaveConfig()
|
||||
if ui.connConfig.DeploymentPackagePath == "" {
|
||||
dialog.ShowInformation("Ошибка", "Пакет развертывания не выбран", ui.window)
|
||||
return
|
||||
}
|
||||
|
||||
dialog.ShowConfirm("Подтверждение", "Это сотрет предыдущие установки и выполнит установку с нуля. Продолжить?", func(b bool) {
|
||||
if !b {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
err := ui.deployer.FullDeploy()
|
||||
if err != nil {
|
||||
ui.addLog(fmt.Sprintf("Ошибка полной установки: %v", err))
|
||||
}
|
||||
}()
|
||||
}, ui.window)
|
||||
}
|
||||
|
||||
func (ui *AppUI) onSelectiveUpdate() {
|
||||
ui.updateAndSaveConfig()
|
||||
var toUpdate []string
|
||||
for name, selected := range ui.selectedComponents {
|
||||
if selected {
|
||||
toUpdate = append(toUpdate, name)
|
||||
}
|
||||
}
|
||||
if len(toUpdate) == 0 {
|
||||
dialog.ShowInformation("Информация", "Не выбраны компоненты для обновления", ui.window)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := ui.deployer.SelectiveUpdate(toUpdate)
|
||||
if err != nil {
|
||||
ui.addLog(fmt.Sprintf("Ошибка выборочного обновления: %v", err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// --- Вспомогательные функции ---
|
||||
|
||||
func (ui *AppUI) updateAndSaveConfig() {
|
||||
// Мьютекс здесь не нужен, т.к. доступ к виджетам идет из основного потока
|
||||
ui.connConfig.IP = ui.ipEntry.Text
|
||||
ui.connConfig.Port = ui.portEntry.Text
|
||||
ui.connConfig.Login = ui.loginEntry.Text
|
||||
ui.connConfig.Password = ui.passwordEntry.Text
|
||||
// Путь к пакету уже обновлен, сохраняем все вместе
|
||||
ui.connConfig.Save(configFileName)
|
||||
}
|
||||
|
||||
func (ui *AppUI) loadManifest() {
|
||||
ui.addLog("Загрузка манифеста...")
|
||||
loadedPackage, err := deployer.LoadDeploymentPackage(ui.connConfig.DeploymentPackagePath)
|
||||
if err != nil {
|
||||
ui.addLog(fmt.Sprintf("Ошибка загрузки манифеста: %v", err))
|
||||
// Можно показать диалог ошибки, но делать это нужно в основном потоке.
|
||||
// Для этого можно использовать канал или просто положиться на лог.
|
||||
return
|
||||
}
|
||||
ui.manifest = loadedPackage.Manifest
|
||||
ui.addLog("Манифест успешно загружен. Компоненты:")
|
||||
for _, c := range ui.manifest.Components {
|
||||
ui.addLog(fmt.Sprintf("- %s (v%s)", c.Name, c.Version))
|
||||
}
|
||||
|
||||
ui.componentList.Refresh()
|
||||
|
||||
// Сразу после успешной загрузки манифеста запускаем проверку статусов.
|
||||
// Нет необходимости делать это через ui.onRefreshStatus, можно напрямую.
|
||||
go func() {
|
||||
statuses, err := ui.deployer.GetServicesStatus(ui.manifest.Components)
|
||||
if err != nil {
|
||||
ui.addLog(fmt.Sprintf("Ошибка первоначальной проверки статуса: %v", err))
|
||||
return
|
||||
}
|
||||
ui.statuses = statuses
|
||||
ui.statusList.Refresh()
|
||||
}()
|
||||
}
|
||||
|
||||
func (ui *AppUI) addLog(message string) {
|
||||
// Форматируем сообщение с временной меткой
|
||||
logLine := fmt.Sprintf("%s: %s", time.Now().Format("15:04:05"), message)
|
||||
|
||||
// Добавляем в историю логов (потокобезопасно через binding)
|
||||
ui.logData.Append(logLine)
|
||||
|
||||
// Обновляем метку с последним логом
|
||||
// Fyne поддерживает обновление виджетов из любого потока
|
||||
if ui.latestLogLabel != nil {
|
||||
fyne.Do(func() {
|
||||
if !strings.Contains(ui.latestLogLabel.Text, "\n") {
|
||||
ui.latestLogLabel.SetText(logLine)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ShowAndRun показывает окно и запускает приложение
|
||||
func (ui *AppUI) ShowAndRun() {
|
||||
ui.window.ShowAndRun()
|
||||
}
|
||||
|
@ -1,175 +0,0 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
|
||||
"gitea.unprism.ru/KRBL/FemaInstaller/internal/updater"
|
||||
"gitea.unprism.ru/KRBL/FemaInstaller/pkg/config"
|
||||
)
|
||||
|
||||
// UpdaterWindow represents the main window for the updater application
|
||||
type UpdaterWindow struct {
|
||||
Window fyne.Window
|
||||
ConfigDisplay *widget.Label
|
||||
UpdateButton *widget.Button
|
||||
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
|
||||
func NewUpdaterWindow(app fyne.App, config *config.UpdaterConfig, femaUpdater *updater.Updater) *UpdaterWindow {
|
||||
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
|
||||
updaterWindow := &UpdaterWindow{
|
||||
Window: window,
|
||||
ConfigDisplay: widget.NewLabel(config.String()),
|
||||
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
|
||||
updaterWindow.UpdateButton = widget.NewButton("Обновить ПО", func() {
|
||||
updaterWindow.StatusLabel.SetText("Начало обновления...")
|
||||
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() {
|
||||
// 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 {
|
||||
updaterWindow.StatusLabel.SetText("Ошибка обновления: " + err.Error())
|
||||
} else {
|
||||
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.UpdateMethodRadio.Enable()
|
||||
}()
|
||||
})
|
||||
|
||||
// Create title
|
||||
title := widget.NewLabel("Программа обновления ПО Фема")
|
||||
title.Alignment = fyne.TextAlignCenter
|
||||
title.TextStyle = fyne.TextStyle{Bold: true}
|
||||
|
||||
// Create config section title
|
||||
configTitle := widget.NewLabel("Текущая конфигурация:")
|
||||
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
|
||||
content := container.NewVBox(
|
||||
title,
|
||||
widget.NewSeparator(),
|
||||
configTitle,
|
||||
updaterWindow.ConfigDisplay,
|
||||
widget.NewSeparator(),
|
||||
updateMethodTitle,
|
||||
updaterWindow.UpdateMethodRadio,
|
||||
widget.NewSeparator(),
|
||||
updaterWindow.UpdateButton,
|
||||
updaterWindow.StatusLabel,
|
||||
progressSection,
|
||||
)
|
||||
|
||||
// Set window content and size
|
||||
window.SetContent(content)
|
||||
window.Resize(fyne.NewSize(500, 400))
|
||||
|
||||
return updaterWindow
|
||||
}
|
||||
|
||||
// Show displays the updater window
|
||||
func (w *UpdaterWindow) Show() {
|
||||
w.Window.Show()
|
||||
}
|
||||
|
||||
// ShowAndRun displays the updater window and starts the application
|
||||
func (w *UpdaterWindow) ShowAndRun() {
|
||||
w.Window.ShowAndRun()
|
||||
}
|
@ -1,229 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gitea.unprism.ru/KRBL/FemaInstaller/internal/ssh"
|
||||
"gitea.unprism.ru/KRBL/FemaInstaller/pkg/config"
|
||||
"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
|
||||
type Updater struct {
|
||||
Config *config.UpdaterConfig
|
||||
BinaryData []byte
|
||||
UpdateMethod UpdateMethod
|
||||
}
|
||||
|
||||
// NewUpdater creates a new updater with the provided configuration and binary data
|
||||
func NewUpdater(config *config.UpdaterConfig, binaryData []byte) *Updater {
|
||||
return &Updater{
|
||||
Config: config,
|
||||
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
|
||||
func (u *Updater) Update(progressCallback ProgressCallback) error {
|
||||
// If no callback is provided, use a no-op callback
|
||||
if progressCallback == nil {
|
||||
progressCallback = func(progress ProgressInfo) {}
|
||||
}
|
||||
|
||||
// Report initial progress
|
||||
progressCallback(ProgressInfo{
|
||||
Stage: "Подготовка",
|
||||
Percentage: 0,
|
||||
EstimatedTimeRemaining: 0,
|
||||
})
|
||||
|
||||
// Create SSH client configuration
|
||||
clientConfig := ssh.NewClientConfig(
|
||||
u.Config.IP,
|
||||
u.Config.Port,
|
||||
u.Config.Login,
|
||||
u.Config.Password,
|
||||
)
|
||||
|
||||
// Connect to SSH server
|
||||
progressCallback(ProgressInfo{
|
||||
Stage: "Подключение к серверу",
|
||||
Percentage: 10,
|
||||
EstimatedTimeRemaining: 0,
|
||||
})
|
||||
|
||||
sshClient, err := ssh.CreateSSHClient(clientConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка подключения SSH: %w", err)
|
||||
}
|
||||
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
|
||||
sftpClient, err := ssh.CreateSFTPClient(clientConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка подключения SFTP: %w", err)
|
||||
}
|
||||
defer sftpClient.Close()
|
||||
|
||||
progressCallback(ProgressInfo{
|
||||
Stage: "Загрузка файла",
|
||||
Percentage: 30,
|
||||
EstimatedTimeRemaining: 0,
|
||||
})
|
||||
|
||||
// Upload binary with progress reporting
|
||||
remotePath := "/root/fema/build.new"
|
||||
|
||||
// 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 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
|
||||
commands := []string{
|
||||
"mv -f /root/fema/build.new /root/fema/build",
|
||||
"chmod +x /root/fema/build",
|
||||
"systemctl restart fema.service",
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("ошибка выполнения команды '%s': %w", cmd, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Report completion
|
||||
progressCallback(ProgressInfo{
|
||||
Stage: "Завершено",
|
||||
Percentage: 100,
|
||||
EstimatedTimeRemaining: 0,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfigFilePath returns the path to the configuration file
|
||||
func GetConfigFilePath() string {
|
||||
// Get executable directory
|
||||
execDir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
||||
if err != nil {
|
||||
execDir = "."
|
||||
}
|
||||
|
||||
return filepath.Join(execDir, "updater_config.json")
|
||||
}
|
@ -1,61 +1,43 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Config represents the application configuration
|
||||
type Config struct {
|
||||
DefaultSettings string
|
||||
// ConnectionConfig содержит параметры для подключения и развертывания
|
||||
type ConnectionConfig struct {
|
||||
IP string `json:"ip"`
|
||||
Port string `json:"port"`
|
||||
Login string `json:"login"`
|
||||
Password string `json:"password"`
|
||||
DeploymentPackagePath string `json:"deployment_package_path"`
|
||||
}
|
||||
|
||||
// SSHConfig holds SSH connection and installation parameters
|
||||
type SSHConfig struct {
|
||||
IP string
|
||||
Port string
|
||||
Login string
|
||||
Password string
|
||||
Serial string
|
||||
TailNumber string
|
||||
DefaultHost string
|
||||
ArchivePath string
|
||||
}
|
||||
|
||||
// NewSSHConfig creates a new SSH configuration with the provided parameters
|
||||
func NewSSHConfig(ip, port, login, password, serial, tailNumber, defaultHost, archivePath string) *SSHConfig {
|
||||
return &SSHConfig{
|
||||
IP: ip,
|
||||
Port: port,
|
||||
Login: login,
|
||||
Password: password,
|
||||
Serial: serial,
|
||||
TailNumber: tailNumber,
|
||||
DefaultHost: defaultHost,
|
||||
ArchivePath: archivePath,
|
||||
// Save сохраняет конфигурацию в файл
|
||||
func (c *ConnectionConfig) Save(filePath string) error {
|
||||
data, err := json.MarshalIndent(c, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filePath, data, 0644)
|
||||
}
|
||||
|
||||
// UpdateSettingsJSON updates the default settings JSON with user-provided values
|
||||
func UpdateSettingsJSON(defaultSettings string, config *SSHConfig) (string, error) {
|
||||
// Replace registration number
|
||||
updated := strings.Replace(defaultSettings, `"REG_NUMBER" : "",`, fmt.Sprintf(`"REG_NUMBER" : "%s",`, config.TailNumber), 1)
|
||||
|
||||
// Split the default host into domain and port
|
||||
hostParts := strings.Split(config.DefaultHost, ":")
|
||||
if len(hostParts) != 2 {
|
||||
return "", fmt.Errorf("invalid default host format, expected domain:port")
|
||||
// Load загружает конфигурацию из файла
|
||||
func Load(filePath string) (*ConnectionConfig, error) {
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
// Возвращаем пустую конфигурацию, если файл не найден
|
||||
return &ConnectionConfig{Port: "22", Login: "root"}, nil
|
||||
}
|
||||
|
||||
// Replace domain and port
|
||||
updated = strings.Replace(updated, `"DOMAIN" : "",`, fmt.Sprintf(`"DOMAIN" : "%s",`, hostParts[0]), 1)
|
||||
updated = strings.Replace(updated, `"PORT" : 0`, fmt.Sprintf(`"PORT" : %s`, hostParts[1]), 1)
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
// SaveSettingsJSON saves the updated settings JSON to a file
|
||||
func SaveSettingsJSON(content string, filePath string) error {
|
||||
return os.WriteFile(filePath, []byte(content), 0644)
|
||||
var cfg ConnectionConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
15
pkg/config/manifest.go
Normal file
15
pkg/config/manifest.go
Normal file
@ -0,0 +1,15 @@
|
||||
package config
|
||||
|
||||
// Component описывает один компонент в пакете развертывания
|
||||
type Component struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type"` // e.g., "infrastructure", "service"
|
||||
}
|
||||
|
||||
// Manifest описывает содержимое пакета развертывания
|
||||
type Manifest struct {
|
||||
PackageVersion string `json:"package_version"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
Components []Component `json:"components"`
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// UpdaterConfig holds the configuration for the updater application
|
||||
type UpdaterConfig struct {
|
||||
IP string `json:"ip"`
|
||||
Port string `json:"port"`
|
||||
Login string `json:"login"`
|
||||
Password string `json:"password"`
|
||||
DownloadURL string `json:"downloadUrl"`
|
||||
}
|
||||
|
||||
// NewUpdaterConfig creates a new updater configuration with default values
|
||||
func NewUpdaterConfig() *UpdaterConfig {
|
||||
return &UpdaterConfig{
|
||||
IP: "127.0.0.1",
|
||||
Port: "22",
|
||||
Login: "root",
|
||||
Password: "",
|
||||
DownloadURL: "http://example.com/fema/build",
|
||||
}
|
||||
}
|
||||
|
||||
// LoadUpdaterConfig loads the updater configuration from a file
|
||||
func LoadUpdaterConfig(filePath string) (*UpdaterConfig, error) {
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
// Create default config if file doesn't exist
|
||||
config := NewUpdaterConfig()
|
||||
if err := SaveUpdaterConfig(config, filePath); err != nil {
|
||||
return nil, fmt.Errorf("не удалось создать конфигурационный файл: %w", err)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// Read file
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось прочитать конфигурационный файл: %w", err)
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
var config UpdaterConfig
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("не удалось разобрать конфигурационный файл: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// SaveUpdaterConfig saves the updater configuration to a file
|
||||
func SaveUpdaterConfig(config *UpdaterConfig, filePath string) error {
|
||||
// Marshal JSON
|
||||
data, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось сериализовать конфигурацию: %w", err)
|
||||
}
|
||||
|
||||
// Write file
|
||||
if err := os.WriteFile(filePath, data, 0644); err != nil {
|
||||
return fmt.Errorf("не удалось записать конфигурационный файл: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns a string representation of the updater configuration
|
||||
func (c *UpdaterConfig) String() string {
|
||||
return fmt.Sprintf("IP: %s\nПорт: %s\nЛогин: %s\nПароль: %s\nURL загрузки: %s", c.IP, c.Port, c.Login, c.Password, c.DownloadURL)
|
||||
}
|
Reference in New Issue
Block a user