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.

370 lines
8.7 KiB

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