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  }