X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=cmd%2Fmicro%2Fpluginmanager.go;h=ad7843e654cc241132ad3f1a52bc5426002d5ee0;hb=41a24e61d6b9017dbe010ae36295cb3c1dd701fc;hp=43296473e401710bea44fd2d66894b993e176b54;hpb=f351c251e46253c334ad641ce6d083dc8565ea5f;p=micro.git diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go index 43296473..ad7843e6 100644 --- a/cmd/micro/pluginmanager.go +++ b/cmd/micro/pluginmanager.go @@ -3,30 +3,28 @@ package main import ( "archive/zip" "bytes" - "encoding/json" "fmt" "io" "io/ioutil" "net/http" "os" "path/filepath" - "regexp" "sort" "strings" "sync" "github.com/blang/semver" + "github.com/flynn/json5" "github.com/yuin/gopher-lua" ) var ( - pluginChannels PluginChannels = PluginChannels{ - PluginChannel("https://www.boombuler.de/channel.json"), - } - - allPluginPackages PluginPackages = nil + allPluginPackages PluginPackages ) +// CorePluginName is a plugin dependency name for the micro core. +const CorePluginName = "micro" + // PluginChannel contains an url to a json list of PluginRepository type PluginChannel string @@ -59,7 +57,7 @@ type PluginVersion struct { // PluginVersions is a slice of PluginVersion type PluginVersions []*PluginVersion -// PluginDenendency descripes a dependency to another plugin or micro itself. +// PluginDependency descripes a dependency to another plugin or micro itself. type PluginDependency struct { Name string Range semver.Range @@ -68,6 +66,23 @@ type PluginDependency struct { // PluginDependencies is a slice of PluginDependency type PluginDependencies []*PluginDependency +func (pp *PluginPackage) String() string { + buf := new(bytes.Buffer) + buf.WriteString("Plugin: ") + buf.WriteString(pp.Name) + buf.WriteRune('\n') + if pp.Author != "" { + buf.WriteString("Author: ") + buf.WriteString(pp.Author) + buf.WriteRune('\n') + } + if pp.Description != "" { + buf.WriteRune('\n') + buf.WriteString(pp.Description) + } + return buf.String() +} + func fetchAllSources(count int, fetcher func(i int) PluginPackages) PluginPackages { wgQuery := new(sync.WaitGroup) wgQuery.Add(count) @@ -105,13 +120,14 @@ func (pc PluginChannels) Fetch() PluginPackages { // Fetch retrieves all available PluginPackages from the given channel func (pc PluginChannel) Fetch() PluginPackages { + // messenger.AddLog(fmt.Sprintf("Fetching channel: %q", string(pc))) resp, err := http.Get(string(pc)) if err != nil { TermMessage("Failed to query plugin channel:\n", err) return PluginPackages{} } defer resp.Body.Close() - decoder := json.NewDecoder(resp.Body) + decoder := json5.NewDecoder(resp.Body) var repositories []PluginRepository if err := decoder.Decode(&repositories); err != nil { @@ -125,20 +141,25 @@ func (pc PluginChannel) Fetch() PluginPackages { // Fetch retrieves all available PluginPackages from the given repository func (pr PluginRepository) Fetch() PluginPackages { + // messenger.AddLog(fmt.Sprintf("Fetching repository: %q", string(pr))) resp, err := http.Get(string(pr)) if err != nil { TermMessage("Failed to query plugin repository:\n", err) return PluginPackages{} } defer resp.Body.Close() - decoder := json.NewDecoder(resp.Body) + decoder := json5.NewDecoder(resp.Body) var plugins PluginPackages if err := decoder.Decode(&plugins); err != nil { TermMessage("Failed to decode repository data:\n", err) return PluginPackages{} } - return plugins + if len(plugins) > 0 { + return PluginPackages{plugins[0]} + } + return nil + // return plugins } // UnmarshalJSON unmarshals raw json to a PluginVersion @@ -149,7 +170,7 @@ func (pv *PluginVersion) UnmarshalJSON(data []byte) error { Require map[string]string } - if err := json.Unmarshal(data, &values); err != nil { + if err := json5.Unmarshal(data, &values); err != nil { return err } pv.Version = values.Version @@ -157,8 +178,13 @@ func (pv *PluginVersion) UnmarshalJSON(data []byte) error { pv.Require = make(PluginDependencies, 0) for k, v := range values.Require { - if vRange, err := semver.ParseRange(v); err == nil { - pv.Require = append(pv.Require, &PluginDependency{k, vRange}) + // don't add the dependency if it's the core and + // we have a unknown version number. + // in that case just accept that dependency (which equals to not adding it.) + if k != CorePluginName || !isUnknownCoreVersion() { + if vRange, err := semver.ParseRange(v); err == nil { + pv.Require = append(pv.Require, &PluginDependency{k, vRange}) + } } } return nil @@ -173,7 +199,7 @@ func (pp *PluginPackage) UnmarshalJSON(data []byte) error { Tags []string Versions PluginVersions } - if err := json.Unmarshal(data, &values); err != nil { + if err := json5.Unmarshal(data, &values); err != nil { return err } pp.Name = values.Name @@ -190,7 +216,39 @@ func (pp *PluginPackage) UnmarshalJSON(data []byte) error { // GetAllPluginPackages gets all PluginPackages which may be available. func GetAllPluginPackages() PluginPackages { if allPluginPackages == nil { - allPluginPackages = pluginChannels.Fetch() + getOption := func(name string) []string { + data := GetOption(name) + if strs, ok := data.([]string); ok { + return strs + } + if ifs, ok := data.([]interface{}); ok { + result := make([]string, len(ifs)) + for i, urlIf := range ifs { + if url, ok := urlIf.(string); ok { + result[i] = url + } else { + return nil + } + } + return result + } + return nil + } + + channels := PluginChannels{} + for _, url := range getOption("pluginchannels") { + channels = append(channels, PluginChannel(url)) + } + repos := []PluginRepository{} + for _, url := range getOption("pluginrepos") { + repos = append(repos, PluginRepository(url)) + } + allPluginPackages = fetchAllSources(len(repos)+1, func(i int) PluginPackages { + if i == 0 { + return channels.Fetch() + } + return repos[i-1].Fetch() + }) } return allPluginPackages } @@ -215,46 +273,71 @@ func (pv PluginVersions) Swap(i, j int) { } // Less returns true if the version at position i is greater then the version at position j (used for sorting) -func (s PluginVersions) Less(i, j int) bool { - return s[i].Version.GT(s[j].Version) +func (pv PluginVersions) Less(i, j int) bool { + return pv[i].Version.GT(pv[j].Version) } // Match returns true if the package matches a given search text func (pp PluginPackage) Match(text string) bool { - // ToDo: improve matching. - text = "(?i)" + text - if r, err := regexp.Compile(text); err == nil { - return r.MatchString(pp.Name) + text = strings.ToLower(text) + for _, t := range pp.Tags { + if strings.ToLower(t) == text { + return true + } + } + if strings.Contains(strings.ToLower(pp.Name), text) { + return true } + + if strings.Contains(strings.ToLower(pp.Description), text) { + return true + } + return false } // IsInstallable returns true if the package can be installed. -func (pp PluginPackage) IsInstallable() bool { - _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{ +func (pp PluginPackage) IsInstallable() error { + _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{ &PluginDependency{ Name: pp.Name, Range: semver.Range(func(v semver.Version) bool { return true }), }}) - return err == nil + return err } // SearchPlugin retrieves a list of all PluginPackages which match the given search text and // could be or are already installed -func SearchPlugin(text string) (plugins PluginPackages) { +func SearchPlugin(texts []string) (plugins PluginPackages) { plugins = make(PluginPackages, 0) + +pluginLoop: for _, pp := range GetAllPluginPackages() { - if pp.Match(text) && pp.IsInstallable() { + for _, text := range texts { + if !pp.Match(text) { + continue pluginLoop + } + } + + if err := pp.IsInstallable(); err == nil { plugins = append(plugins, pp) } } return } +func isUnknownCoreVersion() bool { + _, err := semver.ParseTolerant(Version) + return err != nil +} + func newStaticPluginVersion(name, version string) *PluginVersion { - vers, err := semver.Parse(version) + vers, err := semver.ParseTolerant(version) + if err != nil { - return nil + if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil { + vers = semver.MustParse("0.0.0-unknown") + } } pl := &PluginPackage{ Name: name, @@ -269,13 +352,14 @@ func newStaticPluginVersion(name, version string) *PluginVersion { // GetInstalledVersions returns a list of all currently installed plugins including an entry for // micro itself. This can be used to resolve dependencies. -func GetInstalledVersions() PluginVersions { - result := PluginVersions{ - newStaticPluginVersion("micro", Version), +func GetInstalledVersions(withCore bool) PluginVersions { + result := PluginVersions{} + if withCore { + result = append(result, newStaticPluginVersion(CorePluginName, Version)) } - for _, name := range loadedPlugins { - version := GetInstalledPluginVersion(name) + for name, lpname := range loadedPlugins { + version := GetInstalledPluginVersion(lpname) if pv := newStaticPluginVersion(name, version); pv != nil { result = append(result, pv) } @@ -297,7 +381,9 @@ func GetInstalledPluginVersion(name string) string { return "" } +// DownloadAndInstall downloads and installs the given plugin and version func (pv *PluginVersion) DownloadAndInstall() error { + messenger.AddLog(fmt.Sprintf("Downloading %q (%s) from %q", pv.pack.Name, pv.Version, pv.Url)) resp, err := http.Get(pv.Url) if err != nil { return err @@ -317,25 +403,56 @@ func (pv *PluginVersion) DownloadAndInstall() error { if err = os.MkdirAll(targetDir, dirPerm); err != nil { return err } + + // Check if all files in zip are in the same directory. + // this might be the case if the plugin zip contains the whole plugin dir + // instead of its content. + var prefix string + allPrefixed := false + for i, f := range z.File { + parts := strings.Split(f.Name, "/") + if i == 0 { + prefix = parts[0] + } else if parts[0] != prefix { + allPrefixed = false + break + } else { + // switch to true since we have at least a second file + allPrefixed = true + } + } + + // Install files and directory's for _, f := range z.File { - targetName := filepath.Join(targetDir, filepath.Join(strings.Split(f.Name, "/")...)) + parts := strings.Split(f.Name, "/") + if allPrefixed { + parts = parts[1:] + } + + targetName := filepath.Join(targetDir, filepath.Join(parts...)) if f.FileInfo().IsDir() { if err := os.MkdirAll(targetName, dirPerm); err != nil { return err } } else { + basepath := filepath.Dir(targetName) + + if err := os.MkdirAll(basepath, dirPerm); err != nil { + return err + } + content, err := f.Open() if err != nil { return err } defer content.Close() - if target, err := os.Create(targetName); err != nil { + target, err := os.Create(targetName) + if err != nil { + return err + } + defer target.Close() + if _, err = io.Copy(target, content); err != nil { return err - } else { - defer target.Close() - if _, err = io.Copy(target, content); err != nil { - return err - } } } } @@ -385,6 +502,7 @@ func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies return result } +// Resolve resolves dependencies between different plugins func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) { if len(open) == 0 { return selectedVersions, nil @@ -396,43 +514,53 @@ func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDe return all.Resolve(selectedVersions, stillOpen) } return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name) - } else { - availableVersions := all.GetAllVersions(currentRequirement.Name) - sort.Sort(availableVersions) + } + availableVersions := all.GetAllVersions(currentRequirement.Name) + sort.Sort(availableVersions) - for _, version := range availableVersions { - if currentRequirement.Range(version.Version) { - resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require)) + for _, version := range availableVersions { + if currentRequirement.Range(version.Version) { + resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require)) - if err == nil { - return resolved, nil - } + if err == nil { + return resolved, nil } } - return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name) } - } else { - return selectedVersions, nil + return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name) } + return selectedVersions, nil } -func (versions PluginVersions) install() { +func (pv PluginVersions) install() { anyInstalled := false - for _, sel := range versions { - if sel.pack.Name != "micro" { - installed := GetInstalledPluginVersion(sel.pack.Name) - if v, err := semver.Parse(installed); err != nil || v.NE(sel.Version) { - UninstallPlugin(sel.pack.Name) + currentlyInstalled := GetInstalledVersions(true) + + for _, sel := range pv { + if sel.pack.Name != CorePluginName { + shouldInstall := true + if pv := currentlyInstalled.find(sel.pack.Name); pv != nil { + if pv.Version.NE(sel.Version) { + messenger.AddLog("Uninstalling", sel.pack.Name) + UninstallPlugin(sel.pack.Name) + } else { + shouldInstall = false + } } - if err := sel.DownloadAndInstall(); err != nil { - messenger.Error(err) - return + + if shouldInstall { + if err := sel.DownloadAndInstall(); err != nil { + messenger.Error(err) + return + } + anyInstalled = true } - anyInstalled = true } } if anyInstalled { messenger.Message("One or more plugins installed. Please restart micro.") + } else { + messenger.AddLog("Nothing to install / update") } } @@ -440,11 +568,14 @@ func (versions PluginVersions) install() { func UninstallPlugin(name string) { if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil { messenger.Error(err) + return } + delete(loadedPlugins, name) } +// Install installs the plugin func (pl PluginPackage) Install() { - selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{ + selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{ &PluginDependency{ Name: pl.Name, Range: semver.Range(func(v semver.Version) bool { return true }), @@ -456,13 +587,22 @@ func (pl PluginPackage) Install() { selected.install() } -func UpdatePlugins() { +// UpdatePlugins updates the given plugins +func UpdatePlugins(plugins []string) { + // if no plugins are specified, update all installed plugins. + if len(plugins) == 0 { + for name := range loadedPlugins { + plugins = append(plugins, name) + } + } + + messenger.AddLog("Checking for plugin updates") microVersion := PluginVersions{ - newStaticPluginVersion("micro", Version), + newStaticPluginVersion(CorePluginName, Version), } var updates = make(PluginDependencies, 0) - for _, name := range loadedPlugins { + for _, name := range plugins { pv := GetInstalledPluginVersion(name) r, err := semver.ParseRange(">=" + pv) // Try to get newer versions. if err == nil {