update.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. package command
  2. import (
  3. "archive/tar"
  4. "archive/zip"
  5. "bytes"
  6. "compress/gzip"
  7. "context"
  8. "crypto/md5"
  9. "encoding/hex"
  10. "encoding/json"
  11. "fmt"
  12. swv "github.com/seaweedfs/seaweedfs/weed/util/version"
  13. "io"
  14. "net/http"
  15. "os"
  16. "path/filepath"
  17. "runtime"
  18. "strings"
  19. "time"
  20. "github.com/seaweedfs/seaweedfs/weed/glog"
  21. "github.com/seaweedfs/seaweedfs/weed/util"
  22. util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
  23. "golang.org/x/net/context/ctxhttp"
  24. )
  25. //copied from https://github.com/restic/restic/tree/master/internal/selfupdate
  26. // Release collects data about a single release on GitHub.
  27. type Release struct {
  28. Name string `json:"name"`
  29. TagName string `json:"tag_name"`
  30. Draft bool `json:"draft"`
  31. PreRelease bool `json:"prerelease"`
  32. PublishedAt time.Time `json:"published_at"`
  33. Assets []Asset `json:"assets"`
  34. Version string `json:"-"` // set manually in the code
  35. }
  36. // Asset is a file uploaded and attached to a release.
  37. type Asset struct {
  38. ID int `json:"id"`
  39. Name string `json:"name"`
  40. URL string `json:"url"`
  41. }
  42. const githubAPITimeout = 30 * time.Second
  43. // githubError is returned by the GitHub API, e.g. for rate-limiting.
  44. type githubError struct {
  45. Message string
  46. }
  47. // default version is not full version
  48. var isFullVersion = false
  49. var (
  50. updateOpt UpdateOptions
  51. )
  52. type UpdateOptions struct {
  53. dir *string
  54. name *string
  55. Version *string
  56. }
  57. func init() {
  58. path, _ := os.Executable()
  59. _, name := filepath.Split(path)
  60. updateOpt.dir = cmdUpdate.Flag.String("dir", filepath.Dir(path), "directory to save new weed.")
  61. updateOpt.name = cmdUpdate.Flag.String("name", name, "name of new weed. On windows, name shouldn't be same to the original name.")
  62. updateOpt.Version = cmdUpdate.Flag.String("version", "0", "specific version of weed you want to download. If not specified, get the latest version.")
  63. cmdUpdate.Run = runUpdate
  64. }
  65. var cmdUpdate = &Command{
  66. UsageLine: "update [-dir=/path/to/dir] [-name=name] [-version=x.xx]",
  67. Short: "get latest or specific version from https://github.com/seaweedfs/seaweedfs",
  68. Long: `get latest or specific version from https://github.com/seaweedfs/seaweedfs`,
  69. }
  70. func runUpdate(cmd *Command, args []string) bool {
  71. path, _ := os.Executable()
  72. _, name := filepath.Split(path)
  73. if *updateOpt.dir != "" {
  74. if err := util.TestFolderWritable(util.ResolvePath(*updateOpt.dir)); err != nil {
  75. glog.Fatalf("Check Folder(-dir) Writable %s : %s", *updateOpt.dir, err)
  76. return false
  77. }
  78. } else {
  79. *updateOpt.dir = filepath.Dir(path)
  80. }
  81. if *updateOpt.name == "" {
  82. *updateOpt.name = name
  83. }
  84. target := filepath.Join(*updateOpt.dir, *updateOpt.name)
  85. if runtime.GOOS == "windows" {
  86. if target == path {
  87. glog.Fatalf("On windows, name of the new weed shouldn't be same to the original name.")
  88. return false
  89. }
  90. }
  91. glog.V(0).Infof("new weed will be saved to %s", target)
  92. _, err := downloadRelease(context.Background(), target, *updateOpt.Version)
  93. if err != nil {
  94. glog.Errorf("unable to download weed: %v", err)
  95. return false
  96. }
  97. return true
  98. }
  99. func downloadRelease(ctx context.Context, target string, ver string) (version string, err error) {
  100. currentVersion := swv.VERSION_NUMBER
  101. rel, err := GitHubLatestRelease(ctx, ver, "seaweedfs", "seaweedfs")
  102. if err != nil {
  103. return "", err
  104. }
  105. if rel.Version == currentVersion {
  106. if ver == "0" {
  107. glog.V(0).Infof("weed is up to date")
  108. } else {
  109. glog.V(0).Infof("no need to download the same version of weed ")
  110. }
  111. return currentVersion, nil
  112. }
  113. glog.V(0).Infof("download version: %s", rel.Version)
  114. largeDiskSuffix := ""
  115. if util.VolumeSizeLimitGB == 8000 {
  116. largeDiskSuffix = "_large_disk"
  117. }
  118. fullSuffix := ""
  119. if isFullVersion {
  120. fullSuffix = "_full"
  121. }
  122. ext := "tar.gz"
  123. if runtime.GOOS == "windows" {
  124. ext = "zip"
  125. }
  126. suffix := fmt.Sprintf("%s_%s%s%s.%s", runtime.GOOS, runtime.GOARCH, fullSuffix, largeDiskSuffix, ext)
  127. md5Filename := fmt.Sprintf("%s.md5", suffix)
  128. _, md5Val, err := getGithubDataFile(ctx, rel.Assets, md5Filename)
  129. if err != nil {
  130. return "", err
  131. }
  132. downloadFilename, buf, err := getGithubDataFile(ctx, rel.Assets, suffix)
  133. if err != nil {
  134. return "", err
  135. }
  136. md5Ctx := md5.New()
  137. md5Ctx.Write(buf)
  138. binaryMd5 := md5Ctx.Sum(nil)
  139. if hex.EncodeToString(binaryMd5) != string(md5Val[0:32]) {
  140. glog.Errorf("md5:'%s' '%s'", hex.EncodeToString(binaryMd5), string(md5Val[0:32]))
  141. err = fmt.Errorf("binary md5sum doesn't match")
  142. return "", err
  143. }
  144. err = extractToFile(buf, downloadFilename, target)
  145. if err != nil {
  146. return "", err
  147. } else {
  148. glog.V(0).Infof("successfully updated weed to version %v\n", rel.Version)
  149. }
  150. return rel.Version, nil
  151. }
  152. // GitHubLatestRelease uses the GitHub API to get information about the specific
  153. // release of a repository.
  154. func GitHubLatestRelease(ctx context.Context, ver string, owner, repo string) (Release, error) {
  155. ctx, cancel := context.WithTimeout(ctx, githubAPITimeout)
  156. defer cancel()
  157. url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", owner, repo)
  158. req, err := http.NewRequest(http.MethodGet, url, nil)
  159. if err != nil {
  160. return Release{}, err
  161. }
  162. // pin API version 3
  163. req.Header.Set("Accept", "application/vnd.github.v3+json")
  164. res, err := ctxhttp.Do(ctx, http.DefaultClient, req)
  165. if err != nil {
  166. return Release{}, err
  167. }
  168. defer util_http.CloseResponse(res)
  169. if res.StatusCode != http.StatusOK {
  170. content := res.Header.Get("Content-Type")
  171. if strings.Contains(content, "application/json") {
  172. // try to decode error message
  173. var msg githubError
  174. jerr := json.NewDecoder(res.Body).Decode(&msg)
  175. if jerr == nil {
  176. return Release{}, fmt.Errorf("unexpected status %v (%v) returned, message:\n %v", res.StatusCode, res.Status, msg.Message)
  177. }
  178. }
  179. return Release{}, fmt.Errorf("unexpected status %v (%v) returned", res.StatusCode, res.Status)
  180. }
  181. buf, err := io.ReadAll(res.Body)
  182. if err != nil {
  183. return Release{}, err
  184. }
  185. var release Release
  186. var releaseList []Release
  187. err = json.Unmarshal(buf, &releaseList)
  188. if err != nil {
  189. return Release{}, err
  190. }
  191. if ver == "0" {
  192. release = releaseList[0]
  193. glog.V(0).Infof("latest version is %v\n", release.TagName)
  194. } else {
  195. for _, r := range releaseList {
  196. if r.TagName == ver {
  197. release = r
  198. break
  199. }
  200. }
  201. }
  202. if release.TagName == "" {
  203. return Release{}, fmt.Errorf("can not find the specific version")
  204. }
  205. release.Version = release.TagName
  206. return release, nil
  207. }
  208. func getGithubData(ctx context.Context, url string) ([]byte, error) {
  209. req, err := http.NewRequest(http.MethodGet, url, nil)
  210. if err != nil {
  211. return nil, err
  212. }
  213. // request binary data
  214. req.Header.Set("Accept", "application/octet-stream")
  215. res, err := ctxhttp.Do(ctx, http.DefaultClient, req)
  216. if err != nil {
  217. return nil, err
  218. }
  219. defer util_http.CloseResponse(res)
  220. if res.StatusCode != http.StatusOK {
  221. return nil, fmt.Errorf("unexpected status %v (%v) returned", res.StatusCode, res.Status)
  222. }
  223. buf, err := io.ReadAll(res.Body)
  224. if err != nil {
  225. return nil, err
  226. }
  227. return buf, nil
  228. }
  229. func getGithubDataFile(ctx context.Context, assets []Asset, suffix string) (filename string, data []byte, err error) {
  230. var url string
  231. for _, a := range assets {
  232. if strings.HasSuffix(a.Name, suffix) {
  233. url = a.URL
  234. filename = a.Name
  235. break
  236. }
  237. }
  238. if url == "" {
  239. return "", nil, fmt.Errorf("unable to find file with suffix %v", suffix)
  240. }
  241. glog.V(0).Infof("download %v\n", filename)
  242. data, err = getGithubData(ctx, url)
  243. if err != nil {
  244. return "", nil, err
  245. }
  246. return filename, data, nil
  247. }
  248. func extractToFile(buf []byte, filename, target string) error {
  249. var rd io.Reader = bytes.NewReader(buf)
  250. switch filepath.Ext(filename) {
  251. case ".gz":
  252. gr, err := gzip.NewReader(rd)
  253. if err != nil {
  254. return err
  255. }
  256. defer gr.Close()
  257. trd := tar.NewReader(gr)
  258. hdr, terr := trd.Next()
  259. if terr != nil {
  260. if hdr != nil {
  261. glog.Errorf("uncompress file(%s) failed:%s", hdr.Name, terr)
  262. } else {
  263. glog.Errorf("uncompress file is nil, failed:%s", terr)
  264. }
  265. return terr
  266. }
  267. rd = trd
  268. case ".zip":
  269. zrd, err := zip.NewReader(bytes.NewReader(buf), int64(len(buf)))
  270. if err != nil {
  271. return err
  272. }
  273. if len(zrd.File) != 1 {
  274. return fmt.Errorf("ZIP archive contains more than one file")
  275. }
  276. file, err := zrd.File[0].Open()
  277. if err != nil {
  278. return err
  279. }
  280. defer func() {
  281. _ = file.Close()
  282. }()
  283. rd = file
  284. }
  285. // Write everything to a temp file
  286. dir := filepath.Dir(target)
  287. new, err := os.CreateTemp(dir, "weed")
  288. if err != nil {
  289. return err
  290. }
  291. n, err := io.Copy(new, rd)
  292. if err != nil {
  293. _ = new.Close()
  294. _ = os.Remove(new.Name())
  295. return err
  296. }
  297. if err = new.Sync(); err != nil {
  298. return err
  299. }
  300. if err = new.Close(); err != nil {
  301. return err
  302. }
  303. mode := os.FileMode(0755)
  304. // attempt to find the original mode
  305. if fi, err := os.Lstat(target); err == nil {
  306. mode = fi.Mode()
  307. }
  308. // Rename the temp file to the final location atomically.
  309. if err := os.Rename(new.Name(), target); err != nil {
  310. return err
  311. }
  312. glog.V(0).Infof("saved %d bytes in %v\n", n, target)
  313. return os.Chmod(target, mode)
  314. }