initial commit of pluginmanager
This commit is contained in:
parent
d41f0bb324
commit
567faeb07e
2 changed files with 399 additions and 0 deletions
345
cmd/micro/pluginmanager.go
Normal file
345
cmd/micro/pluginmanager.go
Normal file
|
@ -0,0 +1,345 @@
|
|||
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/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
var Repositories []PluginRepository = []PluginRepository{}
|
||||
|
||||
type PluginRepository string
|
||||
|
||||
type PluginPackage struct {
|
||||
Name string
|
||||
Description string
|
||||
Author string
|
||||
Tags []string
|
||||
Versions PluginVersions
|
||||
}
|
||||
|
||||
type PluginPackages []*PluginPackage
|
||||
|
||||
type PluginVersion struct {
|
||||
pack *PluginPackage
|
||||
Version semver.Version
|
||||
Url string
|
||||
Require PluginDependencies
|
||||
}
|
||||
type PluginVersions []*PluginVersion
|
||||
|
||||
type PluginDependency struct {
|
||||
Name string
|
||||
Range semver.Range
|
||||
}
|
||||
type PluginDependencies []*PluginDependency
|
||||
|
||||
func (pv *PluginVersion) UnmarshalJSON(data []byte) error {
|
||||
var values struct {
|
||||
Version semver.Version
|
||||
Url string
|
||||
Require map[string]string
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &values); err != nil {
|
||||
return err
|
||||
}
|
||||
pv.Version = values.Version
|
||||
pv.Url = values.Url
|
||||
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})
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
|
||||
var values struct {
|
||||
Name string
|
||||
Description string
|
||||
Author string
|
||||
Tags []string
|
||||
Versions PluginVersions
|
||||
}
|
||||
if err := json.Unmarshal(data, &values); err != nil {
|
||||
return err
|
||||
}
|
||||
pp.Name = values.Name
|
||||
pp.Description = values.Description
|
||||
pp.Author = values.Author
|
||||
pp.Tags = values.Tags
|
||||
pp.Versions = values.Versions
|
||||
for _, v := range pp.Versions {
|
||||
v.pack = pp
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pv PluginVersions) Find(name string) *PluginVersion {
|
||||
for _, v := range pv {
|
||||
if v.pack.Name == name {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (pv PluginVersions) Len() int {
|
||||
return len(pv)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (pp *PluginPackage) GetInstallableVersion() *PluginVersion {
|
||||
matching := make(PluginVersions, 0)
|
||||
|
||||
versionLoop:
|
||||
for _, pv := range pp.Versions {
|
||||
for _, req := range pv.Require {
|
||||
curVersion := GetInstalledVersion(req.Name)
|
||||
if curVersion == nil || !req.Range(*curVersion) {
|
||||
continue versionLoop
|
||||
}
|
||||
}
|
||||
matching = append(matching, pv)
|
||||
}
|
||||
if len(matching) > 0 {
|
||||
sort.Sort(matching)
|
||||
return matching[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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 SearchPlugin(text string) (plugins []*PluginPackage) {
|
||||
wgQuery := new(sync.WaitGroup)
|
||||
wgQuery.Add(len(Repositories))
|
||||
results := make(chan *PluginPackage)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
wgDone.Done()
|
||||
}()
|
||||
wgQuery.Wait()
|
||||
close(results)
|
||||
wgDone.Wait()
|
||||
return
|
||||
}
|
||||
|
||||
func GetInstalledVersion(name string) *semver.Version {
|
||||
versionStr := ""
|
||||
if name == "micro" {
|
||||
versionStr = Version
|
||||
|
||||
} else {
|
||||
plugin := L.GetGlobal(name)
|
||||
if plugin == lua.LNil {
|
||||
return nil
|
||||
}
|
||||
version := L.GetField(plugin, "VERSION")
|
||||
if str, ok := version.(lua.LString); ok {
|
||||
versionStr = string(str)
|
||||
}
|
||||
}
|
||||
|
||||
if v, err := semver.Parse(versionStr); err != nil {
|
||||
return nil
|
||||
} else {
|
||||
return &v
|
||||
}
|
||||
}
|
||||
|
||||
func (pv *PluginVersion) Install() {
|
||||
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 {
|
||||
TermMessage("Failed to install plugin:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func UninstallPlugin(name string) {
|
||||
os.RemoveAll(filepath.Join(configDir, name))
|
||||
}
|
||||
|
||||
// Updates...
|
||||
|
||||
func (pl PluginPackages) Get(name string) *PluginPackage {
|
||||
for _, p := range pl {
|
||||
if p.Name == name {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pl PluginPackages) GetAllVersions(name string) PluginVersions {
|
||||
result := make(PluginVersions, 0)
|
||||
p := pl.Get(name)
|
||||
if p != nil {
|
||||
for _, v := range p.Versions {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies {
|
||||
m := make(map[string]*PluginDependency)
|
||||
for _, r := range req {
|
||||
m[r.Name] = r
|
||||
}
|
||||
for _, o := range other {
|
||||
cur, ok := m[o.Name]
|
||||
if ok {
|
||||
m[o.Name] = &PluginDependency{
|
||||
o.Name,
|
||||
o.Range.AND(cur.Range),
|
||||
}
|
||||
} else {
|
||||
m[o.Name] = o
|
||||
}
|
||||
}
|
||||
result := make(PluginDependencies, 0, len(m))
|
||||
for _, v := range m {
|
||||
result = append(result, v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (all PluginPackages) ResolveStep(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 currentRequirement.Range(selVersion.Version) {
|
||||
return all.ResolveStep(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)
|
||||
|
||||
for _, version := range availableVersions {
|
||||
if currentRequirement.Range(version.Version) {
|
||||
resolved, err := all.ResolveStep(append(selectedVersions, version), stillOpen.Join(version.Require))
|
||||
|
||||
if err == nil {
|
||||
return resolved, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
|
||||
}
|
||||
} else {
|
||||
return selectedVersions, nil
|
||||
}
|
||||
}
|
54
cmd/micro/pluginmanager_test.go
Normal file
54
cmd/micro/pluginmanager_test.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/blang/semver"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDependencyResolving(t *testing.T) {
|
||||
js := `
|
||||
[{
|
||||
"Name": "Foo",
|
||||
"Versions": [{ "Version": "1.0.0" }, { "Version": "1.5.0" },{ "Version": "2.0.0" }]
|
||||
}, {
|
||||
"Name": "Bar",
|
||||
"Versions": [{ "Version": "1.0.0", "Require": {"Foo": ">1.0.0 <2.0.0"} }]
|
||||
}, {
|
||||
"Name": "Unresolvable",
|
||||
"Versions": [{ "Version": "1.0.0", "Require": {"Foo": "<=1.0.0", "Bar": ">0.0.0"} }]
|
||||
}]
|
||||
`
|
||||
var all PluginPackages
|
||||
err := json.Unmarshal([]byte(js), &all)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
selected, err := all.ResolveStep(PluginVersions{}, PluginDependencies{
|
||||
&PluginDependency{"Bar", semver.MustParseRange(">=1.0.0")},
|
||||
})
|
||||
|
||||
check := func(name, version string) {
|
||||
v := selected.Find(name)
|
||||
expected := semver.MustParse(version)
|
||||
if v == nil {
|
||||
t.Errorf("Failed to resolve %s", name)
|
||||
} else if expected.NE(v.Version) {
|
||||
t.Errorf("%s resolved in wrong version got %s", name, v)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
check("Foo", "1.5.0")
|
||||
check("Bar", "1.0.0")
|
||||
}
|
||||
|
||||
selected, err = all.ResolveStep(PluginVersions{}, PluginDependencies{
|
||||
&PluginDependency{"Unresolvable", semver.MustParseRange(">0.0.0")},
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("Unresolvable package resolved:", selected)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue