]> git.lizzy.rs Git - micro.git/blob - cmd/micro/pluginmanager.go
better plugin search
[micro.git] / cmd / micro / pluginmanager.go
1 package main
2
3 import (
4         "archive/zip"
5         "bytes"
6         "fmt"
7         "io"
8         "io/ioutil"
9         "net/http"
10         "os"
11         "path/filepath"
12         "sort"
13         "strings"
14         "sync"
15
16         "github.com/blang/semver"
17         "github.com/yosuke-furukawa/json5/encoding/json5"
18         "github.com/yuin/gopher-lua"
19 )
20
21 var (
22         pluginChannels PluginChannels = PluginChannels{
23                 PluginChannel("https://www.boombuler.de/channel.json"),
24         }
25
26         allPluginPackages PluginPackages = nil
27 )
28
29 // CorePluginName is a plugin dependency name for the micro core.
30 const CorePluginName = "micro"
31
32 // PluginChannel contains an url to a json list of PluginRepository
33 type PluginChannel string
34
35 // PluginChannels is a slice of PluginChannel
36 type PluginChannels []PluginChannel
37
38 // PluginRepository contains an url to json file containing PluginPackages
39 type PluginRepository string
40
41 // PluginPackage contains the meta-data of a plugin and all available versions
42 type PluginPackage struct {
43         Name        string
44         Description string
45         Author      string
46         Tags        []string
47         Versions    PluginVersions
48 }
49
50 // PluginPackages is a list of PluginPackage instances.
51 type PluginPackages []*PluginPackage
52
53 // PluginVersion descripes a version of a PluginPackage. Containing a version, download url and also dependencies.
54 type PluginVersion struct {
55         pack    *PluginPackage
56         Version semver.Version
57         Url     string
58         Require PluginDependencies
59 }
60
61 // PluginVersions is a slice of PluginVersion
62 type PluginVersions []*PluginVersion
63
64 // PluginDenendency descripes a dependency to another plugin or micro itself.
65 type PluginDependency struct {
66         Name  string
67         Range semver.Range
68 }
69
70 // PluginDependencies is a slice of PluginDependency
71 type PluginDependencies []*PluginDependency
72
73 func (pp *PluginPackage) String() string {
74         buf := new(bytes.Buffer)
75         buf.WriteString("Plugin: ")
76         buf.WriteString(pp.Name)
77         buf.WriteRune('\n')
78         if pp.Author != "" {
79                 buf.WriteString("Author: ")
80                 buf.WriteString(pp.Author)
81                 buf.WriteRune('\n')
82         }
83         if pp.Description != "" {
84                 buf.WriteString(pp.Description)
85         }
86         return buf.String()
87 }
88
89 func fetchAllSources(count int, fetcher func(i int) PluginPackages) PluginPackages {
90         wgQuery := new(sync.WaitGroup)
91         wgQuery.Add(count)
92
93         results := make(chan PluginPackages)
94
95         wgDone := new(sync.WaitGroup)
96         wgDone.Add(1)
97         var packages PluginPackages
98         for i := 0; i < count; i++ {
99                 go func(i int) {
100                         results <- fetcher(i)
101                         wgQuery.Done()
102                 }(i)
103         }
104         go func() {
105                 packages = make(PluginPackages, 0)
106                 for res := range results {
107                         packages = append(packages, res...)
108                 }
109                 wgDone.Done()
110         }()
111         wgQuery.Wait()
112         close(results)
113         wgDone.Wait()
114         return packages
115 }
116
117 // Fetch retrieves all available PluginPackages from the given channels
118 func (pc PluginChannels) Fetch() PluginPackages {
119         return fetchAllSources(len(pc), func(i int) PluginPackages {
120                 return pc[i].Fetch()
121         })
122 }
123
124 // Fetch retrieves all available PluginPackages from the given channel
125 func (pc PluginChannel) Fetch() PluginPackages {
126         resp, err := http.Get(string(pc))
127         if err != nil {
128                 TermMessage("Failed to query plugin channel:\n", err)
129                 return PluginPackages{}
130         }
131         defer resp.Body.Close()
132         decoder := json5.NewDecoder(resp.Body)
133
134         var repositories []PluginRepository
135         if err := decoder.Decode(&repositories); err != nil {
136                 TermMessage("Failed to decode channel data:\n", err)
137                 return PluginPackages{}
138         }
139         return fetchAllSources(len(repositories), func(i int) PluginPackages {
140                 return repositories[i].Fetch()
141         })
142 }
143
144 // Fetch retrieves all available PluginPackages from the given repository
145 func (pr PluginRepository) Fetch() PluginPackages {
146         resp, err := http.Get(string(pr))
147         if err != nil {
148                 TermMessage("Failed to query plugin repository:\n", err)
149                 return PluginPackages{}
150         }
151         defer resp.Body.Close()
152         decoder := json5.NewDecoder(resp.Body)
153
154         var plugins PluginPackages
155         if err := decoder.Decode(&plugins); err != nil {
156                 TermMessage("Failed to decode repository data:\n", err)
157                 return PluginPackages{}
158         }
159         return plugins
160 }
161
162 // UnmarshalJSON unmarshals raw json to a PluginVersion
163 func (pv *PluginVersion) UnmarshalJSON(data []byte) error {
164         var values struct {
165                 Version semver.Version
166                 Url     string
167                 Require map[string]string
168         }
169
170         if err := json5.Unmarshal(data, &values); err != nil {
171                 return err
172         }
173         pv.Version = values.Version
174         pv.Url = values.Url
175         pv.Require = make(PluginDependencies, 0)
176
177         for k, v := range values.Require {
178                 if vRange, err := semver.ParseRange(v); err == nil {
179                         pv.Require = append(pv.Require, &PluginDependency{k, vRange})
180                 }
181         }
182         return nil
183 }
184
185 // UnmarshalJSON unmarshals raw json to a PluginPackage
186 func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
187         var values struct {
188                 Name        string
189                 Description string
190                 Author      string
191                 Tags        []string
192                 Versions    PluginVersions
193         }
194         if err := json5.Unmarshal(data, &values); err != nil {
195                 return err
196         }
197         pp.Name = values.Name
198         pp.Description = values.Description
199         pp.Author = values.Author
200         pp.Tags = values.Tags
201         pp.Versions = values.Versions
202         for _, v := range pp.Versions {
203                 v.pack = pp
204         }
205         return nil
206 }
207
208 // GetAllPluginPackages gets all PluginPackages which may be available.
209 func GetAllPluginPackages() PluginPackages {
210         if allPluginPackages == nil {
211                 allPluginPackages = pluginChannels.Fetch()
212         }
213         return allPluginPackages
214 }
215
216 func (pv PluginVersions) find(ppName string) *PluginVersion {
217         for _, v := range pv {
218                 if v.pack.Name == ppName {
219                         return v
220                 }
221         }
222         return nil
223 }
224
225 // Len returns the number of pluginversions in this slice
226 func (pv PluginVersions) Len() int {
227         return len(pv)
228 }
229
230 // Swap two entries of the slice
231 func (pv PluginVersions) Swap(i, j int) {
232         pv[i], pv[j] = pv[j], pv[i]
233 }
234
235 // Less returns true if the version at position i is greater then the version at position j (used for sorting)
236 func (s PluginVersions) Less(i, j int) bool {
237         return s[i].Version.GT(s[j].Version)
238 }
239
240 // Match returns true if the package matches a given search text
241 func (pp PluginPackage) Match(text string) bool {
242         text = strings.ToLower(text)
243         for _, t := range pp.Tags {
244                 if strings.ToLower(t) == text {
245                         return true
246                 }
247         }
248         if strings.Contains(strings.ToLower(pp.Name), text) {
249                 return true
250         }
251
252         if strings.Contains(strings.ToLower(pp.Description), text) {
253                 return true
254         }
255
256         return false
257 }
258
259 // IsInstallable returns true if the package can be installed.
260 func (pp PluginPackage) IsInstallable() bool {
261         _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{
262                 &PluginDependency{
263                         Name:  pp.Name,
264                         Range: semver.Range(func(v semver.Version) bool { return true }),
265                 }})
266         return err == nil
267 }
268
269 // SearchPlugin retrieves a list of all PluginPackages which match the given search text and
270 // could be or are already installed
271 func SearchPlugin(text string) (plugins PluginPackages) {
272         plugins = make(PluginPackages, 0)
273         for _, pp := range GetAllPluginPackages() {
274                 if pp.Match(text) && pp.IsInstallable() {
275                         plugins = append(plugins, pp)
276                 }
277         }
278         return
279 }
280
281 func newStaticPluginVersion(name, version string) *PluginVersion {
282         vers, err := semver.ParseTolerant(version)
283
284         if err != nil {
285                 if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil {
286                         vers = semver.MustParse("0.0.0-unknown")
287                 }
288         }
289         pl := &PluginPackage{
290                 Name: name,
291         }
292         pv := &PluginVersion{
293                 pack:    pl,
294                 Version: vers,
295         }
296         pl.Versions = PluginVersions{pv}
297         return pv
298 }
299
300 // GetInstalledVersions returns a list of all currently installed plugins including an entry for
301 // micro itself. This can be used to resolve dependencies.
302 func GetInstalledVersions() PluginVersions {
303         result := PluginVersions{
304                 newStaticPluginVersion(CorePluginName, Version),
305         }
306
307         for _, name := range loadedPlugins {
308                 version := GetInstalledPluginVersion(name)
309                 if pv := newStaticPluginVersion(name, version); pv != nil {
310                         result = append(result, pv)
311                 }
312         }
313
314         return result
315 }
316
317 // GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin
318 func GetInstalledPluginVersion(name string) string {
319         plugin := L.GetGlobal(name)
320         if plugin != lua.LNil {
321                 version := L.GetField(plugin, "VERSION")
322                 if str, ok := version.(lua.LString); ok {
323                         return string(str)
324
325                 }
326         }
327         return ""
328 }
329
330 func (pv *PluginVersion) DownloadAndInstall() error {
331         messenger.AddLog(fmt.Sprintf("Downloading %q (%s) from %q", pv.pack.Name, pv.Version, pv.Url))
332         resp, err := http.Get(pv.Url)
333         if err != nil {
334                 return err
335         }
336         defer resp.Body.Close()
337         data, err := ioutil.ReadAll(resp.Body)
338         if err != nil {
339                 return err
340         }
341         zipbuf := bytes.NewReader(data)
342         z, err := zip.NewReader(zipbuf, zipbuf.Size())
343         if err != nil {
344                 return err
345         }
346         targetDir := filepath.Join(configDir, "plugins", pv.pack.Name)
347         dirPerm := os.FileMode(0755)
348         if err = os.MkdirAll(targetDir, dirPerm); err != nil {
349                 return err
350         }
351         for _, f := range z.File {
352                 targetName := filepath.Join(targetDir, filepath.Join(strings.Split(f.Name, "/")...))
353                 if f.FileInfo().IsDir() {
354                         if err := os.MkdirAll(targetName, dirPerm); err != nil {
355                                 return err
356                         }
357                 } else {
358                         content, err := f.Open()
359                         if err != nil {
360                                 return err
361                         }
362                         defer content.Close()
363                         if target, err := os.Create(targetName); err != nil {
364                                 return err
365                         } else {
366                                 defer target.Close()
367                                 if _, err = io.Copy(target, content); err != nil {
368                                         return err
369                                 }
370                         }
371                 }
372         }
373         return nil
374 }
375
376 func (pl PluginPackages) Get(name string) *PluginPackage {
377         for _, p := range pl {
378                 if p.Name == name {
379                         return p
380                 }
381         }
382         return nil
383 }
384
385 func (pl PluginPackages) GetAllVersions(name string) PluginVersions {
386         result := make(PluginVersions, 0)
387         p := pl.Get(name)
388         if p != nil {
389                 for _, v := range p.Versions {
390                         result = append(result, v)
391                 }
392         }
393         return result
394 }
395
396 func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies {
397         m := make(map[string]*PluginDependency)
398         for _, r := range req {
399                 m[r.Name] = r
400         }
401         for _, o := range other {
402                 cur, ok := m[o.Name]
403                 if ok {
404                         m[o.Name] = &PluginDependency{
405                                 o.Name,
406                                 o.Range.AND(cur.Range),
407                         }
408                 } else {
409                         m[o.Name] = o
410                 }
411         }
412         result := make(PluginDependencies, 0, len(m))
413         for _, v := range m {
414                 result = append(result, v)
415         }
416         return result
417 }
418
419 func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
420         if len(open) == 0 {
421                 return selectedVersions, nil
422         }
423         currentRequirement, stillOpen := open[0], open[1:]
424         if currentRequirement != nil {
425                 if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil {
426                         if currentRequirement.Range(selVersion.Version) {
427                                 return all.Resolve(selectedVersions, stillOpen)
428                         }
429                         return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
430                 } else {
431                         availableVersions := all.GetAllVersions(currentRequirement.Name)
432                         sort.Sort(availableVersions)
433
434                         for _, version := range availableVersions {
435                                 if currentRequirement.Range(version.Version) {
436                                         resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require))
437
438                                         if err == nil {
439                                                 return resolved, nil
440                                         }
441                                 }
442                         }
443                         return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
444                 }
445         } else {
446                 return selectedVersions, nil
447         }
448 }
449
450 func (versions PluginVersions) install() {
451         anyInstalled := false
452         currentlyInstalled := GetInstalledVersions()
453
454         for _, sel := range versions {
455                 if sel.pack.Name != CorePluginName {
456                         shouldInstall := true
457                         if pv := currentlyInstalled.find(sel.pack.Name); pv != nil {
458                                 if pv.Version.NE(sel.Version) {
459                                         messenger.AddLog(fmt.Sprint("Uninstalling %q", sel.pack.Name))
460                                         UninstallPlugin(sel.pack.Name)
461                                 } else {
462                                         shouldInstall = false
463                                 }
464                         }
465
466                         if shouldInstall {
467                                 if err := sel.DownloadAndInstall(); err != nil {
468                                         messenger.Error(err)
469                                         return
470                                 }
471                                 anyInstalled = true
472                         }
473                 }
474         }
475         if anyInstalled {
476                 messenger.Message("One or more plugins installed. Please restart micro.")
477         }
478 }
479
480 // UninstallPlugin deletes the plugin folder of the given plugin
481 func UninstallPlugin(name string) {
482         if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil {
483                 messenger.Error(err)
484         }
485 }
486
487 func (pl PluginPackage) Install() {
488         selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{
489                 &PluginDependency{
490                         Name:  pl.Name,
491                         Range: semver.Range(func(v semver.Version) bool { return true }),
492                 }})
493         if err != nil {
494                 TermMessage(err)
495                 return
496         }
497         selected.install()
498 }
499
500 func UpdatePlugins() {
501         microVersion := PluginVersions{
502                 newStaticPluginVersion(CorePluginName, Version),
503         }
504
505         var updates = make(PluginDependencies, 0)
506         for _, name := range loadedPlugins {
507                 pv := GetInstalledPluginVersion(name)
508                 r, err := semver.ParseRange(">=" + pv) // Try to get newer versions.
509                 if err == nil {
510                         updates = append(updates, &PluginDependency{
511                                 Name:  name,
512                                 Range: r,
513                         })
514                 }
515         }
516
517         selected, err := GetAllPluginPackages().Resolve(microVersion, updates)
518         if err != nil {
519                 TermMessage(err)
520                 return
521         }
522         selected.install()
523 }