upgrade.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. package upgrader
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. _github "github.com/0xJacky/Nginx-UI/.github"
  6. "github.com/0xJacky/Nginx-UI/frontend"
  7. "github.com/0xJacky/Nginx-UI/server/internal/helper"
  8. "github.com/0xJacky/Nginx-UI/server/internal/logger"
  9. "github.com/0xJacky/Nginx-UI/server/settings"
  10. "github.com/pkg/errors"
  11. "io"
  12. "net/http"
  13. "net/url"
  14. "os"
  15. "path/filepath"
  16. "runtime"
  17. "strconv"
  18. "strings"
  19. "time"
  20. )
  21. const (
  22. GithubLatestReleaseAPI = "https://api.github.com/repos/0xJacky/nginx-ui/releases/latest"
  23. GithubReleasesListAPI = "https://api.github.com/repos/0xJacky/nginx-ui/releases"
  24. )
  25. type RuntimeInfo struct {
  26. OS string `json:"os"`
  27. Arch string `json:"arch"`
  28. ExPath string `json:"ex_path"`
  29. }
  30. func GetRuntimeInfo() (r RuntimeInfo, err error) {
  31. ex, err := os.Executable()
  32. if err != nil {
  33. err = errors.Wrap(err, "service.GetRuntimeInfo os.Executable() err")
  34. return
  35. }
  36. realPath, err := filepath.EvalSymlinks(ex)
  37. if err != nil {
  38. err = errors.Wrap(err, "service.GetRuntimeInfo filepath.EvalSymlinks() err")
  39. return
  40. }
  41. r = RuntimeInfo{
  42. OS: runtime.GOOS,
  43. Arch: runtime.GOARCH,
  44. ExPath: realPath,
  45. }
  46. return
  47. }
  48. type TReleaseAsset struct {
  49. Name string `json:"name"`
  50. BrowserDownloadUrl string `json:"browser_download_url"`
  51. Size uint `json:"size"`
  52. }
  53. type TRelease struct {
  54. TagName string `json:"tag_name"`
  55. Name string `json:"name"`
  56. PublishedAt time.Time `json:"published_at"`
  57. Body string `json:"body"`
  58. Prerelease bool `json:"prerelease"`
  59. Assets []TReleaseAsset `json:"assets"`
  60. }
  61. func (t *TRelease) GetAssetsMap() (m map[string]TReleaseAsset) {
  62. m = make(map[string]TReleaseAsset)
  63. for _, v := range t.Assets {
  64. m[v.Name] = v
  65. }
  66. return
  67. }
  68. func getLatestRelease() (data TRelease, err error) {
  69. resp, err := http.Get(GithubLatestReleaseAPI)
  70. if err != nil {
  71. err = errors.Wrap(err, "service.getLatestRelease http.Get err")
  72. return
  73. }
  74. body, err := io.ReadAll(resp.Body)
  75. if err != nil {
  76. err = errors.Wrap(err, "service.getLatestRelease io.ReadAll err")
  77. return
  78. }
  79. defer resp.Body.Close()
  80. if resp.StatusCode != 200 {
  81. err = errors.New(string(body))
  82. return
  83. }
  84. err = json.Unmarshal(body, &data)
  85. if err != nil {
  86. err = errors.Wrap(err, "service.getLatestRelease json.Unmarshal err")
  87. return
  88. }
  89. return
  90. }
  91. func getLatestPrerelease() (data TRelease, err error) {
  92. resp, err := http.Get(GithubReleasesListAPI)
  93. if err != nil {
  94. err = errors.Wrap(err, "service.getLatestPrerelease http.Get err")
  95. return
  96. }
  97. body, err := io.ReadAll(resp.Body)
  98. if err != nil {
  99. err = errors.Wrap(err, "service.getLatestPrerelease io.ReadAll err")
  100. return
  101. }
  102. defer resp.Body.Close()
  103. if resp.StatusCode != 200 {
  104. err = errors.New(string(body))
  105. return
  106. }
  107. var releaseList []TRelease
  108. err = json.Unmarshal(body, &releaseList)
  109. if err != nil {
  110. err = errors.Wrap(err, "service.getLatestPrerelease json.Unmarshal err")
  111. return
  112. }
  113. latestDate := time.Time{}
  114. for _, release := range releaseList {
  115. if release.Prerelease && release.PublishedAt.After(latestDate) {
  116. data = release
  117. latestDate = release.PublishedAt
  118. }
  119. }
  120. return
  121. }
  122. func GetRelease(channel string) (data TRelease, err error) {
  123. switch channel {
  124. default:
  125. fallthrough
  126. case "stable":
  127. return getLatestRelease()
  128. case "prerelease":
  129. return getLatestPrerelease()
  130. }
  131. }
  132. type CurVersion struct {
  133. Version string `json:"version"`
  134. BuildID int `json:"build_id"`
  135. TotalBuild int `json:"total_build"`
  136. }
  137. func GetCurrentVersion() (c CurVersion, err error) {
  138. verJson, err := frontend.DistFS.ReadFile("dist/version.json")
  139. if err != nil {
  140. err = errors.Wrap(err, "service.GetCurrentVersion ReadFile err")
  141. return
  142. }
  143. err = json.Unmarshal(verJson, &c)
  144. if err != nil {
  145. err = errors.Wrap(err, "service.GetCurrentVersion json.Unmarshal err")
  146. return
  147. }
  148. return
  149. }
  150. type Upgrader struct {
  151. Release TRelease
  152. RuntimeInfo
  153. }
  154. func NewUpgrader(channel string) (u *Upgrader, err error) {
  155. data, err := GetRelease(channel)
  156. if err != nil {
  157. return
  158. }
  159. runtimeInfo, err := GetRuntimeInfo()
  160. if err != nil {
  161. return
  162. }
  163. u = &Upgrader{
  164. Release: data,
  165. RuntimeInfo: runtimeInfo,
  166. }
  167. return
  168. }
  169. type ProgressWriter struct {
  170. io.Writer
  171. totalSize int64
  172. currentSize int64
  173. progressChan chan<- float64
  174. }
  175. func (pw *ProgressWriter) Write(p []byte) (int, error) {
  176. n, err := pw.Writer.Write(p)
  177. pw.currentSize += int64(n)
  178. progress := float64(pw.currentSize) / float64(pw.totalSize) * 100
  179. pw.progressChan <- progress
  180. return n, err
  181. }
  182. func downloadRelease(url string, dir string, progressChan chan float64) (tarName string, err error) {
  183. client := &http.Client{}
  184. req, err := http.NewRequest("GET", url, nil)
  185. if err != nil {
  186. return
  187. }
  188. resp, err := client.Do(req)
  189. if err != nil {
  190. return
  191. }
  192. defer resp.Body.Close()
  193. totalSize, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
  194. if err != nil {
  195. return
  196. }
  197. file, err := os.CreateTemp(dir, "nginx-ui-temp-*.tar.gz")
  198. if err != nil {
  199. err = errors.Wrap(err, "service.DownloadLatestRelease CreateTemp error")
  200. return
  201. }
  202. defer file.Close()
  203. progressWriter := &ProgressWriter{Writer: file, totalSize: totalSize, progressChan: progressChan}
  204. multiWriter := io.MultiWriter(progressWriter)
  205. _, err = io.Copy(multiWriter, resp.Body)
  206. close(progressChan)
  207. tarName = file.Name()
  208. return
  209. }
  210. func (u *Upgrader) DownloadLatestRelease(progressChan chan float64) (tarName string, err error) {
  211. bytes, err := _github.DistFS.ReadFile("build/build_info.json")
  212. if err != nil {
  213. err = errors.Wrap(err, "service.DownloadLatestRelease Read build_info.json error")
  214. return
  215. }
  216. type buildArch struct {
  217. Arch string `json:"arch"`
  218. Name string `json:"name"`
  219. }
  220. var buildJson map[string]map[string]buildArch
  221. _ = json.Unmarshal(bytes, &buildJson)
  222. build, ok := buildJson[u.OS]
  223. if !ok {
  224. err = errors.Wrap(err, "os not support upgrade")
  225. return
  226. }
  227. arch, ok := build[u.Arch]
  228. if !ok {
  229. err = errors.Wrap(err, "arch not support upgrade")
  230. return
  231. }
  232. assetsMap := u.Release.GetAssetsMap()
  233. // asset
  234. asset, ok := assetsMap[fmt.Sprintf("nginx-ui-%s.tar.gz", arch.Name)]
  235. if !ok {
  236. err = errors.Wrap(err, "upgrader core asset is empty")
  237. return
  238. }
  239. downloadUrl := asset.BrowserDownloadUrl
  240. if downloadUrl == "" {
  241. err = errors.New("upgrader core downloadUrl is empty")
  242. return
  243. }
  244. // digest
  245. digest, ok := assetsMap[fmt.Sprintf("nginx-ui-%s.tar.gz.digest", arch.Name)]
  246. if !ok || digest.BrowserDownloadUrl == "" {
  247. err = errors.New("upgrader core digest is empty")
  248. return
  249. }
  250. resp, err := http.Get(digest.BrowserDownloadUrl)
  251. if err != nil {
  252. err = errors.Wrap(err, "upgrader core download digest fail")
  253. return
  254. }
  255. defer resp.Body.Close()
  256. dir := filepath.Dir(u.ExPath)
  257. if settings.ServerSettings.GithubProxy != "" {
  258. downloadUrl, err = url.JoinPath(settings.ServerSettings.GithubProxy, downloadUrl)
  259. if err != nil {
  260. err = errors.Wrap(err, "service.DownloadLatestRelease url.JoinPath error")
  261. return
  262. }
  263. }
  264. tarName, err = downloadRelease(downloadUrl, dir, progressChan)
  265. if err != nil {
  266. err = errors.Wrap(err, "service.DownloadLatestRelease downloadFile error")
  267. return
  268. }
  269. // check tar digest
  270. digestFileBytes, err := io.ReadAll(resp.Body)
  271. if err != nil {
  272. err = errors.Wrap(err, "digestFileContent read error")
  273. return
  274. }
  275. digestFileContent := strings.TrimSpace(string(digestFileBytes))
  276. logger.Debug("DownloadLatestRelease tar digest", helper.DigestSHA512(tarName))
  277. logger.Debug("DownloadLatestRelease digestFileContent", digestFileContent)
  278. if digestFileContent != helper.DigestSHA512(tarName) {
  279. err = errors.Wrap(err, "digest not equal")
  280. return
  281. }
  282. return
  283. }
  284. func (u *Upgrader) PerformCoreUpgrade(exPath string, tarPath string) (err error) {
  285. dir := filepath.Dir(exPath)
  286. err = helper.UnTar(dir, tarPath)
  287. if err != nil {
  288. err = errors.Wrap(err, "PerformCoreUpgrade unTar error")
  289. return
  290. }
  291. err = os.Rename(filepath.Join(dir, "nginx-ui"), exPath)
  292. if err != nil {
  293. err = errors.Wrap(err, "PerformCoreUpgrade rename error")
  294. return
  295. }
  296. err = os.Remove(tarPath)
  297. if err != nil {
  298. err = errors.Wrap(err, "PerformCoreUpgrade remove tar error")
  299. return
  300. }
  301. return
  302. }