github.com/GGP1/kure@v0.8.4/commands/card/edit/edit.go (about)

     1  package edit
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"os/exec"
    10  	"strings"
    11  
    12  	"github.com/GGP1/kure/auth"
    13  	cmdutil "github.com/GGP1/kure/commands"
    14  	"github.com/GGP1/kure/db/card"
    15  	"github.com/GGP1/kure/pb"
    16  	"github.com/GGP1/kure/sig"
    17  	"github.com/GGP1/kure/terminal"
    18  
    19  	"github.com/pkg/errors"
    20  	"github.com/spf13/cobra"
    21  	bolt "go.etcd.io/bbolt"
    22  )
    23  
    24  const example = `
    25  * Edit using the standard input
    26  kure card edit Sample
    27  
    28  * Edit using the text editor
    29  kure card edit Sample -i`
    30  
    31  type editOptions struct {
    32  	interactive bool
    33  }
    34  
    35  // NewCmd returns a new command.
    36  func NewCmd(db *bolt.DB) *cobra.Command {
    37  	opts := editOptions{}
    38  	cmd := &cobra.Command{
    39  		Use:   "edit <name>",
    40  		Short: "Edit a card",
    41  		Long: `Edit a card.
    42  
    43  If the name is edited, Kure will remove the old card and create one with the new name.`,
    44  		Example: example,
    45  		Args:    cmdutil.MustExist(db, cmdutil.Card),
    46  		PreRunE: auth.Login(db),
    47  		RunE:    runEdit(db, &opts),
    48  		PostRun: func(cmd *cobra.Command, args []string) {
    49  			// Reset variables (session)
    50  			opts = editOptions{}
    51  		},
    52  	}
    53  
    54  	cmd.Flags().BoolVarP(&opts.interactive, "it", "i", false, "use the text editor")
    55  
    56  	return cmd
    57  }
    58  
    59  func runEdit(db *bolt.DB, opts *editOptions) cmdutil.RunEFunc {
    60  	return func(cmd *cobra.Command, args []string) error {
    61  		name := strings.Join(args, " ")
    62  		name = cmdutil.NormalizeName(name)
    63  
    64  		oldCard, err := card.Get(db, name)
    65  		if err != nil {
    66  			return err
    67  		}
    68  
    69  		if opts.interactive {
    70  			return useTextEditor(db, oldCard)
    71  		}
    72  
    73  		return useStdin(db, os.Stdin, oldCard)
    74  	}
    75  }
    76  
    77  func createTempFile(c *pb.Card) (string, error) {
    78  	f, err := os.CreateTemp("", "*.json")
    79  	if err != nil {
    80  		return "", errors.Wrap(err, "creating temporary file")
    81  	}
    82  
    83  	content, err := json.MarshalIndent(c, "", "  ")
    84  	if err != nil {
    85  		return "", errors.Wrap(err, "encoding card")
    86  	}
    87  
    88  	if _, err := f.Write(content); err != nil {
    89  		return "", errors.Wrap(err, "writing temporary file")
    90  	}
    91  
    92  	if err := f.Close(); err != nil {
    93  		return "", errors.Wrap(err, "closing temporary file")
    94  	}
    95  
    96  	return f.Name(), nil
    97  }
    98  
    99  // readTmpFile reads the modified file and formats the card.
   100  func readTmpFile(filename string) (*pb.Card, error) {
   101  	var c pb.Card
   102  
   103  	f, err := os.Open(filename)
   104  	if err != nil {
   105  		return nil, errors.Wrap(err, "reading file")
   106  	}
   107  	defer f.Close()
   108  
   109  	if err := json.NewDecoder(f).Decode(&c); err != nil {
   110  		return nil, errors.Wrap(err, "decoding file")
   111  	}
   112  
   113  	return &c, nil
   114  }
   115  
   116  // updateCard takes the name of the card that's being edited to check if the name was
   117  // changed. If it was, it will remove the old one.
   118  func updateCard(db *bolt.DB, name string, c *pb.Card) error {
   119  	if c.Name == "" {
   120  		return cmdutil.ErrInvalidName
   121  	}
   122  
   123  	name = cmdutil.NormalizeName(name)
   124  	c.Name = cmdutil.NormalizeName(c.Name)
   125  
   126  	if err := card.Update(db, name, c); err != nil {
   127  		return err
   128  	}
   129  
   130  	fmt.Println(c.Name, "updated")
   131  	return nil
   132  }
   133  
   134  func useStdin(db *bolt.DB, r io.Reader, oldCard *pb.Card) error {
   135  	fmt.Println("Type '-' to clear the field or leave blank to use the current value")
   136  	reader := bufio.NewReader(r)
   137  
   138  	scanln := func(field, value string) string {
   139  		input := terminal.Scanln(reader, fmt.Sprintf("%s [%s]", field, value))
   140  		if input == "-" {
   141  			return ""
   142  		} else if input != "" {
   143  			return input
   144  		}
   145  		return value
   146  	}
   147  
   148  	newCard := &pb.Card{
   149  		Name:         scanln("Name", oldCard.Name),
   150  		Type:         scanln("Type", oldCard.Type),
   151  		Number:       scanln("Number", oldCard.Number),
   152  		SecurityCode: scanln("Security code", oldCard.SecurityCode),
   153  		ExpireDate:   scanln("Expire date", oldCard.ExpireDate),
   154  	}
   155  
   156  	notes := terminal.Scanlns(reader, fmt.Sprintf("Notes [%s]", oldCard.Notes))
   157  	if notes == "" {
   158  		notes = oldCard.Notes
   159  	} else if notes == "-" {
   160  		notes = ""
   161  	}
   162  	newCard.Notes = notes
   163  
   164  	return updateCard(db, oldCard.Name, newCard)
   165  }
   166  
   167  func useTextEditor(db *bolt.DB, oldCard *pb.Card) error {
   168  	editor := cmdutil.SelectEditor()
   169  	bin, err := exec.LookPath(editor)
   170  	if err != nil {
   171  		return errors.Errorf("executable %q not found", editor)
   172  	}
   173  
   174  	filename, err := createTempFile(oldCard)
   175  	if err != nil {
   176  		return err
   177  	}
   178  
   179  	sig.Signal.AddCleanup(func() error { return cmdutil.Erase(filename) })
   180  	defer cmdutil.Erase(filename)
   181  
   182  	// Open the temporary file with the selected text editor
   183  	edit := exec.Command(bin, filename)
   184  	edit.Stdin = os.Stdin
   185  	edit.Stdout = os.Stdout
   186  
   187  	if err := edit.Start(); err != nil {
   188  		return errors.Wrapf(err, "running %s", editor)
   189  	}
   190  
   191  	done := make(chan struct{}, 1)
   192  	errCh := make(chan error, 1)
   193  	go cmdutil.WatchFile(filename, done, errCh)
   194  
   195  	// Block until an event is received or an error occurs
   196  	select {
   197  	case <-done:
   198  	case err := <-errCh:
   199  		return err
   200  	}
   201  
   202  	if err := edit.Wait(); err != nil {
   203  		return err
   204  	}
   205  
   206  	newCard, err := readTmpFile(filename)
   207  	if err != nil {
   208  		return err
   209  	}
   210  
   211  	rmTabs := func(old string) string {
   212  		return strings.ReplaceAll(old, "\t", "")
   213  	}
   214  	newCard.Name = rmTabs(newCard.Name)
   215  	newCard.Type = rmTabs(newCard.Type)
   216  	newCard.Number = rmTabs(newCard.Number)
   217  	newCard.SecurityCode = rmTabs(newCard.SecurityCode)
   218  	newCard.ExpireDate = rmTabs(newCard.ExpireDate)
   219  	newCard.Notes = rmTabs(newCard.Notes)
   220  
   221  	return updateCard(db, oldCard.Name, newCard)
   222  }