From 332eed679efec7936cd8b63c91f03a612ccad1de Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Wed, 18 Dec 2024 14:06:21 +0100 Subject: [PATCH] add import+export, json output, humanize wide output, fixes --- app/db.go | 213 +++++++++++++++++++++++++++++++++++--------- cfg/config.go | 13 +-- cmd/maincommands.go | 172 ++++++++++++++++++++++++++++++++--- cmd/root.go | 11 +-- go.mod | 2 + go.sum | 4 + main.go | 25 ++++++ output/default.go | 58 ------------ output/list.go | 100 +++++++++++++++++++++ output/single.go | 60 +++++++++++++ output/write.go | 34 +++++++ 11 files changed, 570 insertions(+), 122 deletions(-) delete mode 100644 output/default.go create mode 100644 output/list.go create mode 100644 output/single.go create mode 100644 output/write.go diff --git a/app/db.go b/app/db.go index 2d26928..94a8453 100644 --- a/app/db.go +++ b/app/db.go @@ -2,11 +2,11 @@ package app import ( "encoding/json" + "errors" "fmt" "os" "path/filepath" "regexp" - "slices" "strings" "time" @@ -35,7 +35,6 @@ type DbTag struct { } const BucketData string = "data" -const BucketTags string = "tags" func New(file string, debug bool) (*DB, error) { if _, err := os.Stat(filepath.Dir(file)); os.IsNotExist(err) { @@ -141,7 +140,38 @@ func (db *DB) Set(attr *DbAttr) error { Created: time.Now(), } - err := db.DB.Update(func(tx *bolt.Tx) error { + // check if the entry already exists and if yes, check if it has + // any tags. if so, we initialize our update struct with these + // tags unless it has new tags configured. + err := db.DB.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketData)) + if bucket == nil { + return nil + } + + jsonentry := bucket.Get([]byte(entry.Key)) + if jsonentry == nil { + return nil + } + + var oldentry DbEntry + if err := json.Unmarshal(jsonentry, &oldentry); err != nil { + return fmt.Errorf("unable to unmarshal json: %s", err) + } + + if len(oldentry.Tags) > 0 && len(entry.Tags) == 0 { + // initialize update entry with tags from old entry + entry.Tags = oldentry.Tags + } + + return nil + }) + + if err != nil { + return err + } + + err = db.DB.Update(func(tx *bolt.Tx) error { // insert data bucket, err := tx.CreateBucketIfNotExists([]byte(BucketData)) if err != nil { @@ -158,49 +188,150 @@ func (db *DB) Set(attr *DbAttr) error { return fmt.Errorf("insert data: %s", err) } - // insert tag, if any - // FIXME: check removed tags - if len(attr.Tags) > 0 { - bucket, err := tx.CreateBucketIfNotExists([]byte(BucketTags)) - if err != nil { - return fmt.Errorf("create bucket: %s", err) - } - - for _, tag := range entry.Tags { - dbtag := &DbTag{} - - jsontag := bucket.Get([]byte(tag)) - if jsontag == nil { - // the tag is empty so far, initialize it - dbtag.Keys = []string{entry.Key} - } else { - if err := json.Unmarshal(jsontag, dbtag); err != nil { - return fmt.Errorf("unable to unmarshal json: %s", err) - } - - if !slices.Contains(dbtag.Keys, entry.Key) { - // current key is not yet assigned to the tag, append it - dbtag.Keys = append(dbtag.Keys, entry.Key) - } - } - - jsontag, err = json.Marshal(dbtag) - if err != nil { - return fmt.Errorf("json marshalling failure: %s", err) - } - - err = bucket.Put([]byte(tag), []byte(jsontag)) - if err != nil { - return fmt.Errorf("insert data: %s", err) - } - } - } - return nil }) + if err != nil { return err } return nil } + +func (db *DB) Get(attr *DbAttr) (*DbEntry, error) { + if err := db.Open(); err != nil { + return nil, err + } + defer db.Close() + + if err := attr.ParseKV(); err != nil { + return nil, err + } + + entry := DbEntry{} + + err := db.DB.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketData)) + if bucket == nil { + return nil + } + + jsonentry := bucket.Get([]byte(attr.Key)) + if jsonentry == nil { + return nil + } + + if err := json.Unmarshal(jsonentry, &entry); err != nil { + return fmt.Errorf("unable to unmarshal json: %s", err) + } + + return nil + }) + + if err != nil { + return nil, err + } + + return &entry, nil +} + +func (db *DB) Del(attr *DbAttr) error { + if err := db.Open(); err != nil { + return err + } + defer db.Close() + + err := db.DB.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketData)) + + if bucket == nil { + return nil + } + + return bucket.Delete([]byte(attr.Key)) + }) + + return err +} + +func (db *DB) Import(attr *DbAttr) error { + // open json file into attr.Val + if err := attr.GetFileValue(); err != nil { + return err + } + + if attr.Val == "" { + return errors.New("empty json file") + } + + var entries DbEntries + now := time.Now() + newfile := db.Dbfile + now.Format("-02.01.2006T03:04.05") + + if err := json.Unmarshal([]byte(attr.Val), &entries); err != nil { + return cleanError(newfile, fmt.Errorf("unable to unmarshal json: %s", err)) + } + + if fileExists(db.Dbfile) { + // backup the old file + err := os.Rename(db.Dbfile, newfile) + if err != nil { + return err + } + + } + + // should now be a new db file + if err := db.Open(); err != nil { + return cleanError(newfile, err) + } + defer db.Close() + + err := db.DB.Update(func(tx *bolt.Tx) error { + // insert data + bucket, err := tx.CreateBucketIfNotExists([]byte(BucketData)) + if err != nil { + return fmt.Errorf("create bucket: %s", err) + } + + for _, entry := range entries { + jsonentry, err := json.Marshal(entry) + if err != nil { + return fmt.Errorf("json marshalling failure: %s", err) + } + + err = bucket.Put([]byte(entry.Key), []byte(jsonentry)) + if err != nil { + return fmt.Errorf("insert data: %s", err) + } + } + + return nil + }) + + if err != nil { + return cleanError(newfile, err) + } + + fmt.Printf("backed up database file to %s\n", newfile) + fmt.Printf("imported %d database entries\n", len(entries)) + + return nil +} + +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/cfg/config.go b/cfg/config.go index 101e873..33d5cef 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -5,10 +5,11 @@ import "github.com/tlinden/anydb/app" var Version string = "v0.0.1" type Config struct { - Debug bool - Dbfile string - Mode string // wide, table, yaml, json - DB *app.DB - File string - Tags []string + Debug bool + Dbfile string + Mode string // wide, table, yaml, json + NoHeaders bool + DB *app.DB + File string + Tags []string } diff --git a/cmd/maincommands.go b/cmd/maincommands.go index acd315f..69ceacd 100644 --- a/cmd/maincommands.go +++ b/cmd/maincommands.go @@ -3,6 +3,7 @@ package cmd import ( "errors" "os" + "strings" "github.com/spf13/cobra" "github.com/tlinden/anydb/app" @@ -21,7 +22,7 @@ func Set(conf *cfg.Config) *cobra.Command { Long: `Insert key/value pair`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { - return errors.New("No key/value pair specified") + return errors.New("no key/value pair specified") } // errors at this stage do not cause the usage to be shown @@ -31,31 +32,137 @@ func Set(conf *cfg.Config) *cobra.Command { attr.Args = args } - if err := conf.DB.Set(&attr); err != nil { - return err + // turn comma list into slice, if needed + if len(attr.Tags) == 1 && strings.Contains(attr.Tags[0], ",") { + attr.Tags = strings.Split(attr.Tags[0], ",") } - return conf.DB.Close() + return conf.DB.Set(&attr) }, } 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 Get(conf *cfg.Config) *cobra.Command { - return nil + var ( + attr app.DbAttr + ) + + var cmd = &cobra.Command{ + Use: "get [-o ]", + Short: "Retrieve value for a key", + Long: `Retrieve value for 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 + + if len(args) > 0 { + attr.Key = args[0] + } + + entry, err := conf.DB.Get(&attr) + if err != nil { + return err + } + + return output.Print(os.Stdout, conf, entry) + }, + } + + cmd.PersistentFlags().StringVarP(&conf.Mode, "output", "o", "", "output to file") + + cmd.Aliases = append(cmd.Aliases, "show") + cmd.Aliases = append(cmd.Aliases, "g") + cmd.Aliases = append(cmd.Aliases, ".") + + return cmd } func Del(conf *cfg.Config) *cobra.Command { - return nil + var ( + attr app.DbAttr + ) + + var cmd = &cobra.Command{ + Use: "del ", + Short: "Delete key", + Long: `Delete key and value matching 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 + + if len(args) > 0 { + attr.Key = args[0] + } + + return conf.DB.Del(&attr) + }, + } + + cmd.PersistentFlags().StringVarP(&conf.Mode, "output", "o", "", "output to file") + + cmd.Aliases = append(cmd.Aliases, "d") + cmd.Aliases = append(cmd.Aliases, "rm") + + return cmd +} + +func Export(conf *cfg.Config) *cobra.Command { + var ( + attr app.DbAttr + ) + + var cmd = &cobra.Command{ + Use: "export []", + 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 + + if len(args) == 0 { + attr.File = "-" + } else { + attr.File = args[0] + } + + conf.Mode = "json" + + entries, err := conf.DB.List(&attr) + if err != nil { + return err + } + + return output.WriteFile(&attr, conf, entries) + }, + } + + 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 + wide bool ) var cmd = &cobra.Command{ @@ -70,25 +177,66 @@ func List(conf *cfg.Config) *cobra.Command { attr.Args = args } + // turn comma list into slice, if needed + if len(attr.Tags) == 1 && strings.Contains(attr.Tags[0], ",") { + attr.Tags = strings.Split(attr.Tags[0], ",") + } + + if wide { + conf.Mode = "wide" + } + entries, err := conf.DB.List(&attr) if err != nil { return err } - output.List(os.Stdout, conf, entries) - - return conf.DB.Close() + return output.List(os.Stdout, conf, entries) }, } - cmd.PersistentFlags().StringVarP(&conf.Mode, "output-mode", "o", "", "output mode: wide, yaml, json, table") + cmd.PersistentFlags().StringVarP(&conf.Mode, "output-mode", "o", "", "output format (table|wide|json), wide is a verbose table. (default 'table')") + cmd.PersistentFlags().BoolVarP(&wide, "wide-output", "l", false, "output mode: wide") + cmd.PersistentFlags().BoolVarP(&conf.NoHeaders, "no-headers", "n", false, "omit headers in tables") cmd.PersistentFlags().StringArrayVarP(&attr.Tags, "tags", "t", nil, "tags, multiple allowed") + cmd.Aliases = append(cmd.Aliases, "/") + cmd.Aliases = append(cmd.Aliases, "ls") + return cmd } -func Find(conf *cfg.Config) *cobra.Command { - return nil +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 + + if len(args) == 0 { + attr.File = "-" + } else { + attr.File = args[0] + } + + return conf.DB.Import(&attr) + }, + } + + 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 { diff --git a/cmd/root.go b/cmd/root.go index d765352..c70bc6a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -22,7 +22,7 @@ func completion(cmd *cobra.Command, mode string) error { case "powershell": return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) default: - return errors.New("Invalid shell parameter! Valid ones: bash|zsh|fish|powershell") + return errors.New("invalid shell parameter! Valid ones: bash|zsh|fish|powershell") } } @@ -59,7 +59,7 @@ func Execute() { } if len(args) == 0 { - return errors.New("No command specified!") + return errors.New("no command specified") } return nil @@ -79,9 +79,10 @@ func Execute() { rootCmd.AddCommand(Set(&conf)) rootCmd.AddCommand(List(&conf)) - // rootCmd.AddCommand(Set(&conf)) - // rootCmd.AddCommand(Del(&conf)) - // rootCmd.AddCommand(Find(&conf)) + rootCmd.AddCommand(Get(&conf)) + rootCmd.AddCommand(Del(&conf)) + rootCmd.AddCommand(Export(&conf)) + rootCmd.AddCommand(Import(&conf)) // rootCmd.AddCommand(Help(&conf)) // rootCmd.AddCommand(Man(&conf)) diff --git a/go.mod b/go.mod index 03d447b..85dbfd7 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22.1 require ( github.com/alecthomas/repr v0.4.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect @@ -11,4 +12,5 @@ require ( github.com/spf13/pflag v1.0.5 // indirect go.etcd.io/bbolt v1.3.11 // indirect golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect ) diff --git a/go.sum b/go.sum index d230ada..26eb4e0 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -36,6 +38,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/main.go b/main.go index 2bea875..78c9ecf 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,34 @@ package main import ( + "bufio" + "fmt" + "os" + "runtime" + + "github.com/inconshreveable/mousetrap" "github.com/tlinden/anydb/cmd" ) func main() { cmd.Execute() } + +func init() { + // if we're running on Windows AND if the user double clicked the + // exe file from explorer, we tell them and then wait until any + // key has been hit, which will make the cmd window disappear and + // thus give the user time to read it. + if runtime.GOOS == "windows" { + if mousetrap.StartedByExplorer() { + fmt.Println("Please do no double click anydb.exe!") + fmt.Println("Please open a command shell and run it from there.") + fmt.Println() + fmt.Print("Press any key to quit: ") + _, err := bufio.NewReader(os.Stdin).ReadString('\n') + if err != nil { + panic(err) + } + } + } +} diff --git a/output/default.go b/output/default.go deleted file mode 100644 index c5f7092..0000000 --- a/output/default.go +++ /dev/null @@ -1,58 +0,0 @@ -package output - -import ( - "fmt" - "io" - "strings" - - "github.com/olekukonko/tablewriter" - "github.com/tlinden/anydb/app" - "github.com/tlinden/anydb/cfg" -) - -func List(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error { - // FIXME: call sort here - // FIXME: check output mode switch to subs - - tableString := &strings.Builder{} - table := tablewriter.NewWriter(tableString) - - if conf.Mode == "wide" { - table.SetHeader([]string{"KEY", "VALUE", "TAGS", "TIMESTAMP"}) - } else { - table.SetHeader([]string{"KEY", "VALUE"}) - } - - for _, row := range entries { - if row.Value == "" { - row.Value = string(row.Bin)[0:60] - } else if len(row.Value) > 60 { - row.Value = row.Value[0:60] - } - - if conf.Mode == "wide" { - table.Append([]string{row.Key, row.Value, strings.Join(row.Tags, ","), row.Created.Format("02.01.2006T03:04.05")}) - } else { - table.Append([]string{row.Key, row.Value}) - } - } - - table.SetAutoWrapText(false) - table.SetAutoFormatHeaders(true) - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetAlignment(tablewriter.ALIGN_LEFT) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - table.SetRowSeparator("") - table.SetHeaderLine(false) - table.SetBorder(false) - table.SetNoWhiteSpace(true) - - table.SetTablePadding("\t") // pad with tabs - - table.Render() - - fmt.Fprint(writer, tableString.String()) - - return nil -} diff --git a/output/list.go b/output/list.go new file mode 100644 index 0000000..429608a --- /dev/null +++ b/output/list.go @@ -0,0 +1,100 @@ +package output + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "strings" + + "github.com/dustin/go-humanize" + "github.com/olekukonko/tablewriter" + "github.com/tlinden/anydb/app" + "github.com/tlinden/anydb/cfg" +) + +func List(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error { + // FIXME: call sort here + switch conf.Mode { + case "wide": + fallthrough + case "": + fallthrough + case "table": + return ListTable(writer, conf, entries) + case "json": + return ListJson(writer, conf, entries) + default: + return errors.New("unsupported mode") + } + + return nil +} + +func ListJson(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error { + jsonentries, err := json.Marshal(entries) + if err != nil { + return fmt.Errorf("json marshalling failure: %s", err) + } + + fmt.Println(string(jsonentries)) + return nil +} + +func ListTable(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error { + tableString := &strings.Builder{} + table := tablewriter.NewWriter(tableString) + + if !conf.NoHeaders { + if conf.Mode == "wide" { + table.SetHeader([]string{"KEY", "TAGS", "SIZE", "AGE", "VALUE"}) + } else { + table.SetHeader([]string{"KEY", "VALUE"}) + } + } + + for _, row := range entries { + size := len(row.Value) + + if len(row.Bin) > 0 { + row.Value = "binary-content" + size = len(row.Bin) + } + + if len(row.Value) > 60 { + row.Value = row.Value[0:60] + "..." + } + + if conf.Mode == "wide" { + table.Append([]string{ + row.Key, + strings.Join(row.Tags, ","), + humanize.Bytes(uint64(size)), + //row.Created.Format("02.01.2006T03:04.05"), + humanize.Time(row.Created), + row.Value, + }) + } else { + table.Append([]string{row.Key, row.Value}) + } + } + + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAutoWrapText(false) + table.SetAutoFormatHeaders(true) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.SetBorder(false) + table.SetNoWhiteSpace(true) + + table.SetTablePadding("\t") // pad with tabs + + table.Render() + + fmt.Fprint(writer, tableString.String()) + + return nil +} diff --git a/output/single.go b/output/single.go new file mode 100644 index 0000000..a4fb263 --- /dev/null +++ b/output/single.go @@ -0,0 +1,60 @@ +package output + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/tlinden/anydb/app" + "github.com/tlinden/anydb/cfg" + "golang.org/x/term" +) + +func Print(writer io.Writer, conf *cfg.Config, entry *app.DbEntry) error { + if conf.Mode != "" { + // consider this to be a file + fd, err := os.OpenFile(conf.Mode, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + return err + } + defer fd.Close() + + if len(entry.Bin) > 0 { + // binary file content + _, err = fd.Write(entry.Bin) + } else { + val := entry.Value + if !strings.HasSuffix(val, "\n") { + // always add a terminal newline + val += "\n" + } + + _, err = fd.Write([]byte(val)) + } + + if err != nil { + return err + } + + return nil + } + + isatty := term.IsTerminal(int(os.Stdout.Fd())) + if len(entry.Bin) > 0 { + if isatty { + fmt.Println("binary data omitted") + } else { + os.Stdout.Write(entry.Bin) + } + } else { + fmt.Print(entry.Value) + + if !strings.HasSuffix(entry.Value, "\n") { + // always add a terminal newline + fmt.Println() + } + } + + return nil +} diff --git a/output/write.go b/output/write.go new file mode 100644 index 0000000..e61892d --- /dev/null +++ b/output/write.go @@ -0,0 +1,34 @@ +package output + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/tlinden/anydb/app" + "github.com/tlinden/anydb/cfg" +) + +func WriteFile(attr *app.DbAttr, conf *cfg.Config, entries app.DbEntries) error { + jsonentries, err := json.Marshal(entries) + if err != nil { + return fmt.Errorf("json marshalling failure: %s", err) + } + + if attr.File == "-" { + fmt.Println(string(jsonentries)) + } else { + fd, err := os.OpenFile(attr.File, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + + if _, err := fd.Write(jsonentries); err != nil { + return err + } + + fmt.Printf("database contents exported to %s\n", attr.File) + } + + return nil +}