update.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  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. util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
  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 original 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 original 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. defer util_http.CloseResponse(res)
  168. if res.StatusCode != http.StatusOK {
  169. content := res.Header.Get("Content-Type")
  170. if strings.Contains(content, "application/json") {
  171. // try to decode error message
  172. var msg githubError
  173. jerr := json.NewDecoder(res.Body).Decode(&msg)
  174. if jerr == nil {
  175. return Release{}, fmt.Errorf("unexpected status %v (%v) returned, message:\n %v", res.StatusCode, res.Status, msg.Message)
  176. }
  177. }
  178. return Release{}, fmt.Errorf("unexpected status %v (%v) returned", res.StatusCode, res.Status)
  179. }
  180. buf, err := io.ReadAll(res.Body)
  181. if err != nil {
  182. return Release{}, err
  183. }
  184. var release Release
  185. var releaseList []Release
  186. err = json.Unmarshal(buf, &releaseList)
  187. if err != nil {
  188. return Release{}, err
  189. }
  190. if ver == "0" {
  191. release = releaseList[0]
  192. glog.V(0).Infof("latest version is %v\n", release.TagName)
  193. } else {
  194. for _, r := range releaseList {
  195. if r.TagName == ver {
  196. release = r
  197. break
  198. }
  199. }
  200. }
  201. if release.TagName == "" {
  202. return Release{}, fmt.Errorf("can not find the specific version")
  203. }
  204. release.Version = release.TagName
  205. return release, nil
  206. }
  207. func getGithubData(ctx context.Context, url string) ([]byte, error) {
  208. req, err := http.NewRequest(http.MethodGet, url, nil)
  209. if err != nil {
  210. return nil, err
  211. }
  212. // request binary data
  213. req.Header.Set("Accept", "application/octet-stream")
  214. res, err := ctxhttp.Do(ctx, http.DefaultClient, req)
  215. if err != nil {
  216. return nil, err
  217. }
  218. defer util_http.CloseResponse(res)
  219. if res.StatusCode != http.StatusOK {
  220. return nil, fmt.Errorf("unexpected status %v (%v) returned", res.StatusCode, res.Status)
  221. }
  222. buf, err := io.ReadAll(res.Body)
  223. if err != nil {
  224. return nil, err
  225. }
  226. return buf, nil
  227. }
  228. func getGithubDataFile(ctx context.Context, assets []Asset, suffix string) (filename string, data []byte, err error) {
  229. var url string
  230. for _, a := range assets {
  231. if strings.HasSuffix(a.Name, suffix) {
  232. url = a.URL
  233. filename = a.Name
  234. break
  235. }
  236. }
  237. if url == "" {
  238. return "", nil, fmt.Errorf("unable to find file with suffix %v", suffix)
  239. }
  240. glog.V(0).Infof("download %v\n", filename)
  241. data, err = getGithubData(ctx, url)
  242. if err != nil {
  243. return "", nil, err
  244. }
  245. return filename, data, nil
  246. }
  247. func extractToFile(buf []byte, filename, target string) error {
  248. var rd io.Reader = bytes.NewReader(buf)
  249. switch filepath.Ext(filename) {
  250. case ".gz":
  251. gr, err := gzip.NewReader(rd)
  252. if err != nil {
  253. return err
  254. }
  255. defer gr.Close()
  256. trd := tar.NewReader(gr)
  257. hdr, terr := trd.Next()
  258. if terr != nil {
  259. if hdr != nil {
  260. glog.Errorf("uncompress file(%s) failed:%s", hdr.Name, terr)
  261. } else {
  262. glog.Errorf("uncompress file is nil, failed:%s", terr)
  263. }
  264. return terr
  265. }
  266. rd = trd
  267. case ".zip":
  268. zrd, err := zip.NewReader(bytes.NewReader(buf), int64(len(buf)))
  269. if err != nil {
  270. return err
  271. }
  272. if len(zrd.File) != 1 {
  273. return fmt.Errorf("ZIP archive contains more than one file")
  274. }
  275. file, err := zrd.File[0].Open()
  276. if err != nil {
  277. return err
  278. }
  279. defer func() {
  280. _ = file.Close()
  281. }()
  282. rd = file
  283. }
  284. // Write everything to a temp file
  285. dir := filepath.Dir(target)
  286. new, err := os.CreateTemp(dir, "weed")
  287. if err != nil {
  288. return err
  289. }
  290. n, err := io.Copy(new, rd)
  291. if err != nil {
  292. _ = new.Close()
  293. _ = os.Remove(new.Name())
  294. return err
  295. }
  296. if err = new.Sync(); err != nil {
  297. return err
  298. }
  299. if err = new.Close(); err != nil {
  300. return err
  301. }
  302. mode := os.FileMode(0755)
  303. // attempt to find the original mode
  304. if fi, err := os.Lstat(target); err == nil {
  305. mode = fi.Mode()
  306. }
  307. // Rename the temp file to the final location atomically.
  308. if err := os.Rename(new.Name(), target); err != nil {
  309. return err
  310. }
  311. glog.V(0).Infof("saved %d bytes in %v\n", n, target)
  312. return os.Chmod(target, mode)
  313. }