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 }