diff --git a/go.mod b/go.mod index b46a1da..d580f52 100644 --- a/go.mod +++ b/go.mod @@ -52,6 +52,7 @@ require ( github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-shellwords v1.0.12 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/nxadm/tail v1.4.6 // indirect github.com/onsi/ginkgo v1.14.2 // indirect diff --git a/go.sum b/go.sum index cb86a17..b40989d 100644 --- a/go.sum +++ b/go.sum @@ -505,6 +505,8 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.7.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.13.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= diff --git a/internal/action/exec.go b/internal/action/exec.go index d407833..93f648b 100644 --- a/internal/action/exec.go +++ b/internal/action/exec.go @@ -2,9 +2,10 @@ package action import ( "os/exec" - "strings" "time" + "github.com/mattn/go-shellwords" + "github.com/autobrr/autobrr/internal/domain" ) @@ -18,18 +19,13 @@ func (s *service) execCmd(release domain.Release, action domain.Action) { return } - // handle args and replace vars - m := NewMacro(release) - - // parse and replace values in argument string before continuing - parsedArgs, err := m.Parse(action.ExecArgs) + args, err := s.parseExecArgs(release, action.ExecArgs) if err != nil { - s.log.Error().Stack().Err(err).Msgf("exec failed, could not parse arguments: %v", action.ExecCmd) + s.log.Error().Stack().Err(err).Msgf("parsing args failed: command: %v args: %v torrent: %v", cmd, action.ExecArgs, release.TorrentTmpFile) return } // we need to split on space into a string slice, so we can spread the args into exec - args := strings.Split(parsedArgs, " ") start := time.Now() @@ -40,12 +36,33 @@ func (s *service) execCmd(release domain.Release, action domain.Action) { output, err := command.CombinedOutput() if err != nil { // everything other than exit 0 is considered an error - s.log.Error().Stack().Err(err).Msgf("command: %v args: %v failed, torrent: %v", cmd, parsedArgs, release.TorrentTmpFile) + s.log.Error().Stack().Err(err).Msgf("command: %v args: %v failed, torrent: %v", cmd, args, release.TorrentTmpFile) + return } s.log.Trace().Msgf("executed command: '%v'", string(output)) duration := time.Since(start) - s.log.Info().Msgf("executed command: '%v', args: '%v' %v,%v, total time %v", cmd, parsedArgs, release.TorrentName, release.Indexer, duration) + s.log.Info().Msgf("executed command: '%v', args: '%v' %v,%v, total time %v", cmd, args, release.TorrentName, release.Indexer, duration) +} + +func (s *service) parseExecArgs(release domain.Release, execArgs string) ([]string, error) { + // handle args and replace vars + m := NewMacro(release) + + // parse and replace values in argument string before continuing + parsedArgs, err := m.Parse(execArgs) + if err != nil { + return nil, err + } + + p := shellwords.NewParser() + p.ParseBacktick = true + args, err := p.Parse(parsedArgs) + if err != nil { + return nil, err + } + + return args, nil } diff --git a/internal/action/exec_test.go b/internal/action/exec_test.go new file mode 100644 index 0000000..98eb11c --- /dev/null +++ b/internal/action/exec_test.go @@ -0,0 +1,113 @@ +package action + +import ( + "testing" + + "github.com/autobrr/autobrr/internal/domain" + "github.com/autobrr/autobrr/internal/logger" + "github.com/stretchr/testify/assert" +) + +func Test_service_parseExecArgs(t *testing.T) { + type args struct { + release domain.Release + execArgs string + } + tests := []struct { + name string + args args + want []string + wantErr bool + }{ + { + name: "test_1", + args: args{ + release: domain.Release{TorrentName: "Sally Goes to the Mall S04E29"}, + execArgs: `echo "{{ .TorrentName }}"`, + }, + want: []string{ + "echo", + "Sally Goes to the Mall S04E29", + }, + wantErr: false, + }, + { + name: "test_2", + args: args{ + release: domain.Release{TorrentName: "Sally Goes to the Mall S04E29"}, + execArgs: `"{{ .TorrentName }}"`, + }, + want: []string{ + "Sally Goes to the Mall S04E29", + }, + wantErr: false, + }, + { + name: "test_3", + args: args{ + release: domain.Release{TorrentName: "Sally Goes to the Mall S04E29"}, + execArgs: `--header "Content-Type: application/json" --request POST --data '{"release":"{{ .TorrentName }}"}' http://localhost:3000/api/release`, + }, + want: []string{ + "--header", + "Content-Type: application/json", + "--request", + "POST", + "--data", + `{"release":"Sally Goes to the Mall S04E29"}`, + "http://localhost:3000/api/release", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &service{ + log: logger.Mock(), + repo: nil, + clientSvc: nil, + bus: nil, + } + got, _ := s.parseExecArgs(tt.args.release, tt.args.execArgs) + assert.Equalf(t, tt.want, got, "parseExecArgs(%v, %v)", tt.args.release, tt.args.execArgs) + }) + } +} + +func Test_service_execCmd(t *testing.T) { + type args struct { + release domain.Release + action domain.Action + } + tests := []struct { + name string + args args + }{ + { + name: "test_1", + args: args{ + release: domain.Release{ + TorrentName: "This is a test", + TorrentTmpFile: "tmp-10000", + Indexer: "mock", + }, + action: domain.Action{ + Name: "echo", + ExecCmd: "echo", + ExecArgs: "hello", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &service{ + log: logger.Mock(), + repo: nil, + clientSvc: nil, + bus: nil, + } + s.execCmd(tt.args.release, tt.args.action) + }) + } +} diff --git a/internal/logger/mock.go b/internal/logger/mock.go new file mode 100644 index 0000000..1f8ad6d --- /dev/null +++ b/internal/logger/mock.go @@ -0,0 +1,18 @@ +package logger + +import ( + "github.com/rs/zerolog" + "io" +) + +func Mock() Logger { + l := &DefaultLogger{ + writers: make([]io.Writer, 0), + level: zerolog.Disabled, + } + + // init new logger + l.log = zerolog.New(io.MultiWriter(l.writers...)).With().Stack().Logger() + + return l +}