Initial commit

This commit is contained in:
Stash Dev
2019-02-09 04:30:49 -08:00
commit 87eeed7e71
1093 changed files with 558731 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
package plugins
import (
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"os"
"os/user"
"path/filepath"
"sync"
"github.com/gobuffalo/envy"
)
type cachedPlugin struct {
Commands Commands `json:"commands"`
CheckSum string `json:"check_sum"`
}
type cachedPlugins map[string]cachedPlugin
var cachePath = func() string {
home := "."
if usr, err := user.Current(); err == nil {
home = usr.HomeDir
}
return filepath.Join(home, ".buffalo", "plugin.cache")
}()
var cacheMoot sync.RWMutex
var cacheOn = envy.Get("BUFFALO_PLUGIN_CACHE", "on")
var cache = func() cachedPlugins {
m := cachedPlugins{}
if cacheOn != "on" {
return m
}
f, err := os.Open(cachePath)
if err != nil {
return m
}
defer f.Close()
if err := json.NewDecoder(f).Decode(&m); err != nil {
f.Close()
os.Remove(f.Name())
}
return m
}()
func findInCache(path string) (cachedPlugin, bool) {
cacheMoot.RLock()
defer cacheMoot.RUnlock()
cp, ok := cache[path]
return cp, ok
}
func saveCache() error {
if cacheOn != "on" {
return nil
}
cacheMoot.Lock()
defer cacheMoot.Unlock()
os.MkdirAll(filepath.Dir(cachePath), 0744)
f, err := os.Create(cachePath)
if err != nil {
return err
}
return json.NewEncoder(f).Encode(cache)
}
func sum(path string) string {
f, err := os.Open(path)
if err != nil {
return ""
}
defer f.Close()
hash := sha256.New()
if _, err := io.Copy(hash, f); err != nil {
return ""
}
sum := hash.Sum(nil)
s := fmt.Sprintf("%x", sum)
return s
}
func addToCache(path string, cp cachedPlugin) {
if cp.CheckSum == "" {
cp.CheckSum = sum(path)
}
cacheMoot.Lock()
defer cacheMoot.Unlock()
cache[path] = cp
}

View File

@@ -0,0 +1,19 @@
package plugins
// Command that the plugin supplies
type Command struct {
// Name "foo"
Name string `json:"name"`
// UseCommand "bar"
UseCommand string `json:"use_command"`
// BuffaloCommand "generate"
BuffaloCommand string `json:"buffalo_command"`
// Description "generates a foo"
Description string `json:"description,omitempty"`
Aliases []string `json:"aliases,omitempty"`
Binary string `json:"-"`
Flags []string `json:"flags,omitempty"`
}
// Commands is a slice of Command
type Commands []Command

View File

@@ -0,0 +1,98 @@
package plugins
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/gobuffalo/envy"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
// ErrPlugMissing ...
var ErrPlugMissing = errors.New("plugin missing")
func Decorate(c Command) *cobra.Command {
var flags []string
if len(c.Flags) > 0 {
for _, f := range c.Flags {
flags = append(flags, f)
}
}
cc := &cobra.Command{
Use: c.Name,
Short: fmt.Sprintf("[PLUGIN] %s", c.Description),
Aliases: c.Aliases,
RunE: func(cmd *cobra.Command, args []string) error {
plugCmd := c.Name
if c.UseCommand != "" {
plugCmd = c.UseCommand
}
ax := []string{plugCmd}
if plugCmd == "-" {
ax = []string{}
}
ax = append(ax, args...)
ax = append(ax, flags...)
bin, err := LookPath(c.Binary)
if err != nil {
return errors.WithStack(err)
}
ex := exec.Command(bin, ax...)
if runtime.GOOS != "windows" {
ex.Env = append(envy.Environ(), "BUFFALO_PLUGIN=1")
}
ex.Stdin = os.Stdin
ex.Stdout = os.Stdout
ex.Stderr = os.Stderr
return log(strings.Join(ex.Args, " "), ex.Run)
},
}
cc.DisableFlagParsing = true
return cc
}
// LookPath ...
func LookPath(s string) (string, error) {
if _, err := os.Stat(s); err == nil {
return s, nil
}
if lp, err := exec.LookPath(s); err == nil {
return lp, err
}
var bin string
pwd, err := os.Getwd()
if err != nil {
return "", errors.WithStack(err)
}
var looks []string
if from, err := envy.MustGet("BUFFALO_PLUGIN_PATH"); err == nil {
looks = append(looks, from)
} else {
looks = []string{filepath.Join(pwd, "plugins"), filepath.Join(envy.GoPath(), "bin"), envy.Get("PATH", "")}
}
for _, p := range looks {
lp := filepath.Join(p, s)
if lp, err = filepath.EvalSymlinks(lp); err == nil {
bin = lp
break
}
}
if len(bin) == 0 {
return "", errors.Wrapf(ErrPlugMissing, "could not find %s in %q", s, looks)
}
return bin, nil
}

