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/yuin/gopher-lua"
+ "github.com/zyedidia/json5/encoding/json5"
)
-var Repositories []PluginRepository = []PluginRepository{}
+var (
+ 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
+// PluginChannels is a slice of PluginChannel
+type PluginChannels []PluginChannel
+
+// PluginRepository contains an url to json file containing PluginPackages
type PluginRepository string
+// PluginPackage contains the meta-data of a plugin and all available versions
type PluginPackage struct {
Name string
Description string
Versions PluginVersions
}
+// PluginPackages is a list of PluginPackage instances.
type PluginPackages []*PluginPackage
+// PluginVersion descripes a version of a PluginPackage. Containing a version, download url and also dependencies.
type PluginVersion struct {
pack *PluginPackage
Version semver.Version
Url string
Require PluginDependencies
}
+
+// PluginVersions is a slice of PluginVersion
type PluginVersions []*PluginVersion
+// PluginDependency descripes a dependency to another plugin or micro itself.
type PluginDependency struct {
Name string
Range semver.Range
}
+
+// 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)
+
+ results := make(chan PluginPackages)
+
+ wgDone := new(sync.WaitGroup)
+ wgDone.Add(1)
+ var packages PluginPackages
+ for i := 0; i < count; i++ {
+ go func(i int) {
+ results <- fetcher(i)
+ wgQuery.Done()
+ }(i)
+ }
+ go func() {
+ packages = make(PluginPackages, 0)
+ for res := range results {
+ packages = append(packages, res...)
+ }
+ wgDone.Done()
+ }()
+ wgQuery.Wait()
+ close(results)
+ wgDone.Wait()
+ return packages
+}
+
+// Fetch retrieves all available PluginPackages from the given channels
+func (pc PluginChannels) Fetch() PluginPackages {
+ return fetchAllSources(len(pc), func(i int) PluginPackages {
+ return pc[i].Fetch()
+ })
+}
+
+// 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 := json5.NewDecoder(resp.Body)
+
+ var repositories []PluginRepository
+ if err := decoder.Decode(&repositories); err != nil {
+ TermMessage("Failed to decode channel data:\n", err)
+ return PluginPackages{}
+ }
+ return fetchAllSources(len(repositories), func(i int) PluginPackages {
+ return repositories[i].Fetch()
+ })
+}
+
+// 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 := json5.NewDecoder(resp.Body)
+
+ var plugins PluginPackages
+ if err := decoder.Decode(&plugins); err != nil {
+ TermMessage("Failed to decode repository data:\n", err)
+ return PluginPackages{}
+ }
+ if len(plugins) > 0 {
+ return PluginPackages{plugins[0]}
+ }
+ return nil
+ // return plugins
+}
+
+// UnmarshalJSON unmarshals raw json to a PluginVersion
func (pv *PluginVersion) UnmarshalJSON(data []byte) error {
var values struct {
Version semver.Version
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
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
}
-func (pv *PluginVersion) String() string {
- return fmt.Sprintf("%s (%s)", pv.pack.Name, pv.Version)
-}
-
-func (pd *PluginDependency) String() string {
- return pd.Name
-}
-
+// UnmarshalJSON unmarshals raw json to a PluginPackage
func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
var values struct {
Name string
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
return nil
}
-func (pv PluginVersions) Find(name string) *PluginVersion {
+// GetAllPluginPackages gets all PluginPackages which may be available.
+func GetAllPluginPackages() PluginPackages {
+ if allPluginPackages == nil {
+ 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
+}
+
+func (pv PluginVersions) find(ppName string) *PluginVersion {
for _, v := range pv {
- if v.pack.Name == name {
+ if v.pack.Name == ppName {
return v
}
}
return nil
}
+
+// Len returns the number of pluginversions in this slice
func (pv PluginVersions) Len() int {
return len(pv)
}
+// Swap two entries of the slice
func (pv PluginVersions) Swap(i, j int) {
pv[i], pv[j] = pv[j], pv[i]
}
-func (s PluginVersions) Less(i, j int) bool {
- // sort descending
- return s[i].Version.GT(s[j].Version)
+// Less returns true if the version at position i is greater then the version at position j (used for sorting)
+func (pv PluginVersions) Less(i, j int) bool {
+ return pv[i].Version.GT(pv[j].Version)
}
-func (pr PluginRepository) Query() <-chan *PluginPackage {
- resChan := make(chan *PluginPackage)
- go func() {
- defer close(resChan)
-
- resp, err := http.Get(string(pr))
- if err != nil {
- TermMessage("Failed to query plugin repository:\n", err)
- return
+// Match returns true if the package matches a given search text
+func (pp PluginPackage) Match(text string) bool {
+ text = strings.ToLower(text)
+ for _, t := range pp.Tags {
+ if strings.ToLower(t) == text {
+ return true
}
- defer resp.Body.Close()
- decoder := json.NewDecoder(resp.Body)
+ }
+ if strings.Contains(strings.ToLower(pp.Name), text) {
+ return true
+ }
- var plugins PluginPackages
- if err := decoder.Decode(&plugins); err != nil {
- TermMessage("Failed to decode repository data:\n", err)
- return
- }
- for _, p := range plugins {
- resChan <- p
- }
- }()
- return resChan
+ 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() error {
+ _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
+ &PluginDependency{
+ Name: pp.Name,
+ Range: semver.Range(func(v semver.Version) bool { return true }),
+ }})
+ return err
}
-func (pp *PluginPackage) GetInstallableVersion() *PluginVersion {
- matching := make(PluginVersions, 0)
+// SearchPlugin retrieves a list of all PluginPackages which match the given search text and
+// could be or are already installed
+func SearchPlugin(texts []string) (plugins PluginPackages) {
+ plugins = make(PluginPackages, 0)
-versionLoop:
- for _, pv := range pp.Versions {
- for _, req := range pv.Require {
- curVersion := GetInstalledVersion(req.Name)
- if curVersion == nil || !req.Range(*curVersion) {
- continue versionLoop
+pluginLoop:
+ for _, pp := range GetAllPluginPackages() {
+ for _, text := range texts {
+ if !pp.Match(text) {
+ continue pluginLoop
}
}
- matching = append(matching, pv)
- }
- if len(matching) > 0 {
- sort.Sort(matching)
- return matching[0]
+
+ if err := pp.IsInstallable(); err == nil {
+ plugins = append(plugins, pp)
+ }
}
- return nil
+ return
}
-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)
- }
- return false
+func isUnknownCoreVersion() bool {
+ _, err := semver.ParseTolerant(Version)
+ return err != nil
}
-func SearchPlugin(text string) (plugins []*PluginPackage) {
- wgQuery := new(sync.WaitGroup)
- wgQuery.Add(len(Repositories))
- results := make(chan *PluginPackage)
+func newStaticPluginVersion(name, version string) *PluginVersion {
+ vers, err := semver.ParseTolerant(version)
- wgDone := new(sync.WaitGroup)
- wgDone.Add(1)
- for _, repo := range Repositories {
- go func(repo PluginRepository) {
- res := repo.Query()
- for r := range res {
- results <- r
- }
- wgQuery.Done()
- }(repo)
- }
- go func() {
- for res := range results {
- if res.GetInstallableVersion() != nil && res.Match(text) {
- plugins = append(plugins, res)
- }
+ if err != nil {
+ if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil {
+ vers = semver.MustParse("0.0.0-unknown")
}
- wgDone.Done()
- }()
- wgQuery.Wait()
- close(results)
- wgDone.Wait()
- return
+ }
+ pl := &PluginPackage{
+ Name: name,
+ }
+ pv := &PluginVersion{
+ pack: pl,
+ Version: vers,
+ }
+ pl.Versions = PluginVersions{pv}
+ return pv
}
-func GetInstalledVersion(name string) *semver.Version {
- versionStr := ""
- if name == "micro" {
- versionStr = Version
+// GetInstalledVersions returns a list of all currently installed plugins including an entry for
+// micro itself. This can be used to resolve dependencies.
+func GetInstalledVersions(withCore bool) PluginVersions {
+ result := PluginVersions{}
+ if withCore {
+ result = append(result, newStaticPluginVersion(CorePluginName, Version))
+ }
- } else {
- plugin := L.GetGlobal(name)
- if plugin == lua.LNil {
- return nil
+ for name, lpname := range loadedPlugins {
+ version := GetInstalledPluginVersion(lpname)
+ if pv := newStaticPluginVersion(name, version); pv != nil {
+ result = append(result, pv)
}
+ }
+
+ return result
+}
+
+// GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin
+func GetInstalledPluginVersion(name string) string {
+ plugin := L.GetGlobal(name)
+ if plugin != lua.LNil {
version := L.GetField(plugin, "VERSION")
if str, ok := version.(lua.LString); ok {
- versionStr = string(str)
- }
- }
+ return string(str)
- if v, err := semver.Parse(versionStr); err != nil {
- return nil
- } else {
- return &v
+ }
}
+ return ""
}
-func (pv *PluginVersion) Install() {
+// 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 {
- defer resp.Body.Close()
- data, _ := ioutil.ReadAll(resp.Body)
- zipbuf := bytes.NewReader(data)
- z, err := zip.NewReader(zipbuf, zipbuf.Size())
- if err == nil {
- targetDir := filepath.Join(configDir, "plugins", pv.pack.Name)
- dirPerm := os.FileMode(0755)
- if err = os.MkdirAll(targetDir, dirPerm); err == nil {
- for _, f := range z.File {
- targetName := filepath.Join(targetDir, filepath.Join(strings.Split(f.Name, "/")...))
- if f.FileInfo().IsDir() {
- err = os.MkdirAll(targetName, dirPerm)
- } else {
- content, err := f.Open()
- if err == nil {
- defer content.Close()
- if target, err := os.Create(targetName); err == nil {
- defer target.Close()
- _, err = io.Copy(target, content)
- }
- }
- }
- if err != nil {
- break
- }
- }
- }
- }
+ if err != nil {
+ return err
}
+ defer resp.Body.Close()
+ data, err := ioutil.ReadAll(resp.Body)
if err != nil {
- TermMessage("Failed to install plugin:", err)
+ return err
+ }
+ zipbuf := bytes.NewReader(data)
+ z, err := zip.NewReader(zipbuf, zipbuf.Size())
+ if err != nil {
+ return err
+ }
+ targetDir := filepath.Join(configDir, "plugins", pv.pack.Name)
+ dirPerm := os.FileMode(0755)
+ if err = os.MkdirAll(targetDir, dirPerm); err != nil {
+ return err
}
-}
-func UninstallPlugin(name string) {
- os.RemoveAll(filepath.Join(configDir, name))
-}
+ // 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
+ }
+ }
-// Updates...
+ for _, f := range z.File {
+ 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 {
+ content, err := f.Open()
+ if err != nil {
+ return err
+ }
+ defer content.Close()
+ target, err := os.Create(targetName)
+ if err != nil {
+ return err
+ }
+ defer target.Close()
+ if _, err = io.Copy(target, content); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
func (pl PluginPackages) Get(name string) *PluginPackage {
for _, p := range pl {
return result
}
-func (all PluginPackages) ResolveStep(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
+// Resolve resolves dependencies between different plugins
+func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
if len(open) == 0 {
return selectedVersions, nil
}
currentRequirement, stillOpen := open[0], open[1:]
if currentRequirement != nil {
- if selVersion := selectedVersions.Find(currentRequirement.Name); selVersion != nil {
+ if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil {
if currentRequirement.Range(selVersion.Version) {
- return all.ResolveStep(selectedVersions, stillOpen)
+ 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.ResolveStep(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)
}
+ return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
+ }
+ return selectedVersions, nil
+}
+
+func (pv PluginVersions) install() {
+ anyInstalled := false
+ 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(fmt.Sprint("Uninstalling %q", sel.pack.Name))
+ UninstallPlugin(sel.pack.Name)
+ } else {
+ shouldInstall = false
+ }
+ }
+
+ if shouldInstall {
+ if err := sel.DownloadAndInstall(); err != nil {
+ messenger.Error(err)
+ return
+ }
+ anyInstalled = true
+ }
+ }
+ }
+ if anyInstalled {
+ messenger.Message("One or more plugins installed. Please restart micro.")
} else {
- return selectedVersions, nil
+ messenger.AddLog("Nothing to install / update")
+ }
+}
+
+// UninstallPlugin deletes the plugin folder of the given plugin
+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(true), PluginDependencies{
+ &PluginDependency{
+ Name: pl.Name,
+ Range: semver.Range(func(v semver.Version) bool { return true }),
+ }})
+ if err != nil {
+ TermMessage(err)
+ return
+ }
+ selected.install()
+}
+
+// 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(CorePluginName, Version),
+ }
+
+ var updates = make(PluginDependencies, 0)
+ for _, name := range plugins {
+ pv := GetInstalledPluginVersion(name)
+ r, err := semver.ParseRange(">=" + pv) // Try to get newer versions.
+ if err == nil {
+ updates = append(updates, &PluginDependency{
+ Name: name,
+ Range: r,
+ })
+ }
+ }
+
+ selected, err := GetAllPluginPackages().Resolve(microVersion, updates)
+ if err != nil {
+ TermMessage(err)
+ return
}
+ selected.install()
}