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
// 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
// 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)
// 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 {
// 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
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
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
// 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
}
}
// 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,
// 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)
}
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
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
- }
}
}
}
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
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")
}
}
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 }),
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 {