github.com/GGP1/kure@v0.8.4/commands/file/edit/edit.go (about) 1 package edit 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "os/exec" 8 "path/filepath" 9 "strings" 10 "time" 11 12 "github.com/GGP1/kure/auth" 13 cmdutil "github.com/GGP1/kure/commands" 14 "github.com/GGP1/kure/db/file" 15 "github.com/GGP1/kure/pb" 16 "github.com/GGP1/kure/sig" 17 18 "github.com/pkg/errors" 19 "github.com/spf13/cobra" 20 bolt "go.etcd.io/bbolt" 21 ) 22 23 const example = ` 24 * Edit a file 25 kure file edit Sample -e nvim 26 27 * Write a file's content to a temporary file and log its path 28 kure file edit Sample -l` 29 30 type editOptions struct { 31 editor string 32 log 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 file", 41 Long: `Edit a file. 42 43 Caution: a temporary file is created with a random name, it will be erased right after the first save but it could still be read by a malicious actor. 44 45 Notes: 46 - Some editors flush the changes to the disk when closed, Kure won't notice any modifications until then. 47 - Modifying the file with a different program will prevent Kure from erasing the file as its being blocked by another process.`, 48 Example: example, 49 Args: cmdutil.MustExist(db, cmdutil.File), 50 PreRunE: auth.Login(db), 51 RunE: runEdit(db, &opts), 52 PostRun: func(cmd *cobra.Command, args []string) { 53 // Reset variables (session) 54 opts = editOptions{} 55 }, 56 } 57 58 f := cmd.Flags() 59 f.StringVarP(&opts.editor, "editor", "e", "", "file editor command") 60 f.BoolVarP(&opts.log, "log", "l", false, "log the temporary file path and wait for modifications") 61 62 return cmd 63 } 64 65 func runEdit(db *bolt.DB, opts *editOptions) cmdutil.RunEFunc { 66 return func(cmd *cobra.Command, args []string) error { 67 name := strings.Join(args, " ") 68 name = cmdutil.NormalizeName(name) 69 70 var bin string 71 if !opts.log { 72 if opts.editor == "" { 73 opts.editor = cmdutil.SelectEditor() 74 } 75 var err error 76 bin, err = exec.LookPath(opts.editor) 77 if err != nil { 78 return errors.Errorf("executable %q not found", opts.editor) 79 } 80 } 81 82 oldFile, err := file.Get(db, name) 83 if err != nil { 84 return err 85 } 86 87 filename, err := createTempFile(filepath.Ext(oldFile.Name), oldFile.Content) 88 if err != nil { 89 return errors.Wrap(err, "creating temporary file") 90 } 91 sig.Signal.AddCleanup(func() error { return cmdutil.Erase(filename) }) 92 defer cmdutil.Erase(filename) 93 94 if opts.log { 95 logTempFilename(filename) 96 if err := watchFile(filename); err != nil { 97 return err 98 } 99 } else { 100 if err := runEditor(bin, filename, opts.editor); err != nil { 101 return err 102 } 103 } 104 105 if err := update(db, oldFile, filename); err != nil { 106 return err 107 } 108 109 fmt.Printf("\n%q updated\n", name) 110 return nil 111 } 112 } 113 114 // createTempFile creates a temporary file and returns its name. 115 func createTempFile(ext string, content []byte) (string, error) { 116 f, err := os.CreateTemp("", "*"+ext) 117 if err != nil { 118 return "", errors.Wrap(err, "creating file") 119 } 120 121 if _, err := f.Write(content); err != nil { 122 return "", errors.Wrap(err, "writing file") 123 } 124 125 if err := f.Close(); err != nil { 126 return "", errors.Wrap(err, "closing file") 127 } 128 129 return f.Name(), nil 130 } 131 132 func logTempFilename(filename string) { 133 fmt.Printf(`Temporary file path: %s 134 135 Caution: if any process is accessing the file at the time of modification, Kure won't be able to erase it 136 `, filepath.ToSlash(filename)) 137 } 138 139 // runEditor opens the temporary file with the editor chosen and blocks until the user saves the changes. 140 func runEditor(bin, filename, editor string) error { 141 edit := exec.Command(bin, filename) 142 edit.Stdin = os.Stdin 143 edit.Stdout = os.Stdout 144 145 if err := edit.Start(); err != nil { 146 return errors.Wrapf(err, "running %s", editor) 147 } 148 149 if err := watchFile(filename); err != nil { 150 return err 151 } 152 153 return edit.Wait() 154 } 155 156 func watchFile(filename string) error { 157 done := make(chan struct{}, 1) 158 errCh := make(chan error, 1) 159 go cmdutil.WatchFile(filename, done, errCh) 160 161 // Block until an event is received or an error occurs 162 select { 163 case <-done: 164 165 case err := <-errCh: 166 return err 167 } 168 169 return nil 170 } 171 172 // update reads the edited content and updates the file record. 173 func update(db *bolt.DB, old *pb.File, filename string) error { 174 content, err := os.ReadFile(filename) 175 if err != nil { 176 return errors.Wrap(err, "reading file") 177 } 178 content = bytes.TrimSpace(content) 179 180 new := &pb.File{ 181 Name: old.Name, 182 Content: content, 183 Size: int64(len(content)), 184 CreatedAt: old.CreatedAt, 185 UpdatedAt: time.Now().Unix(), 186 } 187 188 if err := file.Create(db, new); err != nil { 189 return errors.Wrap(err, "updating file") 190 } 191 192 return nil 193 }