Something went wrong. Try again.
Fork of micro-editor/micro
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736package config
import ( "archive/zip" "bytes" "fmt" "io" "net/http" "os" "path/filepath" "sort" "strings" "sync"
"github.com/blang/semver" "github.com/micro-editor/json5" ulua "github.com/micro-editor/micro/v2/internal/lua" "github.com/micro-editor/micro/v2/internal/util" lua "github.com/yuin/gopher-lua")
var ( allPluginPackages PluginPackages)
// CorePluginName is a plugin dependency name for the micro core.const CorePluginName = "micro"
// PluginChannel contains an url to a json list of PluginRepositorytype PluginChannel string
// PluginChannels is a slice of PluginChanneltype PluginChannels []PluginChannel
// PluginRepository contains an url to json file containing PluginPackagestype PluginRepository string
// PluginPackage contains the meta-data of a plugin and all available versionstype PluginPackage struct { Name string Description string Author string Tags []string Versions PluginVersions Builtin bool}
// PluginPackages is a list of PluginPackage instances.type PluginPackages []*PluginPackage
// PluginVersion descripes a version of a PluginPackage. Containing a version, download url and also dependencies.type PluginVersion struct { pack *PluginPackage Version semver.Version Url string Require PluginDependencies}
func (pv *PluginVersion) Pack() *PluginPackage { return pv.pack}
// PluginVersions is a slice of PluginVersiontype PluginVersions []*PluginVersion
// PluginDependency descripes a dependency to another plugin or micro itself.type PluginDependency struct { Name string Range semver.Range}
// PluginDependencies is a slice of PluginDependencytype PluginDependencies []*PluginDependency
func (pp *PluginPackage) String() string { buf := new(bytes.Buffer) buf.WriteString("Plugin: ") buf.WriteString(pp.Name) if pp.Builtin { buf.WriteString(" (built-in)") } buf.WriteRune('\n') if pp.Author != "" { buf.WriteString("Author: ") buf.WriteString(pp.Author) buf.WriteRune('\n') } if pp.Description != "" { buf.WriteRune('\n') buf.WriteString(pp.Description) } return buf.String()}
func fetchAllSources(count int, fetcher func(i int) PluginPackages) PluginPackages { wgQuery := new(sync.WaitGroup) wgQuery.Add(count)
results := make(chan PluginPackages)
wgDone := new(sync.WaitGroup) wgDone.Add(1) var packages PluginPackages for i := 0; i < count; i++ { go func(i int) { results <- fetcher(i) wgQuery.Done() }(i) } go func() { packages = make(PluginPackages, 0) for res := range results { packages = append(packages, res...) } wgDone.Done() }() wgQuery.Wait() close(results) wgDone.Wait() return packages}
// Fetch retrieves all available PluginPackages from the given channelsfunc (pc PluginChannels) Fetch(out io.Writer) PluginPackages { return fetchAllSources(len(pc), func(i int) PluginPackages { return pc[i].Fetch(out) })}
// Fetch retrieves all available PluginPackages from the given channelfunc (pc PluginChannel) Fetch(out io.Writer) PluginPackages { resp, err := http.Get(string(pc)) if err != nil { fmt.Fprintln(out, "Failed to query plugin channel:\n", err) return nil } defer resp.Body.Close() decoder := json5.NewDecoder(resp.Body)
var repositories []PluginRepository if err := decoder.Decode(&repositories); err != nil { fmt.Fprintln(out, "Failed to decode channel data:\n", err) return nil } return fetchAllSources(len(repositories), func(i int) PluginPackages { return repositories[i].Fetch(out) })}
// Fetch retrieves all available PluginPackages from the given repositoryfunc (pr PluginRepository) Fetch(out io.Writer) PluginPackages { resp, err := http.Get(string(pr)) if err != nil { fmt.Fprintln(out, "Failed to query plugin repository:\n", err) return nil } defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { fmt.Fprintf(out, "Skipped: %s\n", pr) fmt.Fprintf(out, " Reason: Server error %d (%s)\n", resp.StatusCode, http.StatusText(resp.StatusCode)) return nil }
decoder := json5.NewDecoder(resp.Body)
var plugins PluginPackages if err := decoder.Decode(&plugins); err != nil { fmt.Fprintf(out, "Skipped: %s\n", pr) fmt.Fprintf(out, " Reason: Failed to decode repository data:\n") fmt.Fprintf(out, " %s\n", err) return nil } if len(plugins) > 0 { return PluginPackages{plugins[0]} } return nil // return plugins}
// UnmarshalJSON unmarshals raw json to a PluginVersionfunc (pv *PluginVersion) UnmarshalJSON(data []byte) error { var values struct { Version semver.Version Url string Require map[string]string }
if err := json5.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 { // don't add the dependency if it's the core and // we have a unknown version number. // in that case just accept that dependency (which equals to not adding it.) if k != CorePluginName || !isUnknownCoreVersion() { if vRange, err := semver.ParseRange(v); err == nil { pv.Require = append(pv.Require, &PluginDependency{k, vRange}) } } } return nil}
// UnmarshalJSON unmarshals raw json to a PluginPackagefunc (pp *PluginPackage) UnmarshalJSON(data []byte) error { var values struct { Name string Description string Author string Tags []string Versions PluginVersions } if err := json5.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}
// GetAllPluginPackages gets all PluginPackages which may be available.func GetAllPluginPackages(out io.Writer) PluginPackages { if allPluginPackages == nil { getOption := func(name string) []string { data := GetGlobalOption(name) if strs, ok := data.([]string); ok { return strs } if ifs, ok := data.([]any); ok { result := make([]string, len(ifs)) for i, urlIf := range ifs { if url, ok := urlIf.(string); ok { result[i] = url } else { return nil } } return result } return nil }
channels := PluginChannels{} for _, url := range getOption("pluginchannels") { channels = append(channels, PluginChannel(url)) } repos := []PluginRepository{} for _, url := range getOption("pluginrepos") { repos = append(repos, PluginRepository(url)) } allPluginPackages = fetchAllSources(len(repos)+1, func(i int) PluginPackages { if i == 0 { return channels.Fetch(out) } return repos[i-1].Fetch(out) }) } return allPluginPackages}
func (pv PluginVersions) find(ppName string) *PluginVersion { for _, v := range pv { if v.pack.Name == ppName { return v } } return nil}
// Len returns the number of pluginversions in this slicefunc (pv PluginVersions) Len() int { return len(pv)}
// Swap two entries of the slicefunc (pv PluginVersions) Swap(i, j int) { pv[i], pv[j] = pv[j], pv[i]}
// Less returns true if the version at position i is greater then the version at position j (used for sorting)func (pv PluginVersions) Less(i, j int) bool { return pv[i].Version.GT(pv[j].Version)}
// Match returns true if the package matches a given search textfunc (pp PluginPackage) Match(text string) bool { text = strings.ToLower(text) for _, t := range pp.Tags { if strings.ToLower(t) == text { return true } } if strings.Contains(strings.ToLower(pp.Name), text) { return true }
if strings.Contains(strings.ToLower(pp.Description), text) { return true }
return false}
// IsInstallable returns true if the package can be installed.func (pp PluginPackage) IsInstallable(out io.Writer) error { _, err := GetAllPluginPackages(out).Resolve(GetInstalledVersions(true), PluginDependencies{ &PluginDependency{ Name: pp.Name, Range: semver.Range(func(v semver.Version) bool { return true }), }}) return err}
// SearchPlugin retrieves a list of all PluginPackages which match the given search text and// could be or are already installedfunc SearchPlugin(out io.Writer, texts []string) (plugins PluginPackages) { plugins = make(PluginPackages, 0)
pluginLoop: for _, pp := range GetAllPluginPackages(out) { for _, text := range texts { if !pp.Match(text) { continue pluginLoop } }
if err := pp.IsInstallable(out); err == nil { plugins = append(plugins, pp) } } return}
func isUnknownCoreVersion() bool { _, err := semver.ParseTolerant(util.Version) return err != nil}
func newStaticPluginVersion(name, version string, builtin bool) *PluginVersion { vers, err := semver.ParseTolerant(version)
if err != nil { if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil { vers = semver.MustParse("0.0.0-unknown") } } pl := &PluginPackage{ Name: name, Builtin: builtin, } pv := &PluginVersion{ pack: pl, Version: vers, } pl.Versions = PluginVersions{pv} return pv}
// GetInstalledVersions returns a list of all currently installed plugins including an entry for// micro itself. This can be used to resolve dependencies.func GetInstalledVersions(withCore bool) PluginVersions { result := PluginVersions{} if withCore { result = append(result, newStaticPluginVersion(CorePluginName, util.Version, true)) }
for _, p := range Plugins { if !p.IsLoaded() { continue } version := GetInstalledPluginVersion(p.Name) if pv := newStaticPluginVersion(p.Name, version, p.Builtin); pv != nil { result = append(result, pv) } }
return result}
// GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded pluginfunc GetInstalledPluginVersion(name string) string { plugin := ulua.L.GetGlobal(name) if plugin != lua.LNil { version := ulua.L.GetField(plugin, "VERSION") if str, ok := version.(lua.LString); ok { return string(str)
} } return ""}
// DownloadAndInstall downloads and installs the given plugin and versionfunc (pv *PluginVersion) DownloadAndInstall(out io.Writer) error { fmt.Fprintf(out, "Downloading %q (%s) from %q\n", pv.pack.Name, pv.Version, pv.Url) resp, err := http.Get(pv.Url) if err != nil { return err } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return err } zipbuf := bytes.NewReader(data) z, err := zip.NewReader(zipbuf, zipbuf.Size()) if err != nil { return err } targetDir := filepath.Join(ConfigDir, "plug", pv.pack.Name) dirPerm := os.FileMode(0755) if err = os.MkdirAll(targetDir, dirPerm); err != nil { return err }
// Check if all files in zip are in the same directory. // this might be the case if the plugin zip contains the whole plugin dir // instead of its content. var prefix string allPrefixed := false for i, f := range z.File { parts := strings.Split(f.Name, "/") if i == 0 { prefix = parts[0] } else if parts[0] != prefix { allPrefixed = false break } else { // switch to true since we have at least a second file allPrefixed = true } }
// Install files and directory's for _, f := range z.File { parts := strings.Split(f.Name, "/") if allPrefixed { parts = parts[1:] }
targetName := filepath.Join(targetDir, filepath.Join(parts...)) if f.FileInfo().IsDir() { if err := os.MkdirAll(targetName, dirPerm); err != nil { return err } } else { basepath := filepath.Dir(targetName)
if err := os.MkdirAll(basepath, dirPerm); err != nil { return err }
content, err := f.Open() if err != nil { return err } defer content.Close() target, err := os.Create(targetName) if err != nil { return err } defer target.Close() if _, err = io.Copy(target, content); err != nil { return err } } } return nil}
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 { result = append(result, p.Versions...) } 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}
// Resolve resolves dependencies between different pluginsfunc (all PluginPackages) Resolve(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.Resolve(selectedVersions, stillOpen) } return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name) } availableVersions := all.GetAllVersions(currentRequirement.Name) sort.Sort(availableVersions)
for _, version := range availableVersions { if currentRequirement.Range(version.Version) { resolved, err := all.Resolve(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) } return selectedVersions, nil}
func (pv PluginVersions) install(out io.Writer) { anyInstalled := false currentlyInstalled := GetInstalledVersions(true)
for _, sel := range pv { if sel.pack.Name != CorePluginName { shouldInstall := true if pv := currentlyInstalled.find(sel.pack.Name); pv != nil { if pv.Version.NE(sel.Version) { fmt.Fprintln(out, "Uninstalling", sel.pack.Name) UninstallPlugin(out, sel.pack.Name) } else { shouldInstall = false } }
if shouldInstall { if err := sel.DownloadAndInstall(out); err != nil { fmt.Fprintln(out, err) return } anyInstalled = true } } } if anyInstalled { fmt.Fprintln(out, "One or more plugins installed.") } else { fmt.Fprintln(out, "Nothing to install / update") }}
// UninstallPlugin deletes the plugin folder of the given pluginfunc UninstallPlugin(out io.Writer, name string) { for _, p := range Plugins { if !p.IsLoaded() { continue } if p.Name == name { p.Loaded = false if err := os.RemoveAll(filepath.Join(ConfigDir, "plug", p.DirName)); err != nil { fmt.Fprintln(out, err) return } break } }}
// Install installs the pluginfunc (pl PluginPackage) Install(out io.Writer) { selected, err := GetAllPluginPackages(out).Resolve(GetInstalledVersions(true), PluginDependencies{ &PluginDependency{ Name: pl.Name, Range: semver.Range(func(v semver.Version) bool { return true }), }}) if err != nil { fmt.Fprintln(out, err) return } selected.install(out)}
// UpdatePlugins updates the given pluginsfunc UpdatePlugins(out io.Writer, plugins []string) { // if no plugins are specified, update all installed plugins. if len(plugins) == 0 { for _, p := range Plugins { if !p.IsLoaded() || p.Builtin || p.Name == "initlua" { continue } plugins = append(plugins, p.Name) } }
fmt.Fprintln(out, "Checking for plugin updates") microVersion := PluginVersions{ newStaticPluginVersion(CorePluginName, util.Version, true), }
var updates = make(PluginDependencies, 0) for _, name := range plugins { pv := GetInstalledPluginVersion(name) r, err := semver.ParseRange(">=" + pv) // Try to get newer versions. if err == nil { updates = append(updates, &PluginDependency{ Name: name, Range: r, }) } }
selected, err := GetAllPluginPackages(out).Resolve(microVersion, updates) if err != nil { fmt.Fprintln(out, err) return } selected.install(out)}
func PluginCommand(out io.Writer, cmd string, args []string) { switch cmd { case "install": installedVersions := GetInstalledVersions(false) for _, plugin := range args { pp := GetAllPluginPackages(out).Get(plugin) if pp == nil { fmt.Fprintln(out, "Unknown plugin \""+plugin+"\"") } else if err := pp.IsInstallable(out); err != nil { fmt.Fprintln(out, "Error installing "+plugin+": ", err) } else { for _, installed := range installedVersions { if pp.Name == installed.Pack().Name { if pp.Versions[0].Version.Compare(installed.Version) == 1 { fmt.Fprintln(out, pp.Name, "is already installed but out-of-date: use 'plugin update "+pp.Name+"' to update") } else { fmt.Fprintln(out, pp.Name, "is already installed") } } } pp.Install(out) } }
case "remove": removed := "" for _, plugin := range args { if plugin == "initlua" { fmt.Fprintln(out, "initlua cannot be removed, but can be disabled via settings.") continue } // check if the plugin exists. for _, p := range Plugins { if p.Name == plugin && p.Builtin { fmt.Fprintln(out, p.Name, "is a built-in plugin which cannot be removed, but can be disabled via settings.") continue } if p.Name == plugin { UninstallPlugin(out, plugin) removed += plugin + " " continue } } } removed = strings.TrimSpace(removed) if removed != "" { fmt.Fprintln(out, "Removed", removed) } else { fmt.Fprintln(out, "No plugins removed") } case "update": UpdatePlugins(out, args) case "list": plugins := GetInstalledVersions(false) fmt.Fprintln(out, "The following plugins are currently installed:") for _, p := range plugins { if p.Pack().Name == "initlua" { fmt.Fprintf(out, "%s\n", "initlua") } else if p.Pack().Builtin { fmt.Fprintf(out, "%s (built-in)\n", p.Pack().Name) } else { fmt.Fprintf(out, "%s (%s)\n", p.Pack().Name, p.Version) } } case "search": plugins := SearchPlugin(out, args) fmt.Fprintln(out, len(plugins), "plugins found") for _, p := range plugins { fmt.Fprintln(out, "----------------") fmt.Fprintln(out, p.String()) } fmt.Fprintln(out, "----------------") case "available": packages := GetAllPluginPackages(out) fmt.Fprintln(out, "Available Plugins:") for _, pkg := range packages { fmt.Fprintln(out, pkg.Name) } default: fmt.Fprintln(out, "Invalid plugin command") }}