stash/pkg/plugin/config.go

237 lines
5.4 KiB
Go

package plugin
import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/stashapp/stash/pkg/models"
"gopkg.in/yaml.v2"
)
// Config describes the configuration for a single plugin.
type Config struct {
id string
// path to the configuration file
path string
// The name of the plugin. This will be displayed in the UI.
Name string `yaml:"name"`
// An optional description of what the plugin does.
Description *string `yaml:"description"`
// An optional URL for the plugin.
URL *string `yaml:"url"`
// An optional version string.
Version *string `yaml:"version"`
// The communication interface used when communicating with the spawned
// plugin process. Defaults to 'raw' if not provided.
Interface interfaceEnum `yaml:"interface"`
// The command to execute for the operations in this plugin. The first
// element should be the program name, and subsequent elements are passed
// as arguments.
//
// Note: the execution process will search the path for the program,
// then will attempt to find the program in the plugins
// directory. The exe extension is not necessary on Windows platforms.
// The current working directory is set to that of the stash process.
Exec []string `yaml:"exec,flow"`
// The default log level to output the plugin process's stderr stream.
// Only used if the plugin does not encode its output using log level
// control characters.
// See package common/log for valid values.
// If left unset, defaults to log.ErrorLevel.
PluginErrLogLevel string `yaml:"errLog"`
// The task configurations for tasks provided by this plugin.
Tasks []*OperationConfig `yaml:"tasks"`
}
func (c Config) getPluginTasks(includePlugin bool) []*models.PluginTask {
var ret []*models.PluginTask
for _, o := range c.Tasks {
task := &models.PluginTask{
Name: o.Name,
Description: &o.Description,
}
if includePlugin {
task.Plugin = c.toPlugin()
}
ret = append(ret, task)
}
return ret
}
func (c Config) getName() string {
if c.Name != "" {
return c.Name
}
return c.id
}
func (c Config) toPlugin() *models.Plugin {
return &models.Plugin{
ID: c.id,
Name: c.getName(),
Description: c.Description,
URL: c.URL,
Version: c.Version,
Tasks: c.getPluginTasks(false),
}
}
func (c Config) getTask(name string) *OperationConfig {
for _, o := range c.Tasks {
if o.Name == name {
return o
}
}
return nil
}
func (c Config) getConfigPath() string {
return filepath.Dir(c.path)
}
func (c Config) getExecCommand(task *OperationConfig) []string {
ret := c.Exec
ret = append(ret, task.ExecArgs...)
if len(ret) > 0 {
_, err := exec.LookPath(ret[0])
if err != nil {
// change command to use absolute path
pluginPath := filepath.Dir(c.path)
ret[0] = filepath.Join(pluginPath, ret[0])
}
}
// replace {pluginDir} in arguments with that of the plugin directory
dir := c.getConfigPath()
for i, arg := range ret {
if i == 0 {
continue
}
ret[i] = strings.Replace(arg, "{pluginDir}", dir, -1)
}
return ret
}
type interfaceEnum string
// Valid interfaceEnum values
const (
// InterfaceEnumRPC indicates that the plugin uses the RPCRunner interface
// declared in common/rpc.go.
InterfaceEnumRPC interfaceEnum = "rpc"
// InterfaceEnumRaw interfaces will have the common.PluginInput encoded as
// json (but may be ignored), and output will be decoded as
// common.PluginOutput. If this decoding fails, then the raw output will be
// treated as the output.
InterfaceEnumRaw interfaceEnum = "raw"
InterfaceEnumJS interfaceEnum = "js"
)
func (i interfaceEnum) Valid() bool {
return i == InterfaceEnumRPC || i == InterfaceEnumRaw || i == InterfaceEnumJS
}
func (i *interfaceEnum) getTaskBuilder() taskBuilder {
if *i == InterfaceEnumRaw {
return &rawTaskBuilder{}
}
if *i == InterfaceEnumRPC {
return &rpcTaskBuilder{}
}
if *i == InterfaceEnumJS {
return &jsTaskBuilder{}
}
// shouldn't happen
return nil
}
// OperationConfig describes the configuration for a single plugin operation
// provided by a plugin.
type OperationConfig struct {
// Used to identify the operation. Must be unique within a plugin
// configuration. This name is shown in the button for the operation
// in the UI.
Name string `yaml:"name"`
// A short description of the operation. This description is shown below
// the button in the UI.
Description string `yaml:"description"`
// A list of arguments that will be appended to the plugin's Exec arguments
// when executing this operation.
ExecArgs []string `yaml:"execArgs"`
// A map of argument keys to their default values. The default value is
// used if the applicable argument is not provided during the operation
// call.
DefaultArgs map[string]string `yaml:"defaultArgs"`
}
func loadPluginFromYAML(reader io.Reader) (*Config, error) {
ret := &Config{}
parser := yaml.NewDecoder(reader)
parser.SetStrict(true)
err := parser.Decode(&ret)
if err != nil {
return nil, err
}
if ret.Interface == "" {
ret.Interface = InterfaceEnumRaw
}
if !ret.Interface.Valid() {
return nil, fmt.Errorf("invalid interface type %s", ret.Interface)
}
return ret, nil
}
func loadPluginFromYAMLFile(path string) (*Config, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
ret, err := loadPluginFromYAML(file)
if err != nil {
return nil, err
}
// set id to the filename
id := filepath.Base(path)
ret.id = id[:strings.LastIndex(id, ".")]
ret.path = path
return ret, nil
}