View File

@@ -0,0 +1,7 @@
package plugins
const (
EvtSetupStarted = "buffalo-plugins:setup:started"
EvtSetupErr = "buffalo-plugins:setup:err"
EvtSetupFinished = "buffalo-plugins:setup:finished"
)

View File

@@ -0,0 +1,7 @@
//+build !debug
package plugins
func log(_ string, fn func() error) error {
return fn()
}

View File

@@ -0,0 +1,14 @@
//+build debug
package plugins
import (
"fmt"
"time"
)
func log(name string, fn func() error) error {
start := time.Now()
defer fmt.Println(name, time.Now().Sub(start))
return fn()
}

View File

@@ -0,0 +1,16 @@
package plugdeps
import "encoding/json"
// Command is the plugin command you want to control further
type Command struct {
Name string `toml:"name" json:"name"`
Flags []string `toml:"flags,omitempty" json:"flags,omitempty"`
Commands []Command `toml:"command,omitempty" json:"commands,omitempty"`
}
// String implementation of fmt.Stringer
func (p Command) String() string {
b, _ := json.Marshal(p)
return string(b)
}

View File

@@ -0,0 +1,88 @@
package plugdeps
import (
"os"
"path/filepath"
"strings"
"github.com/gobuffalo/meta"
"github.com/karrick/godirwalk"
"github.com/pkg/errors"
)
// ErrMissingConfig is if config/buffalo-plugins.toml file is not found. Use plugdeps#On(app) to test if plugdeps are being used
var ErrMissingConfig = errors.Errorf("could not find a buffalo-plugins config file at %s", ConfigPath(meta.New(".")))
// List all of the plugins the application depeneds on. Will return ErrMissingConfig
// if the app is not using config/buffalo-plugins.toml to manage their plugins.
// Use plugdeps#On(app) to test if plugdeps are being used.
func List(app meta.App) (*Plugins, error) {
plugs := New()
if app.WithPop {
plugs.Add(pop)
}
lp, err := listLocal(app)
if err != nil {
return plugs, errors.WithStack(err)
}
plugs.Add(lp.List()...)
if !On(app) {
return plugs, ErrMissingConfig
}
p := ConfigPath(app)
tf, err := os.Open(p)
if err != nil {
return plugs, errors.WithStack(err)
}
if err := plugs.Decode(tf); err != nil {
return plugs, errors.WithStack(err)
}
return plugs, nil
}
func listLocal(app meta.App) (*Plugins, error) {
plugs := New()
proot := filepath.Join(app.Root, "plugins")
if _, err := os.Stat(proot); err != nil {
return plugs, nil
}
err := godirwalk.Walk(proot, &godirwalk.Options{
FollowSymbolicLinks: true,
Callback: func(path string, info *godirwalk.Dirent) error {
if info.IsDir() {
return nil
}
base := filepath.Base(path)
if strings.HasPrefix(base, "buffalo-") {
plugs.Add(Plugin{
Binary: base,
Local: "." + strings.TrimPrefix(path, app.Root),
})
}
return nil
},
})
if err != nil {
return plugs, errors.WithStack(err)
}
return plugs, nil
}
// ConfigPath returns the path to the config/buffalo-plugins.toml file
// relative to the app
func ConfigPath(app meta.App) string {
return filepath.Join(app.Root, "config", "buffalo-plugins.toml")
}
// On checks for the existence of config/buffalo-plugins.toml if this
// file exists its contents will be used to list plugins. If the file is not
// found, then the BUFFALO_PLUGIN_PATH and ./plugins folders are consulted.
func On(app meta.App) bool {
_, err := os.Stat(ConfigPath(app))
return err == nil
}

View File

