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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/Jaculabilis/intake/core"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -8,7 +14,7 @@ var actionCmd = &cobra.Command{
|
||||
Use: "action",
|
||||
GroupID: sourceGroup.ID,
|
||||
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
|
||||
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
|
||||
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() {
|
||||
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