diff --git a/addrmgr/addrmanager.go b/addrmgr/addrmanager.go index bdfe9094ce..4e71416656 100644 --- a/addrmgr/addrmanager.go +++ b/addrmgr/addrmanager.go @@ -11,11 +11,11 @@ import ( "encoding/base32" "encoding/binary" "encoding/json" + "errors" "fmt" "io" "math/rand" "net" - "os" "path/filepath" "strconv" "strings" @@ -27,11 +27,21 @@ import ( "github.com/btcsuite/btcd/wire" ) +var ErrNotExist = errors.New("store does not exist") + +// store is a basic storage interface. Either using the file system or localStorage in the browser. +type store interface { + Reader() (io.ReadCloser, error) + Writer() (io.WriteCloser, error) + Remove() error + String() string +} + // AddrManager provides a concurrency safe address manager for caching potential // peers on the bitcoin network. type AddrManager struct { mtx sync.RWMutex - peersFile string + store store lookupFunc func(string) ([]net.IP, error) rand *rand.Rand key [32]byte @@ -407,15 +417,15 @@ func (a *AddrManager) savePeers() { } } - w, err := os.Create(a.peersFile) + w, err := a.store.Writer() if err != nil { - log.Errorf("Error opening file %s: %v", a.peersFile, err) + log.Errorf("Error opening store %s: %v", a.store, err) return } enc := json.NewEncoder(w) defer w.Close() if err := enc.Encode(&sam); err != nil { - log.Errorf("Failed to encode file %s: %v", a.peersFile, err) + log.Errorf("Failed to encode peers %s: %v", a.store, err) return } } @@ -426,30 +436,27 @@ func (a *AddrManager) loadPeers() { a.mtx.Lock() defer a.mtx.Unlock() - err := a.deserializePeers(a.peersFile) + err := a.deserializePeers() if err != nil { - log.Errorf("Failed to parse file %s: %v", a.peersFile, err) + log.Errorf("Failed to parse store %s: %v", a.store, err) // if it is invalid we nuke the old one unconditionally. - err = os.Remove(a.peersFile) + err = a.store.Remove() if err != nil { - log.Warnf("Failed to remove corrupt peers file %s: %v", - a.peersFile, err) + log.Warnf("Failed to remove corrupt peers %s: %v", a.store, err) } a.reset() return } - log.Infof("Loaded %d addresses from file '%s'", a.numAddresses(), a.peersFile) + log.Infof("Loaded %d addresses from store '%s'", a.numAddresses(), a.store) } -func (a *AddrManager) deserializePeers(filePath string) error { - - _, err := os.Stat(filePath) - if os.IsNotExist(err) { +func (a *AddrManager) deserializePeers() error { + r, err := a.store.Reader() + if errors.Is(err, ErrNotExist) { return nil } - r, err := os.Open(filePath) if err != nil { - return fmt.Errorf("%s error opening file: %v", filePath, err) + return fmt.Errorf("error opening store: %v", err) } defer r.Close() @@ -457,7 +464,7 @@ func (a *AddrManager) deserializePeers(filePath string) error { dec := json.NewDecoder(r) err = dec.Decode(&sam) if err != nil { - return fmt.Errorf("error reading %s: %v", filePath, err) + return fmt.Errorf("error reading %s: %v", a.store, err) } // Since decoding JSON is backwards compatible (i.e., only decodes @@ -1206,7 +1213,7 @@ func (a *AddrManager) GetBestLocalAddress(remoteAddr *wire.NetAddressV2) *wire.N // Use Start to begin processing asynchronous address updates. func New(dataDir string, lookupFunc func(string) ([]net.IP, error)) *AddrManager { am := AddrManager{ - peersFile: filepath.Join(dataDir, "peers.json"), + store: NewStore(filepath.Join(dataDir, "peers.json")), lookupFunc: lookupFunc, rand: rand.New(rand.NewSource(time.Now().UnixNano())), quit: make(chan struct{}), diff --git a/addrmgr/addrstore.go b/addrmgr/addrstore.go new file mode 100644 index 0000000000..4317a7324d --- /dev/null +++ b/addrmgr/addrstore.go @@ -0,0 +1,43 @@ +//go:build !js && !wasm + +package addrmgr + +import ( + "io" + "os" +) + +type Store struct { + path string +} + +func (s *Store) Reader() (io.ReadCloser, error) { + // Open the file. + r, err := os.Open(s.path) + + // Convert into a generic error. + if os.IsNotExist(err) { + return nil, ErrNotExist + } + + return r, err +} + +func (s *Store) Writer() (io.WriteCloser, error) { + // Create or open the file. + return os.Create(s.path) +} + +func (s *Store) Remove() error { + return os.Remove(s.path) +} + +func (s *Store) String() string { + return s.path +} + +func NewStore(path string) *Store { + return &Store{ + path: path, + } +} diff --git a/addrmgr/addrstore_js.go b/addrmgr/addrstore_js.go new file mode 100644 index 0000000000..2b3b725fb0 --- /dev/null +++ b/addrmgr/addrstore_js.go @@ -0,0 +1,87 @@ +//go:build js && wasm + +package addrmgr + +import ( + "bytes" + "errors" + "io" + "strings" + + "github.com/linden/localstorage" +) + +type Store struct { + path string +} + +func (s *Store) Reader() (io.ReadCloser, error) { + // Get the value from localStorage. + val := localstorage.Get(s.path) + + // Convert into a generic error. + if val == "" { + return nil, ErrNotExist + } + + // Create a new buffer storing our value. + buf := bytes.NewBufferString(val) + + // Create a NOP closer, we have nothing to do upon close. + return io.NopCloser(buf), nil +} + +func (s *Store) Writer() (io.WriteCloser, error) { + // Create a new writer. + return newWriter(s.path), nil +} + +func (s *Store) Remove() error { + // Remove the key/value from localStorage. + localstorage.Remove(s.path) + + return nil +} + +func (s *Store) String() string { + return s.path +} + +func NewStore(path string) *Store { + return &Store{ + path: path, + } +} + +// writer updates the localStorage on write. +type writer struct { + path string + closed bool + builder strings.Builder +} + +func (w *writer) Write(p []byte) (int, error) { + if w.closed { + return 0, errors.New("writer already closed") + } + + // Write the bytes to our string builder. + n, _ := w.builder.Write(p) + + // Update the localStorage value. + localstorage.Set(w.path, w.builder.String()) + + // Return the length written, + return n, nil +} + +func (w *writer) Close() error { + w.closed = true + return nil +} + +func newWriter(path string) *writer { + return &writer{ + path: path, + } +} diff --git a/btcutil/appdata.go b/btcutil/appdata.go index b6c63b9a29..11d556f31a 100644 --- a/btcutil/appdata.go +++ b/btcutil/appdata.go @@ -23,6 +23,11 @@ func appDataDir(goos, appName string, roaming bool) string { return "." } + // Fallback to an empty string on js since we do not have a file system. + if goos == "js" { + return "" + } + // The caller really shouldn't prepend the appName with a period, but // if they do, handle it gracefully by trimming it. appName = strings.TrimPrefix(appName, ".") diff --git a/go.mod b/go.mod index 6eea83508e..9f814d4db0 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/jessevdk/go-flags v1.4.0 github.com/jrick/logrotate v1.0.0 + github.com/linden/localstorage v0.0.0-20231117043609-5d94f0a86609 github.com/stretchr/testify v1.8.4 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 @@ -62,4 +63,4 @@ retract ( v0.13.0-beta ) -go 1.17 +go 1.21.2 diff --git a/go.sum b/go.sum index ddd7e1ebd0..2127631adf 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,10 @@ github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 h1:FOOIBWrEkLgmlgGfMuZT83xIwfPDxEI2OHu6xUmJMFE= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/linden/localstorage v0.0.0-20231116131807-4984ea9234ae h1:aShKD5W7/reF6oTj+JsQP/Yf/7B3sYiqx6K9tIp6KFI= +github.com/linden/localstorage v0.0.0-20231116131807-4984ea9234ae/go.mod h1:uQBC250C4YSyzxNly4CR02PT2VKoO3H9zlJXbUXVLl8= +github.com/linden/localstorage v0.0.0-20231117043609-5d94f0a86609 h1:oRjHnrgw7Jo+B0MokX+2L7ugroptSEhvJpc3RenpZL8= +github.com/linden/localstorage v0.0.0-20231117043609-5d94f0a86609/go.mod h1:uQBC250C4YSyzxNly4CR02PT2VKoO3H9zlJXbUXVLl8= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=