From 1cf17fbee07d693701dd53a4486668b028fc06e1 Mon Sep 17 00:00:00 2001 From: SA6MWA Michel Date: Thu, 23 Mar 2023 14:34:29 +0100 Subject: [PATCH] Fix bug in SetPersistence, remove Unstash call from EditThing, add docs --- README.md | 1 - anystore.go | 22 +++++++------ editstash.go | 17 +++++++--- examples/edit-stash-2/main.go | 7 ++-- examples/edit-stash/main.go | 27 ++++++++-------- stash.go | 60 +++++++++++++++++++++++++---------- stash_test.go | 60 +++++++++++++++++++++++++++++++++++ 7 files changed, 145 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 09aee85..84dd1b5 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,6 @@ func main() { EncryptionKey: anystore.DefaultEncryptionKey, Key: "configuration", Thing: &configuration, - DefaultThing: defaultConf, // Editor: "/usr/bin/emacs", }); err != nil { log.Fatal(err) diff --git a/anystore.go b/anystore.go index 69eae88..03f6540 100644 --- a/anystore.go +++ b/anystore.go @@ -6,7 +6,7 @@ an AES-128/192/256 encrypted GOB file with HMAC-SHA256 for authentication and validation of the data. For access from multiple instances sharing the same map, POSIX syscall.Flock is used to exclusively lock a lockfile during save. There is no support for -Windows or other non-POSIX systems without flock(2). +Windows or other non-POSIX systems missing flock(2). Example: @@ -107,7 +107,8 @@ variables, using Stash, Unstash and EditThing is simple... EncryptionKey: anystore.DefaultEncryptionKey, Key: "configuration", Thing: &configuration, - }, defaultConf); err != nil { + DefaultThing: defaultConf, + }); err != nil { log.Fatal(err) } @@ -118,7 +119,7 @@ variables, using Stash, Unstash and EditThing is simple... Key: "configuration", Thing: &configuration, // Editor: "/usr/bin/emacs", - }, defaultConf); err != nil { + }); err != nil { log.Fatal(err) } } @@ -161,7 +162,7 @@ const DefaultPersistenceFile string = "~/.config/anystore/anystore.db" var ( ErrKeyLength error = errors.New("key length must be 16, 24 or 32 (for AES-128, AES-192 or AES-256)") ErrWroteTooLittle error = errors.New("wrote too few bytes") - ErrHMACValidationFailed error = errors.New("HMAC validation failed (corrupt message or wrong encryption key)") + ErrHMACValidationFailed error = errors.New("HMAC validation failed (corrupt data or wrong encryption key)") ) // A thread-safe key/value store using string as key and interface{} (any) as @@ -310,13 +311,14 @@ func (a *anyStore) SetPersistenceFile(file string) (AnyStore, error) { file = filepath.Join(dirname, file[2:]) } dir, _ := filepath.Split(file) - if _, err := os.Stat(file); err != nil { - if errors.Is(err, os.ErrNotExist) { - if err := os.MkdirAll(dir, 0777); err != nil { - return a, err + + if dir != "" && dir != "." && dir != ".." { + if _, err := os.Stat(dir); err != nil { + if errors.Is(err, os.ErrNotExist) { + if err := os.MkdirAll(dir, 0777); err != nil { + return a, err + } } - } else { - return a, err } } diff --git a/editstash.go b/editstash.go index aaa731e..a3b1633 100644 --- a/editstash.go +++ b/editstash.go @@ -23,15 +23,23 @@ var ( ErrNotATerminal error = errors.New("os.Stdin is not a terminal") ) +// EditThing is an interactive variant of Stash, editing the Thing +// before Stashing it. EditThing does not Unstash before editing, it +// uses the actual Thing. If you need to load Thing from persistence +// before editing, call Unstash prior to EditThing (make sure Thing is +// zero/new/empty before Unstash). EditThing uses encoding/json for +// editing, beware if a user enters "null" on a non-pointer field and +// saves, encoding/json will ignore it effectively using the original +// value without producing an error. Similarily, if a user removes a +// field while editing, the original value will be retained. +// +// Environment variable EDITOR is used as a json editor falling back +// to conf.Editor and finally one of the DefaultEditors. func EditThing(conf *StashConfig) error { if !IsUnixTerminal(os.Stdin) { return ErrNotATerminal } - if err := Unstash(conf); err != nil { - return err - } - executables := []string{} envEditor := os.Getenv("EDITOR") @@ -106,6 +114,7 @@ func EditThing(conf *StashConfig) error { goto retryQuestion } } + if err := Stash(conf); err != nil { return err } diff --git a/examples/edit-stash-2/main.go b/examples/edit-stash-2/main.go index 01e74a2..fc61cc0 100644 --- a/examples/edit-stash-2/main.go +++ b/examples/edit-stash-2/main.go @@ -13,7 +13,7 @@ import ( type MyConfig struct { ListenAddress string Username string - Token string + Token *string Endpoints []*Endpoint } @@ -27,13 +27,13 @@ func main() { defaultConf := &MyConfig{ ListenAddress: "0.0.0.0:1234", Username: "superuser", - Token: "abc123", + Token: &[]string{"abc123"}[0], Endpoints: []*Endpoint{ {ID: 1, Name: "Endpoint 1", URL: "https://endpoint1.local"}, {ID: 2, Name: "Endpoint 2", URL: "https://endpoint2.local"}, }, } - file := "~/.myconfigfile.db" + file := "~/.anystore/examples-edit-stash-2.db" var configuration MyConfig @@ -53,7 +53,6 @@ func main() { EncryptionKey: anystore.DefaultEncryptionKey, Key: "configuration", Thing: &configuration, - DefaultThing: defaultConf, // Editor: "/usr/bin/emacs", }); err != nil { log.Fatal(err) diff --git a/examples/edit-stash/main.go b/examples/edit-stash/main.go index 912af6b..b444f37 100644 --- a/examples/edit-stash/main.go +++ b/examples/edit-stash/main.go @@ -21,9 +21,10 @@ type Component struct { func main() { log.SetFlags(log.Ldate | log.Ltime | log.LUTC | log.Lshortfile) - thingToEdit := &Thing{ + actualThing := &Thing{} + + defaultThing := &Thing{ Name: &[]string{"Hello World"}[0], - Number: 32, Description: "There is not much to a Hello World thing.", Components: []*Component{ {ID: 1, Name: "Component one"}, @@ -32,24 +33,22 @@ func main() { }, } - defaultThing := &Thing{ - Name: &[]string{"default"}[0], - Description: "the default thing", - Components: []*Component{ - {ID: 1, Name: "hello"}, - }, - } - - file := "~/.testing-edit-stash.db" + file := "~/.anystore/examples-edit-stash.db" - if err := anystore.EditThing(&anystore.StashConfig{ + conf := &anystore.StashConfig{ File: file, GZip: true, EncryptionKey: anystore.DefaultEncryptionKey, Key: "configuration", - Thing: thingToEdit, + Thing: actualThing, DefaultThing: defaultThing, - }); err != nil { + } + + if err := anystore.Unstash(conf); err != nil { + log.Fatal(err) + } + + if err := anystore.EditThing(conf); err != nil { log.Fatal(err) } diff --git a/stash.go b/stash.go index dba9bfe..cba1109 100644 --- a/stash.go +++ b/stash.go @@ -30,27 +30,55 @@ var ( // on success and failure. If File is an empty string (== "") and Writer is not // nil, Stash will only write to the io.Writer. type StashConfig struct { - File string // AnyStore DB file, if empty, use Reader/Writer - Reader io.Reader // If nil, use File for Unstash, if not, prefer Reader over File - Writer io.WriteCloser // If nil, use File for Stash, if not, write to both Writer and File (if File is not an empty string) - GZip bool // GZip data before encryption - EncryptionKey string // 16, 24 or 32 byte long base64-encoded string - Key string // Key name where to store Thing - Thing any // Usually a struct with data, properties, configuration, etc - DefaultThing any // If Unstash get os.ErrNotExist or key is missing, use this as default Thing if not nil - Editor string // Editor to use to edit Thing as JSON + // AnyStore DB file, if empty, use Reader/Writer. + File string + + // If nil, use File for Unstash, if not, prefer Reader over File. + Reader io.Reader + + // If nil, use File for Stash, if not, write to both Writer and File + // (if File is not an empty string). + Writer io.WriteCloser + + // GZip data before encryption. + GZip bool + + // 16, 24 or 32 byte long base64-encoded string. + EncryptionKey string + + // Key name where to store Thing. + Key string + + // Thing is usually a struct with data, properties, configuration, + // etc. Must be a pointer. On Unstash (and EditThing), Thing should + // be zeroed (new/empty) or the result of underlying gob.Decode is + // unpredictable. + Thing any + + // If Unstash get os.ErrNotExist or key is missing, use this as + // default Thing if not nil. Must be a pointer. + DefaultThing any + + // Editor to use to edit Thing as JSON. + Editor string } // "stash, verb. to put (something of future use or value) in a safe or secret // place" // -// Unstash loads a "Thing" from a place specified in a StashConfig, usually an -// AnyStore DB file, but the Stash and Unstash functions also support io.Reader -// and io.Writer (io.WriteCloser). Reader/writer is essentially an in-memory -// version of the physical DB file, Unstash does io.ReadAll into memory in order -// to decrypt and de-GOB the data. A previous file-Stash command can be -// Unstashed via the io.Reader. Unstash prefers io.Reader when both -// StashConfig.File and StashConfig.Reader are defined. +// Unstash loads a "Thing" from a place specified in a StashConfig, +// usually an AnyStoreDB file, into the object pointed to by the Thing +// field in conf. Thing should be an uninitialized/new/empty object +// for predictable results, full overwrite of any present value is not +// guaranteed. +// +// The Stash and Unstash functions also support io.Reader and +// io.Writer (io.WriteCloser). Reader/writer is essentially an +// in-memory version of the physical DB file, Unstash does io.ReadAll +// into memory in order to decrypt and de-GOB the data. A previous +// file-Stash command can be Unstashed via the io.Reader. Unstash +// prefers io.Reader when both StashConfig.File and StashConfig.Reader +// are defined. // // StashConfig instructs how functions anystore.Stash and anystore.Unstash // should save/load a "stash". If Reader is not nil and File is not an empty diff --git a/stash_test.go b/stash_test.go index 951fd56..78dc037 100644 --- a/stash_test.go +++ b/stash_test.go @@ -1,6 +1,7 @@ package anystore_test import ( + "encoding/json" "errors" "fmt" "io" @@ -582,3 +583,62 @@ func ExampleStash_reader_writer() { // Output: // Hello world } + +func ExampleStash() { + type Endpoint struct { + Name string + URL string + } + type MyConfig struct { + Username string + Token *string + Endpoints []Endpoint + } + + defaultConfig := &MyConfig{ + Username: "anonymous", + Endpoints: []Endpoint{ + { + Name: "Default", + URL: "https://localhost:8081/default", + }, + }, + } + + var configuration MyConfig + + conf := &anystore.StashConfig{ + File: "~/.anystore/stash-example-01.db", + GZip: true, + EncryptionKey: anystore.DefaultEncryptionKey, + Key: "configuration", + Thing: &configuration, + DefaultThing: defaultConfig, + } + + // First, load persisted configuration from file. If none found, use + // defaultConfig as values for configuration. + + if err := anystore.Unstash(conf); err != nil { + log.Fatal(err) + } + + // Override if something has been provided from the command line, etc. + + token, ok := os.LookupEnv("TOKEN") + if ok { + configuration.Token = &token + } + + // Persist configuration to disk. + + if err := anystore.Stash(conf); err != nil { + log.Fatal(err) + } + + j, err := json.MarshalIndent(&configuration, "", " ") + if err != nil { + log.Fatal(err) + } + fmt.Println(string(j)) +}