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.

388 lines
8.8 KiB

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