mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
* Avoid redundant logging in migrations
Return the error and let the caller handle the logging of the error if
needed.
While here, defer m.Close() to the function boundary.
* Treat errors as values
Use %v rather than %s and pass the errors directly.
* Generate a wrapped error on stat-failure
* Log 3 unchecked errors
Rather than ignore errors, log them at
the WARNING log level.
The server has been functioning without these, so assume they are not at
the ERROR level.
* Propagate errors upward
Failure in path generation was ignored. Propagate the errors upward the
call stack, so it can be handled at the level of orchestration.
* Warn on errors
Log errors rather than quenching them.
Errors are logged at the Warn-level for now.
* Check error when creating test databases
Use the builtin log package and stop the program fatally on error.
* Add warnings to uncheck task errors
Focus on the task system in a single commit, logging unchecked
errors as warnings.
* Warn-on-error in API routes
Look through the API routes, and make sure errors are being logged if
they occur. Prefer the Warn-log-level because none of these has proven
to be fatal in the system up until now.
* Propagate error when adding Util API
* Propagate error on adding util API
* Return unhandled error
* JS log API: propagate and log errors
* JS Plugins: log GQL addition failures.
* Warn on failure to write to stdin
* Warn on failure to stop task
* Wrap viper.BindEnv
The current viper code only errors if no name is provided, so it should
never fail. Rewrite the code flow to factor through a panic-function.
This removes error warnings from this part of the code.
* Log errors in concurrency test
If we can't initialize the configuration, treat the test as a failure.
* Warn on errors in configuration code
* Plug an unchecked error in gallery zip walking
* Warn on screenshot serving failure
* Warn on encoder screenshot failure
* Warn on errors in path-handling code
* Undo the errcheck on configurations for now.
* Use one-line initializers where applicable
rather than using
err := f()
if err!= nil { ..
prefer the shorter
if err := f(); err != nil { ..
If f() isn't too long of a name, or wraps a function with a body.
254 lines
6.6 KiB
Go
254 lines
6.6 KiB
Go
// 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 (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/stashapp/stash/pkg/logger"
|
|
"github.com/stashapp/stash/pkg/manager/config"
|
|
"github.com/stashapp/stash/pkg/models"
|
|
"github.com/stashapp/stash/pkg/plugin/common"
|
|
"github.com/stashapp/stash/pkg/session"
|
|
"github.com/stashapp/stash/pkg/utils"
|
|
)
|
|
|
|
// Cache stores plugin details.
|
|
type Cache struct {
|
|
config *config.Instance
|
|
plugins []Config
|
|
sessionStore *session.Store
|
|
gqlHandler http.Handler
|
|
}
|
|
|
|
// NewCache returns a new Cache.
|
|
//
|
|
// 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 {
|
|
return &Cache{
|
|
config: config,
|
|
}
|
|
}
|
|
|
|
func (c *Cache) RegisterGQLHandler(handler http.Handler) {
|
|
c.gqlHandler = handler
|
|
}
|
|
|
|
func (c *Cache) RegisterSessionStore(sessionStore *session.Store) {
|
|
c.sessionStore = sessionStore
|
|
}
|
|
|
|
// LoadPlugins clears the plugin cache and loads from the plugin path.
|
|
// In the event of an error during loading, the cache will be left empty.
|
|
func (c *Cache) LoadPlugins() error {
|
|
c.plugins = nil
|
|
plugins, err := loadPlugins(c.config.GetPluginsPath())
|
|
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
|
|
}
|
|
|
|
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",
|
|
Host: c.config.GetHost(),
|
|
Port: c.config.GetPort(),
|
|
SessionCookie: cookie,
|
|
Dir: c.config.GetConfigPath(),
|
|
}
|
|
|
|
if c.config.HasTLSConfig() {
|
|
serverConnection.Scheme = "https"
|
|
}
|
|
|
|
return serverConnection
|
|
}
|
|
|
|
// 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.
|
|
func (c Cache) CreateTask(ctx context.Context, pluginID string, operationName string, args []*models.PluginArgInput, progress chan float64) (Task, error) {
|
|
serverConnection := c.makeServerConnection(ctx)
|
|
|
|
// 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{
|
|
plugin: plugin,
|
|
operation: operation,
|
|
input: buildPluginInput(plugin, operation, serverConnection, args),
|
|
progress: progress,
|
|
gqlHandler: c.gqlHandler,
|
|
}
|
|
return task.createTask(), nil
|
|
}
|
|
|
|
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())
|
|
}
|
|
}
|
|
|
|
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():
|
|
if err := task.Stop(); err != nil {
|
|
logger.Warnf("could not stop task: %v", err)
|
|
}
|
|
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
|
|
}
|
|
|
|
func (c Cache) getPlugin(pluginID string) *Config {
|
|
for _, s := range c.plugins {
|
|
if s.id == pluginID {
|
|
return &s
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|