You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

376 lines
8.6 KiB

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