Unified action command
This commit is contained in:
parent
672591bafe
commit
a5902f3c11
147
cmd/action.go
147
cmd/action.go
@ -1,6 +1,12 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Jaculabilis/intake/core"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -8,7 +14,7 @@ var actionCmd = &cobra.Command{
|
|||||||
Use: "action",
|
Use: "action",
|
||||||
GroupID: sourceGroup.ID,
|
GroupID: sourceGroup.ID,
|
||||||
Short: "Manage and run source actions",
|
Short: "Manage and run source actions",
|
||||||
Long: `Add, edit, delete, and run source actions on items.
|
Long: fmt.Sprintf(`Manage and run source actions on items.
|
||||||
|
|
||||||
A feed source is updated by the "fetch" action, which receives no input and
|
A feed source is updated by the "fetch" action, which receives no input and
|
||||||
returns one JSON item per line on stdout. Other source actions are run on a
|
returns one JSON item per line on stdout. Other source actions are run on a
|
||||||
@ -27,9 +33,146 @@ the action, you need another action with the same command as "on_create".
|
|||||||
If an item's "on_create" fails, the item is still created, but without any
|
If an item's "on_create" fails, the item is still created, but without any
|
||||||
changes from the "on_create", if any.
|
changes from the "on_create", if any.
|
||||||
|
|
||||||
To execute the "fetch" action, use "intake source fetch".`,
|
To test and execute "fetch" actions, use "intake fetch".
|
||||||
|
|
||||||
|
In a dry run, the item will be printed in the chosen format and not updated.
|
||||||
|
|
||||||
|
%s`, makeFormatHelpText()),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
action(
|
||||||
|
stringArg(cmd, "source"),
|
||||||
|
stringArg(cmd, "action"),
|
||||||
|
stringArg(cmd, "item"),
|
||||||
|
getArgv(cmd, args),
|
||||||
|
stringArg(cmd, "format"),
|
||||||
|
stringArg(cmd, "timeout"),
|
||||||
|
boolArg(cmd, "dry-run"),
|
||||||
|
boolArg(cmd, "diff"),
|
||||||
|
boolArg(cmd, "force"),
|
||||||
|
)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(actionCmd)
|
rootCmd.AddCommand(actionCmd)
|
||||||
|
|
||||||
|
actionCmd.Flags().StringP("source", "s", "", "Source to add action")
|
||||||
|
actionCmd.MarkFlagRequired("source")
|
||||||
|
actionCmd.Flags().StringP("action", "a", "", "Action name")
|
||||||
|
actionCmd.MarkFlagRequired("action")
|
||||||
|
actionCmd.PersistentFlags().StringP("item", "i", "", "Item to run action on")
|
||||||
|
|
||||||
|
actionCmd.Flags().StringP("format", "f", "headlines", "Feed format for returned items")
|
||||||
|
actionCmd.Flags().StringP("timeout", "t", core.DefaultTimeout.String(), "Timeout duration")
|
||||||
|
actionCmd.Flags().BoolP("dry-run", "n", false, "Instead of updating the item, print it")
|
||||||
|
actionCmd.Flags().Bool("diff", false, "Show which fields of the item changed")
|
||||||
|
actionCmd.Flags().Bool("force", false, "Execute the action even if the item does not support it")
|
||||||
|
}
|
||||||
|
|
||||||
|
func action(
|
||||||
|
source,
|
||||||
|
action,
|
||||||
|
itemId string,
|
||||||
|
argv []string,
|
||||||
|
format string,
|
||||||
|
timeout string,
|
||||||
|
dryRun,
|
||||||
|
diff,
|
||||||
|
force bool,
|
||||||
|
) {
|
||||||
|
if source == "" {
|
||||||
|
log.Fatal("error: --source is empty")
|
||||||
|
}
|
||||||
|
if action == "" {
|
||||||
|
log.Fatal("error: --action is empty")
|
||||||
|
}
|
||||||
|
formatter := formatAs(format)
|
||||||
|
duration, err := time.ParseDuration(timeout)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error: invalid duration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db := openAndMigrateDb()
|
||||||
|
|
||||||
|
if itemId == "" {
|
||||||
|
if len(argv) > 0 {
|
||||||
|
err := core.SetAction(db, source, action, argv)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error: failed to set action: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("Defined action %s on source %s", action, source)
|
||||||
|
} else {
|
||||||
|
err := core.DeleteAction(db, source, action)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error: failed to delete action: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("Deleted action %s from source %s", action, source)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state, envs, argv, err := core.GetSourceActionInputs(db, source, action)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error: failed to load data for %s: %v", source, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
item, err := core.GetItem(db, source, itemId)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error: failed to get item: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.Action[action] == nil {
|
||||||
|
if force {
|
||||||
|
log.Printf("warning: force-executing %s on %s/%s", action, source, itemId)
|
||||||
|
} else {
|
||||||
|
log.Fatalf("error: %s/%s does not support %s", source, itemId, action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newItem, newState, errItem, err := core.ExecuteItemAction(item, argv, envs, state, duration)
|
||||||
|
if err != nil {
|
||||||
|
core.AddErrorItem(db, errItem)
|
||||||
|
log.Fatalf("error executing %s: %v", action, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff {
|
||||||
|
if item.Title != newItem.Title {
|
||||||
|
log.Printf("title: %s => %s", item.Title, newItem.Title)
|
||||||
|
}
|
||||||
|
if item.Author != newItem.Author {
|
||||||
|
log.Printf("author: %s => %s", item.Author, newItem.Author)
|
||||||
|
}
|
||||||
|
if item.Body != newItem.Body {
|
||||||
|
log.Printf("body: %s => %s", item.Body, newItem.Body)
|
||||||
|
}
|
||||||
|
if item.Link != newItem.Link {
|
||||||
|
log.Printf("link: %s => %s", item.Link, newItem.Link)
|
||||||
|
}
|
||||||
|
if item.Time != newItem.Time {
|
||||||
|
log.Printf("time: %d => %d", item.Time, newItem.Time)
|
||||||
|
}
|
||||||
|
if core.ItemsAreEqual(item, newItem) {
|
||||||
|
log.Printf("no changes\n")
|
||||||
|
}
|
||||||
|
if !slices.Equal(state, newState) {
|
||||||
|
log.Printf("state changed (%d => %d bytes)", len(state), len(newState))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dryRun {
|
||||||
|
fmt.Println(formatter(newItem))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.Transact(func(tx core.DB) error {
|
||||||
|
if _err := core.UpdateItems(tx, []core.Item{newItem}); err != nil {
|
||||||
|
return fmt.Errorf("failed to update item: %v", _err)
|
||||||
|
}
|
||||||
|
if _err := core.SetState(tx, source, newState); err != nil {
|
||||||
|
return fmt.Errorf("failed to set state for %s: %v", source, _err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
log.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/Jaculabilis/intake/core"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var actionAddCmd = &cobra.Command{
|
|
||||||
Use: "add [flags] -- argv...",
|
|
||||||
Short: "Add an action to a source",
|
|
||||||
Long: `Add an action to a source.
|
|
||||||
`,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
actionAdd(stringArg(cmd, "source"), stringArg(cmd, "action"), getArgv(cmd, args))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
actionCmd.AddCommand(actionAddCmd)
|
|
||||||
|
|
||||||
actionAddCmd.Flags().StringP("source", "s", "", "Source to add action")
|
|
||||||
actionAddCmd.MarkFlagRequired("source")
|
|
||||||
|
|
||||||
actionAddCmd.Flags().StringP("action", "a", "", "Action name")
|
|
||||||
actionAddCmd.MarkFlagRequired("action")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This is a duplicate of `action edit`, the action CLI should be simplified
|
|
||||||
|
|
||||||
func actionAdd(source string, action string, argv []string) {
|
|
||||||
if source == "" {
|
|
||||||
log.Fatal("error: --source is empty")
|
|
||||||
}
|
|
||||||
if action == "" {
|
|
||||||
log.Fatal("error: --action is empty")
|
|
||||||
}
|
|
||||||
if len(argv) == 0 {
|
|
||||||
log.Fatal("error: no argv provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
db := openAndMigrateDb()
|
|
||||||
|
|
||||||
err := core.SetAction(db, source, action, argv)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("error: failed to add action: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Added action %s to source %s", action, source)
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/Jaculabilis/intake/core"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var actionDeleteCmd = &cobra.Command{
|
|
||||||
Use: "delete",
|
|
||||||
Aliases: []string{"rm"},
|
|
||||||
Short: "Delete an action from a source",
|
|
||||||
Long: `Delete an action from a source.
|
|
||||||
`,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
actionDelete(stringArg(cmd, "source"), stringArg(cmd, "action"))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
actionCmd.AddCommand(actionDeleteCmd)
|
|
||||||
|
|
||||||
actionDeleteCmd.Flags().StringP("source", "s", "", "Source to add action")
|
|
||||||
actionDeleteCmd.MarkFlagRequired("source")
|
|
||||||
|
|
||||||
actionDeleteCmd.Flags().StringP("action", "a", "", "Action name")
|
|
||||||
actionDeleteCmd.MarkFlagRequired("action")
|
|
||||||
}
|
|
||||||
|
|
||||||
func actionDelete(source string, action string) {
|
|
||||||
if source == "" {
|
|
||||||
log.Fatal("error: --source is empty")
|
|
||||||
}
|
|
||||||
if action == "" {
|
|
||||||
log.Fatal("error: --action is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
db := openAndMigrateDb()
|
|
||||||
|
|
||||||
err := core.DeleteAction(db, source, action)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("error: failed to delete action: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Deleted action %s from source %s", action, source)
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/Jaculabilis/intake/core"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var actionEditCmd = &cobra.Command{
|
|
||||||
Use: "edit",
|
|
||||||
Short: "Edit an action on a source",
|
|
||||||
Long: `Edit an action on a source.
|
|
||||||
`,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
actionEdit(stringArg(cmd, "source"), stringArg(cmd, "action"), getArgv(cmd, args))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
actionCmd.AddCommand(actionEditCmd)
|
|
||||||
|
|
||||||
actionEditCmd.Flags().StringP("source", "s", "", "Source to edit action")
|
|
||||||
actionEditCmd.MarkFlagRequired("source")
|
|
||||||
|
|
||||||
actionEditCmd.Flags().StringP("action", "a", "", "Action name")
|
|
||||||
actionEditCmd.MarkFlagRequired("action")
|
|
||||||
}
|
|
||||||
|
|
||||||
func actionEdit(source string, action string, argv []string) {
|
|
||||||
if source == "" {
|
|
||||||
log.Fatal("error: --source is empty")
|
|
||||||
}
|
|
||||||
if action == "" {
|
|
||||||
log.Fatal("error: --action is empty")
|
|
||||||
}
|
|
||||||
if len(argv) == 0 {
|
|
||||||
log.Fatal("error: no argv provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
db := openAndMigrateDb()
|
|
||||||
|
|
||||||
err := core.SetAction(db, source, action, argv)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("error: failed to update action: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Updated action %s on source %s", action, source)
|
|
||||||
}
|
|
@ -1,158 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"slices"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Jaculabilis/intake/core"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var actionExecuteCmd = &cobra.Command{
|
|
||||||
Use: "execute",
|
|
||||||
Aliases: []string{"exec"},
|
|
||||||
Short: "Run a source action for an item",
|
|
||||||
Long: fmt.Sprintf(`Execute a source action for an item.
|
|
||||||
|
|
||||||
The item must declare support for the action by having the action's name
|
|
||||||
in its "action" field. Use --force to execute the action anyway.
|
|
||||||
|
|
||||||
The "fetch" action is special and does not execute for any specific item.
|
|
||||||
Use "intake source fetch" to run the fetch action.
|
|
||||||
|
|
||||||
In a dry run, the item will be printed in the chosen format and not updated.
|
|
||||||
|
|
||||||
%s`, makeFormatHelpText()),
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
actionExecute(
|
|
||||||
stringArg(cmd, "source"),
|
|
||||||
stringArg(cmd, "action"),
|
|
||||||
stringArg(cmd, "item"),
|
|
||||||
stringArg(cmd, "format"),
|
|
||||||
stringArg(cmd, "timeout"),
|
|
||||||
boolArg(cmd, "dry-run"),
|
|
||||||
boolArg(cmd, "diff"),
|
|
||||||
boolArg(cmd, "force"),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
actionCmd.AddCommand(actionExecuteCmd)
|
|
||||||
|
|
||||||
actionExecuteCmd.PersistentFlags().StringP("source", "s", "", "Source of the item")
|
|
||||||
actionExecuteCmd.MarkFlagRequired("source")
|
|
||||||
|
|
||||||
actionExecuteCmd.PersistentFlags().StringP("item", "i", "", "Item to run action on")
|
|
||||||
actionExecuteCmd.MarkFlagRequired("item")
|
|
||||||
|
|
||||||
actionExecuteCmd.PersistentFlags().StringP("action", "a", "", "Action to run")
|
|
||||||
actionExecuteCmd.MarkFlagRequired("action")
|
|
||||||
|
|
||||||
actionExecuteCmd.Flags().StringP("format", "f", "headlines", "Feed format for returned items")
|
|
||||||
|
|
||||||
actionExecuteCmd.Flags().StringP("timeout", "t", core.DefaultTimeout.String(),
|
|
||||||
fmt.Sprintf("Timeout duration (default: %s)", core.DefaultTimeout.String()))
|
|
||||||
|
|
||||||
actionExecuteCmd.Flags().Bool("dry-run", false, "Instead of updating the item, print it")
|
|
||||||
|
|
||||||
actionExecuteCmd.Flags().Bool("diff", false, "Show which fields of the item changed")
|
|
||||||
|
|
||||||
actionExecuteCmd.Flags().Bool("force", false, "Execute the action even if the item does not support it")
|
|
||||||
}
|
|
||||||
|
|
||||||
func actionExecute(
|
|
||||||
source string,
|
|
||||||
action string,
|
|
||||||
itemId string,
|
|
||||||
format string,
|
|
||||||
timeout string,
|
|
||||||
dryRun bool,
|
|
||||||
diff bool,
|
|
||||||
force bool,
|
|
||||||
) {
|
|
||||||
formatter := formatAs(format)
|
|
||||||
|
|
||||||
if source == "" {
|
|
||||||
log.Fatal("error: --source is empty")
|
|
||||||
}
|
|
||||||
if action == "" {
|
|
||||||
log.Fatal("error: --action is empty")
|
|
||||||
}
|
|
||||||
if itemId == "" {
|
|
||||||
log.Fatal("error: --item is empty")
|
|
||||||
}
|
|
||||||
duration, err := time.ParseDuration(timeout)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("error: invalid duration: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
db := openAndMigrateDb()
|
|
||||||
|
|
||||||
state, envs, argv, err := core.GetSourceActionInputs(db, source, action)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("error: failed to load data for %s: %v", source, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
item, err := core.GetItem(db, source, itemId)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("error: failed to get item: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if item.Action[action] == nil {
|
|
||||||
if force {
|
|
||||||
log.Printf("warning: force-executing %s on %s/%s", action, source, itemId)
|
|
||||||
} else {
|
|
||||||
log.Fatalf("error: %s/%s does not support %s", source, itemId, action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newItem, newState, errItem, err := core.ExecuteItemAction(item, argv, envs, state, duration)
|
|
||||||
if err != nil {
|
|
||||||
core.AddErrorItem(db, errItem)
|
|
||||||
log.Fatalf("error executing %s: %v", action, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if diff {
|
|
||||||
if item.Title != newItem.Title {
|
|
||||||
log.Printf("title: %s => %s", item.Title, newItem.Title)
|
|
||||||
}
|
|
||||||
if item.Author != newItem.Author {
|
|
||||||
log.Printf("author: %s => %s", item.Author, newItem.Author)
|
|
||||||
}
|
|
||||||
if item.Body != newItem.Body {
|
|
||||||
log.Printf("body: %s => %s", item.Body, newItem.Body)
|
|
||||||
}
|
|
||||||
if item.Link != newItem.Link {
|
|
||||||
log.Printf("link: %s => %s", item.Link, newItem.Link)
|
|
||||||
}
|
|
||||||
if item.Time != newItem.Time {
|
|
||||||
log.Printf("time: %d => %d", item.Time, newItem.Time)
|
|
||||||
}
|
|
||||||
if core.ItemsAreEqual(item, newItem) {
|
|
||||||
log.Printf("no changes\n")
|
|
||||||
}
|
|
||||||
if !slices.Equal(state, newState) {
|
|
||||||
log.Printf("state changed (%d => %d bytes)", len(state), len(newState))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if dryRun {
|
|
||||||
fmt.Println(formatter(newItem))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = db.Transact(func(tx core.DB) error {
|
|
||||||
if _err := core.UpdateItems(tx, []core.Item{newItem}); err != nil {
|
|
||||||
return fmt.Errorf("failed to update item: %v", _err)
|
|
||||||
}
|
|
||||||
if _err := core.SetState(tx, source, newState); err != nil {
|
|
||||||
return fmt.Errorf("failed to set state for %s: %v", source, _err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
log.Fatalf("error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user