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.

391 lines
9.1 KiB

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