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