func downloadReleaseFromGithub(r repo, artifact string, alias string) { client := github.NewClient(nil) config, err := loadConfig() if err != nil { DieWithError(err, "Could not load user config") } if token, ok := config.Config["token"]; ok { log.Debug("Using Github token from config.") // if the user has a token stored, use it ts := oauth2.StaticTokenSource( &oauth2.Token{AccessToken: token}, ) tc := oauth2.NewClient(oauth2.NoContext, ts) client = github.NewClient(tc) } else { log.Debug("No Github token found in config. Add it with `mup config` if you need it") } // with a provided token, this will also get repos for the authenticated user // TODO: support pagination releases, resp, err := client.Repositories.ListReleases(r.Owner, r.Repo, nil) if resp.StatusCode == 404 { Die("Could not find repository %s/%s. Maybe it doesn't exist, or you need to add your token for μpdater to have access.", r.Owner, r.Repo) } else if err != nil { DieWithError(err, "Error reaching Github") } updateFromGithubReleases(client, r, releases, "", artifact, alias) }
// RemoveApp - remove an existing app func RemoveApp(c *cli.Context) { r := getRepoFromCli(c) cacheDir := getCacheDir() ownerPath := filepath.Join(cacheDir, r.Owner) repoPath := filepath.Join(cacheDir, r.Owner, r.Repo) // remove the repo directory if err := os.RemoveAll(repoPath); err != nil { DieWithError(err, "Could not remove app") } // if the owner directory is empty, remove it too if _, err := os.Stat(ownerPath); os.IsNotExist(err) { log.Debug("No libraries from user/org %s have been added. Nothing left to do", r.Owner) os.Exit(0) } files, err := ioutil.ReadDir(ownerPath) if err != nil { DieWithError(err, "Could not get contents of %s to see if it needed to be removed", ownerPath) } if len(files) > 0 { log.Debug("Directory for user/org %s is not empty, so not deleting it.", r.Owner) os.Exit(0) } log.Debug("No remaining apps for %s. Deleting parent directory", r.Owner) if err = os.RemoveAll(ownerPath); err != nil { DieWithError(err, "Unable to cleanup parent directory for user/org at %s", ownerPath) } }
func loadConfig() (config, error) { var cfg config configPath, err := getConfigPath() if err != nil { return cfg, err } // if the config file doesn't exist, return a new one if _, err = os.Stat(configPath); os.IsNotExist(err) { log.Debug("Config file doesn't exist. Creating an empty config.") cfg = config{ Version: 1, Config: map[string]string{}, } return cfg, nil } configFile, err := os.Open(configPath) if err != nil { return cfg, err } jsonParser := json.NewDecoder(configFile) if err = jsonParser.Decode(&cfg); err != nil { return cfg, err } return cfg, nil }
// DieWithError - print the msg and exit with unsuccessful code func DieWithError(err error, msg string, a ...interface{}) { log.Error(msg, a...) if err != nil { log.Debug(err.Error()) } os.Exit(1) }
// AddApp - add a new app to be managed func AddApp(c *cli.Context) { r := getRepoFromCli(c) artifact := c.String("artifact") alias := c.String("alias") // create the repo dir; if it already exists, exit with success _, alreadyExists := ensureRepoDir(r) if alreadyExists { log.Debug("This app has already been added") os.Exit(0) } // TODO: if we fail to add an app, we don't want to leave the directory around downloadReleaseFromGithub(r, artifact, alias) }
// RemoveConfigEntry - remove config func RemoveConfigEntry(c *cli.Context) { if len(c.Args()) != 1 { Die("You must provide the key of the config to remove") } cfg, err := loadConfig() if err != nil { DieWithError(err, "Could not load existing config") } key := c.Args()[0] log.Debug("Removing config entry for %s", key) delete(cfg.Config, key) persistConfig(cfg) }
// AddConfigEntry - add config func AddConfigEntry(c *cli.Context) { if len(c.Args()) != 2 { Die("You must provide a key and a value.") } cfg, err := loadConfig() if err != nil { DieWithError(err, "Could not load existing config") } key := c.Args()[0] value := c.Args()[1] log.Debug("Addding config entry %s=%s", key, value) cfg.Config[key] = value persistConfig(cfg) }
// ListConfigEntries - list config func ListConfigEntries(c *cli.Context) { cfg, err := loadConfig() if err != nil { DieWithError(err, "Could not load existing config") } log.Debug("Listing existing config") keys := make([]string, len(cfg.Config)) longest := 0 i := 0 for k := range cfg.Config { keys[i] = k if len(k) > longest { longest = len(k) } i++ } sort.Strings(keys) for _, key := range keys { fmt.Fprintf(os.Stdout, "%-*s -> %v\n", longest, key, cfg.Config[key]) } }
func updateFromGithubReleases(client *github.Client, r repo, releases []github.RepositoryRelease, version string, artifact string, alias string) { // TODO: clean up this logic (e.g. use a Matcher -> is the version requested or greater than existing version) searchSV, err := semver.Make(cleanVersionName(version)) hasSearchVersion := false if version != "" && err != nil { DieWithError(err, "Cannot update to a non-semver compatible version %s", version) } else if version != "" { hasSearchVersion = true } // TODO: shouldn't need to load this again cacheDir := getCacheDir() repoPath := filepath.Join(cacheDir, r.Owner, r.Repo) // find the ID of the last version we updated to (directory on disk) files, err := ioutil.ReadDir(repoPath) if err != nil { DieWithError(err, "Could not read from repo directory %s", r.Repo) } var newest semver.Version for _, f := range files { fname := cleanVersionName(f.Name()) sv, err := semver.Make(fname) if err != nil { log.Debug("Found directory with a non-semver name: %s. Skipping.", f.Name()) continue } // just skip non-semver directories for now if sv.Compare(newest) > 0 { newest = sv } } var releaseToInstall github.RepositoryRelease foundRelease := false for _, release := range releases { rname := cleanVersionName(*release.TagName) sv, err := semver.Make(rname) if err != nil { log.Debug("Found release version with a non-semver name: %s. Skipping", *release.TagName) } if *release.Draft || *release.Prerelease { log.Debug("Skipping release %s because it's a draft or a pre-release", *release.TagName) } if hasSearchVersion && searchSV.Equals(sv) { foundRelease = true releaseToInstall = release } else if sv.Compare(newest) > 0 { foundRelease = true releaseToInstall = release } if foundRelease { break } } if !foundRelease { if hasSearchVersion { Die("Could not find version %s", version) } else { Die("Could not find a newer version to install") } } // TODO: install this version releasePath := filepath.Join(cacheDir, r.Owner, r.Repo, *releaseToInstall.TagName) if err := os.Mkdir(releasePath, 0740); err != nil { DieWithError(err, "Could not create cache directory for %s/%s/%s", r.Owner, r.Repo, *releaseToInstall.TagName) } if len(releaseToInstall.Assets) > 1 && len(artifact) == 0 { // TODO: we shouldn't create the directory until we're sure we can install Die("Multiple artifacts for this app - you must specify only one with --artifact") } else { artifact = *releaseToInstall.Assets[0].Name } if len(alias) == 0 { log.Debug("No alias specified - using app name: %s", artifact) alias = artifact } for _, asset := range releaseToInstall.Assets { if *asset.Name != artifact { continue } closer, redirect, err := client.Repositories.DownloadReleaseAsset(r.Owner, r.Repo, *asset.ID) if closer == nil { resp, e := http.Get(redirect) if e != nil { DieWithError(e, "Could not follow redirect to asset: %s", redirect) } closer = resp.Body } log.Debug("Following redirect to download %s", *asset.Name) if err != nil { DieWithError(err, "Could not download asset %s", *asset.Name) } assetFile := filepath.Join(releasePath, *asset.Name) file, err := os.Create(assetFile) if err != nil { DieWithError(err, "Could not create file for %s", *asset.Name) } defer closer.Close() if _, err = io.Copy(file, closer); err != nil { DieWithError(err, "Could not write %s", *asset.Name) } log.Debug("Wrote asset to disk: %s", *asset.Name) if err = os.Chmod(assetFile, 0744); err != nil { DieWithError(err, "Could not add execute permission to %s", *asset.Name) } // create symlink in /usr/local/bin linkPath := filepath.Join("/usr/local/bin", alias) log.Debug("Creating link %s -> %s", linkPath, assetFile) os.Symlink(assetFile, linkPath) } }