diff --git a/README.md b/README.md index 436fc5b..f6546b7 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,14 @@ curl localhost:8787/anydb/v1/foo # list keys curl localhost:8787/anydb/v1/ +# as you might correctly suspect you can store multi-line values or +# the content of text files. but what to do if you want to change it? +# here's one way: +anydb get contract24 > file.txt && vi file.txt && anydb set contract24 -r file.txt + +# annoying. better do this +anydb edit contract24 + # sometimes you need to know some details about the current database # add -d for more details anydb info diff --git a/TODO.md b/TODO.md index 82df59c..4e27081 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,2 @@ - repl - mime-type => exec app + value -- [edit command](https://github.com/TLINDEN/rpnc/blob/master/command.go#L249) diff --git a/app/db.go b/app/db.go index 634ad6f..3bb60b9 100644 --- a/app/db.go +++ b/app/db.go @@ -76,6 +76,13 @@ func (entry *DbEntry) Normalize() { entry.Size = len(entry.Bin) } + if strings.Contains(entry.Value, "\n") { + parts := strings.Split(entry.Value, "\n") + if len(parts) > 0 { + entry.Value = parts[0] + } + } + if len(entry.Value) > MaxValueWidth { entry.Value = entry.Value[0:MaxValueWidth] + "..." } diff --git a/cfg/config.go b/cfg/config.go index b22f140..756bee1 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -26,7 +26,7 @@ import ( "github.com/tlinden/anydb/common" ) -var Version string = "v0.0.5" +var Version string = "v0.0.6" type BucketConfig struct { Encrypt bool diff --git a/cmd/crud.go b/cmd/crud.go index e3074b4..8b389bd 100644 --- a/cmd/crud.go +++ b/cmd/crud.go @@ -17,11 +17,8 @@ along with this program. If not, see . package cmd import ( - "bytes" "errors" - "fmt" "os" - "os/exec" "strings" "unicode/utf8" @@ -29,7 +26,6 @@ import ( "github.com/tlinden/anydb/app" "github.com/tlinden/anydb/cfg" "github.com/tlinden/anydb/output" - "github.com/tlinden/anydb/rest" ) func Set(conf *cfg.Config) *cobra.Command { @@ -185,38 +181,6 @@ func Del(conf *cfg.Config) *cobra.Command { return cmd } -func Export(conf *cfg.Config) *cobra.Command { - var ( - attr app.DbAttr - ) - - var cmd = &cobra.Command{ - Use: "export [-o ]", - Short: "Export database to json", - Long: `Export database to json`, - RunE: func(cmd *cobra.Command, args []string) error { - // errors at this stage do not cause the usage to be shown - cmd.SilenceUsage = true - - conf.Mode = "json" - - entries, err := conf.DB.List(&attr) - if err != nil { - return err - } - - return output.WriteJSON(&attr, conf, entries) - }, - } - - cmd.PersistentFlags().StringVarP(&attr.File, "output", "o", "", "output to file") - - cmd.Aliases = append(cmd.Aliases, "dump") - cmd.Aliases = append(cmd.Aliases, "backup") - - return cmd -} - func List(conf *cfg.Config) *cobra.Command { var ( attr app.DbAttr @@ -266,116 +230,6 @@ func List(conf *cfg.Config) *cobra.Command { return cmd } -func Import(conf *cfg.Config) *cobra.Command { - var ( - attr app.DbAttr - ) - - var cmd = &cobra.Command{ - Use: "import []", - Short: "Import database dump", - Long: `Import database dump`, - RunE: func(cmd *cobra.Command, args []string) error { - // errors at this stage do not cause the usage to be shown - cmd.SilenceUsage = true - - out, err := conf.DB.Import(&attr) - if err != nil { - return err - } - - fmt.Print(out) - return nil - }, - } - - cmd.PersistentFlags().StringVarP(&attr.File, "file", "r", "", "Filename or - for STDIN") - cmd.PersistentFlags().StringArrayVarP(&attr.Tags, "tags", "t", nil, "tags, multiple allowed") - - cmd.Aliases = append(cmd.Aliases, "add") - cmd.Aliases = append(cmd.Aliases, "s") - cmd.Aliases = append(cmd.Aliases, "+") - - return cmd -} - -func Help(conf *cfg.Config) *cobra.Command { - return nil -} - -func Man(conf *cfg.Config) *cobra.Command { - var cmd = &cobra.Command{ - Use: "man", - Short: "show manual page", - Long: `show manual page`, - RunE: func(cmd *cobra.Command, args []string) error { - // errors at this stage do not cause the usage to be shown - cmd.SilenceUsage = true - - man := exec.Command("less", "-") - - var b bytes.Buffer - - b.WriteString(manpage) - - man.Stdout = os.Stdout - man.Stdin = &b - man.Stderr = os.Stderr - - err := man.Run() - - if err != nil { - return fmt.Errorf("failed to execute 'less': %w", err) - } - - return nil - }, - } - - return cmd -} - -func Serve(conf *cfg.Config) *cobra.Command { - var cmd = &cobra.Command{ - Use: "serve [-l host:port]", - Short: "run REST API listener", - Long: `run REST API listener`, - RunE: func(cmd *cobra.Command, args []string) error { - // errors at this stage do not cause the usage to be shown - cmd.SilenceUsage = true - - return rest.Runserver(conf, nil) - }, - } - - cmd.PersistentFlags().StringVarP(&conf.Listen, "listen", "l", "localhost:8787", "host:port") - - return cmd -} - -func Info(conf *cfg.Config) *cobra.Command { - var cmd = &cobra.Command{ - Use: "info", - Short: "info", - Long: `show info about database`, - RunE: func(cmd *cobra.Command, args []string) error { - // errors at this stage do not cause the usage to be shown - cmd.SilenceUsage = true - - info, err := conf.DB.Info() - if err != nil { - return err - } - - return output.Info(os.Stdout, conf, info) - }, - } - - cmd.PersistentFlags().BoolVarP(&conf.NoHumanize, "no-human", "N", false, "do not translate to human readable values") - - return cmd -} - func getPassword() ([]byte, error) { var pass []byte diff --git a/cmd/extra.go b/cmd/extra.go new file mode 100644 index 0000000..8c889e0 --- /dev/null +++ b/cmd/extra.go @@ -0,0 +1,326 @@ +/* +Copyright © 2024 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +package cmd + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "os/exec" + "unicode/utf8" + + "github.com/spf13/cobra" + "github.com/tlinden/anydb/app" + "github.com/tlinden/anydb/cfg" + "github.com/tlinden/anydb/output" + "github.com/tlinden/anydb/rest" +) + +func Export(conf *cfg.Config) *cobra.Command { + var ( + attr app.DbAttr + ) + + var cmd = &cobra.Command{ + Use: "export [-o ]", + Short: "Export database to json", + Long: `Export database to json`, + RunE: func(cmd *cobra.Command, args []string) error { + // errors at this stage do not cause the usage to be shown + cmd.SilenceUsage = true + + conf.Mode = "json" + + entries, err := conf.DB.List(&attr) + if err != nil { + return err + } + + return output.WriteJSON(&attr, conf, entries) + }, + } + + cmd.PersistentFlags().StringVarP(&attr.File, "output", "o", "", "output to file") + + cmd.Aliases = append(cmd.Aliases, "dump") + cmd.Aliases = append(cmd.Aliases, "backup") + + return cmd +} + +func Import(conf *cfg.Config) *cobra.Command { + var ( + attr app.DbAttr + ) + + var cmd = &cobra.Command{ + Use: "import []", + Short: "Import database dump", + Long: `Import database dump`, + RunE: func(cmd *cobra.Command, args []string) error { + // errors at this stage do not cause the usage to be shown + cmd.SilenceUsage = true + + out, err := conf.DB.Import(&attr) + if err != nil { + return err + } + + fmt.Print(out) + return nil + }, + } + + cmd.PersistentFlags().StringVarP(&attr.File, "file", "r", "", "Filename or - for STDIN") + cmd.PersistentFlags().StringArrayVarP(&attr.Tags, "tags", "t", nil, "tags, multiple allowed") + + cmd.Aliases = append(cmd.Aliases, "restore") + + return cmd +} + +func Help(conf *cfg.Config) *cobra.Command { + return nil +} +func Man(conf *cfg.Config) *cobra.Command { + var cmd = &cobra.Command{ + Use: "man", + Short: "show manual page", + Long: `show manual page`, + RunE: func(cmd *cobra.Command, args []string) error { + // errors at this stage do not cause the usage to be shown + cmd.SilenceUsage = true + + man := exec.Command("less", "-") + + var b bytes.Buffer + + b.WriteString(manpage) + + man.Stdout = os.Stdout + man.Stdin = &b + man.Stderr = os.Stderr + + err := man.Run() + + if err != nil { + return fmt.Errorf("failed to execute 'less': %w", err) + } + + return nil + }, + } + + return cmd +} + +func Serve(conf *cfg.Config) *cobra.Command { + var cmd = &cobra.Command{ + Use: "serve [-l host:port]", + Short: "run REST API listener", + Long: `run REST API listener`, + RunE: func(cmd *cobra.Command, args []string) error { + // errors at this stage do not cause the usage to be shown + cmd.SilenceUsage = true + + return rest.Runserver(conf, nil) + }, + } + + cmd.PersistentFlags().StringVarP(&conf.Listen, "listen", "l", "localhost:8787", "host:port") + + return cmd +} + +func Info(conf *cfg.Config) *cobra.Command { + var cmd = &cobra.Command{ + Use: "info", + Short: "info", + Long: `show info about database`, + RunE: func(cmd *cobra.Command, args []string) error { + // errors at this stage do not cause the usage to be shown + cmd.SilenceUsage = true + + info, err := conf.DB.Info() + if err != nil { + return err + } + + return output.Info(os.Stdout, conf, info) + }, + } + + cmd.PersistentFlags().BoolVarP(&conf.NoHumanize, "no-human", "N", false, "do not translate to human readable values") + + return cmd +} + +func Edit(conf *cfg.Config) *cobra.Command { + var ( + attr app.DbAttr + ) + + var cmd = &cobra.Command{ + Use: "edit ", + Short: "Edit a key", + Long: `Edit a key`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("no key specified") + } + + // errors at this stage do not cause the usage to be shown + cmd.SilenceUsage = true + password := []byte{} + + if len(args) > 0 { + attr.Key = args[0] + } + + // fetch entry + entry, err := conf.DB.Get(&attr) + if err != nil { + return err + } + + if len(entry.Value) == 0 && len(entry.Bin) > 0 { + return errors.New("key contains binary uneditable content") + } + + // decrypt if needed + if entry.Encrypted { + pass, err := getPassword() + if err != nil { + return err + } + password = pass + + clear, err := app.Decrypt(pass, entry.Value) + if err != nil { + return err + } + + if utf8.ValidString(string(clear)) { + entry.Value = string(clear) + } else { + entry.Bin = clear + } + + entry.Encrypted = false + } + + // determine editor, vi is default + editor := getEditor() + + // save file to a temp file, call the editor with it, read + // it back in and compare the content with the original + // one + newcontent, err := editContent(editor, entry.Value) + if err != nil { + return err + } + + // all is valid, fill our DB feeder + newattr := app.DbAttr{ + Key: attr.Key, + Tags: attr.Tags, + Encrypted: attr.Encrypted, + Val: newcontent, + } + + // encrypt if needed + if conf.Encrypt { + err = app.Encrypt(password, &attr) + if err != nil { + return err + } + } + + // done + return conf.DB.Set(&newattr) + }, + } + + cmd.Aliases = append(cmd.Aliases, "modify") + cmd.Aliases = append(cmd.Aliases, "mod") + cmd.Aliases = append(cmd.Aliases, "ed") + cmd.Aliases = append(cmd.Aliases, "vi") + + return cmd +} + +func getEditor() string { + editor := "vi" + + enveditor, present := os.LookupEnv("EDITOR") + if present { + if editor != "" { + editor = enveditor + } + } + + return editor +} + +// taken from github.com/tlinden/rpn/ (my own program) +func editContent(editor string, content string) (string, error) { + // create a temp file + tmp, err := os.CreateTemp("", "stack") + if err != nil { + return "", fmt.Errorf("failed to create templ file: %w", err) + } + defer os.Remove(tmp.Name()) + + // put the content into a tmp file + _, err = tmp.WriteString(content) + if err != nil { + return "", fmt.Errorf("failed to write value to temp file: %w", err) + } + + // execute editor with our tmp file containing current stack + cmd := exec.Command(editor, tmp.Name()) + + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err = cmd.Run() + if err != nil { + return "", fmt.Errorf("failed to run editor command %s: %w", editor, err) + } + + // read the file back in + modified, err := os.Open(tmp.Name()) + if err != nil { + return "", fmt.Errorf("failed to open temp file: %w", err) + } + defer modified.Close() + + newcontent, err := io.ReadAll(modified) + if err != nil { + return "", fmt.Errorf("failed to read from temp file: %w", err) + } + + newcontentstr := string(newcontent) + if content == newcontentstr { + return "", fmt.Errorf("content not modified, aborting") + } + + return newcontentstr, nil +} diff --git a/cmd/root.go b/cmd/root.go index f476eae..c6d1f57 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -119,15 +119,23 @@ func Execute() { app.BucketData, "use other bucket (default: "+app.BucketData+")") rootCmd.PersistentFlags().StringVarP(&configfile, "config", "c", "", "toml config file") + // CRUD rootCmd.AddCommand(Set(&conf)) rootCmd.AddCommand(List(&conf)) rootCmd.AddCommand(Get(&conf)) rootCmd.AddCommand(Del(&conf)) + + // backup rootCmd.AddCommand(Export(&conf)) rootCmd.AddCommand(Import(&conf)) + + // REST API rootCmd.AddCommand(Serve(&conf)) + + // auxiliary rootCmd.AddCommand(Man(&conf)) rootCmd.AddCommand(Info(&conf)) + rootCmd.AddCommand(Edit(&conf)) err = rootCmd.Execute() if err != nil { diff --git a/output/single.go b/output/single.go index 453b71e..7294f83 100644 --- a/output/single.go +++ b/output/single.go @@ -71,15 +71,25 @@ func Print(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEn } func WriteFile(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEntry) error { - fd, err := os.OpenFile(attr.File, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) - if err != nil { - return fmt.Errorf("failed to open file %s for writing: %w", attr.File, err) + var fileHandle *os.File + var err error + + if attr.File == "-" { + fileHandle = os.Stdout + } else { + + fd, err := os.OpenFile(attr.File, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + return fmt.Errorf("failed to open file %s for writing: %w", attr.File, err) + } + defer fd.Close() + + fileHandle = fd } - defer fd.Close() if len(entry.Bin) > 0 { // binary file content - _, err = fd.Write(entry.Bin) + _, err = fileHandle.Write(entry.Bin) } else { val := entry.Value if !strings.HasSuffix(val, "\n") { @@ -87,7 +97,7 @@ func WriteFile(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app. val += "\n" } - _, err = fd.Write([]byte(val)) + _, err = fileHandle.Write([]byte(val)) } if err != nil {