upgrade.go 5.7 KB

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