]> git.lizzy.rs Git - micro.git/blob - cmd/micro/pluginmanager.go
Merge pull request #489 from november-eleven/refactor/plugin-name
[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/yuin/gopher-lua"
18         "github.com/zyedidia/json5/encoding/json5"
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         for _, f := range z.File {
426                 parts := strings.Split(f.Name, "/")
427                 if allPrefixed {
428                         parts = parts[1:]
429                 }
430
431                 targetName := filepath.Join(targetDir, filepath.Join(parts...))
432                 if f.FileInfo().IsDir() {
433                         if err := os.MkdirAll(targetName, dirPerm); err != nil {
434                                 return err
435                         }
436                 } else {
437                         content, err := f.Open()
438                         if err != nil {
439                                 return err
440                         }
441                         defer content.Close()
442                         target, err := os.Create(targetName)
443                         if err != nil {
444                                 return err
445                         }
446                         defer target.Close()
447                         if _, err = io.Copy(target, content); err != nil {
448                                 return err
449                         }
450                 }
451         }
452         return nil
453 }
454
455 func (pl PluginPackages) Get(name string) *PluginPackage {
456         for _, p := range pl {
457                 if p.Name == name {
458                         return p
459                 }
460         }
461         return nil
462 }
463
464 func (pl PluginPackages) GetAllVersions(name string) PluginVersions {
465         result := make(PluginVersions, 0)
466         p := pl.Get(name)
467         if p != nil {
468                 for _, v := range p.Versions {
469                         result = append(result, v)
470                 }
471         }
472         return result
473 }
474
475 func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies {
476         m := make(map[string]*PluginDependency)
477         for _, r := range req {
478                 m[r.Name] = r
479         }
480         for _, o := range other {
481                 cur, ok := m[o.Name]
482                 if ok {
483                         m[o.Name] = &PluginDependency{
484                                 o.Name,
485                                 o.Range.AND(cur.Range),
486                         }
487                 } else {
488                         m[o.Name] = o
489                 }
490         }
491         result := make(PluginDependencies, 0, len(m))
492         for _, v := range m {
493                 result = append(result, v)
494         }
495         return result
496 }
497
498 // Resolve resolves dependencies between different plugins
499 func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
500         if len(open) == 0 {
501                 return selectedVersions, nil
502         }
503         currentRequirement, stillOpen := open[0], open[1:]
504         if currentRequirement != nil {
505                 if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil {
506                         if currentRequirement.Range(selVersion.Version) {
507                                 return all.Resolve(selectedVersions, stillOpen)
508                         }
509                         return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
510                 }
511                 availableVersions := all.GetAllVersions(currentRequirement.Name)
512                 sort.Sort(availableVersions)
513
514                 for _, version := range availableVersions {
515                         if currentRequirement.Range(version.Version) {
516                                 resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require))
517
518                                 if err == nil {
519                                         return resolved, nil
520                                 }
521                         }
522                 }
523                 return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
524         }
525         return selectedVersions, nil
526 }
527
528 func (pv PluginVersions) install() {
529         anyInstalled := false
530         currentlyInstalled := GetInstalledVersions(true)
531
532         for _, sel := range pv {
533                 if sel.pack.Name != CorePluginName {
534                         shouldInstall := true
535                         if pv := currentlyInstalled.find(sel.pack.Name); pv != nil {
536                                 if pv.Version.NE(sel.Version) {
537                                         messenger.AddLog(fmt.Sprint("Uninstalling %q", sel.pack.Name))
538                                         UninstallPlugin(sel.pack.Name)
539                                 } else {
540                                         shouldInstall = false
541                                 }
542                         }
543
544                         if shouldInstall {
545                                 if err := sel.DownloadAndInstall(); err != nil {
546                                         messenger.Error(err)
547                                         return
548                                 }
549                                 anyInstalled = true
550                         }
551                 }
552         }
553         if anyInstalled {
554                 messenger.Message("One or more plugins installed. Please restart micro.")
555         } else {
556                 messenger.AddLog("Nothing to install / update")
557         }
558 }
559
560 // UninstallPlugin deletes the plugin folder of the given plugin
561 func UninstallPlugin(name string) {
562         if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil {
563                 messenger.Error(err)
564                 return
565         }
566         delete(loadedPlugins, name)
567 }
568
569 // Install installs the plugin
570 func (pl PluginPackage) Install() {
571         selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
572                 &PluginDependency{
573                         Name:  pl.Name,
574                         Range: semver.Range(func(v semver.Version) bool { return true }),
575                 }})
576         if err != nil {
577                 TermMessage(err)
578                 return
579         }
580         selected.install()
581 }
582
583 // UpdatePlugins updates the given plugins
584 func UpdatePlugins(plugins []string) {
585         // if no plugins are specified, update all installed plugins.
586         if len(plugins) == 0 {
587                 for name := range loadedPlugins {
588                         plugins = append(plugins, name)
589                 }
590         }
591
592         messenger.AddLog("Checking for plugin updates")
593         microVersion := PluginVersions{
594                 newStaticPluginVersion(CorePluginName, Version),
595         }
596
597         var updates = make(PluginDependencies, 0)
598         for _, name := range plugins {
599                 pv := GetInstalledPluginVersion(name)
600                 r, err := semver.ParseRange(">=" + pv) // Try to get newer versions.
601                 if err == nil {
602                         updates = append(updates, &PluginDependency{
603                                 Name:  name,
604                                 Range: r,
605                         })
606                 }
607         }
608
609         selected, err := GetAllPluginPackages().Resolve(microVersion, updates)
610         if err != nil {
611                 TermMessage(err)
612                 return
613         }
614         selected.install()
615 }