@@ -0,0 +1,26 @@
package plugdeps
import (
"encoding/json"
"github.com/gobuffalo/meta"
)
// Plugin represents a Go plugin for Buffalo applications
type Plugin struct {
Binary string `toml:"binary" json:"binary"`
GoGet string `toml:"go_get,omitempty" json:"go_get,omitempty"`
Local string `toml:"local,omitempty" json:"local,omitempty"`
Commands []Command `toml:"command,omitempty" json:"commands,omitempty"`
Tags meta.BuildTags `toml:"tags,omitempty" json:"tags,omitempty"`
}
// String implementation of fmt.Stringer
func (p Plugin) String() string {
b, _ := json.Marshal(p)
return string(b)
}
func (p Plugin) key() string {
return p.Binary + p.GoGet + p.Local
}

View File

@@ -0,0 +1,98 @@
package plugdeps
import (
"io"
"sort"
"sync"
"github.com/BurntSushi/toml"
"github.com/pkg/errors"
)
// Plugins manages the config/buffalo-plugins.toml file
// as well as the plugins available from the file.
type Plugins struct {
plugins map[string]Plugin
moot *sync.RWMutex
}
// Encode the list of plugins, in TOML format, to the reader
func (plugs *Plugins) Encode(w io.Writer) error {
tp := tomlPlugins{
Plugins: plugs.List(),
}
if err := toml.NewEncoder(w).Encode(tp); err != nil {
return errors.WithStack(err)
}
return nil
}
// Decode the list of plugins, in TOML format, from the reader
func (plugs *Plugins) Decode(r io.Reader) error {
tp := &tomlPlugins{
Plugins: []Plugin{},
}
if _, err := toml.DecodeReader(r, tp); err != nil {
return errors.WithStack(err)
}
for _, p := range tp.Plugins {
plugs.Add(p)
}
return nil
}
// List of dependent plugins listed in order of Plugin.String()
func (plugs *Plugins) List() []Plugin {
m := map[string]Plugin{}
plugs.moot.RLock()
for _, p := range plugs.plugins {
m[p.key()] = p
}
plugs.moot.RUnlock()
var pp []Plugin
for _, v := range m {
pp = append(pp, v)
}
sort.Slice(pp, func(a, b int) bool {
return pp[a].Binary < pp[b].Binary
})
return pp
}
// Add plugin(s) to the list of dependencies
func (plugs *Plugins) Add(pp ...Plugin) {
plugs.moot.Lock()
for _, p := range pp {
plugs.plugins[p.key()] = p
}
plugs.moot.Unlock()
}
// Remove plugin(s) from the list of dependencies
func (plugs *Plugins) Remove(pp ...Plugin) {
plugs.moot.Lock()
for _, p := range pp {
delete(plugs.plugins, p.key())
}
plugs.moot.Unlock()
}
// New returns a configured *Plugins value
func New() *Plugins {
plugs := &Plugins{
plugins: map[string]Plugin{},
moot: &sync.RWMutex{},
}
plugs.Add(self)
return plugs
}
type tomlPlugins struct {
Plugins []Plugin `toml:"plugin"`
}
var self = Plugin{
Binary: "buffalo-plugins",
GoGet: "github.com/gobuffalo/buffalo-plugins",
}

View File

@@ -0,0 +1,6 @@
package plugdeps
var pop = Plugin{
Binary: "buffalo-pop",
GoGet: "github.com/gobuffalo/buffalo-pop",
}

View File

