micro/internal/config/plugin_installer.go

711 lines
18 KiB
Go
Raw Permalink Normal View History

2020-02-02 07:54:38 +03:00
package config
import (
"archive/zip"
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/blang/semver"
lua "github.com/yuin/gopher-lua"
"github.com/zyedidia/json5"
2020-05-04 17:16:15 +03:00
ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/util"
2020-02-02 07:54:38 +03:00
)
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
Author string
Tags []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
}
func (pv *PluginVersion) Pack() *PluginPackage {
return pv.pack
}
// 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(out io.Writer) PluginPackages {
2020-02-02 07:54:38 +03:00
return fetchAllSources(len(pc), func(i int) PluginPackages {
return pc[i].Fetch(out)
2020-02-02 07:54:38 +03:00
})
}
// Fetch retrieves all available PluginPackages from the given channel
func (pc PluginChannel) Fetch(out io.Writer) PluginPackages {
2020-02-02 07:54:38 +03:00
resp, err := http.Get(string(pc))
if err != nil {
fmt.Fprintln(out, "Failed to query plugin channel:\n", err)
2020-02-02 07:54:38 +03:00
return PluginPackages{}
}
defer resp.Body.Close()
decoder := json5.NewDecoder(resp.Body)
var repositories []PluginRepository
if err := decoder.Decode(&repositories); err != nil {
fmt.Fprintln(out, "Failed to decode channel data:\n", err)
2020-02-02 07:54:38 +03:00
return PluginPackages{}
}
return fetchAllSources(len(repositories), func(i int) PluginPackages {
return repositories[i].Fetch(out)
2020-02-02 07:54:38 +03:00
})
}
// Fetch retrieves all available PluginPackages from the given repository
func (pr PluginRepository) Fetch(out io.Writer) PluginPackages {
2020-02-02 07:54:38 +03:00
resp, err := http.Get(string(pr))
if err != nil {
fmt.Fprintln(out, "Failed to query plugin repository:\n", err)
2020-02-02 07:54:38 +03:00
return PluginPackages{}
}
defer resp.Body.Close()
decoder := json5.NewDecoder(resp.Body)
var plugins PluginPackages
if err := decoder.Decode(&plugins); err != nil {
fmt.Fprintln(out, "Failed to decode repository data:\n", err)
2020-02-02 07:54:38 +03:00
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
Url string
Require map[string]string
}
if err := json5.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 {
// 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
}
// UnmarshalJSON unmarshals raw json to a PluginPackage
func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
var values struct {
Name string
Description string
Author string
Tags []string
Versions PluginVersions
}
if err := json5.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
}
// GetAllPluginPackages gets all PluginPackages which may be available.
func GetAllPluginPackages(out io.Writer) PluginPackages {
2020-02-02 07:54:38 +03:00
if allPluginPackages == nil {
getOption := func(name string) []string {
data := GetGlobalOption(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(out)
2020-02-02 07:54:38 +03:00
}
return repos[i-1].Fetch(out)
2020-02-02 07:54:38 +03:00
})
}
return allPluginPackages
}
func (pv PluginVersions) find(ppName string) *PluginVersion {
for _, v := range pv {
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 (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 {
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(out io.Writer) error {
_, err := GetAllPluginPackages(out).Resolve(GetInstalledVersions(true), PluginDependencies{
2020-02-02 07:54:38 +03:00
&PluginDependency{
Name: pp.Name,
Range: semver.Range(func(v semver.Version) bool { return true }),
}})
return err
}
// SearchPlugin retrieves a list of all PluginPackages which match the given search text and
// could be or are already installed
func SearchPlugin(out io.Writer, texts []string) (plugins PluginPackages) {
2020-02-02 07:54:38 +03:00
plugins = make(PluginPackages, 0)
pluginLoop:
for _, pp := range GetAllPluginPackages(out) {
2020-02-02 07:54:38 +03:00
for _, text := range texts {
if !pp.Match(text) {
continue pluginLoop
}
}
if err := pp.IsInstallable(out); err == nil {
2020-02-02 07:54:38 +03:00
plugins = append(plugins, pp)
}
}
return
}
func isUnknownCoreVersion() bool {
_, err := semver.ParseTolerant(util.Version)
return err != nil
}
func newStaticPluginVersion(name, version string) *PluginVersion {
vers, err := semver.ParseTolerant(version)
if err != nil {
if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil {
vers = semver.MustParse("0.0.0-unknown")
}
}
pl := &PluginPackage{
Name: name,
}
pv := &PluginVersion{
pack: pl,
Version: vers,
}
pl.Versions = PluginVersions{pv}
return pv
}
// 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, util.Version))
}
for _, p := range Plugins {
if !p.IsLoaded() {
continue
}
2020-02-02 07:54:38 +03:00
version := GetInstalledPluginVersion(p.Name)
if pv := newStaticPluginVersion(p.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 := ulua.L.GetGlobal(name)
if plugin != lua.LNil {
version := ulua.L.GetField(plugin, "VERSION")
if str, ok := version.(lua.LString); ok {
return string(str)
}
}
return ""
}
// DownloadAndInstall downloads and installs the given plugin and version
func (pv *PluginVersion) DownloadAndInstall(out io.Writer) error {
fmt.Fprintf(out, "Downloading %q (%s) from %q\n", pv.pack.Name, pv.Version, pv.Url)
2020-02-02 07:54:38 +03:00
resp, err := http.Get(pv.Url)
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, "plug", pv.pack.Name)
dirPerm := os.FileMode(0755)
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 {
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()
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 {
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 {
result = append(result, p.Versions...)
2020-02-02 07:54:38 +03:00
}
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
}
// 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 currentRequirement.Range(selVersion.Version) {
return all.Resolve(selectedVersions, stillOpen)
}
return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
}
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))
if err == nil {
return resolved, nil
}
}
}
return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
}
return selectedVersions, nil
}
func (pv PluginVersions) install(out io.Writer) {
2020-02-02 07:54:38 +03:00
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) {
fmt.Fprintln(out, "Uninstalling", sel.pack.Name)
UninstallPlugin(out, sel.pack.Name)
2020-02-02 07:54:38 +03:00
} else {
shouldInstall = false
}
}
if shouldInstall {
if err := sel.DownloadAndInstall(out); err != nil {
fmt.Fprintln(out, err)
2020-02-02 07:54:38 +03:00
return
}
anyInstalled = true
}
}
}
if anyInstalled {
fmt.Fprintln(out, "One or more plugins installed.")
2020-02-02 07:54:38 +03:00
} else {
fmt.Fprintln(out, "Nothing to install / update")
2020-02-02 07:54:38 +03:00
}
}
// UninstallPlugin deletes the plugin folder of the given plugin
func UninstallPlugin(out io.Writer, name string) {
2020-02-02 07:54:38 +03:00
for _, p := range Plugins {
if !p.IsLoaded() {
continue
}
2020-02-02 07:54:38 +03:00
if p.Name == name {
p.Loaded = false
if err := os.RemoveAll(filepath.Join(ConfigDir, "plug", p.DirName)); err != nil {
fmt.Fprintln(out, err)
2020-02-02 07:54:38 +03:00
return
}
break
2020-02-02 07:54:38 +03:00
}
}
}
// Install installs the plugin
func (pl PluginPackage) Install(out io.Writer) {
selected, err := GetAllPluginPackages(out).Resolve(GetInstalledVersions(true), PluginDependencies{
2020-02-02 07:54:38 +03:00
&PluginDependency{
Name: pl.Name,
Range: semver.Range(func(v semver.Version) bool { return true }),
}})
if err != nil {
fmt.Fprintln(out, err)
2020-02-02 07:54:38 +03:00
return
}
selected.install(out)
2020-02-02 07:54:38 +03:00
}
// UpdatePlugins updates the given plugins
func UpdatePlugins(out io.Writer, plugins []string) {
2020-02-02 07:54:38 +03:00
// if no plugins are specified, update all installed plugins.
if len(plugins) == 0 {
for _, p := range Plugins {
if !p.IsLoaded() || p.Default {
continue
}
2020-02-02 07:54:38 +03:00
plugins = append(plugins, p.Name)
}
}
fmt.Fprintln(out, "Checking for plugin updates")
2020-02-02 07:54:38 +03:00
microVersion := PluginVersions{
newStaticPluginVersion(CorePluginName, util.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(out).Resolve(microVersion, updates)
2020-02-02 07:54:38 +03:00
if err != nil {
fmt.Fprintln(out, err)
2020-02-02 07:54:38 +03:00
return
}
selected.install(out)
}
func PluginCommand(out io.Writer, cmd string, args []string) {
switch cmd {
case "install":
installedVersions := GetInstalledVersions(false)
for _, plugin := range args {
pp := GetAllPluginPackages(out).Get(plugin)
if pp == nil {
fmt.Fprintln(out, "Unknown plugin \""+plugin+"\"")
} else if err := pp.IsInstallable(out); err != nil {
fmt.Fprintln(out, "Error installing ", plugin, ": ", err)
} else {
for _, installed := range installedVersions {
if pp.Name == installed.Pack().Name {
if pp.Versions[0].Version.Compare(installed.Version) == 1 {
fmt.Fprintln(out, pp.Name, " is already installed but out-of-date: use 'plugin update ", pp.Name, "' to update")
} else {
fmt.Fprintln(out, pp.Name, " is already installed")
}
}
}
pp.Install(out)
}
}
case "remove":
removed := ""
for _, plugin := range args {
// check if the plugin exists.
for _, p := range Plugins {
if p.Name == plugin && p.Default {
fmt.Fprintln(out, "Default plugins cannot be removed, but can be disabled via settings.")
continue
}
if p.Name == plugin {
UninstallPlugin(out, plugin)
removed += plugin + " "
continue
}
}
}
if removed != "" {
fmt.Fprintln(out, "Removed ", removed)
} else {
fmt.Fprintln(out, "No plugins removed")
}
case "update":
UpdatePlugins(out, args)
case "list":
plugins := GetInstalledVersions(false)
fmt.Fprintln(out, "The following plugins are currently installed:")
for _, p := range plugins {
fmt.Fprintf(out, "%s (%s)\n", p.Pack().Name, p.Version)
}
case "search":
plugins := SearchPlugin(out, args)
fmt.Fprintln(out, len(plugins), " plugins found")
for _, p := range plugins {
fmt.Fprintln(out, "----------------")
fmt.Fprintln(out, p.String())
}
fmt.Fprintln(out, "----------------")
case "available":
packages := GetAllPluginPackages(out)
fmt.Fprintln(out, "Available Plugins:")
for _, pkg := range packages {
fmt.Fprintln(out, pkg.Name)
}
default:
fmt.Fprintln(out, "Invalid plugin command")
}
2020-02-02 07:54:38 +03:00
}