]> git.lizzy.rs Git - micro.git/blob - cmd/micro/pluginmanager.go
Update readme
[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/flynn/json5"
18         "github.com/yuin/gopher-lua"
19 )
20
21 var (
22         allPluginPackages PluginPackages
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 // PluginDependency 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         if len(plugins) > 0 {
159                 return PluginPackages{plugins[0]}
160         }
161         return nil
162         // return plugins
163 }
164
165 // UnmarshalJSON unmarshals raw json to a PluginVersion
166 func (pv *PluginVersion) UnmarshalJSON(data []byte) error {
167         var values struct {
168                 Version semver.Version
169                 Url     string
170                 Require map[string]string
171         }
172
173         if err := json5.Unmarshal(data, &values); err != nil {
174                 return err
175         }
176         pv.Version = values.Version
177         pv.Url = values.Url
178         pv.Require = make(PluginDependencies, 0)
179
180         for k, v := range values.Require {
181                 // don't add the dependency if it's the core and
182                 // we have a unknown version number.
183                 // in that case just accept that dependency (which equals to not adding it.)
184                 if k != CorePluginName || !isUnknownCoreVersion() {
185                         if vRange, err := semver.ParseRange(v); err == nil {
186                                 pv.Require = append(pv.Require, &PluginDependency{k, vRange})
187                         }
188                 }
189         }
190         return nil
191 }
192
193 // UnmarshalJSON unmarshals raw json to a PluginPackage
194 func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
195         var values struct {
196                 Name        string
197                 Description string
198                 Author      string
199                 Tags        []string
200                 Versions    PluginVersions
201         }
202         if err := json5.Unmarshal(data, &values); err != nil {
203                 return err
204         }
205         pp.Name = values.Name
206         pp.Description = values.Description
207         pp.Author = values.Author
208         pp.Tags = values.Tags
209         pp.Versions = values.Versions
210         for _, v := range pp.Versions {
211                 v.pack = pp
212         }
213         return nil
214 }
215
216 // GetAllPluginPackages gets all PluginPackages which may be available.
217 func GetAllPluginPackages() PluginPackages {
218         if allPluginPackages == nil {
219                 getOption := func(name string) []string {
220                         data := GetOption(name)
221                         if strs, ok := data.([]string); ok {
222                                 return strs
223                         }
224                         if ifs, ok := data.([]interface{}); ok {
225                                 result := make([]string, len(ifs))
226                                 for i, urlIf := range ifs {
227                                         if url, ok := urlIf.(string); ok {
228                                                 result[i] = url
229                                         } else {
230                                                 return nil
231                                         }
232                                 }
233                                 return result
234                         }
235                         return nil
236                 }
237
238                 channels := PluginChannels{}
239                 for _, url := range getOption("pluginchannels") {
240                         channels = append(channels, PluginChannel(url))
241                 }
242                 repos := []PluginRepository{}
243                 for _, url := range getOption("pluginrepos") {
244                         repos = append(repos, PluginRepository(url))
245                 }
246                 allPluginPackages = fetchAllSources(len(repos)+1, func(i int) PluginPackages {
247                         if i == 0 {
248                                 return channels.Fetch()
249                         }
250                         return repos[i-1].Fetch()
251                 })
252         }
253         return allPluginPackages
254 }
255
256 func (pv PluginVersions) find(ppName string) *PluginVersion {
257         for _, v := range pv {
258                 if v.pack.Name == ppName {
259                         return v
260                 }
261         }
262         return nil
263 }
264
265 // Len returns the number of pluginversions in this slice
266 func (pv PluginVersions) Len() int {
267         return len(pv)
268 }
269
270 // Swap two entries of the slice
271 func (pv PluginVersions) Swap(i, j int) {
272         pv[i], pv[j] = pv[j], pv[i]
273 }
274
275 // Less returns true if the version at position i is greater then the version at position j (used for sorting)
276 func (pv PluginVersions) Less(i, j int) bool {
277         return pv[i].Version.GT(pv[j].Version)
278 }
279
280 // Match returns true if the package matches a given search text
281 func (pp PluginPackage) Match(text string) bool {
282         text = strings.ToLower(text)
283         for _, t := range pp.Tags {
284                 if strings.ToLower(t) == text {
285                         return true
286                 }
287         }
288         if strings.Contains(strings.ToLower(pp.Name), text) {
289                 return true
290         }
291
292         if strings.Contains(strings.ToLower(pp.Description), text) {
293                 return true
294         }
295
296         return false
297 }
298
299 // IsInstallable returns true if the package can be installed.
300 func (pp PluginPackage) IsInstallable() error {
301         _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
302                 &PluginDependency{
303                         Name:  pp.Name,
304                         Range: semver.Range(func(v semver.Version) bool { return true }),
305                 }})
306         return err
307 }
308
309 // SearchPlugin retrieves a list of all PluginPackages which match the given search text and
310 // could be or are already installed
311 func SearchPlugin(texts []string) (plugins PluginPackages) {
312         plugins = make(PluginPackages, 0)
313
314 pluginLoop:
315         for _, pp := range GetAllPluginPackages() {
316                 for _, text := range texts {
317                         if !pp.Match(text) {
318                                 continue pluginLoop
319                         }
320                 }
321
322                 if err := pp.IsInstallable(); err == nil {
323                         plugins = append(plugins, pp)
324                 }
325         }
326         return
327 }
328
329 func isUnknownCoreVersion() bool {
330         _, err := semver.ParseTolerant(Version)
331         return err != nil
332 }
333
334 func newStaticPluginVersion(name, version string) *PluginVersion {
335         vers, err := semver.ParseTolerant(version)
336
337         if err != nil {
338                 if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil {
339                         vers = semver.MustParse("0.0.0-unknown")
340                 }
341         }
342         pl := &PluginPackage{
343                 Name: name,
344         }
345         pv := &PluginVersion{
346                 pack:    pl,
347                 Version: vers,
348         }
349         pl.Versions = PluginVersions{pv}
350         return pv
351 }
352
353 // GetInstalledVersions returns a list of all currently installed plugins including an entry for
354 // micro itself. This can be used to resolve dependencies.
355 func GetInstalledVersions(withCore bool) PluginVersions {
356         result := PluginVersions{}
357         if withCore {
358                 result = append(result, newStaticPluginVersion(CorePluginName, Version))
359         }
360
361         for name, lpname := range loadedPlugins {
362                 version := GetInstalledPluginVersion(lpname)
363                 if pv := newStaticPluginVersion(name, version); pv != nil {
364                         result = append(result, pv)
365                 }
366         }
367
368         return result
369 }
370
371 // GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin
372 func GetInstalledPluginVersion(name string) string {
373         plugin := L.GetGlobal(name)
374         if plugin != lua.LNil {
375                 version := L.GetField(plugin, "VERSION")
376                 if str, ok := version.(lua.LString); ok {
377                         return string(str)
378
379                 }
380         }
381         return ""
382 }
383
384 // DownloadAndInstall downloads and installs the given plugin and version
385 func (pv *PluginVersion) DownloadAndInstall() error {
386         messenger.AddLog(fmt.Sprintf("Downloading %q (%s) from %q", pv.pack.Name, pv.Version, pv.Url))
387         resp, err := http.Get(pv.Url)
388         if err != nil {
389                 return err
390         }
391         defer resp.Body.Close()
392         data, err := ioutil.ReadAll(resp.Body)
393         if err != nil {
394                 return err
395         }
396         zipbuf := bytes.NewReader(data)
397         z, err := zip.NewReader(zipbuf, zipbuf.Size())
398         if err != nil {
399                 return err
400         }
401         targetDir := filepath.Join(configDir, "plugins", pv.pack.Name)
402         dirPerm := os.FileMode(0755)
403         if err = os.MkdirAll(targetDir, dirPerm); err != nil {
404                 return err
405         }
406
407         // Check if all files in zip are in the same directory.
408         // this might be the case if the plugin zip contains the whole plugin dir
409         // instead of its content.
410         var prefix string
411         allPrefixed := false
412         for i, f := range z.File {
413                 parts := strings.Split(f.Name, "/")
414                 if i == 0 {
415                         prefix = parts[0]
416                 } else if parts[0] != prefix {
417                         allPrefixed = false
418                         break
419                 } else {
420                         // switch to true since we have at least a second file
421                         allPrefixed = true
422                 }
423         }
424
425         // Install files and directory's
426         for _, f := range z.File {
427                 parts := strings.Split(f.Name, "/")
428                 if allPrefixed {
429                         parts = parts[1:]
430                 }
431
432                 targetName := filepath.Join(targetDir, filepath.Join(parts...))
433                 if f.FileInfo().IsDir() {
434                         if err := os.MkdirAll(targetName, dirPerm); err != nil {
435                                 return err
436                         }
437                 } else {
438                         basepath := filepath.Dir(targetName)
439
440                         if err := os.MkdirAll(basepath, dirPerm); err != nil {
441                                 return err
442                         }
443
444                         content, err := f.Open()
445                         if err != nil {
446                                 return err
447                         }
448                         defer content.Close()
449                         target, err := os.Create(targetName)
450                         if err != nil {
451                                 return err
452                         }
453                         defer target.Close()
454                         if _, err = io.Copy(target, content); err != nil {
455                                 return err
456                         }
457                 }
458         }
459         return nil
460 }
461
462 func (pl PluginPackages) Get(name string) *PluginPackage {
463         for _, p := range pl {
464                 if p.Name == name {
465                         return p
466                 }
467         }
468         return nil
469 }
470
471 func (pl PluginPackages) GetAllVersions(name string) PluginVersions {
472         result := make(PluginVersions, 0)
473         p := pl.Get(name)
474         if p != nil {
475                 for _, v := range p.Versions {
476                         result = append(result, v)
477                 }
478         }
479         return result
480 }
481
482 func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies {
483         m := make(map[string]*PluginDependency)
484         for _, r := range req {
485                 m[r.Name] = r
486         }
487         for _, o := range other {
488                 cur, ok := m[o.Name]
489                 if ok {
490                         m[o.Name] = &PluginDependency{
491                                 o.Name,
492                                 o.Range.AND(cur.Range),
493                         }
494                 } else {
495                         m[o.Name] = o
496                 }
497         }
498         result := make(PluginDependencies, 0, len(m))
499         for _, v := range m {
500                 result = append(result, v)
501         }
502         return result
503 }
504
505 // Resolve resolves dependencies between different plugins
506 func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
507         if len(open) == 0 {
508                 return selectedVersions, nil
509         }
510         currentRequirement, stillOpen := open[0], open[1:]
511         if currentRequirement != nil {
512                 if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil {
513                         if currentRequirement.Range(selVersion.Version) {
514                                 return all.Resolve(selectedVersions, stillOpen)
515                         }
516                         return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
517                 }
518                 availableVersions := all.GetAllVersions(currentRequirement.Name)
519                 sort.Sort(availableVersions)
520
521                 for _, version := range availableVersions {
522                         if currentRequirement.Range(version.Version) {
523                                 resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require))
524
525                                 if err == nil {
526                                         return resolved, nil
527                                 }
528                         }
529                 }
530                 return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
531         }
532         return selectedVersions, nil
533 }
534
535 func (pv PluginVersions) install() {
536         anyInstalled := false
537         currentlyInstalled := GetInstalledVersions(true)
538
539         for _, sel := range pv {
540                 if sel.pack.Name != CorePluginName {
541                         shouldInstall := true
542                         if pv := currentlyInstalled.find(sel.pack.Name); pv != nil {
543                                 if pv.Version.NE(sel.Version) {
544                                         messenger.AddLog("Uninstalling", sel.pack.Name)
545                                         UninstallPlugin(sel.pack.Name)
546                                 } else {
547                                         shouldInstall = false
548                                 }
549                         }
550
551                         if shouldInstall {
552                                 if err := sel.DownloadAndInstall(); err != nil {
553                                         messenger.Error(err)
554                                         return
555                                 }
556                                 anyInstalled = true
557                         }
558                 }
559         }
560         if anyInstalled {
561                 messenger.Message("One or more plugins installed. Please restart micro.")
562         } else {
563                 messenger.AddLog("Nothing to install / update")
564         }
565 }
566
567 // UninstallPlugin deletes the plugin folder of the given plugin
568 func UninstallPlugin(name string) {
569         if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil {
570                 messenger.Error(err)
571                 return
572         }
573         delete(loadedPlugins, name)
574 }
575
576 // Install installs the plugin
577 func (pl PluginPackage) Install() {
578         selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
579                 &PluginDependency{
580                         Name:  pl.Name,
581                         Range: semver.Range(func(v semver.Version) bool { return true }),
582                 }})
583         if err != nil {
584                 TermMessage(err)
585                 return
586         }
587         selected.install()
588 }
589
590 // UpdatePlugins updates the given plugins
591 func UpdatePlugins(plugins []string) {
592         // if no plugins are specified, update all installed plugins.
593         if len(plugins) == 0 {
594                 for name := range loadedPlugins {
595                         plugins = append(plugins, name)
596                 }
597         }
598
599         messenger.AddLog("Checking for plugin updates")
600         microVersion := PluginVersions{
601                 newStaticPluginVersion(CorePluginName, Version),
602         }
603
604         var updates = make(PluginDependencies, 0)
605         for _, name := range plugins {
606                 pv := GetInstalledPluginVersion(name)
607                 r, err := semver.ParseRange(">=" + pv) // Try to get newer versions.
608                 if err == nil {
609                         updates = append(updates, &PluginDependency{
610                                 Name:  name,
611                                 Range: r,
612                         })
613                 }
614         }
615
616         selected, err := GetAllPluginPackages().Resolve(microVersion, updates)
617         if err != nil {
618                 TermMessage(err)
619                 return
620         }
621         selected.install()
622 }