package main import ( "archive/zip" "bytes" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "os" "path/filepath" "regexp" "sort" "strings" "sync" "github.com/blang/semver" "github.com/yuin/gopher-lua" ) var Repositories []PluginRepository = []PluginRepository{} type PluginRepository string type PluginPackage struct { Name string Description string Author string Tags []string Versions PluginVersions } type PluginPackages []*PluginPackage type PluginVersion struct { pack *PluginPackage Version semver.Version Url string Require PluginDependencies } type PluginVersions []*PluginVersion type PluginDependency struct { Name string Range semver.Range } type PluginDependencies []*PluginDependency func (pv *PluginVersion) UnmarshalJSON(data []byte) error { var values struct { Version semver.Version Url string Require map[string]string } if err := json.Unmarshal(data, &values); err != nil { return err } pv.Version = values.Version pv.Url = values.Url pv.Require = make(PluginDependencies, 0) for k, v := range values.Require { if vRange, err := semver.ParseRange(v); err == nil { pv.Require = append(pv.Require, &PluginDependency{k, vRange}) } } return nil } func (pv *PluginVersion) String() string { return fmt.Sprintf("%s (%s)", pv.pack.Name, pv.Version) } func (pd *PluginDependency) String() string { return pd.Name } func (pp *PluginPackage) UnmarshalJSON(data []byte) error { var values struct { Name string Description string Author string Tags []string Versions PluginVersions } if err := json.Unmarshal(data, &values); err != nil { return err } pp.Name = values.Name pp.Description = values.Description pp.Author = values.Author pp.Tags = values.Tags pp.Versions = values.Versions for _, v := range pp.Versions { v.pack = pp } return nil } func (pv PluginVersions) Find(name string) *PluginVersion { for _, v := range pv { if v.pack.Name == name { return v } } return nil } func (pv PluginVersions) Len() int { return len(pv) } func (pv PluginVersions) Swap(i, j int) { pv[i], pv[j] = pv[j], pv[i] } func (s PluginVersions) Less(i, j int) bool { // sort descending return s[i].Version.GT(s[j].Version) } func (pr PluginRepository) Query() <-chan *PluginPackage { resChan := make(chan *PluginPackage) go func() { defer close(resChan) resp, err := http.Get(string(pr)) if err != nil { TermMessage("Failed to query plugin repository:\n", err) return } defer resp.Body.Close() decoder := json.NewDecoder(resp.Body) var plugins PluginPackages if err := decoder.Decode(&plugins); err != nil { TermMessage("Failed to decode repository data:\n", err) return } for _, p := range plugins { resChan <- p } }() return resChan } func (pp *PluginPackage) GetInstallableVersion() *PluginVersion { matching := make(PluginVersions, 0) versionLoop: for _, pv := range pp.Versions { for _, req := range pv.Require { curVersion := GetInstalledVersion(req.Name) if curVersion == nil || !req.Range(*curVersion) { continue versionLoop } } matching = append(matching, pv) } if len(matching) > 0 { sort.Sort(matching) return matching[0] } return nil } func (pp PluginPackage) Match(text string) bool { // ToDo: improve matching. text = "(?i)" + text if r, err := regexp.Compile(text); err == nil { return r.MatchString(pp.Name) } return false } func SearchPlugin(text string) (plugins []*PluginPackage) { wgQuery := new(sync.WaitGroup) wgQuery.Add(len(Repositories)) results := make(chan *PluginPackage) wgDone := new(sync.WaitGroup) wgDone.Add(1) for _, repo := range Repositories { go func(repo PluginRepository) { res := repo.Query() for r := range res { results <- r } wgQuery.Done() }(repo) } go func() { for res := range results { if res.GetInstallableVersion() != nil && res.Match(text) { plugins = append(plugins, res) } } wgDone.Done() }() wgQuery.Wait() close(results) wgDone.Wait() return } func GetInstalledVersion(name string) *semver.Version { versionStr := "" if name == "micro" { versionStr = Version } else { plugin := L.GetGlobal(name) if plugin == lua.LNil { return nil } version := L.GetField(plugin, "VERSION") if str, ok := version.(lua.LString); ok { versionStr = string(str) } } if v, err := semver.Parse(versionStr); err != nil { return nil } else { return &v } } func (pv *PluginVersion) Install() { resp, err := http.Get(pv.Url) if err == nil { defer resp.Body.Close() data, _ := ioutil.ReadAll(resp.Body) zipbuf := bytes.NewReader(data) z, err := zip.NewReader(zipbuf, zipbuf.Size()) if err == nil { targetDir := filepath.Join(configDir, "plugins", pv.pack.Name) dirPerm := os.FileMode(0755) if err = os.MkdirAll(targetDir, dirPerm); err == nil { for _, f := range z.File { targetName := filepath.Join(targetDir, filepath.Join(strings.Split(f.Name, "/")...)) if f.FileInfo().IsDir() { err = os.MkdirAll(targetName, dirPerm) } else { content, err := f.Open() if err == nil { defer content.Close() if target, err := os.Create(targetName); err == nil { defer target.Close() _, err = io.Copy(target, content) } } } if err != nil { break } } } } } if err != nil { TermMessage("Failed to install plugin:", err) } } func UninstallPlugin(name string) { os.RemoveAll(filepath.Join(configDir, name)) } // Updates... func (pl PluginPackages) Get(name string) *PluginPackage { for _, p := range pl { if p.Name == name { return p } } return nil } func (pl PluginPackages) GetAllVersions(name string) PluginVersions { result := make(PluginVersions, 0) p := pl.Get(name) if p != nil { for _, v := range p.Versions { result = append(result, v) } } return result } func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies { m := make(map[string]*PluginDependency) for _, r := range req { m[r.Name] = r } for _, o := range other { cur, ok := m[o.Name] if ok { m[o.Name] = &PluginDependency{ o.Name, o.Range.AND(cur.Range), } } else { m[o.Name] = o } } result := make(PluginDependencies, 0, len(m)) for _, v := range m { result = append(result, v) } return result } func (all PluginPackages) ResolveStep(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) { if len(open) == 0 { return selectedVersions, nil } currentRequirement, stillOpen := open[0], open[1:] if currentRequirement != nil { if selVersion := selectedVersions.Find(currentRequirement.Name); selVersion != nil { if currentRequirement.Range(selVersion.Version) { return all.ResolveStep(selectedVersions, stillOpen) } return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name) } else { availableVersions := all.GetAllVersions(currentRequirement.Name) sort.Sort(availableVersions) for _, version := range availableVersions { if currentRequirement.Range(version.Version) { resolved, err := all.ResolveStep(append(selectedVersions, version), stillOpen.Join(version.Require)) if err == nil { return resolved, nil } } } return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name) } } else { return selectedVersions, nil } }