upgrade.go 6.1 KB

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