upgrade.go 5.7 KB

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