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 {