upgrade.go 6.2 KB

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