first few pm commands

This commit is contained in:
Florian Sundermann
2016-09-26 16:53:39 +02:00
parent 6791759440
commit f351c251e4
3 changed files with 314 additions and 142 deletions

View File

@@ -39,6 +39,7 @@ var commandActions = map[string]func([]string){
"Tab": NewTab,
"Help": Help,
"Eval": Eval,
"Plugin": PluginCmd,
}
// InitCommands initializes the default commands
@@ -84,6 +85,40 @@ func DefaultCommands() map[string]StrCommand {
"tab": {"Tab", []Completion{FileCompletion, NoCompletion}},
"help": {"Help", []Completion{HelpCompletion, NoCompletion}},
"eval": {"Eval", []Completion{NoCompletion}},
"plugin": {"Plugin", []Completion{NoCompletion}},
}
}
// InstallPlugin installs the given plugin by exact name match
func PluginCmd(args []string) {
if len(args) >= 1 {
switch args[0] {
case "install":
for _, plugin := range args[1:] {
pp := GetAllPluginPackages().Get(plugin)
if pp == nil {
messenger.Error("Unknown plugin \"" + plugin + "\"")
} else if !pp.IsInstallable() {
messenger.Error("Plugin \"" + plugin + "\" can not be installed.")
} else {
pp.Install()
}
}
case "remove":
for _, plugin := range args[1:] {
// check if the plugin exists.
for _, lp := range loadedPlugins {
if lp == plugin {
UninstallPlugin(plugin)
continue
}
}
}
case "update":
UpdatePlugins()
}
} else {
messenger.Error("Not enough arguments")
}
}

View File

@@ -44,7 +44,7 @@ var (
// Version is the version number or commit hash
// These variables should be set by the linker when compiling
Version = "Unknown"
Version = "2.0.0"
CommitHash = "Unknown"
CompileDate = "Unknown"

View File

@@ -19,10 +19,24 @@ import (
"github.com/yuin/gopher-lua"
)
var Repositories []PluginRepository = []PluginRepository{}
var (
pluginChannels PluginChannels = PluginChannels{
PluginChannel("https://www.boombuler.de/channel.json"),
}
allPluginPackages PluginPackages = nil
)
// 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
@@ -31,22 +45,103 @@ type PluginPackage struct {
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
// PluginDenendency 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 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 {
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)
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 {
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)
var plugins PluginPackages
if err := decoder.Decode(&plugins); err != nil {
TermMessage("Failed to decode repository data:\n", err)
return PluginPackages{}
}
return plugins
}
// UnmarshalJSON unmarshals raw json to a PluginVersion
func (pv *PluginVersion) UnmarshalJSON(data []byte) error {
var values struct {
Version semver.Version
@@ -69,14 +164,7 @@ func (pv *PluginVersion) UnmarshalJSON(data []byte) error {
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
@@ -99,72 +187,39 @@ func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
return nil
}
func (pv PluginVersions) Find(name string) *PluginVersion {
// GetAllPluginPackages gets all PluginPackages which may be available.
func GetAllPluginPackages() PluginPackages {
if allPluginPackages == nil {
allPluginPackages = pluginChannels.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]
}
// 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 {
// 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
}
// Match returns true if the package matches a given search text
func (pp PluginPackage) Match(text string) bool {
// ToDo: improve matching.
text = "(?i)" + text
@@ -174,102 +229,119 @@ func (pp PluginPackage) Match(text string) bool {
return false
}
func SearchPlugin(text string) (plugins []*PluginPackage) {
wgQuery := new(sync.WaitGroup)
wgQuery.Add(len(Repositories))
results := make(chan *PluginPackage)
// IsInstallable returns true if the package can be installed.
func (pp PluginPackage) IsInstallable() bool {
_, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{
&PluginDependency{
Name: pp.Name,
Range: semver.Range(func(v semver.Version) bool { return true }),
}})
return err == nil
}
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)
}
// 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) {
plugins = make(PluginPackages, 0)
for _, pp := range GetAllPluginPackages() {
if pp.Match(text) && pp.IsInstallable() {
plugins = append(plugins, pp)
}
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 {
func newStaticPluginVersion(name, version string) *PluginVersion {
vers, err := semver.Parse(version)
if err != nil {
return nil
} else {
return &v
}
pl := &PluginPackage{
Name: name,
}
pv := &PluginVersion{
pack: pl,
Version: vers,
}
pl.Versions = PluginVersions{pv}
return pv
}
func (pv *PluginVersion) Install() {
// 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),
}
for _, name := range loadedPlugins {
version := GetInstalledPluginVersion(name)
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 {
return string(str)
}
}
return ""
}
func (pv *PluginVersion) DownloadAndInstall() error {
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 {
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
}
for _, f := range z.File {
targetName := filepath.Join(targetDir, filepath.Join(strings.Split(f.Name, "/")...))
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()
if target, err := os.Create(targetName); err != nil {
return err
} else {
defer target.Close()
if _, err = io.Copy(target, content); err != nil {
return err
}
}
}
}
if err != nil {
TermMessage("Failed to install plugin:", err)
}
return nil
}
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 {
@@ -313,15 +385,15 @@ func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies
return result
}
func (all PluginPackages) ResolveStep(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
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 {
@@ -330,7 +402,7 @@ func (all PluginPackages) ResolveStep(selectedVersions PluginVersions, open Plug
for _, version := range availableVersions {
if currentRequirement.Range(version.Version) {
resolved, err := all.ResolveStep(append(selectedVersions, version), stillOpen.Join(version.Require))
resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require))
if err == nil {
return resolved, nil
@@ -343,3 +415,68 @@ func (all PluginPackages) ResolveStep(selectedVersions PluginVersions, open Plug
return selectedVersions, nil
}
}
func (versions 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)
}
if err := sel.DownloadAndInstall(); err != nil {
messenger.Error(err)
return
}
anyInstalled = true
}
}
if anyInstalled {
messenger.Message("One or more plugins installed. Please restart micro.")
}
}
// 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)
}
}
func (pl PluginPackage) Install() {
selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{
&PluginDependency{
Name: pl.Name,
Range: semver.Range(func(v semver.Version) bool { return true }),
}})
if err != nil {
TermMessage(err)
return
}
selected.install()
}
func UpdatePlugins() {
microVersion := PluginVersions{
newStaticPluginVersion("micro", Version),
}
var updates = make(PluginDependencies, 0)
for _, name := range loadedPlugins {
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()
}