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.

382 lines
8.8 KiB

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