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