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