]> git.lizzy.rs Git - micro.git/blob - cmd/micro/pluginmanager.go
autocomplete plugin commands
[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         messenger.AddLog(fmt.Sprintf("Fetching channel: %q", string(pc)))
127         resp, err := http.Get(string(pc))
128         if err != nil {
129                 TermMessage("Failed to query plugin channel:\n", err)
130                 return PluginPackages{}
131         }
132         defer resp.Body.Close()
133         decoder := json5.NewDecoder(resp.Body)
134
135         var repositories []PluginRepository
136         if err := decoder.Decode(&repositories); err != nil {
137                 TermMessage("Failed to decode channel data:\n", err)
138                 return PluginPackages{}
139         }
140         return fetchAllSources(len(repositories), func(i int) PluginPackages {
141                 return repositories[i].Fetch()
142         })
143 }
144
145 // Fetch retrieves all available PluginPackages from the given repository
146 func (pr PluginRepository) Fetch() PluginPackages {
147         messenger.AddLog(fmt.Sprintf("Fetching repository: %q", string(pr)))
148         resp, err := http.Get(string(pr))
149         if err != nil {
150                 TermMessage("Failed to query plugin repository:\n", err)
151                 return PluginPackages{}
152         }
153         defer resp.Body.Close()
154         decoder := json5.NewDecoder(resp.Body)
155
156         var plugins PluginPackages
157         if err := decoder.Decode(&plugins); err != nil {
158                 TermMessage("Failed to decode repository data:\n", err)
159                 return PluginPackages{}
160         }
161         return plugins
162 }
163
164 // UnmarshalJSON unmarshals raw json to a PluginVersion
165 func (pv *PluginVersion) UnmarshalJSON(data []byte) error {
166         var values struct {
167                 Version semver.Version
168                 Url     string
169                 Require map[string]string
170         }
171
172         if err := json5.Unmarshal(data, &values); err != nil {
173                 return err
174         }
175         pv.Version = values.Version
176         pv.Url = values.Url
177         pv.Require = make(PluginDependencies, 0)
178
179         for k, v := range values.Require {
180                 if vRange, err := semver.ParseRange(v); err == nil {
181                         pv.Require = append(pv.Require, &PluginDependency{k, vRange})
182                 }
183         }
184         return nil
185 }
186
187 // UnmarshalJSON unmarshals raw json to a PluginPackage
188 func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
189         var values struct {
190                 Name        string
191                 Description string
192                 Author      string
193                 Tags        []string
194                 Versions    PluginVersions
195         }
196         if err := json5.Unmarshal(data, &values); err != nil {
197                 return err
198         }
199         pp.Name = values.Name
200         pp.Description = values.Description
201         pp.Author = values.Author
202         pp.Tags = values.Tags
203         pp.Versions = values.Versions
204         for _, v := range pp.Versions {
205                 v.pack = pp
206         }
207         return nil
208 }
209
210 // GetAllPluginPackages gets all PluginPackages which may be available.
211 func GetAllPluginPackages() PluginPackages {
212         if allPluginPackages == nil {
213                 allPluginPackages = pluginChannels.Fetch()
214         }
215         return allPluginPackages
216 }
217
218 func (pv PluginVersions) find(ppName string) *PluginVersion {
219         for _, v := range pv {
220                 if v.pack.Name == ppName {
221                         return v
222                 }
223         }
224         return nil
225 }
226
227 // Len returns the number of pluginversions in this slice
228 func (pv PluginVersions) Len() int {
229         return len(pv)
230 }
231
232 // Swap two entries of the slice
233 func (pv PluginVersions) Swap(i, j int) {
234         pv[i], pv[j] = pv[j], pv[i]
235 }
236
237 // Less returns true if the version at position i is greater then the version at position j (used for sorting)
238 func (s PluginVersions) Less(i, j int) bool {
239         return s[i].Version.GT(s[j].Version)
240 }
241
242 // Match returns true if the package matches a given search text
243 func (pp PluginPackage) Match(text string) bool {
244         text = strings.ToLower(text)
245         for _, t := range pp.Tags {
246                 if strings.ToLower(t) == text {
247                         return true
248                 }
249         }
250         if strings.Contains(strings.ToLower(pp.Name), text) {
251                 return true
252         }
253
254         if strings.Contains(strings.ToLower(pp.Description), text) {
255                 return true
256         }
257
258         return false
259 }
260
261 // IsInstallable returns true if the package can be installed.
262 func (pp PluginPackage) IsInstallable() bool {
263         _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{
264                 &PluginDependency{
265                         Name:  pp.Name,
266                         Range: semver.Range(func(v semver.Version) bool { return true }),
267                 }})
268         return err == nil
269 }
270
271 // SearchPlugin retrieves a list of all PluginPackages which match the given search text and
272 // could be or are already installed
273 func SearchPlugin(text string) (plugins PluginPackages) {
274         plugins = make(PluginPackages, 0)
275         for _, pp := range GetAllPluginPackages() {
276                 if pp.Match(text) && pp.IsInstallable() {
277                         plugins = append(plugins, pp)
278                 }
279         }
280         return
281 }
282
283 func newStaticPluginVersion(name, version string) *PluginVersion {
284         vers, err := semver.ParseTolerant(version)
285
286         if err != nil {
287                 if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil {
288                         vers = semver.MustParse("0.0.0-unknown")
289                 }
290         }
291         pl := &PluginPackage{
292                 Name: name,
293         }
294         pv := &PluginVersion{
295                 pack:    pl,
296                 Version: vers,
297         }
298         pl.Versions = PluginVersions{pv}
299         return pv
300 }
301
302 // GetInstalledVersions returns a list of all currently installed plugins including an entry for
303 // micro itself. This can be used to resolve dependencies.
304 func GetInstalledVersions() PluginVersions {
305         result := PluginVersions{
306                 newStaticPluginVersion(CorePluginName, Version),
307         }
308
309         for _, name := range loadedPlugins {
310                 version := GetInstalledPluginVersion(name)
311                 if pv := newStaticPluginVersion(name, version); pv != nil {
312                         result = append(result, pv)
313                 }
314         }
315
316         return result
317 }
318
319 // GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin
320 func GetInstalledPluginVersion(name string) string {
321         plugin := L.GetGlobal(name)
322         if plugin != lua.LNil {
323                 version := L.GetField(plugin, "VERSION")
324                 if str, ok := version.(lua.LString); ok {
325                         return string(str)
326
327                 }
328         }
329         return ""
330 }
331
332 func (pv *PluginVersion) DownloadAndInstall() error {
333         messenger.AddLog(fmt.Sprintf("Downloading %q (%s) from %q", pv.pack.Name, pv.Version, pv.Url))
334         resp, err := http.Get(pv.Url)
335         if err != nil {
336                 return err
337         }
338         defer resp.Body.Close()
339         data, err := ioutil.ReadAll(resp.Body)
340         if err != nil {
341                 return err
342         }
343         zipbuf := bytes.NewReader(data)
344         z, err := zip.NewReader(zipbuf, zipbuf.Size())
345         if err != nil {
346                 return err
347         }
348         targetDir := filepath.Join(configDir, "plugins", pv.pack.Name)
349         dirPerm := os.FileMode(0755)
350         if err = os.MkdirAll(targetDir, dirPerm); err != nil {
351                 return err
352         }
353         for _, f := range z.File {
354                 targetName := filepath.Join(targetDir, filepath.Join(strings.Split(f.Name, "/")...))
355                 if f.FileInfo().IsDir() {
356                         if err := os.MkdirAll(targetName, dirPerm); err != nil {
357                                 return err
358                         }
359                 } else {
360                         content, err := f.Open()
361                         if err != nil {
362                                 return err
363                         }
364                         defer content.Close()
365                         if target, err := os.Create(targetName); err != nil {
366                                 return err
367                         } else {
368                                 defer target.Close()
369                                 if _, err = io.Copy(target, content); err != nil {
370                                         return err
371                                 }
372                         }
373                 }
374         }
375         return nil
376 }
377
378 func (pl PluginPackages) Get(name string) *PluginPackage {
379         for _, p := range pl {
380                 if p.Name == name {
381                         return p
382                 }
383         }
384         return nil
385 }
386
387 func (pl PluginPackages) GetAllVersions(name string) PluginVersions {
388         result := make(PluginVersions, 0)
389         p := pl.Get(name)
390         if p != nil {
391                 for _, v := range p.Versions {
392                         result = append(result, v)
393                 }
394         }
395         return result
396 }
397
398 func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies {
399         m := make(map[string]*PluginDependency)
400         for _, r := range req {
401                 m[r.Name] = r
402         }
403         for _, o := range other {
404                 cur, ok := m[o.Name]
405                 if ok {
406                         m[o.Name] = &PluginDependency{
407                                 o.Name,
408                                 o.Range.AND(cur.Range),
409                         }
410                 } else {
411                         m[o.Name] = o
412                 }
413         }
414         result := make(PluginDependencies, 0, len(m))
415         for _, v := range m {
416                 result = append(result, v)
417         }
418         return result
419 }
420
421 func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
422         if len(open) == 0 {
423                 return selectedVersions, nil
424         }
425         currentRequirement, stillOpen := open[0], open[1:]
426         if currentRequirement != nil {
427                 if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil {
428                         if currentRequirement.Range(selVersion.Version) {
429                                 return all.Resolve(selectedVersions, stillOpen)
430                         }
431                         return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
432                 } else {
433                         availableVersions := all.GetAllVersions(currentRequirement.Name)
434                         sort.Sort(availableVersions)
435
436                         for _, version := range availableVersions {
437                                 if currentRequirement.Range(version.Version) {
438                                         resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require))
439
440                                         if err == nil {
441                                                 return resolved, nil
442                                         }
443                                 }
444                         }
445                         return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
446                 }
447         } else {
448                 return selectedVersions, nil
449         }
450 }
451
452 func (versions PluginVersions) install() {
453         anyInstalled := false
454         currentlyInstalled := GetInstalledVersions()
455
456         for _, sel := range versions {
457                 if sel.pack.Name != CorePluginName {
458                         shouldInstall := true
459                         if pv := currentlyInstalled.find(sel.pack.Name); pv != nil {
460                                 if pv.Version.NE(sel.Version) {
461                                         messenger.AddLog(fmt.Sprint("Uninstalling %q", sel.pack.Name))
462                                         UninstallPlugin(sel.pack.Name)
463                                 } else {
464                                         shouldInstall = false
465                                 }
466                         }
467
468                         if shouldInstall {
469                                 if err := sel.DownloadAndInstall(); err != nil {
470                                         messenger.Error(err)
471                                         return
472                                 }
473                                 anyInstalled = true
474                         }
475                 }
476         }
477         if anyInstalled {
478                 messenger.Message("One or more plugins installed. Please restart micro.")
479         } else {
480                 messenger.AddLog("Nothing to install / update")
481         }
482 }
483
484 // UninstallPlugin deletes the plugin folder of the given plugin
485 func UninstallPlugin(name string) {
486         if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil {
487                 messenger.Error(err)
488         }
489 }
490
491 func (pl PluginPackage) Install() {
492         selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{
493                 &PluginDependency{
494                         Name:  pl.Name,
495                         Range: semver.Range(func(v semver.Version) bool { return true }),
496                 }})
497         if err != nil {
498                 TermMessage(err)
499                 return
500         }
501         selected.install()
502 }
503
504 func UpdatePlugins(plugins []string) {
505         // if no plugins are specified, update all installed plugins.
506         if len(plugins) == 0 {
507                 plugins = loadedPlugins
508         }
509
510         messenger.AddLog("Checking for plugin updates")
511         microVersion := PluginVersions{
512                 newStaticPluginVersion(CorePluginName, Version),
513         }
514
515         var updates = make(PluginDependencies, 0)
516         for _, name := range plugins {
517                 pv := GetInstalledPluginVersion(name)
518                 r, err := semver.ParseRange(">=" + pv) // Try to get newer versions.
519                 if err == nil {
520                         updates = append(updates, &PluginDependency{
521                                 Name:  name,
522                                 Range: r,
523                         })
524                 }
525         }
526
527         selected, err := GetAllPluginPackages().Resolve(microVersion, updates)
528         if err != nil {
529                 TermMessage(err)
530                 return
531         }
532         selected.install()
533 }