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