@@ -0,0 +1,232 @@
package plugins
import (
"bytes"
"context"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/gobuffalo/buffalo-plugins/plugins/plugdeps"
"github.com/gobuffalo/envy"
"github.com/gobuffalo/meta"
"github.com/karrick/godirwalk"
"github.com/markbates/oncer"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const timeoutEnv = "BUFFALO_PLUGIN_TIMEOUT"
var t = time.Second * 2
func timeout() time.Duration {
oncer.Do("plugins.timeout", func() {
rawTimeout, err := envy.MustGet(timeoutEnv)
if err == nil {
if parsed, err := time.ParseDuration(rawTimeout); err == nil {
t = parsed
} else {
logrus.Errorf("%q value is malformed assuming default %q: %v", timeoutEnv, t, err)
}
} else {
logrus.Debugf("%q not set, assuming default of %v", timeoutEnv, t)
}
})
return t
}
// List maps a Buffalo command to a slice of Command
type List map[string]Commands
var _list List
// Available plugins for the `buffalo` command.
// It will look in $GOPATH/bin and the `./plugins` directory.
// This can be changed by setting the $BUFFALO_PLUGIN_PATH
// environment variable.
//
// Requirements:
// * file/command must be executable
// * file/command must start with `buffalo-`
// * file/command must respond to `available` and return JSON of
// plugins.Commands{}
//
// Limit full path scan with direct plugin path
//
// If a file/command doesn't respond to being invoked with `available`
// within one second, buffalo will assume that it is unable to load. This
// can be changed by setting the $BUFFALO_PLUGIN_TIMEOUT environment
// variable. It must be set to a duration that `time.ParseDuration` can
// process.
func Available() (List, error) {
var err error
oncer.Do("plugins.Available", func() {
defer func() {
if err := saveCache(); err != nil {
logrus.Error(err)
}
}()
app := meta.New(".")
if plugdeps.On(app) {
_list, err = listPlugDeps(app)
return
}
paths := []string{"plugins"}
from, err := envy.MustGet("BUFFALO_PLUGIN_PATH")
if err != nil {
from, err = envy.MustGet("GOPATH")
if err != nil {
return
}
from = filepath.Join(from, "bin")
}
paths = append(paths, strings.Split(from, string(os.PathListSeparator))...)
list := List{}
for _, p := range paths {
if ignorePath(p) {
continue
}
if _, err := os.Stat(p); err != nil {
continue
}
err := godirwalk.Walk(p, &godirwalk.Options{
FollowSymbolicLinks: true,
Callback: func(path string, info *godirwalk.Dirent) error {
if err != nil {
// May indicate a permissions problem with the path, skip it
return nil
}
if info.IsDir() {
return nil
}
base := filepath.Base(path)
if strings.HasPrefix(base, "buffalo-") {
ctx, cancel := context.WithTimeout(context.Background(), timeout())
commands := askBin(ctx, path)
cancel()
for _, c := range commands {
bc := c.BuffaloCommand
if _, ok := list[bc]; !ok {
list[bc] = Commands{}
}
c.Binary = path
list[bc] = append(list[bc], c)
}
}
return nil
},
})
if err != nil {
return
}
}
_list = list
})
return _list, err
}
func askBin(ctx context.Context, path string) Commands {
start := time.Now()
defer func() {
logrus.Debugf("askBin %s=%.4f s", path, time.Since(start).Seconds())
}()
commands := Commands{}
defer func() {
addToCache(path, cachedPlugin{
Commands: commands,
})
}()
if cp, ok := findInCache(path); ok {
s := sum(path)
if s == cp.CheckSum {
logrus.Debugf("cache hit: %s", path)
commands = cp.Commands
return commands
}
}
logrus.Debugf("cache miss: %s", path)
if strings.HasPrefix(filepath.Base(path), "buffalo-no-sqlite") {
return commands
}
cmd := exec.CommandContext(ctx, path, "available")
bb := &bytes.Buffer{}
cmd.Stdout = bb
err := cmd.Run()
if err != nil {
return commands
}
msg := bb.String()
for len(msg) > 0 {
err = json.NewDecoder(strings.NewReader(msg)).Decode(&commands)
if err == nil {
return commands
}
msg = msg[1:]
}
logrus.Errorf("[PLUGIN] error decoding plugin %s: %s\n%s\n", path, err, msg)
return commands
}
func ignorePath(p string) bool {
p = strings.ToLower(p)
for _, x := range []string{`c:\windows`, `c:\program`} {
if strings.HasPrefix(p, x) {
return true
}
}
return false
}
func listPlugDeps(app meta.App) (List, error) {
list := List{}
plugs, err := plugdeps.List(app)
if err != nil {
return list, err
}
for _, p := range plugs.List() {
ctx, cancel := context.WithTimeout(context.Background(), timeout())
defer cancel()
bin := p.Binary
if len(p.Local) != 0 {
bin = p.Local
}
bin, err := LookPath(bin)
if err != nil {
if errors.Cause(err) != ErrPlugMissing {
return list, err
}
continue
}
commands := askBin(ctx, bin)
cancel()
for _, c := range commands {
bc := c.BuffaloCommand
if _, ok := list[bc]; !ok {
list[bc] = Commands{}
}
c.Binary = p.Binary
for _, pc := range p.Commands {
if c.Name == pc.Name {
c.Flags = pc.Flags
break
}
}
list[bc] = append(list[bc], c)
}
}
return list, nil
}

View File

@@ -0,0 +1,3 @@
package plugins
const Version = "v1.11.0"