]> git.lizzy.rs Git - micro.git/blob - cmd/micro/pluginmanager.go
1e00f6dbe5808ec4968aaca35adf162e511a3032
[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         resp, err := http.Get(pv.Url)
324         if err != nil {
325                 return err
326         }
327         defer resp.Body.Close()
328         data, err := ioutil.ReadAll(resp.Body)
329         if err != nil {
330                 return err
331         }
332         zipbuf := bytes.NewReader(data)
333         z, err := zip.NewReader(zipbuf, zipbuf.Size())
334         if err != nil {
335                 return err
336         }
337         targetDir := filepath.Join(configDir, "plugins", pv.pack.Name)
338         dirPerm := os.FileMode(0755)
339         if err = os.MkdirAll(targetDir, dirPerm); err != nil {
340                 return err
341         }
342         for _, f := range z.File {
343                 targetName := filepath.Join(targetDir, filepath.Join(strings.Split(f.Name, "/")...))
344                 if f.FileInfo().IsDir() {
345                         if err := os.MkdirAll(targetName, dirPerm); err != nil {
346                                 return err
347                         }
348                 } else {
349                         content, err := f.Open()
350                         if err != nil {
351                                 return err
352                         }
353                         defer content.Close()
354                         if target, err := os.Create(targetName); err != nil {
355                                 return err
356                         } else {
357                                 defer target.Close()
358                                 if _, err = io.Copy(target, content); err != nil {
359                                         return err
360                                 }
361                         }
362                 }
363         }
364         return nil
365 }
366
367 func (pl PluginPackages) Get(name string) *PluginPackage {
368         for _, p := range pl {
369                 if p.Name == name {
370                         return p
371                 }
372         }
373         return nil
374 }
375
376 func (pl PluginPackages) GetAllVersions(name string) PluginVersions {
377         result := make(PluginVersions, 0)
378         p := pl.Get(name)
379         if p != nil {
380                 for _, v := range p.Versions {
381                         result = append(result, v)
382                 }
383         }
384         return result
385 }
386
387 func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies {
388         m := make(map[string]*PluginDependency)
389         for _, r := range req {
390                 m[r.Name] = r
391         }
392         for _, o := range other {
393                 cur, ok := m[o.Name]
394                 if ok {
395                         m[o.Name] = &PluginDependency{
396                                 o.Name,
397                                 o.Range.AND(cur.Range),
398                         }
399                 } else {
400                         m[o.Name] = o
401                 }
402         }
403         result := make(PluginDependencies, 0, len(m))
404         for _, v := range m {
405                 result = append(result, v)
406         }
407         return result
408 }
409
410 func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
411         if len(open) == 0 {
412                 return selectedVersions, nil
413         }
414         currentRequirement, stillOpen := open[0], open[1:]
415         if currentRequirement != nil {
416                 if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil {
417                         if currentRequirement.Range(selVersion.Version) {
418                                 return all.Resolve(selectedVersions, stillOpen)
419                         }
420                         return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
421                 } else {
422                         availableVersions := all.GetAllVersions(currentRequirement.Name)
423                         sort.Sort(availableVersions)
424
425                         for _, version := range availableVersions {
426                                 if currentRequirement.Range(version.Version) {
427                                         resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require))
428
429                                         if err == nil {
430                                                 return resolved, nil
431                                         }
432                                 }
433                         }
434                         return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
435                 }
436         } else {
437                 return selectedVersions, nil
438         }
439 }
440
441 func (versions PluginVersions) install() {
442         anyInstalled := false
443         for _, sel := range versions {
444                 if sel.pack.Name != CorePluginName {
445                         installed := GetInstalledPluginVersion(sel.pack.Name)
446                         if v, err := semver.ParseTolerant(installed); err != nil || v.NE(sel.Version) {
447                                 UninstallPlugin(sel.pack.Name)
448                         }
449                         if err := sel.DownloadAndInstall(); err != nil {
450                                 messenger.Error(err)
451                                 return
452                         }
453                         anyInstalled = true
454                 }
455         }
456         if anyInstalled {
457                 messenger.Message("One or more plugins installed. Please restart micro.")
458         }
459 }
460
461 // UninstallPlugin deletes the plugin folder of the given plugin
462 func UninstallPlugin(name string) {
463         if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil {
464                 messenger.Error(err)
465         }
466 }
467
468 func (pl PluginPackage) Install() {
469         selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{
470                 &PluginDependency{
471                         Name:  pl.Name,
472                         Range: semver.Range(func(v semver.Version) bool { return true }),
473                 }})
474         if err != nil {
475                 TermMessage(err)
476                 return
477         }
478         selected.install()
479 }
480
481 func UpdatePlugins() {
482         microVersion := PluginVersions{
483                 newStaticPluginVersion(CorePluginName, Version),
484         }
485
486         var updates = make(PluginDependencies, 0)
487         for _, name := range loadedPlugins {
488                 pv := GetInstalledPluginVersion(name)
489                 r, err := semver.ParseRange(">=" + pv) // Try to get newer versions.
490                 if err == nil {
491                         updates = append(updates, &PluginDependency{
492                                 Name:  name,
493                                 Range: r,
494                         })
495                 }
496         }
497
498         selected, err := GetAllPluginPackages().Resolve(microVersion, updates)
499         if err != nil {
500                 TermMessage(err)
501                 return
502         }
503         selected.install()
504 }