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