diff --git a/README.md b/README.md index 1118b84..cddf5ea 100644 --- a/README.md +++ b/README.md @@ -95,9 +95,29 @@ anydb import -r backup.json # you can encrypt entries. anydb asks for a passphrase # and will do the same when you retrieve the key using the -# get command. +# get command. anydb will ask you interactively for a password anydb set mypassword -e +# but you can provide it via an environment variable too +ANYDB_PASSWORD=foo anydb set -e secretkey blahblah + +# too tiresome to add -e every time you add an entry? +# use a per bucket config +cat ~/.config/anydb/anydb.toml +[buckets.data] +encrypt = true +anydb set foo bar # will be encrypted + +# speaking of buckets, you can use different buckets +anydb -b test set foo bar + +# and speaking of configs, you can place a config file at these places: +# ~/.config/anydb/anydb.toml +# ~/.anydb.toml +# anydb.toml (current directory) +# or specify one using -c +# look at example.toml + # using template output mode you can freely design how to print stuff # here, we print the values in CSV format ONLY if they have some tag anydb ls -m template -T "{{ if .Tags }}{{ .Key }},{{ .Value }},{{ .Created}}{{ end }}" diff --git a/TODO.md b/TODO.md index dcbca0e..82df59c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,3 @@ - repl - mime-type => exec app + value -- custom buckets (like skate: key@bucket or key+bucket) -- encryption per bucket, one key for all entries (in that bucket) -- `-b bucket`, use B for encrypted bucke? - [edit command](https://github.com/TLINDEN/rpnc/blob/master/command.go#L249) -- env var for password diff --git a/anydb.1 b/anydb.1 index fd67455..addf163 100644 --- a/anydb.1 +++ b/anydb.1 @@ -1,4 +1,4 @@ -.\" Automatically generated by Pod::Man 4.14 (Pod::Simple 3.40) +.\" Automatically generated by Pod::Man 4.14 (Pod::Simple 3.42) .\" .\" Standard preamble: .\" ======================================================================== @@ -133,7 +133,7 @@ .\" ======================================================================== .\" .IX Title "ANYDB 1" -.TH ANYDB 1 "2024-12-18" "1" "User Commands" +.TH ANYDB 1 "2024-12-22" "1" "User Commands" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l diff --git a/app/db.go b/app/db.go index c201aef..634ad6f 100644 --- a/app/db.go +++ b/app/db.go @@ -50,9 +50,11 @@ type DbEntry struct { } type BucketInfo struct { - Name string - Keys int - Size int + Name string + Keys int + Size int + Sequence uint64 + Stats bolt.BucketStats } type DbInfo struct { @@ -264,6 +266,7 @@ func (db *DB) Get(attr *DbAttr) (*DbEntry, error) { jsonentry := bucket.Get([]byte(attr.Key)) if jsonentry == nil { + // FIXME: shall we return a key not found error? return nil } @@ -373,16 +376,23 @@ func (db *DB) Info() (*DbInfo, error) { info := &DbInfo{Path: db.Dbfile} err := db.DB.View(func(tx *bolt.Tx) error { - err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error { - binfo := BucketInfo{Name: string(name)} + err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error { + stats := bucket.Stats() + + binfo := BucketInfo{ + Name: string(name), + Sequence: bucket.Sequence(), + Keys: stats.KeyN, + Stats: bucket.Stats(), + } + err := bucket.ForEach(func(key, entry []byte) error { - binfo.Size += len(entry) - binfo.Keys++ + binfo.Size += len(entry) + len(key) return nil }) if err != nil { - return err + return fmt.Errorf("failed to read keys: %w", err) } info.Buckets = append(info.Buckets, binfo) @@ -391,11 +401,12 @@ func (db *DB) Info() (*DbInfo, error) { }) if err != nil { - return err + return fmt.Errorf("failed to read from DB: %w", err) } return nil }) + return info, err } diff --git a/cfg/config.go b/cfg/config.go index 11d08e7..50683d3 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -1,20 +1,114 @@ +/* +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 cfg -import "github.com/tlinden/anydb/app" +import ( + "fmt" + "io" + "os" + + "github.com/pelletier/go-toml" + "github.com/tlinden/anydb/app" + "github.com/tlinden/anydb/common" +) var Version string = "v0.0.4" +type BucketConfig struct { + Encrypt bool +} + type Config struct { Debug bool Dbfile string - Dbbucket string + Dbbucket string Template string Mode string // wide, table, yaml, json NoHeaders bool NoHumanize bool - Encrypt bool - DB *app.DB - File string - Tags []string + Encrypt bool // one entry Listen string + Buckets map[string]BucketConfig // config file only + + Tags []string // internal + DB *app.DB // internal + File string // internal +} + +func (conf *Config) GetConfig(files []string) error { + for _, file := range files { + if err := conf.ParseConfigFile(file); err != nil { + return err + } + } + + return nil +} + +func (conf *Config) ParseConfigFile(file string) error { + if !common.FileExists(file) { + return nil + } + + fd, err := os.OpenFile(file, os.O_RDONLY, 0600) + if err != nil { + return fmt.Errorf("failed to open config file %s: %w", file, err) + } + + data, err := io.ReadAll(fd) + if err != nil { + return fmt.Errorf("failed to read from config file: %w", err) + } + + add := Config{} + err = toml.Unmarshal(data, &add) + if err != nil { + return fmt.Errorf("failed to unmarshall toml: %w", err) + } + + // merge new values into existing config + switch { + case add.Debug != conf.Debug: + conf.Debug = add.Debug + case add.Dbfile != "": + conf.Dbfile = add.Dbfile + case add.Dbbucket != "": + conf.Dbbucket = add.Dbbucket + case add.Template != "": + conf.Template = add.Template + case add.NoHeaders != conf.NoHeaders: + conf.NoHeaders = add.NoHeaders + case add.NoHumanize != conf.NoHumanize: + conf.NoHumanize = add.NoHumanize + case add.Encrypt != conf.Encrypt: + conf.Encrypt = add.Encrypt + case add.Listen != "": + conf.Listen = add.Listen + } + + // only supported in config files + conf.Buckets = add.Buckets + + // determine bucket encryption mode + for name, bucket := range conf.Buckets { + if name == conf.Dbbucket { + conf.Encrypt = bucket.Encrypt + } + } + + return nil } diff --git a/cmd/crud.go b/cmd/crud.go index 62eb3a6..e3074b4 100644 --- a/cmd/crud.go +++ b/cmd/crud.go @@ -65,7 +65,7 @@ func Set(conf *cfg.Config) *cobra.Command { // encrypt? if conf.Encrypt { - pass, err := app.AskForPassword() + pass, err := getPassword() if err != nil { return err } @@ -118,7 +118,7 @@ func Get(conf *cfg.Config) *cobra.Command { } if entry.Encrypted { - pass, err := app.AskForPassword() + pass, err := getPassword() if err != nil { return err } @@ -371,7 +371,26 @@ func Info(conf *cfg.Config) *cobra.Command { }, } - cmd.PersistentFlags().StringVarP(&conf.Listen, "listen", "l", "localhost:8787", "host:port") + 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 + + envpass := os.Getenv("ANYDB_PASSWORD") + + if envpass == "" { + readpass, err := app.AskForPassword() + if err != nil { + return nil, err + } + + pass = readpass + } else { + pass = []byte(envpass) + } + + return pass, nil +} diff --git a/cmd/root.go b/cmd/root.go index efd532c..f476eae 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -22,6 +22,7 @@ import ( "os" "path/filepath" + "github.com/alecthomas/repr" "github.com/spf13/cobra" "github.com/tlinden/anydb/app" "github.com/tlinden/anydb/cfg" @@ -45,10 +46,22 @@ func completion(cmd *cobra.Command, mode string) error { func Execute() { var ( conf cfg.Config + configfile string ShowVersion bool ShowCompletion string ) + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + + SearchConfigs := []string{ + filepath.Join(home, ".config", "anydb", "anydb.toml"), + filepath.Join(home, ".anydb.toml"), + "anydb.toml", + } + var rootCmd = &cobra.Command{ Use: "anydb [options]", Short: "anydb", @@ -61,6 +74,21 @@ func Execute() { conf.DB = db + var configs []string + if configfile != "" { + configs = []string{configfile} + } else { + configs = SearchConfigs + } + + if err := conf.GetConfig(configs); err != nil { + return err + } + + if conf.Debug { + repr.Println(conf) + } + return nil }, @@ -82,11 +110,6 @@ func Execute() { }, } - home, err := os.UserHomeDir() - if err != nil { - panic(err) - } - // options rootCmd.PersistentFlags().BoolVarP(&ShowVersion, "version", "v", false, "Print program version") rootCmd.PersistentFlags().BoolVarP(&conf.Debug, "debug", "d", false, "Enable debugging") @@ -94,6 +117,7 @@ func Execute() { filepath.Join(home, ".config", "anydb", "default.db"), "DB file to use") rootCmd.PersistentFlags().StringVarP(&conf.Dbbucket, "bucket", "b", app.BucketData, "use other bucket (default: "+app.BucketData+")") + rootCmd.PersistentFlags().StringVarP(&configfile, "config", "c", "", "toml config file") rootCmd.AddCommand(Set(&conf)) rootCmd.AddCommand(List(&conf)) diff --git a/common/io.go b/common/io.go new file mode 100644 index 0000000..0cf69d2 --- /dev/null +++ b/common/io.go @@ -0,0 +1,36 @@ +/* +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 common + +import "os" + +func CleanError(file string, err error) error { + // remove given [backup] file and forward the given error + os.Remove(file) + return err +} + +func FileExists(filename string) bool { + info, err := os.Stat(filename) + + if err != nil { + // return false on any error + return false + } + + return !info.IsDir() +} diff --git a/go.mod b/go.mod index 2b07973..676bba0 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/spf13/cobra v1.8.1 // indirect diff --git a/go.sum b/go.sum index f3c00a8..0b195c4 100644 --- a/go.sum +++ b/go.sum @@ -45,6 +45,8 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/output/single.go b/output/single.go index 6a19e02..9bf686b 100644 --- a/output/single.go +++ b/output/single.go @@ -21,8 +21,10 @@ import ( "fmt" "io" "os" + "reflect" "strings" + "github.com/dustin/go-humanize" "github.com/tlinden/anydb/app" "github.com/tlinden/anydb/cfg" "golang.org/x/term" @@ -96,12 +98,33 @@ func WriteFile(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app. } func Info(writer io.Writer, conf *cfg.Config, info *app.DbInfo) error { - // repr.Println(info) fmt.Fprintf(writer, "Database: %s\n", info.Path) for _, bucket := range info.Buckets { + if conf.NoHumanize { + fmt.Fprintf( + writer, + "%19s: %s\n%19s: %d\n%19s: %d\n", + "Bucket", bucket.Name, + "Size", bucket.Size, + "Keys", bucket.Keys) + } else { + fmt.Fprintf( + writer, + "%19s: %s\n%19s: %s\n%19s: %d\n", + "Bucket", bucket.Name, + "Size", humanize.Bytes(uint64(bucket.Size)), + "Keys", bucket.Keys) + } - fmt.Fprintf(writer, "Bucket: %s\n Size: %d\n Keys: %d\n\n", bucket.Name, bucket.Size, bucket.Keys) + if conf.Debug { + val := reflect.ValueOf(&bucket.Stats).Elem() + for i := 0; i < val.NumField(); i++ { + fmt.Fprintf(writer, "%19s: %d\n", val.Type().Field(i).Name, val.Field(i)) + } + } + + fmt.Fprintln(writer) } return nil }