]> git.lizzy.rs Git - micro.git/blob - cmd/micro/pluginmanager.go
changed json5 repo
[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 = nil
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 // PluginDenendency 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         return plugins
159 }
160
161 // UnmarshalJSON unmarshals raw json to a PluginVersion
162 func (pv *PluginVersion) UnmarshalJSON(data []byte) error {
163         var values struct {
164                 Version semver.Version
165                 Url     string
166                 Require map[string]string
167         }
168
169         if err := json5.Unmarshal(data, &values); err != nil {
170                 return err
171         }
172         pv.Version = values.Version
173         pv.Url = values.Url
174         pv.Require = make(PluginDependencies, 0)
175
176         for k, v := range values.Require {
177                 // don't add the dependency if it's the core and
178                 // we have a unknown version number.
179                 // in that case just accept that dependency (which equals to not adding it.)
180                 if k != CorePluginName || !isUnknownCoreVersion() {
181                         if vRange, err := semver.ParseRange(v); err == nil {
182                                 pv.Require = append(pv.Require, &PluginDependency{k, vRange})
183                         }
184                 }
185         }
186         return nil
187 }
188
189 // UnmarshalJSON unmarshals raw json to a PluginPackage
190 func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
191         var values struct {
192                 Name        string
193                 Description string
194                 Author      string
195                 Tags        []string
196                 Versions    PluginVersions
197         }
198         if err := json5.Unmarshal(data, &values); err != nil {
199                 return err
200         }
201         pp.Name = values.Name
202         pp.Description = values.Description
203         pp.Author = values.Author
204         pp.Tags = values.Tags
205         pp.Versions = values.Versions
206         for _, v := range pp.Versions {
207                 v.pack = pp
208         }
209         return nil
210 }
211
212 // GetAllPluginPackages gets all PluginPackages which may be available.
213 func GetAllPluginPackages() PluginPackages {
214         if allPluginPackages == nil {
215                 getOption := func(name string) []string {
216                         data := GetOption(name)
217                         if strs, ok := data.([]string); ok {
218                                 return strs
219                         }
220                         if ifs, ok := data.([]interface{}); ok {
221                                 result := make([]string, len(ifs))
222                                 for i, urlIf := range ifs {
223                                         if url, ok := urlIf.(string); ok {
224                                                 result[i] = url
225                                         } else {
226                                                 return nil
227                                         }
228                                 }
229                                 return result
230                         }
231                         return nil
232                 }
233
234                 channels := PluginChannels{}
235                 for _, url := range getOption("pluginchannels") {
236                         channels = append(channels, PluginChannel(url))
237                 }
238                 repos := []PluginRepository{}
239                 for _, url := range getOption("pluginrepos") {
240                         repos = append(repos, PluginRepository(url))
241                 }
242                 allPluginPackages = fetchAllSources(len(repos)+1, func(i int) PluginPackages {
243                         if i == 0 {
244                                 return channels.Fetch()
245                         } else {
246                                 return repos[i-1].Fetch()
247                         }
248                 })
249         }
250         return allPluginPackages
251 }
252
253 func (pv PluginVersions) find(ppName string) *PluginVersion {
254         for _, v := range pv {
255                 if v.pack.Name == ppName {
256                         return v
257                 }
258         }
259         return nil
260 }
261
262 // Len returns the number of pluginversions in this slice
263 func (pv PluginVersions) Len() int {
264         return len(pv)
265 }
266
267 // Swap two entries of the slice
268 func (pv PluginVersions) Swap(i, j int) {
269         pv[i], pv[j] = pv[j], pv[i]
270 }
271
272 // Less returns true if the version at position i is greater then the version at position j (used for sorting)
273 func (s PluginVersions) Less(i, j int) bool {
274         return s[i].Version.GT(s[j].Version)
275 }
276
277 // Match returns true if the package matches a given search text
278 func (pp PluginPackage) Match(text string) bool {
279         text = strings.ToLower(text)
280         for _, t := range pp.Tags {
281                 if strings.ToLower(t) == text {
282                         return true
283                 }
284         }
285         if strings.Contains(strings.ToLower(pp.Name), text) {
286                 return true
287         }
288
289         if strings.Contains(strings.ToLower(pp.Description), text) {
290                 return true
291         }
292
293         return false
294 }
295
296 // IsInstallable returns true if the package can be installed.
297 func (pp PluginPackage) IsInstallable() bool {
298         _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
299                 &PluginDependency{
300                         Name:  pp.Name,
301                         Range: semver.Range(func(v semver.Version) bool { return true }),
302                 }})
303         return err == nil
304 }
305
306 // SearchPlugin retrieves a list of all PluginPackages which match the given search text and
307 // could be or are already installed
308 func SearchPlugin(texts []string) (plugins PluginPackages) {
309         plugins = make(PluginPackages, 0)
310
311 pluginLoop:
312         for _, pp := range GetAllPluginPackages() {
313                 for _, text := range texts {
314                         if !pp.Match(text) {
315                                 continue pluginLoop
316                         }
317                 }
318
319                 if pp.IsInstallable() {
320                         plugins = append(plugins, pp)
321                 }
322         }
323         return
324 }
325
326 func isUnknownCoreVersion() bool {
327         _, err := semver.ParseTolerant(Version)
328         return err != nil
329 }
330
331 func newStaticPluginVersion(name, version string) *PluginVersion {
332         vers, err := semver.ParseTolerant(version)
333
334         if err != nil {
335                 if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil {
336                         vers = semver.MustParse("0.0.0-unknown")
337                 }
338         }
339         pl := &PluginPackage{
340                 Name: name,
341         }
342         pv := &PluginVersion{
343                 pack:    pl,
344                 Version: vers,
345         }
346         pl.Versions = PluginVersions{pv}
347         return pv
348 }
349
350 // GetInstalledVersions returns a list of all currently installed plugins including an entry for
351 // micro itself. This can be used to resolve dependencies.
352 func GetInstalledVersions(withCore bool) PluginVersions {
353         result := PluginVersions{}
354         if withCore {
355                 result = append(result, newStaticPluginVersion(CorePluginName, Version))
356         }
357
358         for _, name := range loadedPlugins {
359                 version := GetInstalledPluginVersion(name)
360                 if pv := newStaticPluginVersion(name, version); pv != nil {
361                         result = append(result, pv)
362                 }
363         }
364
365         return result
366 }
367
368 // GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin
369 func GetInstalledPluginVersion(name string) string {
370         plugin := L.GetGlobal(name)
371         if plugin != lua.LNil {
372                 version := L.GetField(plugin, "VERSION")
373                 if str, ok := version.(lua.LString); ok {
374                         return string(str)
375
376                 }
377         }
378         return ""
379 }
380
381 func (pv *PluginVersion) DownloadAndInstall() error {
382         messenger.AddLog(fmt.Sprintf("Downloading %q (%s) from %q", pv.pack.Name, pv.Version, pv.Url))
383         resp, err := http.Get(pv.Url)
384         if err != nil {
385                 return err
386         }
387         defer resp.Body.Close()
388         data, err := ioutil.ReadAll(resp.Body)
389         if err != nil {
390                 return err
391         }
392         zipbuf := bytes.NewReader(data)
393         z, err := zip.NewReader(zipbuf, zipbuf.Size())
394         if err != nil {
395                 return err
396         }
397         targetDir := filepath.Join(configDir, "plugins", pv.pack.Name)
398         dirPerm := os.FileMode(0755)
399         if err = os.MkdirAll(targetDir, dirPerm); err != nil {
400                 return err
401         }
402
403         // Check if all files in zip are in the same directory.
404         // this might be the case if the plugin zip contains the whole plugin dir
405         // instead of its content.
406         var prefix string
407         allPrefixed := false
408         for i, f := range z.File {
409                 parts := strings.Split(f.Name, "/")
410                 if i == 0 {
411                         prefix = parts[0]
412                 } else if parts[0] != prefix {
413                         allPrefixed = false
414                         break
415                 } else {
416                         // switch to true since we have at least a second file
417                         allPrefixed = true
418                 }
419         }
420
421         for _, f := range z.File {
422                 parts := strings.Split(f.Name, "/")
423                 if allPrefixed {
424                         parts = parts[1:]
425                 }
426
427                 targetName := filepath.Join(targetDir, filepath.Join(parts...))
428                 if f.FileInfo().IsDir() {
429                         if err := os.MkdirAll(targetName, dirPerm); err != nil {
430                                 return err
431                         }
432                 } else {
433                         content, err := f.Open()
434                         if err != nil {
435                                 return err
436                         }
437                         defer content.Close()
438                         if target, err := os.Create(targetName); err != nil {
439                                 return err
440                         } else {
441                                 defer target.Close()
442                                 if _, err = io.Copy(target, content); err != nil {
443                                         return err
444                                 }
445                         }
446                 }
447         }
448         return nil
449 }
450
451 func (pl PluginPackages) Get(name string) *PluginPackage {
452         for _, p := range pl {
453                 if p.Name == name {
454                         return p
455                 }
456         }
457         return nil
458 }
459
460 func (pl PluginPackages) GetAllVersions(name string) PluginVersions {
461         result := make(PluginVersions, 0)
462         p := pl.Get(name)
463         if p != nil {
464                 for _, v := range p.Versions {
465                         result = append(result, v)
466                 }
467         }
468         return result
469 }
470
471 func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies {
472         m := make(map[string]*PluginDependency)
473         for _, r := range req {
474                 m[r.Name] = r
475         }
476         for _, o := range other {
477                 cur, ok := m[o.Name]
478                 if ok {
479                         m[o.Name] = &PluginDependency{
480                                 o.Name,
481                                 o.Range.AND(cur.Range),
482                         }
483                 } else {
484                         m[o.Name] = o
485                 }
486         }
487         result := make(PluginDependencies, 0, len(m))
488         for _, v := range m {
489                 result = append(result, v)
490         }
491         return result
492 }
493
494 func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
495         if len(open) == 0 {
496                 return selectedVersions, nil
497         }
498         currentRequirement, stillOpen := open[0], open[1:]
499         if currentRequirement != nil {
500                 if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil {
501                         if currentRequirement.Range(selVersion.Version) {
502                                 return all.Resolve(selectedVersions, stillOpen)
503                         }
504                         return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
505                 } else {
506                         availableVersions := all.GetAllVersions(currentRequirement.Name)
507                         sort.Sort(availableVersions)
508
509                         for _, version := range availableVersions {
510                                 if currentRequirement.Range(version.Version) {
511                                         resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require))
512
513                                         if err == nil {
514                                                 return resolved, nil
515                                         }
516                                 }
517                         }
518                         return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
519                 }
520         } else {
521                 return selectedVersions, nil
522         }
523 }
524
525 func (versions PluginVersions) install() {
526         anyInstalled := false
527         currentlyInstalled := GetInstalledVersions(true)
528
529         for _, sel := range versions {
530                 if sel.pack.Name != CorePluginName {
531                         shouldInstall := true
532                         if pv := currentlyInstalled.find(sel.pack.Name); pv != nil {
533                                 if pv.Version.NE(sel.Version) {
534                                         messenger.AddLog(fmt.Sprint("Uninstalling %q", sel.pack.Name))
535                                         UninstallPlugin(sel.pack.Name)
536                                 } else {
537                                         shouldInstall = false
538                                 }
539                         }
540
541                         if shouldInstall {
542                                 if err := sel.DownloadAndInstall(); err != nil {
543                                         messenger.Error(err)
544                                         return
545                                 }
546                                 anyInstalled = true
547                         }
548                 }
549         }
550         if anyInstalled {
551                 messenger.Message("One or more plugins installed. Please restart micro.")
552         } else {
553                 messenger.AddLog("Nothing to install / update")
554         }
555 }
556
557 // UninstallPlugin deletes the plugin folder of the given plugin
558 func UninstallPlugin(name string) {
559         if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil {
560                 messenger.Error(err)
561         }
562 }
563
564 func (pl PluginPackage) Install() {
565         selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
566                 &PluginDependency{
567                         Name:  pl.Name,
568                         Range: semver.Range(func(v semver.Version) bool { return true }),
569                 }})
570         if err != nil {
571                 TermMessage(err)
572                 return
573         }
574         selected.install()
575 }
576
577 func UpdatePlugins(plugins []string) {
578         // if no plugins are specified, update all installed plugins.
579         if len(plugins) == 0 {
580                 plugins = loadedPlugins
581         }
582
583         messenger.AddLog("Checking for plugin updates")
584         microVersion := PluginVersions{
585                 newStaticPluginVersion(CorePluginName, Version),
586         }
587
588         var updates = make(PluginDependencies, 0)
589         for _, name := range plugins {
590                 pv := GetInstalledPluginVersion(name)
591                 r, err := semver.ParseRange(">=" + pv) // Try to get newer versions.
592                 if err == nil {
593                         updates = append(updates, &PluginDependency{
594                                 Name:  name,
595                                 Range: r,
596                         })
597                 }
598         }
599
600         selected, err := GetAllPluginPackages().Resolve(microVersion, updates)
601         if err != nil {
602                 TermMessage(err)
603                 return
604         }
605         selected.install()
606 }