added edit command, fixed set -o -

This commit is contained in:
2024-12-22 17:46:44 +01:00
parent fe3951f3c2
commit 66d6bd2a41
8 changed files with 366 additions and 154 deletions

View File

@@ -145,6 +145,14 @@ curl localhost:8787/anydb/v1/foo
# list keys # list keys
curl localhost:8787/anydb/v1/ 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 # sometimes you need to know some details about the current database
# add -d for more details # add -d for more details
anydb info anydb info

View File

@@ -1,3 +1,2 @@
- repl - repl
- mime-type => exec app + value - mime-type => exec app + value
- [edit command](https://github.com/TLINDEN/rpnc/blob/master/command.go#L249)

View File

@@ -76,6 +76,13 @@ func (entry *DbEntry) Normalize() {
entry.Size = len(entry.Bin) 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 { if len(entry.Value) > MaxValueWidth {
entry.Value = entry.Value[0:MaxValueWidth] + "..." entry.Value = entry.Value[0:MaxValueWidth] + "..."
} }

View File

@@ -26,7 +26,7 @@ import (
"github.com/tlinden/anydb/common" "github.com/tlinden/anydb/common"
) )
var Version string = "v0.0.5" var Version string = "v0.0.6"
type BucketConfig struct { type BucketConfig struct {
Encrypt bool Encrypt bool

View File

@@ -17,11 +17,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd package cmd
import ( import (
"bytes"
"errors" "errors"
"fmt"
"os" "os"
"os/exec"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
@@ -29,7 +26,6 @@ import (
"github.com/tlinden/anydb/app" "github.com/tlinden/anydb/app"
"github.com/tlinden/anydb/cfg" "github.com/tlinden/anydb/cfg"
"github.com/tlinden/anydb/output" "github.com/tlinden/anydb/output"
"github.com/tlinden/anydb/rest"
) )
func Set(conf *cfg.Config) *cobra.Command { func Set(conf *cfg.Config) *cobra.Command {
@@ -185,38 +181,6 @@ func Del(conf *cfg.Config) *cobra.Command {
return cmd return cmd
} }
func Export(conf *cfg.Config) *cobra.Command {
var (
attr app.DbAttr
)
var cmd = &cobra.Command{
Use: "export [-o <json filename>]",
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 { func List(conf *cfg.Config) *cobra.Command {
var ( var (
attr app.DbAttr attr app.DbAttr
@@ -266,116 +230,6 @@ func List(conf *cfg.Config) *cobra.Command {
return cmd return cmd
} }
func Import(conf *cfg.Config) *cobra.Command {
var (
attr app.DbAttr
)
var cmd = &cobra.Command{
Use: "import [<json file>]",
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) { func getPassword() ([]byte, error) {
var pass []byte var pass []byte

326
cmd/extra.go Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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 <json filename>]",
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 [<json file>]",
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 <key>",
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
}

View File

@@ -119,15 +119,23 @@ func Execute() {
app.BucketData, "use other bucket (default: "+app.BucketData+")") app.BucketData, "use other bucket (default: "+app.BucketData+")")
rootCmd.PersistentFlags().StringVarP(&configfile, "config", "c", "", "toml config file") rootCmd.PersistentFlags().StringVarP(&configfile, "config", "c", "", "toml config file")
// CRUD
rootCmd.AddCommand(Set(&conf)) rootCmd.AddCommand(Set(&conf))
rootCmd.AddCommand(List(&conf)) rootCmd.AddCommand(List(&conf))
rootCmd.AddCommand(Get(&conf)) rootCmd.AddCommand(Get(&conf))
rootCmd.AddCommand(Del(&conf)) rootCmd.AddCommand(Del(&conf))
// backup
rootCmd.AddCommand(Export(&conf)) rootCmd.AddCommand(Export(&conf))
rootCmd.AddCommand(Import(&conf)) rootCmd.AddCommand(Import(&conf))
// REST API
rootCmd.AddCommand(Serve(&conf)) rootCmd.AddCommand(Serve(&conf))
// auxiliary
rootCmd.AddCommand(Man(&conf)) rootCmd.AddCommand(Man(&conf))
rootCmd.AddCommand(Info(&conf)) rootCmd.AddCommand(Info(&conf))
rootCmd.AddCommand(Edit(&conf))
err = rootCmd.Execute() err = rootCmd.Execute()
if err != nil { if err != nil {

View File

@@ -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 { 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) var fileHandle *os.File
if err != nil { var err error
return fmt.Errorf("failed to open file %s for writing: %w", attr.File, err)
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 { if len(entry.Bin) > 0 {
// binary file content // binary file content
_, err = fd.Write(entry.Bin) _, err = fileHandle.Write(entry.Bin)
} else { } else {
val := entry.Value val := entry.Value
if !strings.HasSuffix(val, "\n") { if !strings.HasSuffix(val, "\n") {
@@ -87,7 +97,7 @@ func WriteFile(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.
val += "\n" val += "\n"
} }
_, err = fd.Write([]byte(val)) _, err = fileHandle.Write([]byte(val))
} }
if err != nil { if err != nil {