github.com/GGP1/kure@v0.8.4/commands/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  	"time"
    12  
    13  	"github.com/GGP1/kure/auth"
    14  	cmdutil "github.com/GGP1/kure/commands"
    15  	"github.com/GGP1/kure/db/entry"
    16  	"github.com/GGP1/kure/pb"
    17  	"github.com/GGP1/kure/sig"
    18  	"github.com/GGP1/kure/terminal"
    19  
    20  	"github.com/pkg/errors"
    21  	"github.com/spf13/cobra"
    22  	bolt "go.etcd.io/bbolt"
    23  )
    24  
    25  const example = `
    26  * Edit using the standard input
    27  kure edit Sample
    28  
    29  * Edit using the text editor
    30  kure edit Sample -i`
    31  
    32  type editOptions struct {
    33  	interactive bool
    34  }
    35  
    36  // NewCmd returns a new command.
    37  func NewCmd(db *bolt.DB) *cobra.Command {
    38  	opts := editOptions{}
    39  	cmd := &cobra.Command{
    40  		Use:   "edit <name>",
    41  		Short: "Edit an entry",
    42  		Long: `Edit an entry. 
    43  		
    44  If the name is edited, Kure will remove the entry with the old name and create one with the new name.`,
    45  		Example: example,
    46  		Args:    cmdutil.MustExist(db, cmdutil.Entry),
    47  		PreRunE: auth.Login(db),
    48  		RunE:    runEdit(db, &opts),
    49  		PostRun: func(cmd *cobra.Command, args []string) {
    50  			// Reset variables (session)
    51  			opts = editOptions{}
    52  		},
    53  	}
    54  
    55  	cmd.Flags().BoolVarP(&opts.interactive, "it", "i", false, "use the text editor")
    56  
    57  	return cmd
    58  }
    59  
    60  func runEdit(db *bolt.DB, opts *editOptions) cmdutil.RunEFunc {
    61  	return func(cmd *cobra.Command, args []string) error {
    62  		name := strings.Join(args, " ")
    63  		name = cmdutil.NormalizeName(name)
    64  
    65  		oldEntry, err := entry.Get(db, name)
    66  		if err != nil {
    67  			return err
    68  		}
    69  
    70  		// Format the expires fields so it's easy to read
    71  		if oldEntry.Expires != "Never" {
    72  			// This never fails as the field "expires" is parsed before stored
    73  			expires, _ := time.Parse(time.RFC1123Z, oldEntry.Expires)
    74  			oldEntry.Expires = expires.Format("02/01/2006")
    75  		}
    76  
    77  		if opts.interactive {
    78  			return useTextEditor(db, oldEntry)
    79  		}
    80  
    81  		return useStdin(db, os.Stdin, oldEntry)
    82  	}
    83  }
    84  
    85  func createTempFile(e *pb.Entry) (string, error) {
    86  	f, err := os.CreateTemp("", "*.json")
    87  	if err != nil {
    88  		return "", errors.Wrap(err, "creating temporary file")
    89  	}
    90  
    91  	content, err := json.MarshalIndent(e, "", "  ")
    92  	if err != nil {
    93  		return "", errors.Wrap(err, "encoding entry")
    94  	}
    95  
    96  	if _, err := f.Write(content); err != nil {
    97  		return "", errors.Wrap(err, "writing temporary file")
    98  	}
    99  
   100  	if err := f.Close(); err != nil {
   101  		return "", errors.Wrap(err, "closing temporary file")
   102  	}
   103  
   104  	return f.Name(), nil
   105  }
   106  
   107  // readTmpFile reads the modified file and formats the card.
   108  func readTmpFile(filename string) (*pb.Entry, error) {
   109  	var e pb.Entry
   110  
   111  	f, err := os.Open(filename)
   112  	if err != nil {
   113  		return nil, errors.Wrap(err, "reading file")
   114  	}
   115  	defer f.Close()
   116  
   117  	if err := json.NewDecoder(f).Decode(&e); err != nil {
   118  		return nil, errors.Wrap(err, "decoding file")
   119  	}
   120  
   121  	return &e, nil
   122  }
   123  
   124  // updateEntry takes the name of the entry that's being edited to check if the name was
   125  // changed. If it was, it will remove the old one.
   126  func updateEntry(db *bolt.DB, name string, e *pb.Entry) error {
   127  	if e.Name == "" {
   128  		return cmdutil.ErrInvalidName
   129  	}
   130  
   131  	// Verify that the "expires" field has a valid format
   132  	expires, err := cmdutil.FmtExpires(e.Expires)
   133  	if err != nil {
   134  		return err
   135  	}
   136  
   137  	name = cmdutil.NormalizeName(name)
   138  	e.Name = cmdutil.NormalizeName(e.Name)
   139  	e.Expires = expires
   140  
   141  	if err := entry.Update(db, name, e); err != nil {
   142  		return err
   143  	}
   144  
   145  	fmt.Println(e.Name, "updated")
   146  	return nil
   147  }
   148  
   149  func useStdin(db *bolt.DB, r io.Reader, oldEntry *pb.Entry) error {
   150  	fmt.Println("Type '-' to clear the field (except Name and Password) or leave blank to use the current value")
   151  	reader := bufio.NewReader(r)
   152  
   153  	scanln := func(field, value string) string {
   154  		input := terminal.Scanln(reader, fmt.Sprintf("%s [%s]", field, value))
   155  		if input == "-" {
   156  			return ""
   157  		} else if input != "" {
   158  			return input
   159  		}
   160  		return value
   161  	}
   162  
   163  	newEntry := &pb.Entry{}
   164  	newEntry.Name = scanln("Name", oldEntry.Name)
   165  	newEntry.Username = scanln("Username", oldEntry.Username)
   166  
   167  	enclave, err := terminal.ScanPassword("Password", true)
   168  	if err != nil {
   169  		if err == terminal.ErrInvalidPassword {
   170  			// Assume the user typed an empty string to not modify the password
   171  			newEntry.Password = oldEntry.Password
   172  		} else {
   173  			return err
   174  		}
   175  	} else {
   176  		pwd, err := enclave.Open()
   177  		if err != nil {
   178  			return errors.Wrap(err, "opening enclave")
   179  		}
   180  
   181  		newEntry.Password = pwd.String()
   182  	}
   183  
   184  	newEntry.URL = scanln("URL", oldEntry.URL)
   185  	newEntry.Expires = scanln("Expires", oldEntry.Expires)
   186  
   187  	notes := terminal.Scanlns(reader, fmt.Sprintf("Notes [%s]", oldEntry.Notes))
   188  	if notes == "" {
   189  		notes = oldEntry.Notes
   190  	} else if notes == "-" {
   191  		notes = ""
   192  	}
   193  	newEntry.Notes = notes
   194  
   195  	return updateEntry(db, oldEntry.Name, newEntry)
   196  }
   197  
   198  func useTextEditor(db *bolt.DB, oldEntry *pb.Entry) error {
   199  	editor := cmdutil.SelectEditor()
   200  	bin, err := exec.LookPath(editor)
   201  	if err != nil {
   202  		return errors.Errorf("executable %q not found", editor)
   203  	}
   204  
   205  	filename, err := createTempFile(oldEntry)
   206  	if err != nil {
   207  		return err
   208  	}
   209  
   210  	sig.Signal.AddCleanup(func() error { return cmdutil.Erase(filename) })
   211  	defer cmdutil.Erase(filename)
   212  
   213  	// Open the temporary file with the selected text editor
   214  	edit := exec.Command(bin, filename)
   215  	edit.Stdin = os.Stdin
   216  	edit.Stdout = os.Stdout
   217  
   218  	if err := edit.Start(); err != nil {
   219  		return errors.Wrapf(err, "running %s", editor)
   220  	}
   221  
   222  	done := make(chan struct{}, 1)
   223  	errCh := make(chan error, 1)
   224  	go cmdutil.WatchFile(filename, done, errCh)
   225  
   226  	// Block until an event is received or an error occurs
   227  	select {
   228  	case <-done:
   229  	case err := <-errCh:
   230  		return err
   231  	}
   232  
   233  	if err := edit.Wait(); err != nil {
   234  		return err
   235  	}
   236  
   237  	// Read the file and update the entry
   238  	newEntry, err := readTmpFile(filename)
   239  	if err != nil {
   240  		return err
   241  	}
   242  
   243  	rmTabs := func(old string) string {
   244  		return strings.ReplaceAll(old, "\t", "")
   245  	}
   246  	newEntry.Name = rmTabs(newEntry.Name)
   247  	newEntry.Username = rmTabs(newEntry.Username)
   248  	newEntry.URL = rmTabs(newEntry.URL)
   249  	newEntry.Notes = rmTabs(newEntry.Notes)
   250  
   251  	return updateEntry(db, oldEntry.Name, newEntry)
   252  }