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