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