2020-08-08 02:05:35 +00:00
|
|
|
// Package plugin implements functions and types for maintaining and running
|
|
|
|
// stash plugins.
|
|
|
|
//
|
|
|
|
// Stash plugins are configured using yml files in the configured plugins
|
|
|
|
// directory. These yml files must follow the Config structure format.
|
|
|
|
//
|
|
|
|
// The main entry into the plugin sub-system is via the Cache type.
|
|
|
|
package plugin
|
|
|
|
|
|
|
|
import (
|
2021-06-11 07:24:58 +00:00
|
|
|
"context"
|
2020-08-08 02:05:35 +00:00
|
|
|
"fmt"
|
2021-05-26 04:17:53 +00:00
|
|
|
"net/http"
|
2020-08-08 02:05:35 +00:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2021-10-28 03:25:17 +00:00
|
|
|
"strconv"
|
2020-08-08 02:05:35 +00:00
|
|
|
|
|
|
|
"github.com/stashapp/stash/pkg/logger"
|
2021-06-03 01:00:17 +00:00
|
|
|
"github.com/stashapp/stash/pkg/manager/config"
|
2020-08-08 02:05:35 +00:00
|
|
|
"github.com/stashapp/stash/pkg/models"
|
|
|
|
"github.com/stashapp/stash/pkg/plugin/common"
|
2021-06-11 07:24:58 +00:00
|
|
|
"github.com/stashapp/stash/pkg/session"
|
|
|
|
"github.com/stashapp/stash/pkg/utils"
|
2020-08-08 02:05:35 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Cache stores plugin details.
|
|
|
|
type Cache struct {
|
2021-06-11 07:24:58 +00:00
|
|
|
config *config.Instance
|
|
|
|
plugins []Config
|
|
|
|
sessionStore *session.Store
|
|
|
|
gqlHandler http.Handler
|
2020-08-08 02:05:35 +00:00
|
|
|
}
|
|
|
|
|
2021-06-03 01:00:17 +00:00
|
|
|
// NewCache returns a new Cache.
|
2020-08-08 02:05:35 +00:00
|
|
|
//
|
2021-06-03 01:00:17 +00:00
|
|
|
// Plugins configurations are loaded from yml files in the plugin
|
|
|
|
// directory in the config and any subdirectories.
|
|
|
|
//
|
|
|
|
// Does not load plugins. Plugins will need to be
|
|
|
|
// loaded explicitly using ReloadPlugins.
|
|
|
|
func NewCache(config *config.Instance) *Cache {
|
2020-08-08 02:05:35 +00:00
|
|
|
return &Cache{
|
2021-06-03 01:00:17 +00:00
|
|
|
config: config,
|
|
|
|
}
|
2020-08-08 02:05:35 +00:00
|
|
|
}
|
|
|
|
|
2021-06-11 07:24:58 +00:00
|
|
|
func (c *Cache) RegisterGQLHandler(handler http.Handler) {
|
2021-05-26 04:17:53 +00:00
|
|
|
c.gqlHandler = handler
|
|
|
|
}
|
|
|
|
|
2021-06-11 07:24:58 +00:00
|
|
|
func (c *Cache) RegisterSessionStore(sessionStore *session.Store) {
|
|
|
|
c.sessionStore = sessionStore
|
|
|
|
}
|
|
|
|
|
2021-06-03 01:00:17 +00:00
|
|
|
// LoadPlugins clears the plugin cache and loads from the plugin path.
|
2020-08-08 02:05:35 +00:00
|
|
|
// In the event of an error during loading, the cache will be left empty.
|
2021-06-03 01:00:17 +00:00
|
|
|
func (c *Cache) LoadPlugins() error {
|
2020-08-08 02:05:35 +00:00
|
|
|
c.plugins = nil
|
2021-06-03 01:00:17 +00:00
|
|
|
plugins, err := loadPlugins(c.config.GetPluginsPath())
|
2020-08-08 02:05:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
c.plugins = plugins
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func loadPlugins(path string) ([]Config, error) {
|
|
|
|
plugins := make([]Config, 0)
|
|
|
|
|
|
|
|
logger.Debugf("Reading plugin configs from %s", path)
|
|
|
|
pluginFiles := []string{}
|
|
|
|
err := filepath.Walk(path, func(fp string, f os.FileInfo, err error) error {
|
|
|
|
if filepath.Ext(fp) == ".yml" {
|
|
|
|
pluginFiles = append(pluginFiles, fp)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, file := range pluginFiles {
|
|
|
|
plugin, err := loadPluginFromYAMLFile(file)
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorf("Error loading plugin %s: %s", file, err.Error())
|
|
|
|
} else {
|
|
|
|
plugins = append(plugins, *plugin)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return plugins, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ListPlugins returns plugin details for all of the loaded plugins.
|
|
|
|
func (c Cache) ListPlugins() []*models.Plugin {
|
|
|
|
var ret []*models.Plugin
|
|
|
|
for _, s := range c.plugins {
|
|
|
|
ret = append(ret, s.toPlugin())
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
|
|
|
// ListPluginTasks returns all runnable plugin tasks in all loaded plugins.
|
|
|
|
func (c Cache) ListPluginTasks() []*models.PluginTask {
|
|
|
|
var ret []*models.PluginTask
|
|
|
|
for _, s := range c.plugins {
|
|
|
|
ret = append(ret, s.getPluginTasks(true)...)
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
2021-06-11 07:24:58 +00:00
|
|
|
func buildPluginInput(plugin *Config, operation *OperationConfig, serverConnection common.StashServerConnection, args []*models.PluginArgInput) common.PluginInput {
|
|
|
|
args = applyDefaultArgs(args, operation.DefaultArgs)
|
|
|
|
serverConnection.PluginDir = plugin.getConfigPath()
|
|
|
|
return common.PluginInput{
|
|
|
|
ServerConnection: serverConnection,
|
|
|
|
Args: toPluginArgs(args),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c Cache) makeServerConnection(ctx context.Context) common.StashServerConnection {
|
|
|
|
cookie := c.sessionStore.MakePluginCookie(ctx)
|
|
|
|
|
|
|
|
serverConnection := common.StashServerConnection{
|
|
|
|
Scheme: "http",
|
2021-06-21 04:38:44 +00:00
|
|
|
Host: c.config.GetHost(),
|
2021-06-11 07:24:58 +00:00
|
|
|
Port: c.config.GetPort(),
|
|
|
|
SessionCookie: cookie,
|
|
|
|
Dir: c.config.GetConfigPath(),
|
|
|
|
}
|
|
|
|
|
2021-08-31 09:37:45 +00:00
|
|
|
if c.config.HasTLSConfig() {
|
2021-06-11 07:24:58 +00:00
|
|
|
serverConnection.Scheme = "https"
|
|
|
|
}
|
|
|
|
|
|
|
|
return serverConnection
|
|
|
|
}
|
|
|
|
|
2020-08-08 02:05:35 +00:00
|
|
|
// CreateTask runs the plugin operation for the pluginID and operation
|
|
|
|
// name provided. Returns an error if the plugin or the operation could not be
|
|
|
|
// resolved.
|
2021-06-11 07:24:58 +00:00
|
|
|
func (c Cache) CreateTask(ctx context.Context, pluginID string, operationName string, args []*models.PluginArgInput, progress chan float64) (Task, error) {
|
|
|
|
serverConnection := c.makeServerConnection(ctx)
|
|
|
|
|
2020-08-08 02:05:35 +00:00
|
|
|
// find the plugin and operation
|
|
|
|
plugin := c.getPlugin(pluginID)
|
|
|
|
|
|
|
|
if plugin == nil {
|
|
|
|
return nil, fmt.Errorf("no plugin with ID %s", pluginID)
|
|
|
|
}
|
|
|
|
|
|
|
|
operation := plugin.getTask(operationName)
|
|
|
|
if operation == nil {
|
|
|
|
return nil, fmt.Errorf("no task with name %s in plugin %s", operationName, plugin.getName())
|
|
|
|
}
|
|
|
|
|
|
|
|
task := pluginTask{
|
2021-06-11 07:24:58 +00:00
|
|
|
plugin: plugin,
|
|
|
|
operation: operation,
|
|
|
|
input: buildPluginInput(plugin, operation, serverConnection, args),
|
|
|
|
progress: progress,
|
|
|
|
gqlHandler: c.gqlHandler,
|
2020-08-08 02:05:35 +00:00
|
|
|
}
|
|
|
|
return task.createTask(), nil
|
|
|
|
}
|
|
|
|
|
2021-06-11 07:24:58 +00:00
|
|
|
func (c Cache) ExecutePostHooks(ctx context.Context, id int, hookType HookTriggerEnum, input interface{}, inputFields []string) {
|
|
|
|
if err := c.executePostHooks(ctx, hookType, common.HookContext{
|
|
|
|
ID: id,
|
|
|
|
Type: hookType.String(),
|
|
|
|
Input: input,
|
|
|
|
InputFields: inputFields,
|
|
|
|
}); err != nil {
|
|
|
|
logger.Errorf("error executing post hooks: %s", err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-28 03:25:17 +00:00
|
|
|
func (c Cache) ExecuteSceneUpdatePostHooks(ctx context.Context, input models.SceneUpdateInput, inputFields []string) {
|
|
|
|
id, err := strconv.Atoi(input.ID)
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorf("error converting id in SceneUpdatePostHooks: %v", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
c.ExecutePostHooks(ctx, id, SceneUpdatePost, input, inputFields)
|
|
|
|
}
|
|
|
|
|
2021-06-11 07:24:58 +00:00
|
|
|
func (c Cache) executePostHooks(ctx context.Context, hookType HookTriggerEnum, hookContext common.HookContext) error {
|
|
|
|
visitedPlugins := session.GetVisitedPlugins(ctx)
|
|
|
|
|
|
|
|
for _, p := range c.plugins {
|
|
|
|
hooks := p.getHooks(hookType)
|
|
|
|
// don't revisit a plugin we've already visited
|
|
|
|
// only log if there's hooks that we're skipping
|
|
|
|
if len(hooks) > 0 && utils.StrInclude(visitedPlugins, p.id) {
|
|
|
|
logger.Debugf("plugin ID '%s' already triggered, not re-triggering", p.id)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, h := range hooks {
|
|
|
|
newCtx := session.AddVisitedPlugin(ctx, p.id)
|
|
|
|
serverConnection := c.makeServerConnection(newCtx)
|
|
|
|
|
|
|
|
pluginInput := buildPluginInput(&p, &h.OperationConfig, serverConnection, nil)
|
|
|
|
addHookContext(pluginInput.Args, hookContext)
|
|
|
|
|
|
|
|
pt := pluginTask{
|
|
|
|
plugin: &p,
|
|
|
|
operation: &h.OperationConfig,
|
|
|
|
input: pluginInput,
|
|
|
|
gqlHandler: c.gqlHandler,
|
|
|
|
}
|
|
|
|
|
|
|
|
task := pt.createTask()
|
|
|
|
if err := task.Start(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// handle cancel from context
|
|
|
|
c := make(chan struct{})
|
|
|
|
go func() {
|
|
|
|
task.Wait()
|
|
|
|
close(c)
|
|
|
|
}()
|
|
|
|
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
2021-09-20 23:34:25 +00:00
|
|
|
if err := task.Stop(); err != nil {
|
|
|
|
logger.Warnf("could not stop task: %v", err)
|
|
|
|
}
|
2021-06-11 07:24:58 +00:00
|
|
|
return fmt.Errorf("operation cancelled")
|
|
|
|
case <-c:
|
|
|
|
// task finished normally
|
|
|
|
}
|
|
|
|
|
|
|
|
output := task.GetResult()
|
|
|
|
if output == nil {
|
|
|
|
logger.Debugf("%s [%s]: returned no result", hookType.String(), p.Name)
|
|
|
|
} else {
|
|
|
|
if output.Error != nil {
|
|
|
|
logger.Errorf("%s [%s]: returned error: %s", hookType.String(), p.Name, *output.Error)
|
|
|
|
} else if output.Output != nil {
|
|
|
|
logger.Debugf("%s [%s]: returned: %v", hookType.String(), p.Name, output.Output)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-08-08 02:05:35 +00:00
|
|
|
func (c Cache) getPlugin(pluginID string) *Config {
|
|
|
|
for _, s := range c.plugins {
|
|
|
|
if s.id == pluginID {
|
|
|
|
return &s
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|