1
0

upgrade.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. package upgrader
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "os"
  8. "path/filepath"
  9. "runtime"
  10. "strconv"
  11. "strings"
  12. "sync/atomic"
  13. "time"
  14. "code.pfad.fr/risefront"
  15. _github "github.com/0xJacky/Nginx-UI/.github"
  16. "github.com/0xJacky/Nginx-UI/internal/helper"
  17. "github.com/0xJacky/Nginx-UI/internal/version"
  18. "github.com/minio/selfupdate"
  19. "github.com/pkg/errors"
  20. "github.com/uozi-tech/cosy/logger"
  21. )
  22. const (
  23. UpgradeStatusInfo = "info"
  24. UpgradeStatusError = "error"
  25. UpgradeStatusProgress = "progress"
  26. )
  27. type CoreUpgradeResp struct {
  28. Status string `json:"status"`
  29. Progress float64 `json:"progress"`
  30. Message string `json:"message"`
  31. }
  32. type Upgrader struct {
  33. Channel string
  34. Release version.TRelease
  35. version.RuntimeInfo
  36. }
  37. func NewUpgrader(channel string) (u *Upgrader, err error) {
  38. data, err := version.GetRelease(channel)
  39. if err != nil {
  40. return
  41. }
  42. runtimeInfo, err := version.GetRuntimeInfo()
  43. if err != nil {
  44. return
  45. }
  46. u = &Upgrader{
  47. Channel: channel,
  48. Release: data,
  49. RuntimeInfo: runtimeInfo,
  50. }
  51. return
  52. }
  53. type ProgressWriter struct {
  54. io.Writer
  55. totalSize int64
  56. currentSize int64
  57. progressChan chan<- float64
  58. }
  59. func (pw *ProgressWriter) Write(p []byte) (int, error) {
  60. n, err := pw.Writer.Write(p)
  61. pw.currentSize += int64(n)
  62. progress := float64(pw.currentSize) / float64(pw.totalSize) * 100
  63. pw.progressChan <- progress
  64. return n, err
  65. }
  66. func downloadRelease(url string, dir string, progressChan chan float64) (tarName string, err error) {
  67. client := &http.Client{}
  68. req, err := http.NewRequest("GET", url, nil)
  69. if err != nil {
  70. return
  71. }
  72. resp, err := client.Do(req)
  73. if err != nil {
  74. return
  75. }
  76. defer resp.Body.Close()
  77. totalSize, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
  78. if err != nil {
  79. return
  80. }
  81. file, err := os.CreateTemp(dir, "nginx-ui-temp-*.tar.gz")
  82. if err != nil {
  83. err = errors.Wrap(err, "service.DownloadLatestRelease CreateTemp error")
  84. return
  85. }
  86. defer file.Close()
  87. progressWriter := &ProgressWriter{Writer: file, totalSize: totalSize, progressChan: progressChan}
  88. multiWriter := io.MultiWriter(progressWriter)
  89. _, err = io.Copy(multiWriter, resp.Body)
  90. tarName = file.Name()
  91. return
  92. }
  93. func (u *Upgrader) DownloadLatestRelease(progressChan chan float64) (tarName string, err error) {
  94. bytes, err := _github.DistFS.ReadFile("build/build_info.json")
  95. if err != nil {
  96. err = errors.Wrap(err, "service.DownloadLatestRelease Read build_info.json error")
  97. return
  98. }
  99. type buildArch struct {
  100. Arch string `json:"arch"`
  101. Name string `json:"name"`
  102. }
  103. var buildJson map[string]map[string]buildArch
  104. _ = json.Unmarshal(bytes, &buildJson)
  105. build, ok := buildJson[u.OS]
  106. if !ok {
  107. err = errors.Wrap(err, "os not support upgrade")
  108. return
  109. }
  110. arch, ok := build[u.Arch]
  111. if !ok {
  112. err = errors.Wrap(err, "arch not support upgrade")
  113. return
  114. }
  115. assetsMap := u.Release.GetAssetsMap()
  116. // asset
  117. asset, ok := assetsMap[fmt.Sprintf("nginx-ui-%s.tar.gz", arch.Name)]
  118. if !ok {
  119. err = errors.Wrap(err, "upgrader core asset is empty")
  120. return
  121. }
  122. downloadUrl := asset.BrowserDownloadUrl
  123. if downloadUrl == "" {
  124. err = errors.New("upgrader core downloadUrl is empty")
  125. return
  126. }
  127. // digest
  128. digest, ok := assetsMap[fmt.Sprintf("nginx-ui-%s.tar.gz.digest", arch.Name)]
  129. if !ok || digest.BrowserDownloadUrl == "" {
  130. err = errors.New("upgrader core digest is empty")
  131. return
  132. }
  133. if u.Channel != string(version.ReleaseTypeDev) {
  134. digest.BrowserDownloadUrl = version.GetUrl(digest.BrowserDownloadUrl)
  135. }
  136. resp, err := http.Get(digest.BrowserDownloadUrl)
  137. if err != nil {
  138. err = errors.Wrap(err, "upgrader core download digest fail")
  139. return
  140. }
  141. defer resp.Body.Close()
  142. dir := filepath.Dir(u.ExPath)
  143. if u.Channel != string(version.ReleaseTypeDev) {
  144. downloadUrl = version.GetUrl(downloadUrl)
  145. }
  146. tarName, err = downloadRelease(downloadUrl, dir, progressChan)
  147. if err != nil {
  148. err = errors.Wrap(err, "service.DownloadLatestRelease downloadFile error")
  149. return
  150. }
  151. // check tar digest
  152. digestFileBytes, err := io.ReadAll(resp.Body)
  153. if err != nil {
  154. err = errors.Wrap(err, "digest file content read error")
  155. return
  156. }
  157. digestFileContent := strings.TrimSpace(string(digestFileBytes))
  158. logger.Debug("DownloadLatestRelease tar digest", helper.DigestSHA512(tarName))
  159. logger.Debug("DownloadLatestRelease digestFileContent", digestFileContent)
  160. if digestFileContent == "" {
  161. err = errors.New("digest file content is empty")
  162. return
  163. }
  164. exeSHA512 := helper.DigestSHA512(tarName)
  165. if exeSHA512 == "" {
  166. err = errors.New("executable binary file is empty")
  167. return
  168. }
  169. if digestFileContent != exeSHA512 {
  170. err = errors.Wrap(err, "digest not equal")
  171. return
  172. }
  173. return
  174. }
  175. var updateInProgress atomic.Bool
  176. func (u *Upgrader) PerformCoreUpgrade(tarPath string) (err error) {
  177. if !updateInProgress.CompareAndSwap(false, true) {
  178. return errors.New("update already in progress")
  179. }
  180. defer updateInProgress.Store(false)
  181. oldExe := ""
  182. if runtime.GOOS == "windows" {
  183. oldExe = filepath.Join(filepath.Dir(u.ExPath), ".nginx-ui.old."+strconv.FormatInt(time.Now().Unix(), 10))
  184. }
  185. opts := selfupdate.Options{
  186. OldSavePath: oldExe,
  187. }
  188. if err = opts.CheckPermissions(); err != nil {
  189. return err
  190. }
  191. tempDir, err := os.MkdirTemp("", "nginx-ui-upgrade-*")
  192. if err != nil {
  193. return err
  194. }
  195. defer os.RemoveAll(tempDir)
  196. err = helper.UnTar(tempDir, tarPath)
  197. if err != nil {
  198. err = errors.Wrap(err, "PerformCoreUpgrade unTar error")
  199. return
  200. }
  201. nginxUIExName := "nginx-ui"
  202. if u.OS == "windows" {
  203. nginxUIExName = "nginx-ui.exe"
  204. }
  205. f, err := os.Open(filepath.Join(tempDir, nginxUIExName))
  206. if err != nil {
  207. err = errors.Wrap(err, "PerformCoreUpgrade open error")
  208. return
  209. }
  210. defer f.Close()
  211. if err = selfupdate.PrepareAndCheckBinary(f, opts); err != nil {
  212. var pathErr *os.PathError
  213. if errors.As(err, &pathErr) {
  214. return pathErr.Err
  215. }
  216. return err
  217. }
  218. if err = selfupdate.CommitBinary(opts); err != nil {
  219. if rerr := selfupdate.RollbackError(err); rerr != nil {
  220. return rerr
  221. }
  222. var pathErr *os.PathError
  223. if errors.As(err, &pathErr) {
  224. return pathErr.Err
  225. }
  226. return err
  227. }
  228. // wait for the file to be written
  229. time.Sleep(1 * time.Second)
  230. // gracefully restart
  231. risefront.Restart()
  232. return
  233. }